diff --git a/package.json b/package.json index f5b9716e43..1552064855 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "lodash.bindall": "4.4.0", "lodash.omit": "4.5.0", "minilog": "3.1.0", - "parse-color": "1.0.0", "prop-types": "^15.5.10", "scratch-svg-renderer": "0.2.0-prerelease.20200610220938" }, diff --git a/src/components/color-button/color-button.jsx b/src/components/color-button/color-button.jsx index 07d9b8a785..68ead385a6 100644 --- a/src/components/color-button/color-button.jsx +++ b/src/components/color-button/color-button.jsx @@ -9,11 +9,15 @@ import mixedFillIcon from './mixed-fill.svg'; import styles from './color-button.css'; import GradientTypes from '../../lib/gradient-types'; import log from '../../log/log'; +import ColorProptype from '../../lib/color-proptype'; const colorToBackground = (color, color2, gradientType) => { if (color === MIXED || (gradientType !== GradientTypes.SOLID && color2 === MIXED)) return 'white'; - if (color === null) color = 'white'; - if (color2 === null) color2 = 'white'; + if (color === MIXED || color2 === MIXED) return 'white'; + + color = (color === null) ? 'white' : color.toCSS(); + color2 = (color2 === null) ? 'white' : color2.toCSS(); + switch (gradientType) { case GradientTypes.SOLID: return color; case GradientTypes.HORIZONTAL: return `linear-gradient(to right, ${color}, ${color2})`; @@ -55,8 +59,8 @@ const ColorButtonComponent = props => ( ); ColorButtonComponent.propTypes = { - color: PropTypes.string, - color2: PropTypes.string, + color: ColorProptype, + color2: ColorProptype, gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, onClick: PropTypes.func.isRequired, outline: PropTypes.bool.isRequired diff --git a/src/components/color-indicator.jsx b/src/components/color-indicator.jsx index d04ea2bbf2..f552276613 100644 --- a/src/components/color-indicator.jsx +++ b/src/components/color-indicator.jsx @@ -8,6 +8,7 @@ import InputGroup from './input-group/input-group.jsx'; import Label from './forms/label.jsx'; import GradientTypes from '../lib/gradient-types'; +import ColorProptype from '../lib/color-proptype'; const ColorIndicatorComponent = props => ( ( ColorIndicatorComponent.propTypes = { className: PropTypes.string, disabled: PropTypes.bool.isRequired, - color: PropTypes.string, - color2: PropTypes.string, + color: ColorProptype, + color2: ColorProptype, colorModalVisible: PropTypes.bool.isRequired, gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, label: PropTypes.string.isRequired, diff --git a/src/components/color-picker/color-picker.jsx b/src/components/color-picker/color-picker.jsx index 37035dd5bc..d83dc0fa3f 100644 --- a/src/components/color-picker/color-picker.jsx +++ b/src/components/color-picker/color-picker.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl'; import classNames from 'classnames'; -import parseColor from 'parse-color'; import Slider from '../forms/slider.jsx'; import LabeledIconButton from '../labeled-icon-button/labeled-icon-button.jsx'; @@ -20,11 +19,23 @@ import fillSolidIcon from './icons/fill-solid-enabled.svg'; import fillVertGradientIcon from './icons/fill-vert-gradient-enabled.svg'; import swapIcon from './icons/swap.svg'; import Modes from '../../lib/modes'; +import ColorProptype from '../../lib/color-proptype'; -const hsvToHex = (h, s, v) => - // Scale hue back up to [0, 360] from [0, 100] - parseColor(`hsv(${3.6 * h}, ${s}, ${v})`).hex -; +/** + * Converts the color picker's internal color representation (HSV 0-100) into a CSS color string. + * @param {number} h Hue, from 0 to 100. + * @param {number} s Saturation, from 0 to 100. + * @param {number} v Value, from 0 to 100. + * @returns {string} A valid CSS color string representing the input HSV color. + */ +const hsvToCssString = (h, s, v) => { + const scaledValue = v * 0.01; + const hslLightness = scaledValue - ((scaledValue * (s * 0.01)) / 2); + const m = Math.min(hslLightness, 1 - hslLightness); + const hslSaturation = (m === 0) ? 0 : (scaledValue - hslLightness) / m; + + return `hsl(${h * 3.6}, ${hslSaturation * 100}%, ${hslLightness * 100}%)`; +}; const messages = defineMessages({ swap: { @@ -41,13 +52,13 @@ class ColorPickerComponent extends React.Component { for (let n = 100; n >= 0; n -= 10) { switch (channel) { case 'hue': - stops.push(hsvToHex(n, this.props.saturation, this.props.brightness)); + stops.push(hsvToCssString(n, this.props.saturation, this.props.brightness)); break; case 'saturation': - stops.push(hsvToHex(this.props.hue, n, this.props.brightness)); + stops.push(hsvToCssString(this.props.hue, n, this.props.brightness)); break; case 'brightness': - stops.push(hsvToHex(this.props.hue, this.props.saturation, n)); + stops.push(hsvToCssString(this.props.hue, this.props.saturation, n)); break; default: throw new Error(`Unknown channel for color sliders: ${channel}`); @@ -122,7 +133,7 @@ class ColorPickerComponent extends React.Component { })} style={{ backgroundColor: this.props.color === null || this.props.color === MIXED ? - 'white' : this.props.color + 'white' : this.props.color.toCSS() }} onClick={this.props.onSelectColor} > @@ -155,7 +166,7 @@ class ColorPickerComponent extends React.Component { })} style={{ backgroundColor: this.props.color2 === null || this.props.color2 === MIXED ? - 'white' : this.props.color2 + 'white' : this.props.color2.toCSS() }} onClick={this.props.onSelectColor2} > @@ -290,8 +301,8 @@ class ColorPickerComponent extends React.Component { ColorPickerComponent.propTypes = { brightness: PropTypes.number.isRequired, - color: PropTypes.string, - color2: PropTypes.string, + color: ColorProptype, + color2: ColorProptype, colorIndex: PropTypes.number.isRequired, gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, hue: PropTypes.number.isRequired, diff --git a/src/containers/bit-brush-mode.jsx b/src/containers/bit-brush-mode.jsx index 652395d8eb..aafb4e6256 100644 --- a/src/containers/bit-brush-mode.jsx +++ b/src/containers/bit-brush-mode.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; +import ColorProptype from '../lib/color-proptype'; import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; import {MIXED} from '../helper/style-path'; @@ -84,7 +85,7 @@ BitBrushMode.propTypes = { bitBrushSize: PropTypes.number.isRequired, clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, - color: PropTypes.string, + color: ColorProptype, handleMouseDown: PropTypes.func.isRequired, isBitBrushModeActive: PropTypes.bool.isRequired, onChangeFillColor: PropTypes.func.isRequired, diff --git a/src/containers/bit-fill-mode.jsx b/src/containers/bit-fill-mode.jsx index 8ed9a2af9e..77b4f84af2 100644 --- a/src/containers/bit-fill-mode.jsx +++ b/src/containers/bit-fill-mode.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; +import ColorProptype from '../lib/color-proptype'; import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; import GradientTypes from '../lib/gradient-types'; @@ -102,8 +103,8 @@ class BitFillMode extends React.Component { BitFillMode.propTypes = { changeGradientType: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, - color: PropTypes.string, - color2: PropTypes.string, + color: ColorProptype, + color2: ColorProptype, styleGradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, fillModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)), handleMouseDown: PropTypes.func.isRequired, diff --git a/src/containers/bit-line-mode.jsx b/src/containers/bit-line-mode.jsx index 5b57cdafb1..6fc8020d7d 100644 --- a/src/containers/bit-line-mode.jsx +++ b/src/containers/bit-line-mode.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; +import ColorProptype from '../lib/color-proptype'; import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; import {MIXED} from '../helper/style-path'; @@ -84,7 +85,7 @@ BitLineMode.propTypes = { bitBrushSize: PropTypes.number.isRequired, clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, - color: PropTypes.string, + color: ColorProptype, handleMouseDown: PropTypes.func.isRequired, isBitLineModeActive: PropTypes.bool.isRequired, onChangeFillColor: PropTypes.func.isRequired, diff --git a/src/containers/color-indicator.jsx b/src/containers/color-indicator.jsx index bd119408da..520792bc94 100644 --- a/src/containers/color-indicator.jsx +++ b/src/containers/color-indicator.jsx @@ -1,21 +1,20 @@ import PropTypes from 'prop-types'; import React from 'react'; import bindAll from 'lodash.bindall'; -import parseColor from 'parse-color'; import {injectIntl, intlShape} from 'react-intl'; import {getSelectedLeafItems} from '../helper/selection'; import Formats from '../lib/format'; import {isBitmap} from '../lib/format'; import GradientTypes from '../lib/gradient-types'; +import ColorProptype from '../lib/color-proptype'; import ColorIndicatorComponent from '../components/color-indicator.jsx'; import {applyColorToSelection, applyGradientTypeToSelection, applyStrokeWidthToSelection, generateSecondaryColor, - swapColorsInSelection, - MIXED} from '../helper/style-path'; + swapColorsInSelection} from '../helper/style-path'; const makeColorIndicator = (label, isStroke) => { class ColorIndicator extends React.Component { @@ -123,12 +122,8 @@ const makeColorIndicator = (label, isStroke) => { this.props.setSelectedItems(this.props.format); this._hasChanged = this._hasChanged || isDifferent; } else { - let color1 = this.props.color; - let color2 = this.props.color2; - color1 = color1 === null || color1 === MIXED ? color1 : parseColor(color1).hex; - color2 = color2 === null || color2 === MIXED ? color2 : parseColor(color2).hex; - this.props.onChangeColor(color1, 1); - this.props.onChangeColor(color2, 0); + this.props.onChangeColor(this.props.color, 1); + this.props.onChangeColor(this.props.color2, 0); } } render () { @@ -149,8 +144,8 @@ const makeColorIndicator = (label, isStroke) => { ColorIndicator.propTypes = { colorIndex: PropTypes.number.isRequired, disabled: PropTypes.bool.isRequired, - color: PropTypes.string, - color2: PropTypes.string, + color: ColorProptype, + color2: ColorProptype, colorModalVisible: PropTypes.bool.isRequired, fillBitmapShapes: PropTypes.bool.isRequired, format: PropTypes.oneOf(Object.keys(Formats)), diff --git a/src/containers/color-picker.jsx b/src/containers/color-picker.jsx index 1411008847..23947bb6c8 100644 --- a/src/containers/color-picker.jsx +++ b/src/containers/color-picker.jsx @@ -1,7 +1,7 @@ import bindAll from 'lodash.bindall'; import {connect} from 'react-redux'; import paper from '@scratch/paper'; -import parseColor from 'parse-color'; +import ColorProptype from '../lib/color-proptype'; import PropTypes from 'prop-types'; import React from 'react'; @@ -14,23 +14,6 @@ import ColorPickerComponent from '../components/color-picker/color-picker.jsx'; import {MIXED} from '../helper/style-path'; import Modes from '../lib/modes'; -const colorStringToHsv = hexString => { - const hsv = parseColor(hexString).hsv; - // Hue comes out in [0, 360], limit to [0, 100] - hsv[0] = hsv[0] / 3.6; - // Black is parsed as {0, 0, 0}, but turn saturation up to 100 - // to make it easier to see slider values. - if (hsv[1] === 0 && hsv[2] === 0) { - hsv[1] = 100; - } - return hsv; -}; - -const hsvToHex = (h, s, v) => - // Scale hue back up to [0, 360] from [0, 100] - parseColor(`hsv(${3.6 * h}, ${s}, ${v})`).hex -; - // Important! This component ignores new color props except when isEyeDropping // This is to make the HSV <=> RGB conversion stable. The sliders manage their // own changes until unmounted or color changes with props.isEyeDropping = true. @@ -38,7 +21,6 @@ class ColorPicker extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'getHsv', 'handleChangeGradientTypeHorizontal', 'handleChangeGradientTypeRadial', 'handleChangeGradientTypeSolid', @@ -75,7 +57,11 @@ class ColorPicker extends React.Component { const isTransparent = color === null; const isMixed = color === MIXED; return isTransparent || isMixed ? - [50, 100, 100] : colorStringToHsv(color); + [50, 100, 100] : [ + color.hue * (100 / 360), + color.saturation * 100, + color.brightness * 100 + ]; } handleHueChange (hue) { this.setState({hue: hue}, () => { @@ -93,11 +79,11 @@ class ColorPicker extends React.Component { }); } handleColorChange () { - this.props.onChangeColor(hsvToHex( - this.state.hue, - this.state.saturation, - this.state.brightness - )); + this.props.onChangeColor(new paper.Color({ + hue: this.state.hue * (360 / 100), + saturation: this.state.saturation / 100, + brightness: this.state.brightness / 100 + })); } handleTransparent () { this.props.onChangeColor(null); @@ -152,8 +138,8 @@ class ColorPicker extends React.Component { } ColorPicker.propTypes = { - color: PropTypes.string, - color2: PropTypes.string, + color: ColorProptype, + color2: ColorProptype, colorIndex: PropTypes.number.isRequired, gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, isEyeDropping: PropTypes.bool.isRequired, diff --git a/src/containers/fill-mode.jsx b/src/containers/fill-mode.jsx index 555863c845..758f2ddb87 100644 --- a/src/containers/fill-mode.jsx +++ b/src/containers/fill-mode.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; +import ColorProptype from '../lib/color-proptype'; import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; import GradientTypes from '../lib/gradient-types'; @@ -112,8 +113,8 @@ FillMode.propTypes = { changeGradientType: PropTypes.func.isRequired, clearHoveredItem: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, - fillColor: PropTypes.string, - fillColor2: PropTypes.string, + fillColor: ColorProptype, + fillColor2: ColorProptype, fillStyleGradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, fillModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)), handleMouseDown: PropTypes.func.isRequired, diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index 3d0925f50a..5161926b85 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -23,7 +23,7 @@ class LineMode extends React.Component { return 6; } static get DEFAULT_COLOR () { - return '#000000'; + return new paper.Color({hue: 0, saturation: 0, brightness: 0}); } constructor (props) { super(props); diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 23658563ad..430cebcf2a 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -254,14 +254,14 @@ class PaintEditor extends React.Component { } onMouseUp () { if (this.props.isEyeDropping) { - const colorString = this.eyeDropper.colorString; + const color = this.eyeDropper.color; const callback = this.props.changeColorToEyeDropper; this.eyeDropper.remove(); if (!this.eyeDropper.hideLoupe) { // If not hide loupe, that means the click is inside the canvas, // so apply the new color - callback(colorString); + callback(color); } if (this.props.previousTool) this.props.previousTool.activate(); this.props.onDeactivateEyeDropper(); diff --git a/src/containers/stroke-width-indicator.jsx b/src/containers/stroke-width-indicator.jsx index ed27ff967e..b0caf8cad4 100644 --- a/src/containers/stroke-width-indicator.jsx +++ b/src/containers/stroke-width-indicator.jsx @@ -2,8 +2,7 @@ import {connect} from 'react-redux'; import PropTypes from 'prop-types'; import React from 'react'; import bindAll from 'lodash.bindall'; -import parseColor from 'parse-color'; -import {changeStrokeColor, changeStrokeColor2, changeStrokeGradientType} from '../reducers/stroke-style'; +import {changeStrokeColor, changeStrokeColor2, changeStrokeGradientType, DEFAULT_COLOR} from '../reducers/stroke-style'; import {changeStrokeWidth} from '../reducers/stroke-width'; import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx'; import {getSelectedLeafItems} from '../helper/selection'; @@ -13,6 +12,7 @@ import GradientTypes from '../lib/gradient-types'; import Modes from '../lib/modes'; import Formats from '../lib/format'; import {isBitmap} from '../lib/format'; +import paper from '@scratch/paper'; class StrokeWidthIndicator extends React.Component { constructor (props) { @@ -34,7 +34,7 @@ class StrokeWidthIndicator extends React.Component { if (wasNull) { changed = applyColorToSelection( - '#000', + DEFAULT_COLOR, 0, // colorIndex, true, // isSolidGradient true, // applyToStroke @@ -42,12 +42,12 @@ class StrokeWidthIndicator extends React.Component { changed; // If there's no previous stroke color, default to solid black this.props.onChangeStrokeGradientType(GradientTypes.SOLID); - this.props.onChangeStrokeColor('#000'); + this.props.onChangeStrokeColor(DEFAULT_COLOR); } else if (currentColorState.strokeColor !== MIXED) { // Set color state from the selected item's stroke color this.props.onChangeStrokeGradientType(currentColorState.strokeGradientType); - this.props.onChangeStrokeColor(parseColor(currentColorState.strokeColor).hex); - this.props.onChangeStrokeColor2(parseColor(currentColorState.strokeColor2).hex); + this.props.onChangeStrokeColor(currentColorState.strokeColor); + this.props.onChangeStrokeColor2(currentColorState.strokeColor2); } } this.props.onChangeStrokeWidth(newWidth); diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 59907b6901..2751b5b379 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -10,13 +10,29 @@ import log from '../log/log'; const MIXED = 'scratch-paint/style-path/mixed'; -// Check if the item color matches the incoming color. If the item color is a gradient, we assume -// that the incoming color never matches, since we don't support gradients yet. -const _colorMatch = function (itemColor, incomingColor) { - if (itemColor && itemColor.type === 'gradient') return false; - // Either both are null or both are the same color when converted to CSS. - return (!itemColor && !incomingColor) || - (itemColor && incomingColor && itemColor.toCSS() === new paper.Color(incomingColor).toCSS()); +// Check if two colors, possibly null, are (approximately) equal. +const _colorsEqual = (color1, color2) => { + if (color1 === color2) return true; + if (!color1 || !color2) return false; + + if (!(color1 instanceof paper.Color && color2 instanceof paper.Color)) { + log.warn('_colorsEqual passed non-Colors'); + return false; + } + + const hsb1 = color1.type === 'hsb' ? color1 : color1.convert('hsb'); + const hsb2 = color2.type === 'hsb' ? color2 : color2.convert('hsb'); + + // Colors that are similar enough, as determined by this threshold, will be considered equal. + // Copied from paper.js' Numerical.js. This is a good number. I like this number. + const EPSILON = 1e-12; + + return ( + Math.abs(hsb1.hue - hsb2.hue) < EPSILON && + Math.abs(hsb1.saturation - hsb2.saturation) < EPSILON && + Math.abs(hsb1.brightness - hsb2.brightness) < EPSILON && + Math.abs(hsb1.alpha - hsb2.alpha) < EPSILON + ); }; // Selected items and currently active text edit items respond to color changes. @@ -37,13 +53,14 @@ const _getColorStateListeners = function (textEditTargetId) { * Transparent R, G, B values need to match the other color of the gradient * in order to form a smooth gradient, otherwise it fades through black. This * function gets the transparent color for a given color string. - * @param {?string} colorToMatch CSS string of other color of gradient, or null for transparent - * @return {string} CSS string for matching color of transparent + * @param {?paper.Color} colorToMatch Other color of gradient, or null for transparent + * @return {paper.Color} Color for matching color of transparent */ -const getColorStringForTransparent = function (colorToMatch) { - const color = new paper.Color(colorToMatch); +const getColorForTransparent = function (colorToMatch) { + if (colorToMatch === null) return new paper.Color({hue: 0, saturation: 0, brightness: 0, alpha: 0}); + const color = colorToMatch.clone(); color.alpha = 0; - return color.toCSS(); + return color; }; /** @@ -66,19 +83,20 @@ const generateSecondaryColor = function (firstColor) { } // If the color was desaturated, don't do a hue shift, as it would be hard to see anyway. if (!desaturated) { - color.hue -= 72; + // adding 288 and taking the result modulo 360 is equivalent to subtracting 72, and ensures hue is kept positive + color.hue = (color.hue + 288) % 360; } // The returned color will be one of three things: // 1. If the color was bright and saturated (e.g. colorful), it will be that color, hue-shifted. // 2. If the color was dark and saturated, it will be that color, brightened and hue-shifted. // 3. If the color was not saturated, it will be that color, brightened or darkened as needed to contrast most. - return color.toCSS(true /* hex */); + return color; }; /** * Convert params to a paper.Color gradient object - * @param {?string} color1 CSS string, or null for transparent - * @param {?string} color2 CSS string, or null for transparent + * @param {?paper.Color} color1 Primary gradient color, or null for transparent + * @param {?paper.Color} color2 Secondary gradient color, or null for transparent * @param {GradientType} gradientType gradient type * @param {paper.Rectangle} bounds Bounds of the object * @param {?paper.Point} [radialCenter] Where the center of a radial gradient should be, if the gradient is radial. @@ -89,10 +107,10 @@ const generateSecondaryColor = function (firstColor) { const createGradientObject = function (color1, color2, gradientType, bounds, radialCenter, minSize) { if (gradientType === GradientTypes.SOLID) return color1; if (color1 === null) { - color1 = getColorStringForTransparent(color2); + color1 = getColorForTransparent(color2); } if (color2 === null) { - color2 = getColorStringForTransparent(color1); + color2 = getColorForTransparent(color1); } // Force gradients to have a minimum length. If the gradient start and end points are the same or very close @@ -150,7 +168,7 @@ const createGradientObject = function (color1, color2, gradientType, bounds, rad /** * Called when setting an item's color - * @param {string} colorString color, css format, or null if completely transparent + * @param {paper.Color} color new color, or null if completely transparent * @param {number} colorIndex index of color being changed * @param {boolean} isSolidGradient True if is solid gradient. Sometimes the item has a gradient but the color * picker is set to a solid gradient. This happens when a mix of colors and gradient types is selected. @@ -160,7 +178,7 @@ const createGradientObject = function (color1, color2, gradientType, bounds, rad * @return {boolean} Whether the color application actually changed visibly. */ const applyColorToSelection = function ( - colorString, + color, colorIndex, isSolidGradient, applyToStroke, @@ -179,29 +197,29 @@ const applyColorToSelection = function ( if (isSolidGradient || !itemColor || !itemColor.gradient || !itemColor.gradient.stops.length === 2) { // Applying a solid color - if (!_colorMatch(itemColor, colorString)) { + if (!_colorsEqual(itemColor, color)) { changed = true; - if (isPointTextItem(item) && !colorString) { + if (isPointTextItem(item) && !color) { // Allows transparent text to be hit item[itemColorProp] = 'rgba(0,0,0,0)'; } else { - item[itemColorProp] = colorString; + item[itemColorProp] = color; } } - } else if (!_colorMatch(itemColor.gradient.stops[colorIndex].color, colorString)) { + } else if (!_colorsEqual(itemColor.gradient.stops[colorIndex].color, color)) { // Changing one color of an existing gradient changed = true; const otherIndex = colorIndex === 0 ? 1 : 0; - if (colorString === null) { - colorString = getColorStringForTransparent(itemColor.gradient.stops[otherIndex].color.toCSS()); + if (color === null) { + color = getColorForTransparent(itemColor.gradient.stops[otherIndex].color); } const colors = [0, 0]; - colors[colorIndex] = colorString; + colors[colorIndex] = color; // If the other color is transparent, its RGB values need to be adjusted for the gradient to be smooth if (itemColor.gradient.stops[otherIndex].color.alpha === 0) { - colors[otherIndex] = getColorStringForTransparent(colorString); + colors[otherIndex] = getColorForTransparent(color); } else { - colors[otherIndex] = itemColor.gradient.stops[otherIndex].color.toCSS(); + colors[otherIndex] = itemColor.gradient.stops[otherIndex].color; } // There seems to be a bug where setting colors on stops doesn't always update the view, so set gradient. itemColor.gradient = {stops: colors, radial: itemColor.gradient.radial}; @@ -233,8 +251,8 @@ const swapColorsInSelection = function (applyToStroke, textEditTargetId) { // Changing one color of an existing gradient changed = true; const colors = [ - itemColor.gradient.stops[1].color.toCSS(), - itemColor.gradient.stops[0].color.toCSS() + itemColor.gradient.stops[1].color, + itemColor.gradient.stops[0].color ]; // There seems to be a bug where setting colors on stops doesn't always update the view, so set gradient. itemColor.gradient = {stops: colors, radial: itemColor.gradient.radial}; @@ -269,13 +287,13 @@ const applyGradientTypeToSelection = function (gradientType, applyToStroke, text itemColor1 = null; } else if (!hasGradient) { // Solid color - itemColor1 = itemColor.toCSS(); + itemColor1 = itemColor; } else if (!itemColor.gradient.stops[0] || itemColor.gradient.stops[0].color.alpha === 0) { // Gradient where first color is transparent itemColor1 = null; } else { // Gradient where first color is not transparent - itemColor1 = itemColor.gradient.stops[0].color.toCSS(); + itemColor1 = itemColor.gradient.stops[0].color; } let itemColor2; @@ -287,7 +305,7 @@ const applyGradientTypeToSelection = function (gradientType, applyToStroke, text itemColor2 = null; } else { // Gradient has 2nd color which is not transparent - itemColor2 = itemColor.gradient.stops[1].color.toCSS(); + itemColor2 = itemColor.gradient.stops[1].color; } if (gradientType === GradientTypes.SOLID) { @@ -299,10 +317,10 @@ const applyGradientTypeToSelection = function (gradientType, applyToStroke, text } if (itemColor1 === null) { - itemColor1 = getColorStringForTransparent(itemColor2); + itemColor1 = getColorForTransparent(itemColor2); } if (itemColor2 === null) { - itemColor2 = getColorStringForTransparent(itemColor1); + itemColor2 = getColorForTransparent(itemColor1); } let gradientTypeDiffers = false; @@ -378,10 +396,10 @@ const _colorStateFromGradient = gradient => { } colorState.primary = gradient.stops[0].color.alpha === 0 ? null : - gradient.stops[0].color.toCSS(); + gradient.stops[0].color; colorState.secondary = gradient.stops[1].color.alpha === 0 ? null : - gradient.stops[1].color.toCSS(); + gradient.stops[1].color; } else { if (gradient.stops.length < 2) log.warn(`Gradient has ${gradient.stops.length} stop(s)`); @@ -403,10 +421,11 @@ const _colorStateFromGradient = gradient => { */ const getColorsFromSelection = function (selectedItems, bitmapMode) { // TODO: DRY out this code - let selectionFillColorString; - let selectionFillColor2String; - let selectionStrokeColorString; - let selectionStrokeColor2String; + // TODO: Handle cases in which the paper.Color of the selection is an RGB color, and convert to an HSB color + let selectionFillColor; + let selectionFillColor2; + let selectionStrokeColor; + let selectionStrokeColor2; let selectionStrokeWidth; let selectionThickness; let selectionFillGradientType; @@ -418,10 +437,10 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { // Compound path children inherit fill and stroke color from their parent. item = item.parent; } - let itemFillColorString; - let itemFillColor2String; - let itemStrokeColorString; - let itemStrokeColor2String; + let itemFillColor; + let itemFillColor2; + let itemStrokeColor; + let itemStrokeColor2; let itemFillGradientType = GradientTypes.SOLID; let itemStrokeGradientType = GradientTypes.SOLID; @@ -429,16 +448,16 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { if (item.fillColor) { // hack bc text items with null fill can't be detected by fill-hitTest anymore if (isPointTextItem(item) && item.fillColor.alpha === 0) { - itemFillColorString = null; + itemFillColor = null; } else if (item.fillColor.type === 'gradient') { const {primary, secondary, gradientType} = _colorStateFromGradient(item.fillColor.gradient); - itemFillColorString = primary; - itemFillColor2String = secondary; + itemFillColor = primary; + itemFillColor2 = secondary; itemFillGradientType = gradientType; } else { - itemFillColorString = item.fillColor.alpha === 0 ? + itemFillColor = item.fillColor.alpha === 0 ? null : - item.fillColor.toCSS(); + item.fillColor; } } if (item.strokeColor) { @@ -457,66 +476,67 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { // Stroke color is fill color in bitmap if (bitmapMode) { - itemFillColorString = strokeColorString; - itemFillColor2String = strokeColor2String; + itemFillColor = strokeColorString; + itemFillColor2 = strokeColor2String; itemFillGradientType = strokeGradientType; } else { - itemStrokeColorString = strokeColorString; - itemStrokeColor2String = strokeColor2String; + itemStrokeColor = strokeColorString; + itemStrokeColor2 = strokeColor2String; itemStrokeGradientType = strokeGradientType; } } else { const strokeColorString = item.strokeColor.alpha === 0 || !item.strokeWidth ? null : - item.strokeColor.toCSS(); + item.strokeColor; // Stroke color is fill color in bitmap if (bitmapMode) { - itemFillColorString = strokeColorString; + itemFillColor = strokeColorString; } else { - itemStrokeColorString = strokeColorString; + itemStrokeColor = strokeColorString; } } } else { - itemStrokeColorString = null; + itemStrokeColor = null; } // check every style against the first of the items if (firstChild) { firstChild = false; - selectionFillColorString = itemFillColorString; - selectionFillColor2String = itemFillColor2String; - selectionStrokeColorString = itemStrokeColorString; - selectionStrokeColor2String = itemStrokeColor2String; + selectionFillColor = itemFillColor; + selectionFillColor2 = itemFillColor2; + selectionStrokeColor = itemStrokeColor; + selectionStrokeColor2 = itemStrokeColor2; selectionFillGradientType = itemFillGradientType; selectionStrokeGradientType = itemStrokeGradientType; - selectionStrokeWidth = itemStrokeColorString ? item.strokeWidth : 0; + selectionStrokeWidth = itemStrokeColor ? item.strokeWidth : 0; if (item.strokeWidth && item.data && item.data.zoomLevel) { selectionThickness = item.strokeWidth / item.data.zoomLevel; } } - if (itemFillColorString !== selectionFillColorString) { - selectionFillColorString = MIXED; + + if (!_colorsEqual(itemFillColor, selectionFillColor)) { + selectionFillColor = MIXED; } - if (itemFillColor2String !== selectionFillColor2String) { - selectionFillColor2String = MIXED; + if (!_colorsEqual(itemFillColor2, selectionFillColor2)) { + selectionFillColor2 = MIXED; } if (itemFillGradientType !== selectionFillGradientType) { selectionFillGradientType = GradientTypes.SOLID; - selectionFillColorString = MIXED; - selectionFillColor2String = MIXED; + selectionFillColor = MIXED; + selectionFillColor2 = MIXED; } if (itemStrokeGradientType !== selectionStrokeGradientType) { selectionStrokeGradientType = GradientTypes.SOLID; - selectionStrokeColorString = MIXED; - selectionStrokeColor2String = MIXED; + selectionStrokeColor = MIXED; + selectionStrokeColor2 = MIXED; } - if (itemStrokeColorString !== selectionStrokeColorString) { - selectionStrokeColorString = MIXED; + if (itemStrokeColor !== selectionStrokeColor) { + selectionStrokeColor = MIXED; } - if (itemStrokeColor2String !== selectionStrokeColor2String) { - selectionStrokeColor2String = MIXED; + if (itemStrokeColor2 !== selectionStrokeColor2) { + selectionStrokeColor2 = MIXED; } - const itemStrokeWidth = itemStrokeColorString ? item.strokeWidth : 0; + const itemStrokeWidth = itemStrokeColor ? item.strokeWidth : 0; if (selectionStrokeWidth !== itemStrokeWidth) { selectionStrokeWidth = null; } @@ -549,19 +569,19 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { } if (bitmapMode) { return { - fillColor: selectionFillColorString ? selectionFillColorString : null, - fillColor2: selectionFillColor2String ? selectionFillColor2String : null, + fillColor: selectionFillColor ? selectionFillColor : null, + fillColor2: selectionFillColor2 ? selectionFillColor2 : null, fillGradientType: selectionFillGradientType, thickness: selectionThickness }; } return { - fillColor: selectionFillColorString ? selectionFillColorString : null, - fillColor2: selectionFillColor2String ? selectionFillColor2String : null, + fillColor: selectionFillColor ? selectionFillColor : null, + fillColor2: selectionFillColor2 ? selectionFillColor2 : null, fillGradientType: selectionFillGradientType, - strokeColor: selectionStrokeColorString ? selectionStrokeColorString : null, - strokeColor2: selectionStrokeColor2String ? selectionStrokeColor2String : null, + strokeColor: selectionStrokeColor ? selectionStrokeColor : null, + strokeColor2: selectionStrokeColor2 ? selectionStrokeColor2 : null, strokeGradientType: selectionStrokeGradientType, strokeWidth: selectionStrokeWidth || (selectionStrokeWidth === null) ? selectionStrokeWidth : 0 }; diff --git a/src/helper/tools/eye-dropper.js b/src/helper/tools/eye-dropper.js index 447509154c..b15f023899 100644 --- a/src/helper/tools/eye-dropper.js +++ b/src/helper/tools/eye-dropper.js @@ -43,7 +43,7 @@ class EyeDropperTool extends paper.Tool { this.width = width * this.zoom * this.pixelRatio; this.height = height * this.zoom * this.pixelRatio; this.rect = canvas.getBoundingClientRect(); - this.colorString = ''; + this.color = null; this.pickX = -1; this.pickY = -1; this.hideLoupe = true; @@ -71,20 +71,20 @@ class EyeDropperTool extends paper.Tool { if (!colorInfo) return; if (colorInfo.color[3] === 0) { // Alpha 0 - this.colorString = null; + this.color = null; return; } - const r = colorInfo.color[0]; - const g = colorInfo.color[1]; - const b = colorInfo.color[2]; - // from https://github.com/LLK/scratch-gui/blob/77e54a80a31b6cd4684d4b2a70f1aeec671f229e/src/containers/stage.jsx#L218-L222 - // formats the color info from the canvas into hex for parsing by the color picker - const componentToString = c => { - const hex = c.toString(16); - return hex.length === 1 ? `0${hex}` : hex; - }; - this.colorString = `#${componentToString(r)}${componentToString(g)}${componentToString(b)}`; + const color = new paper.Color( + colorInfo.color[0] / 255, + colorInfo.color[1] / 255, + colorInfo.color[2] / 255 + ); + + // Convert color's backing components to HSV a.k.a. HSB + color.type = 'hsb'; + + this.color = color; } } getColorInfo (x, y, hideLoupe) { diff --git a/src/lib/color-proptype.js b/src/lib/color-proptype.js new file mode 100644 index 0000000000..8e70bef57b --- /dev/null +++ b/src/lib/color-proptype.js @@ -0,0 +1,10 @@ +import paper from '@scratch/paper'; +import PropTypes from 'prop-types'; +import {MIXED} from '../helper/style-path'; + +const colorProptype = PropTypes.oneOfType([ + PropTypes.instanceOf(paper.Color), + PropTypes.oneOf([MIXED]) +]); + +export default colorProptype; diff --git a/src/lib/color-style-proptype.js b/src/lib/color-style-proptype.js index 6e59603f63..67fe9aa4d1 100644 --- a/src/lib/color-style-proptype.js +++ b/src/lib/color-style-proptype.js @@ -1,9 +1,10 @@ import {PropTypes} from 'prop-types'; import GradientTypes from './gradient-types'; +import ColorProptype from './color-proptype'; export default PropTypes.shape({ - primary: PropTypes.string, - secondary: PropTypes.string, + primary: ColorProptype, + secondary: ColorProptype, gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired }); diff --git a/src/lib/make-color-style-reducer.js b/src/lib/make-color-style-reducer.js index db71697987..357c185a88 100644 --- a/src/lib/make-color-style-reducer.js +++ b/src/lib/make-color-style-reducer.js @@ -2,13 +2,11 @@ import log from '../log/log'; import {CHANGE_SELECTED_ITEMS} from '../reducers/selected-items'; import {getColorsFromSelection, MIXED} from '../helper/style-path'; import GradientTypes from './gradient-types'; +import paper from '@scratch/paper'; -// Matches hex colors -const hexRegex = /^#([0-9a-f]{3}){1,2}$/i; - -const isValidHexColor = color => { - if (!hexRegex.test(color) && color !== null && color !== MIXED) { - log.warn(`Invalid hex color code: ${color}`); +const isValidColor = color => { + if (!(color instanceof paper.Color) && color !== null && color !== MIXED) { + log.warn(`Invalid color: ${color}`); return false; } return true; @@ -44,10 +42,10 @@ const makeColorStyleReducer = ({ } switch (action.type) { case changePrimaryColorAction: - if (!isValidHexColor(action.color)) return state; + if (!isValidColor(action.color)) return state; return {...state, primary: action.color}; case changeSecondaryColorAction: - if (!isValidHexColor(action.color)) return state; + if (!isValidColor(action.color)) return state; return {...state, secondary: action.color}; case CHANGE_SELECTED_ITEMS: { // Don't change state if no selection diff --git a/src/reducers/fill-style.js b/src/reducers/fill-style.js index b97c4befdc..41eda29184 100644 --- a/src/reducers/fill-style.js +++ b/src/reducers/fill-style.js @@ -1,10 +1,11 @@ import makeColorStyleReducer from '../lib/make-color-style-reducer'; +import paper from '@scratch/paper'; const CHANGE_FILL_COLOR = 'scratch-paint/fill-style/CHANGE_FILL_COLOR'; const CHANGE_FILL_COLOR_2 = 'scratch-paint/fill-style/CHANGE_FILL_COLOR_2'; const CHANGE_FILL_GRADIENT_TYPE = 'scratch-paint/fill-style/CHANGE_FILL_GRADIENT_TYPE'; const CLEAR_FILL_GRADIENT = 'scratch-paint/fill-style/CLEAR_FILL_GRADIENT'; -const DEFAULT_COLOR = '#9966FF'; +const DEFAULT_COLOR = new paper.Color({hue: 259, saturation: 0.6, brightness: 1}); const reducer = makeColorStyleReducer({ changePrimaryColorAction: CHANGE_FILL_COLOR, diff --git a/src/reducers/stroke-style.js b/src/reducers/stroke-style.js index 1d61135609..1c5f72b8ed 100644 --- a/src/reducers/stroke-style.js +++ b/src/reducers/stroke-style.js @@ -1,10 +1,11 @@ import makeColorStyleReducer from '../lib/make-color-style-reducer'; +import paper from '@scratch/paper'; const CHANGE_STROKE_COLOR = 'scratch-paint/stroke-style/CHANGE_STROKE_COLOR'; const CHANGE_STROKE_COLOR_2 = 'scratch-paint/stroke-style/CHANGE_STROKE_COLOR_2'; const CHANGE_STROKE_GRADIENT_TYPE = 'scratch-paint/stroke-style/CHANGE_STROKE_GRADIENT_TYPE'; const CLEAR_STROKE_GRADIENT = 'scratch-paint/stroke-style/CLEAR_STROKE_GRADIENT'; -const DEFAULT_COLOR = '#000000'; +const DEFAULT_COLOR = new paper.Color({hue: 0, saturation: 0, brightness: 0}); import {CHANGE_STROKE_WIDTH} from './stroke-width';