diff --git a/engine/src/base/Path.js b/engine/src/base/Path.js index dec5f3300..5bcdde4d3 100644 --- a/engine/src/base/Path.js +++ b/engine/src/base/Path.js @@ -88,6 +88,10 @@ Wick.Path = class extends Wick.Base { data.json = this.json; delete data.json[1].data; + if (typeof data.json[0] !== "string") { + // This is a gradient-colored path + delete data.json[1][1].data; + } // optimization: replace dataurls with asset uuids if (data.json[0] === 'Raster' && data.json[1].source.startsWith('data:')) { diff --git a/engine/src/base/Selection.js b/engine/src/base/Selection.js index 1cd699c8e..d7e3370c8 100644 --- a/engine/src/base/Selection.js +++ b/engine/src/base/Selection.js @@ -1046,4 +1046,38 @@ Wick.Selection = class extends Wick.Base { this._selectedObjectsUUIDs.push(frame.uuid); }); } + + get useGradientGUI () { + return this._useGradientGUI || false; + } + set useGradientGUI (type) { + this._useGradientGUI = type; + } + get selectedStopIndex () { + return this._selectedStopIndex || 0; + } + set selectedStopIndex (index) { + this._selectedStopIndex = index; + } + deleteSelectedStop() { + if (this.useGradientGUI) { + let fillColor = this.fillColor; + if (fillColor) { + let stops = fillColor.gradient.stops; + let stopIndex = this.selectedStopIndex; + if (stops.length <= 2) { + stops[stopIndex].color = stops[1 - stopIndex].color; + stopIndex = 1 - stopIndex; + } + else { + stops.splice(stopIndex, 1); + if (stopIndex >= stops.length) { + stopIndex = stops.length - 1; + } + } + this.selectedStopIndex = stopIndex; + this.fillColor = fillColor; + } + } + } } \ No newline at end of file diff --git a/engine/src/tools/Cursor.js b/engine/src/tools/Cursor.js index a3a5515e8..5e418a4b1 100644 --- a/engine/src/tools/Cursor.js +++ b/engine/src/tools/Cursor.js @@ -74,6 +74,20 @@ Wick.Tools.Cursor = class extends Wick.Tool { // Update the image being used for the cursor this._setCursor(this._getCursor()); + + if(this._selection.useGradientGUI) { + // Update the gradient hover stop + const widget = this._widget; + if(widget._gradientGUI.container.visible) { + let item = this.hitResult.item; + if(item && item.data.parentItem) item = item.data.parentItem; + if(!item || !item.data.handleType) { + widget._buildHoverStop(e.point); + } else { + widget._gradientGUI.hoverStop.visible = false; + } + } + } } onMouseDown (e) { @@ -85,6 +99,35 @@ Wick.Tools.Cursor = class extends Wick.Tool { this._widget.transformMode = this.getSetting('cursorTransformMode'); + if(this._selection.useGradientGUI) { + // Clicked the gradient editor GUI, check for stop creation/selection + const widget = this._widget; + widget._gradientGUI.createdStopOnDown = false; + if(widget._gradientGUI.container.visible) { + let item = this.hitResult.item; + if(item && item.data.parentItem) item = item.data.parentItem; + let stopIndex = null; + + if(item && item.data.handleType === 'gradient-stop') { + widget._selectStop(item); + stopIndex = widget._gradientGUI.stops.indexOf(item); + } else if(!item || !item.data.handleType) { + stopIndex = widget._createStopFromPoint(e.point); + if(stopIndex !== null) { + widget._gradientGUI.createdStopOnDown = true; + } + } + + if(stopIndex !== null) { + this._selection.selectedStopIndex = stopIndex; + widget._updateItems(); + this.fireEvent({eventName: 'canvasModified', actionName: 'cursorSelectStop'}); + } + + if((item && item.data.handleType && item.data.handleType.startsWith('gradient-')) + || stopIndex !== null) return; + } + } if(this.hitResult.item && this.hitResult.item.data.isSelectionBoxGUI) { // Clicked the selection box GUI, do nothing } else if(this.hitResult.item && this._isItemSelected(this.hitResult.item)) { @@ -140,7 +183,7 @@ Wick.Tools.Cursor = class extends Wick.Tool { if(this.hitResult.item && this.hitResult.item.data.isSelectionBoxGUI) { // Update selection drag if(!this._widget.currentTransformation) { - this._widget.startTransformation(this.hitResult.item); + this._widget.startTransformation(this.hitResult.item, e); } this._widget.updateTransformation(this.hitResult.item, e); } else if (this.selectionBox.active) { @@ -149,7 +192,7 @@ Wick.Tools.Cursor = class extends Wick.Tool { } else if(this.hitResult.item && this.hitResult.type === 'fill') { // We're dragging the selection itself, so move the whole item. if(!this._widget.currentTransformation) { - this._widget.startTransformation(this.hitResult.item); + this._widget.startTransformation(this.hitResult.item, e); } this._widget.updateTransformation(this.hitResult.item, e); } else { @@ -241,6 +284,12 @@ Wick.Tools.Cursor = class extends Wick.Tool { _getCursor () { if(!this.hitResult.item) { return this.CURSOR_DEFAULT; + /*} else if ( + (this.hitResult.item.data.parentItem + && this.hitResult.item.data.parentItem.data.handleType === 'gradient-stop') + || this.hitResult.item.data.handleType === 'gradient-point' + ) { + return this.CURSOR_GRAD;*/ } else if (this.hitResult.item.data.isSelectionBoxGUI) { // Don't show any custom cursor if the mouse is over the border, the border does nothing if(this.hitResult.item.name === 'border') { diff --git a/engine/src/view/View.Selection.js b/engine/src/view/View.Selection.js index 322052fd7..9c9f71e3b 100644 --- a/engine/src/view/View.Selection.js +++ b/engine/src/view/View.Selection.js @@ -168,6 +168,8 @@ Wick.View.Selection = class extends Wick.View { boxRotation: this.model.widgetRotation, items: this._getSelectedObjectViews(), pivot: new paper.Point(this.model.pivotPoint.x, this.model.pivotPoint.y), + useGradientGUI: this.model.useGradientGUI, + selectedStopIndex: this.model.selectedStopIndex }); } diff --git a/engine/src/view/paper-ext/Paper.SelectionWidget.js b/engine/src/view/paper-ext/Paper.SelectionWidget.js index e02105f9e..c27779703 100644 --- a/engine/src/view/paper-ext/Paper.SelectionWidget.js +++ b/engine/src/view/paper-ext/Paper.SelectionWidget.js @@ -26,8 +26,55 @@ class SelectionWidget { if(!args.layer) args.layer = paper.project.activeLayer; this._layer = args.layer; - this._item = new paper.Group({ insert:false }); this.transformMode = 'freescale'; + + this._item = new paper.Group({ insert:false }); + let startPath = new paper.Path.Circle({ + radius: SelectionWidget.ENDPOINT_RADIUS, + fillColor: SelectionWidget.BOX_STROKE_COLOR, + insert: false, + applyMatrix: false, + data: { + handleType: 'gradient-point', + handleEdge: 'start' + } + }); + let endPath = new paper.Path.Circle({ + radius: SelectionWidget.ENDPOINT_RADIUS, + fillColor: SelectionWidget.BOX_STROKE_COLOR, + insert: false, + applyMatrix: false, + data: { + handleType: 'gradient-point', + handleEdge: 'end' + } + }); + let linePath = new paper.Path.Line({ + from: [0, 0], + to: [0, 0], + insert: false, + strokeColor: SelectionWidget.BOX_STROKE_COLOR, + strokeWidth: SelectionWidget.BOX_STROKE_WIDTH, + strokeScaling: false, + applyMatrix: false + }); + let hoverStop = this._buildGradientStop(true); + this._gradientGUI = { + container: new paper.Group({ + applyMatrix: false, + data: { isSelectionBoxGUI: true } + }), + startPath, endPath, linePath, + stops: [], + selectedStop: null, + hoverStop, + createdStopOnDown: false, + stroke: false, + radial: false, + startpoint: new paper.Point(0,0), + endpoint: new paper.Point(0,0), + lineVector: new paper.Point(0,0) + }; } /** @@ -169,7 +216,7 @@ class SelectionWidget { } set currentTransformation (currentTransformation) { - if(['translate', 'scale', 'rotate'].indexOf(currentTransformation) === -1) { + if(['translate', 'scale', 'rotate', 'gradient-stop', 'gradient-point', 'gradient-none'].indexOf(currentTransformation) === -1) { console.error('Paper.SelectionWidget: Invalid transformation type: ' + currentTransformation); currentTransformation = null; } else { @@ -182,6 +229,7 @@ class SelectionWidget { * @param {number} boxRotation - the rotation of the selection GUI. Optional, defaults to 0 * @param {paper.Item[]} items - the items to build the GUI around * @param {paper.Point} pivot - the pivot point that the selection rotates around. Defaults to (0,0) + * @param {string|boolean} useGradientGUI - whether to use a gradient editing GUI. Defaults to false */ build (args) { if(!args) args = {}; @@ -192,6 +240,7 @@ class SelectionWidget { this._itemsInSelection = args.items; this._boxRotation = args.boxRotation; this._pivot = args.pivot; + this._useGradientGUI = args.useGradientGUI; this._boundingBox = this._calculateBoundingBox(); @@ -207,7 +256,11 @@ class SelectionWidget { if(this._itemsInSelection.length > 0) { this._center = this._calculateBoundingBoxOfItems(this._itemsInSelection).center; - this._buildGUI(); + if(args.useGradientGUI) { + this._buildGradientGUI(args.selectedStopIndex); + } else { + this._buildGUI(); + } this.layer.addChild(this.item); } } @@ -215,7 +268,10 @@ class SelectionWidget { /** * */ - startTransformation (item) { + startTransformation (item, e) { + if(this._useGradientGUI) + return this.startGradientTransformation(item, e); + this._ghost = this._buildGhost(); this._layer.addChild(this._ghost); @@ -232,8 +288,9 @@ class SelectionWidget { * */ updateTransformation (item, e) { - // Wick. What is wrong with you. It would be nice if I could place the initiation in this function. But why in the world do I get a - // Uncaught TypeError: Cannot read properties of undefined (reading 'includes') + if(this.currentTransformation.substring(0,8) === 'gradient') + return this.updateGradientTransformation(item, e); + if (!this.mod?.initiated) { this.mod = { initiated: true @@ -394,7 +451,9 @@ class SelectionWidget { */ finishTransformation (item) { if (!this._currentTransformation) return; - + if(this.currentTransformation.substring(0,8) === 'gradient') + return this.finishGradientTransformation(); + this._ghost.remove(); if (this.mod.action === 'translate') { @@ -673,6 +732,411 @@ class SelectionWidget { }); return bounds || new paper.Rectangle(); } + + _buildGradientGUI (selectedStopIndex) { + // this better not be a group + let item = this._itemsInSelection[0]; + let color, stops, startpoint, endpoint; + if(this._useGradientGUI === 'stroke') { + color = item.strokeColor; + this._gradientGUI.stroke = true; + } else { + color = item.fillColor; + this._gradientGUI.stroke = false; + } + + if(!color) color = new paper.Color(0, 0, 0); + + if(color.gradient) { + this._gradientGUI.radial = color.gradient.radial; + stops = color.gradient.stops; + startpoint = color.origin; + endpoint = color.destination; + } else { + // This is a solid color. + this._gradientGUI.radial = false; + stops = [{ color: color.clone(), offset: 0 }, { color: color.clone(), offset: 1 }]; + let bounds = this._calculateBoundingBoxOfItems(this._itemsInSelection); + startpoint = bounds.topCenter; + endpoint = bounds.bottomCenter; + } + this._gradientGUI.startpoint = startpoint; + this._gradientGUI.endpoint = endpoint; + this._gradientGUI.lineVector = endpoint.subtract(startpoint); + + let container = this._gradientGUI.container; + container.removeChildren(); + this._transformContainer(); + + container.addChildren(this._buildGradientLine()); + container.addChildren(this._buildGradientStops(stops)); + container.addChild(this._buildHoverStop()); + this._selectStop(this._gradientGUI.stops[selectedStopIndex]); + + this.item.addChild(container); + container.children.forEach(child => { + child.data.isSelectionBoxGUI = true; + }); + } + + /** + * Update the gradient line GUI. + * @param {paper.Color} color The paper.js gradient color object. + * @returns {paper.Path[]} The start point, end point, and connecting line paths. + */ + _buildGradientLine () { + let length = this._gradientGUI.lineVector.length; + this._gradientGUI.endPath.position.x = length; + this._gradientGUI.linePath.segments[1].point.x = length; + + // Scale the GUI to appear the same size + const scaling = 1 / paper.view.zoom; + this._gradientGUI.startPath.scaling = scaling; + this._gradientGUI.endPath.scaling = scaling; + + return [this._gradientGUI.linePath, this._gradientGUI.startPath, this._gradientGUI.endPath]; + } + + /** + * Update the gradient stops GUI. + * @param {paper.Color} color The paper.js gradient color object. + * @returns {paper.Path[]} The list of color stop paths. + */ + _buildGradientStops (paperStops) { + let stopList = this._gradientGUI.stops; + + paperStops.forEach((paperStop, idx) => { + if(idx >= stopList.length) { + stopList.push(this._buildGradientStop()); + } + let stop = stopList[idx]; + stop.data.setColor(paperStop.color); + stop.data.setOffset(paperStop.offset); + stop.data.setScaling(); + }); + stopList.length = paperStops.length; + return stopList; + } + + _buildGradientStop (isHover) { + const ARROW_HEIGHT = SelectionWidget.COLOR_STOP_RECT_RADIUS / 5; + const COLOR_BOX_CENTER = [0, -(SelectionWidget.COLOR_STOP_RECT_RADIUS + ARROW_HEIGHT)] + const COLOR_BOX_INNER_SIZE = 2 * (SelectionWidget.COLOR_STOP_RECT_RADIUS - SelectionWidget.COLOR_STOP_RECT_PADDING); + const COLOR_BOX_OUTER_SIZE = 2 * SelectionWidget.COLOR_STOP_RECT_RADIUS; + const CHECKER_SIZE = 8; + + let stopObj = new paper.Group({ + pivot: [0,0], + position: [0, -SelectionWidget.ENDPOINT_RADIUS], + applyMatrix: false, + insert: false, + data: { + handleType: 'gradient-stop', + color: 'black', + offset: 0, + selected: false + } + }); + let colorBox = new paper.Path.Rectangle({ + center: COLOR_BOX_CENTER, + size: [COLOR_BOX_INNER_SIZE, COLOR_BOX_INNER_SIZE], + fillColor: 'red', + strokeWidth: 0, + data: { + isSelectionBoxGUI: true, + parentItem: stopObj + } + }); + let opaqueColorBox = new paper.Path.Rectangle({ + center: [-COLOR_BOX_INNER_SIZE/4, COLOR_BOX_CENTER[1]], + size: [COLOR_BOX_INNER_SIZE/2, COLOR_BOX_INNER_SIZE], + fillColor: 'red', + strokeWidth: 0, + data: { + isSelectionBoxGUI: true, + parentItem: stopObj, + isBorder: true + } + }); + let outerBox = new paper.Path.Rectangle({ + center: COLOR_BOX_CENTER, + size: [COLOR_BOX_OUTER_SIZE, COLOR_BOX_OUTER_SIZE], + fillColor: '#ffffff', + strokeWidth: SelectionWidget.COLOR_STOP_OUTLINE_WIDTH, + data: { + isSelectionBoxGUI: true, + parentItem: stopObj + } + }); + let checker = new paper.Group({ + children: [ + new paper.Path.Rectangle({ position: [0,0], size: CHECKER_SIZE*3, fillColor: '#e6e6e6', + data: { isSelectionBoxGUI: true, parentItem: stopObj, isBorder: true } + }), + new paper.Path.Rectangle({ position: [0,-CHECKER_SIZE], size: CHECKER_SIZE, fillColor: '#d4d4d4', + data: { isSelectionBoxGUI: true, parentItem: stopObj, isBorder: true } + }), + new paper.Path.Rectangle({ position: [-CHECKER_SIZE,0], size: CHECKER_SIZE, fillColor: '#d4d4d4', + data: { isSelectionBoxGUI: true, parentItem: stopObj, isBorder: true } + }), + new paper.Path.Rectangle({ position: [0,CHECKER_SIZE], size: CHECKER_SIZE, fillColor: '#d4d4d4', + data: { isSelectionBoxGUI: true, parentItem: stopObj, isBorder: true } + }), + new paper.Path.Rectangle({ position: [CHECKER_SIZE,0], size: CHECKER_SIZE, fillColor: '#d4d4d4', + data: { isSelectionBoxGUI: true, parentItem: stopObj, isBorder: true } + }) + ], + strokeWidth: 0 + }); + outerBox.addTo(stopObj); + checker.position = COLOR_BOX_CENTER; + checker.scaling = COLOR_BOX_INNER_SIZE / (CHECKER_SIZE*3); + checker.addTo(stopObj); + colorBox.addTo(stopObj); + opaqueColorBox.addTo(stopObj); + let arrow; + if(!isHover) { + arrow = new paper.Path({ + segments: [ + [-ARROW_HEIGHT, -ARROW_HEIGHT], [0,0], [ARROW_HEIGHT, -ARROW_HEIGHT] + ], + closed: true, + fillColor: SelectionWidget.DESELECTED_COLOR, + strokeWidth: SelectionWidget.COLOR_STOP_OUTLINE_WIDTH, + data: { + isSelectionBoxGUI: true, + parentItem: stopObj + } + }); + arrow.addTo(stopObj); + } + stopObj.strokeColor = SelectionWidget.DESELECTED_COLOR; + + if(isHover) { + // Don't include the hover stop in cursor hit tests + stopObj.data.isBorder = true; + outerBox.data.isBorder = true; + colorBox.data.isBorder = true; + } + + stopObj.data.setColor = (color) => { + // if color is null, display black as placeholder + colorBox.fillColor = color || 'black'; + opaqueColorBox.fillColor = color || 'black'; + opaqueColorBox.fillColor.alpha = 1; + + stopObj.data.color = color; + } + stopObj.data.setOffset = (offset) => { + stopObj.position.x = this._gradientGUI.lineVector.length * offset; + stopObj.data.offset = offset; + } + stopObj.data.setSelected = (selected) => { + stopObj.strokeColor = selected ? SelectionWidget.SELECTED_COLOR : SelectionWidget.DESELECTED_COLOR; + if(arrow) arrow.fillColor = selected ? SelectionWidget.SELECTED_COLOR : SelectionWidget.DESELECTED_COLOR; + stopObj.data.selected = selected; + } + stopObj.data.setScaling = () => { + const scaling = 1 / paper.view.zoom; + stopObj.scaling = scaling; + stopObj.position.y = -SelectionWidget.ENDPOINT_RADIUS * scaling; + } + stopObj.data.setScaling(); + return stopObj; + } + + _buildHoverStop (point) { + this._gradientGUI.hoverStop.visible = false; + if(point) { + let offset = this._calculateValidOffset(point); + if(offset !== null) { + this._interpolateStop(this._gradientGUI.hoverStop, offset); + this._gradientGUI.hoverStop.visible = true; + this._gradientGUI.hoverStop.data.setScaling(); + } + } + return this._gradientGUI.hoverStop; + } + + startGradientTransformation (item, e) { + if(item && item.data.parentItem) item = item.data.parentItem; + + this._gradientGUI.hoverStop.remove(); + if(item && item.data.handleType === 'gradient-stop') { + this.currentTransformation = 'gradient-stop'; + } else if(item && item.data.handleType === 'gradient-point') { + this.currentTransformation = 'gradient-point'; + this._gradientGUI.initialStartpoint = this._gradientGUI.startpoint; + this._gradientGUI.initialEndpoint = this._gradientGUI.endpoint; + this._gradientGUI.initialLineVector = this._gradientGUI.lineVector; + } else if(this._gradientGUI.createdStopOnDown) { + // Move the new color stop + this.currentTransformation = 'gradient-stop'; + this._gradientGUI.createdStopOnDown = false; + } else { + // We have to set a currentTransformation for Cursor.js + this.currentTransformation = 'gradient-none'; + } + } + + updateGradientTransformation (item, e) { + if(item && item.data.parentItem) item = item.data.parentItem; + + if(this.currentTransformation === 'gradient-stop') { + let offset = this._calculateOffset(e.point); + if(offset < 0) offset = 0; + if(offset > 1) offset = 1; + this._gradientGUI.selectedStop.data.setOffset(offset); + } else if(this.currentTransformation === 'gradient-point') { + if(item.data.handleEdge === 'start') { + this._gradientGUI.startpoint = e.point; + } else { + this._gradientGUI.endpoint = e.point; + } + if(e.modifiers.shift) { + this._gradientGUI.lineVector = this._gradientGUI.initialLineVector; + if(item.data.handleEdge === 'start') { + this._gradientGUI.endpoint = e.point.add(this._gradientGUI.lineVector); + } else { + this._gradientGUI.startpoint = e.point.subtract(this._gradientGUI.lineVector); + } + } else { + if(item.data.handleEdge === 'start') { + this._gradientGUI.endpoint = this._gradientGUI.initialEndpoint; + } else { + this._gradientGUI.startpoint = this._gradientGUI.initialStartpoint; + } + this._gradientGUI.lineVector = this._gradientGUI.endpoint.subtract(this._gradientGUI.startpoint); + } + this._transformContainer(); + this._buildGradientLine(); + this._gradientGUI.stops.forEach((stopObj) => { + stopObj.data.setOffset(stopObj.data.offset); + stopObj.data.setScaling(); + }); + } + + if(this.currentTransformation !== 'gradient-none') this._updateItems(); + } + + finishGradientTransformation (item, e) { + if(!this._currentTransformation) return; + + if(this.currentTransformation !== 'gradient-none') this._updateItems(); + + this._currentTransformation = null; + } + + _updateItems () { + let colorObj = { + origin: this._gradientGUI.startpoint, + destination: this._gradientGUI.endpoint, + stops: this._gradientGUI.stops.map((stopPath) => { + return { color: stopPath.data.color, offset: stopPath.data.offset } + }), + radial: this._gradientGUI.radial + }; + + // there better not be any groups + if(this._gradientGUI.stroke) { + this._itemsInSelection.forEach((item) => { + item.strokeColor = colorObj; + }); + } else { + this._itemsInSelection.forEach((item) => { + item.fillColor = colorObj; + }); + } + } + + _selectStop (stopObj) { + if(this._gradientGUI.selectedStop) { + this._gradientGUI.selectedStop.data.setSelected(false); + } + this._gradientGUI.selectedStop = stopObj; + stopObj.data.setSelected(true); + } + + _createStopFromPoint (point) { + let offset = this._calculateValidOffset(point); + if(offset !== null) { + // Create and select a new color stop + let newStop = this._buildGradientStop(); + this._interpolateStop(newStop, offset); + + this._gradientGUI.stops.push(newStop); + this._gradientGUI.container.addChild(newStop); + this._selectStop(newStop); + this._updateItems(); + + return this._gradientGUI.stops.length - 1; + } + return null; + } + + _interpolateStop (stop, offset) { + // Assuming unsorted stops list, find the stops right before and after given offset + let stops = this._gradientGUI.stops; + let stop1, stop2; + let index1 = 0; let index2 = 1; + stops.forEach(stop => { + let stopOffset = stop.data.offset; + if (index1 <= stopOffset && stopOffset <= offset) { + stop1 = stop; + index1 = stopOffset; + } + else if (offset <= stopOffset && stopOffset <= index2) { + stop2 = stop; + index2 = stopOffset; + } + }); + + let color; + if(!stop1) { + // Offset is the leftmost stop, use the color of nextStop + color = stop2.data.color ? stop2.data.color.clone() : new paper.Color('black'); + } else if(!stop2) { + // Offset is the rightmost stop, use the color of prevStop + color = stop1.data.color ? stop1.data.color.clone() : new paper.Color('black'); + } else { + // Both stops exist, interpolate the color + let offsetRelative = (offset - index1) / (index2 - index1); + let color1 = stop1.data.color || new paper.Color('black'); + let color2 = stop2.data.color || new paper.Color('black'); + color = color1.add(color2.subtract(color1).multiply(offsetRelative)); + color.alpha = color1.alpha + (color2.alpha - color1.alpha) * offsetRelative; + } + + stop.data.setColor(color); + stop.data.setOffset(offset); + } + + _transformContainer () { + let container = this._gradientGUI.container; + container.matrix.reset(); + container.translate(this._gradientGUI.startpoint); + container.rotate(this._gradientGUI.lineVector.angle, this._gradientGUI.startpoint); + } + + _calculateDistanceFromLine (point) { + let pointVector = point.subtract(this._gradientGUI.startpoint); + let lineVector = this._gradientGUI.lineVector.normalize(); + return lineVector.cross(pointVector); + } + + _calculateOffset (point) { + let pointVector = point.subtract(this._gradientGUI.startpoint); + let lineVector = this._gradientGUI.lineVector; + return lineVector.dot(pointVector) / (lineVector.length * lineVector.length); + } + _calculateValidOffset (point) { + let distance = -this._calculateDistanceFromLine(point); + if (distance < 0 || distance > (SelectionWidget.COLOR_STOP_CREATION_DISTANCE / paper.view.zoom)) { + return null; + } + let offset = this._calculateOffset(point); + return (0 <= offset && offset <= 1) ? offset : null; + } }; SelectionWidget.BOX_STROKE_WIDTH = 1; @@ -689,6 +1153,13 @@ SelectionWidget.ROTATION_HOTSPOT_RADIUS = 20; SelectionWidget.ROTATION_HOTSPOT_FILLCOLOR = 'rgba(100,150,255,0.5)'; SelectionWidget.GHOST_STROKE_COLOR = 'rgba(0, 0, 0, 1.0)'; SelectionWidget.GHOST_STROKE_WIDTH = 1; +SelectionWidget.ENDPOINT_RADIUS = 8; +SelectionWidget.COLOR_STOP_RECT_RADIUS = 12; +SelectionWidget.COLOR_STOP_RECT_PADDING = 2; +SelectionWidget.COLOR_STOP_OUTLINE_WIDTH = 2; +SelectionWidget.COLOR_STOP_CREATION_DISTANCE = SelectionWidget.ENDPOINT_RADIUS + 2.2 * SelectionWidget.COLOR_STOP_RECT_RADIUS; +SelectionWidget.SELECTED_COLOR = '#0c8ce9'; +SelectionWidget.DESELECTED_COLOR = '#cccccc'; paper.PaperScope.inject({ SelectionWidget: SelectionWidget, diff --git a/src/Editor/Editor.jsx b/src/Editor/Editor.jsx index a1b9594a7..8e50512bb 100644 --- a/src/Editor/Editor.jsx +++ b/src/Editor/Editor.jsx @@ -414,25 +414,35 @@ class Editor extends EditorCore { ); } - updateLastColors = (color) => { - let newArray = this.state.lastColorsUsed.concat([]); // make a deep copy. + updateLastColors = (color, edit) => { + let newArray = this.state.lastColorsUsed.concat([]); // make a deep copy. + + let index = newArray.indexOf(color); + if (edit) { + // Replace the last color with the new color. + if (index > -1) { + newArray.splice(index, 1); + newArray.unshift(color); + } + else newArray[0] = color; + } + else { + // Remove a color from the array. If the new color is in the array, remove it. + if (index > -1) { + newArray.splice(index, 1); + } else { + newArray.pop(); + } + + // Add the new color to the front of the array. + newArray.unshift(color); + } - // Remove a color from the array. If the new color is in the array, remove it. - let index = newArray.indexOf(color); - if (index > -1) { - newArray.splice(index, 1); - } else { - newArray.pop(); + this.setState({ + lastColorsUsed: newArray, + }); } - // Add the new color to the front of the array. - newArray.unshift(color); - - this.setState({ - lastColorsUsed: newArray, - }); - } - toggleOutliner = () => { this.setState({outlinerPoppedOut: !this.state.outlinerPoppedOut}); } diff --git a/src/Editor/EditorCore.jsx b/src/Editor/EditorCore.jsx index 5f58124b7..d6e75bf36 100644 --- a/src/Editor/EditorCore.jsx +++ b/src/Editor/EditorCore.jsx @@ -581,8 +581,13 @@ class EditorCore extends Component { cancelText: "Cancel", }); } else { - this.project.deleteSelectedObjects(); - this.projectDidChange({actionName: "Delete Selected Objects"}); + if(this.project.selection.useGradientGUI) { + this.project.selection.deleteSelectedStop(); + this.projectDidChange({actionName: "Delete Selected Stop"}); + } else { + this.project.deleteSelectedObjects(); + this.projectDidChange({actionName: "Delete Selected Objects"}); + } } } diff --git a/src/Editor/Panels/Inspector/Inspector.jsx b/src/Editor/Panels/Inspector/Inspector.jsx index 08f6745dd..121b86649 100644 --- a/src/Editor/Panels/Inspector/Inspector.jsx +++ b/src/Editor/Panels/Inspector/Inspector.jsx @@ -126,7 +126,12 @@ class Inspector extends Component { * @return {string} fill color opacity from 0 to 1. */ getSelectionFillColorOpacity = () => { - return this.getSelectionAttribute('fillColor').alpha; + let color = this.getSelectionAttribute('fillColor'); + if (color instanceof window.paper.Color && color.gradient) { + let maxOpacity = color.gradient.stops.reduce((total, stop) => stop.color.alpha > total ? stop.color.alpha : total, 0); + return maxOpacity; + } + return color.alpha; } /** @@ -135,8 +140,25 @@ class Inspector extends Component { */ setSelectionFillColorOpacity = (value) => { var color = this.getSelectionAttribute('fillColor'); - color.alpha = value; - this.setSelectionAttribute('fillColor', color); + if (color instanceof window.paper.Color && color.gradient) { + let maxOpacity = color.gradient.stops.reduce((total, stop) => stop.color.alpha > total ? stop.color.alpha : total, 0); + if (maxOpacity === 0) { + color.gradient.stops.forEach(stop => { + stop.color.alpha = value; + }); + } + else { + let changeFactor = value / maxOpacity; + color.gradient.stops.forEach(stop => { + stop.color.alpha *= changeFactor; + }); + } + + } + else { + color.alpha = value; + this.setSelectionAttribute('fillColor', color); + } } /** @@ -151,6 +173,20 @@ class Inspector extends Component { this.props.setSelectionAttribute(attribute, newValue); } + /** + * Updates the value of a selection attribute without adding to the undo stack. + * @param {string} attribute Name of the attribute to update. + * @param {string|number} newValue New value of the attribute to update. + */ + setSelectionAttributeIntermediate = (attribute, newValue) => { + if (attribute === 'fillColorOpacity') { + return this.setSelectionFillColorOpacity(newValue); + } + this.props.project.selection[attribute] = newValue; + this.props.project.view.render(); + this.props.project.guiElement.draw(); + } + // Inspector Row Types /** @@ -177,8 +213,15 @@ class Inspector extends Component { this.setSelectionAttribute('fillColor', col)} + onChangeIntermediate1={(col) => this.setSelectionAttributeIntermediate('fillColor', col)} + enableGradient={true} + selectionProps={{ + getSelection: () => this.props.project.selection, + renderSelection: () => this.props.project.view.render(), + targetCanvas: this.props.project.view._svgCanvas + }} id={"inspector-selection-fill-color"} val2={this.getSelectionAttribute('fillColorOpacity')} onChange2={(val) => this.setSelectionAttribute('fillColorOpacity', val)} @@ -192,8 +235,15 @@ class Inspector extends Component { tooltip1="Stroke" tooltip2="Weight" - val1={this.getSelectionAttribute('strokeColor').toCSS()} + val1={this.getSelectionAttribute('strokeColor')} onChange1={(col) => this.setSelectionAttribute('strokeColor', col)} + onChangeIntermediate1={(col) => this.setSelectionAttributeIntermediate('strokeColor', col)} + enableGradient={true} + selectionProps={{ + getSelection: () => this.props.project.selection, + renderSelection: () => this.props.project.view.render(), + targetCanvas: this.props.project.view._svgCanvas + }} id={"inspector-selection-stroke-color"} stroke={true} diff --git a/src/Editor/Panels/Inspector/InspectorRow/InspectorRowTypes/InspectorColorNumericInput.jsx b/src/Editor/Panels/Inspector/InspectorRow/InspectorRowTypes/InspectorColorNumericInput.jsx index 4a1b7d779..db8981d36 100644 --- a/src/Editor/Panels/Inspector/InspectorRow/InspectorRowTypes/InspectorColorNumericInput.jsx +++ b/src/Editor/Panels/Inspector/InspectorRow/InspectorRowTypes/InspectorColorNumericInput.jsx @@ -43,6 +43,9 @@ class InspectorColorNumericInput extends Component { type: "color", color: this.props.val1, onChange: this.props.onChange1, + onChangeIntermediate: this.props.onChangeIntermediate1, + enableGradient: this.props.enableGradient, + ...this.props.selectionProps, id: this.props.id, stroke: !this.props.stroke ? false : this.props.stroke, placement: "left", diff --git a/src/Editor/Panels/MobileContainer/MobileInspector/MobileInspector.jsx b/src/Editor/Panels/MobileContainer/MobileInspector/MobileInspector.jsx index f4acc8b79..971da3b0b 100644 --- a/src/Editor/Panels/MobileContainer/MobileInspector/MobileInspector.jsx +++ b/src/Editor/Panels/MobileContainer/MobileInspector/MobileInspector.jsx @@ -153,7 +153,12 @@ class MobileInspector extends Component { * @return {string} fill color opacity from 0 to 1. */ getSelectionFillColorOpacity = () => { - return this.getSelectionAttribute('fillColor').alpha; + let color = this.getSelectionAttribute('fillColor'); + if (color instanceof window.paper.Color && color.gradient) { + let maxOpacity = color.gradient.stops.reduce((total, stop) => stop.color.alpha > total ? stop.color.alpha : total, 0); + return maxOpacity; + } + return color.alpha; } /** @@ -162,8 +167,25 @@ class MobileInspector extends Component { */ setSelectionFillColorOpacity = (value) => { var color = this.getSelectionAttribute('fillColor'); - color.alpha = value; - this.setSelectionAttribute('fillColor', color); + if (color instanceof window.paper.Color && color.gradient) { + let maxOpacity = color.gradient.stops.reduce((total, stop) => stop.color.alpha > total ? stop.color.alpha : total, 0); + if (maxOpacity === 0) { + color.gradient.stops.forEach(stop => { + stop.color.alpha = value; + }); + } + else { + let changeFactor = value / maxOpacity; + color.gradient.stops.forEach(stop => { + stop.color.alpha *= changeFactor; + }); + } + + } + else { + color.alpha = value; + this.setSelectionAttribute('fillColor', color); + } } /** @@ -178,6 +200,20 @@ class MobileInspector extends Component { this.props.setSelectionAttribute(attribute, newValue); } + /** + * Updates the value of a selection attribute without adding to the undo stack. + * @param {string} attribute Name of the attribute to update. + * @param {string|number} newValue New value of the attribute to update. + */ + setSelectionAttributeIntermediate = (attribute, newValue) => { + if (attribute === 'fillColorOpacity') { + return this.setSelectionFillColorOpacity(newValue); + } + this.props.project.selection[attribute] = newValue; + this.props.project.view.render(); + this.props.project.guiElement.draw(); + } + // Inspector Row Types /** @@ -189,8 +225,15 @@ class MobileInspector extends Component {
this.setSelectionAttribute('strokeColor', col)} + onChangeIntermediate={(col) => this.setSelectionAttributeIntermediate('strokeColor', col)} + enableGradient={true} + selectionProps={{ + getSelection: () => this.props.project.selection, + renderSelection: () => this.props.project.view.render(), + targetCanvas: this.props.project.view._svgCanvas + }} id={"mobile-inspector-selection-stroke-color"} stroke={true} divider={false} @@ -202,8 +245,15 @@ class MobileInspector extends Component { this.setSelectionAttribute('fillColor', col)} + onChangeIntermediate={(col) => this.setSelectionAttributeIntermediate('fillColor', col)} + enableGradient={true} + selectionProps={{ + getSelection: () => this.props.project.selection, + renderSelection: () => this.props.project.view.render(), + targetCanvas: this.props.project.view._svgCanvas + }} id={"mobile-inspector-selection-fill-color"} divider={false} colorPickerType={this.props.colorPickerType} diff --git a/src/Editor/Panels/MobileContainer/MobileInspector/MobileInspectorRow/MobileInspectorRowTypes/MobileInspectorColor.jsx b/src/Editor/Panels/MobileContainer/MobileInspector/MobileInspectorRow/MobileInspectorRowTypes/MobileInspectorColor.jsx index ce3548f49..458146385 100644 --- a/src/Editor/Panels/MobileContainer/MobileInspector/MobileInspectorRow/MobileInspectorRowTypes/MobileInspectorColor.jsx +++ b/src/Editor/Panels/MobileContainer/MobileInspector/MobileInspectorRow/MobileInspectorRowTypes/MobileInspectorColor.jsx @@ -42,6 +42,9 @@ class MobileInspectorColor extends Component { type: "color", color: this.props.val, onChange: this.props.onChange, + onChangeIntermediate: this.props.onChangeIntermediate, + enableGradient: this.props.enableGradient, + ...this.props.selectionProps, id: this.props.id, stroke: !this.props.stroke ? false : this.props.stroke, placement: "left", diff --git a/src/Editor/Util/ColorPicker/ColorPicker.jsx b/src/Editor/Util/ColorPicker/ColorPicker.jsx index 970d50ef8..39ab7ddaa 100644 --- a/src/Editor/Util/ColorPicker/ColorPicker.jsx +++ b/src/Editor/Util/ColorPicker/ColorPicker.jsx @@ -19,40 +19,179 @@ import React, { useState } from 'react'; import { Popover } from 'reactstrap'; -import WickColorPicker from 'Editor/Util/ColorPicker/WickColorPicker'; +import WickColorPicker from 'Editor/Util/ColorPicker/WickColorPicker'; +import { CHECKERBOARD_URL } from 'Editor/Util/ColorPicker/ColorPickerComponents/ColorPickerComponents'; import './_colorpicker.scss'; +// Check if mouseclick started on popover +const oldComponentDidMount = Popover.prototype.componentDidMount; +Popover.prototype.componentDidMount = function () { + this.downPopover = false; + this.handleDocumentMouseDown = (e) => { + this.downPopover = this._popover && this._popover.contains(e.target); + this.downTarget = e.target; + } + + oldComponentDidMount.call(this); +} +Popover.prototype.addTargetEvents = function () { + ['click', 'touchstart'].forEach(event => + document.addEventListener(event, this.handleDocumentClick, true) + ); + document.addEventListener('mousedown', this.handleDocumentMouseDown) +} + +Popover.prototype.removeTargetEvents = function () { + ['click', 'touchstart'].forEach(event => + document.removeEventListener(event, this.handleDocumentClick, true) + ); + document.removeEventListener('mousedown', this.handleDocumentMouseDown) +} +Popover.prototype.handleDocumentClick = function (e) { + if (this._target) { + if (e.target !== this._target && !this._target.contains(e.target) && e.target !== this._popover && !(this._popover && this._popover.contains(e.target))) { + if (this._hideTimeout) { + this.clearHideTimeout(); + } + + if (this.props.isOpen) { + this.toggle(e, { clickedPopover: this.downPopover, downTarget: this.downTarget }); + } + } + } + this.downPopover = false; +} +Popover.prototype.toggle = function (e, data) { + if (this.props.disabled) { + return e && e.preventDefault(); + } + + return this.props.toggle(e, data); +} + +function arraysEqual(arr1, arr2) { + if (arr1 === arr2) return true; + if (!arr1 || !arr2) return false; + if (arr1.length !== arr2.length) return false; + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) return false; + } + return true; +} + export default function ColorPicker (props) { + let selectedObjects = + (typeof props.getSelection === 'function') ? + [...props.getSelection()._selectedObjectsUUIDs] : null; + if (selectedObjects) selectedObjects.sort(); + const [open, setOpen] = useState(false); + const [lastObjects, setLastObjects] = useState(selectedObjects); + if (!arraysEqual(selectedObjects, lastObjects)) { + setLastObjects(selectedObjects); - let color = props.color ? props.color : new window.Wick.Color("#FFFFFF") + // Close pop-up if selection changed + if (open) + toggle(); + } let itemID = props.id; let popoverID = itemID+'-popover'; - function toggle () { + function toggle (e, data) { if (!open) { setTimeout(selectPopover, 200); } + if (!e || !data || !open) { + // Either `toggle()` was used, or no mouse-down data was needed + setOpen(!open); + return; + } - setOpen(!open) + // Don't close if click started on popover + // Don't close if clicked on selected objects + let clickedCanvas = (e.touches ? e.target : data.downTarget) === props.targetCanvas; + let selectionUnchanged = arraysEqual(selectedObjects, lastObjects); + if (!data.clickedPopover && !(clickedCanvas && selectionUnchanged)) + setOpen(false); } function selectPopover () { let ele = document.getElementById(popoverID); - if (ele) { + if (ele) ele.focus(); + } + + let color = props.color ? props.color : new window.Wick.Color("#FFFFFF") + let colorCSS = color; + let colorCSSOpaque = color; + if (color instanceof window.paper.Color) { + if (color.gradient) { + colorCSS = colorCSSOpaque = 'linear-gradient(to right'; + + const sortedControlStops = [...color.gradient.stops]; + sortedControlStops.sort((objectA, objectB) => objectA.offset - objectB.offset); + sortedControlStops.forEach(paperControlStop => { + colorCSS += `, ${paperControlStop.color.toCSS()} ${paperControlStop.offset * 100}%`; + let { red, green, blue } = paperControlStop.color; + colorCSSOpaque += `, rgb(${red*255},${green*255},${blue*255}) ${paperControlStop.offset * 100}%`; + }); + colorCSS += ')'; + colorCSSOpaque += ')'; } + else { + colorCSS = color.toCSS(); colorCSSOpaque = `rgb(${color.red*255},${color.green*255},${color.blue*255})`; + colorCSS = `linear-gradient(${colorCSS}, ${colorCSS})`; + colorCSSOpaque = `linear-gradient(${colorCSSOpaque}, ${colorCSSOpaque})`; + } + } + else if (typeof color === 'string') { + // Used as a background-image + colorCSS = colorCSSOpaque = `linear-gradient(${color}, ${color})`; + // regex to remove alpha from color string + colorCSSOpaque = colorCSSOpaque.replaceAll(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/g, 'rgb($1, $2, $3)'); } + // Bring desynced color state up, so if the solid-gradient state updates, the pop-up position updates + const [desyncedColor, setDesyncedColor] = useState(color); + + // Bring functions involving selection down to avoid repeating code + let selectionGiven = (typeof props.getSelection === 'function'); + let setGradientActive = (stopIndex) => { + if (!selectionGiven) return; + let selection = props.getSelection(); + + if (stopIndex !== undefined && selection.useGradientGUI) return; + selection.useGradientGUI = props.stroke ? 'stroke' : 'fill'; + selection.selectedStopIndex = stopIndex || 0; + props.renderSelection(); + }; + let setGradientInactive = () => { + if (!selectionGiven) return; + let selection = props.getSelection(); + + selection.useGradientGUI = false; + selection.selectedStopIndex = 0; + props.renderSelection(); + }; + let getSelectedStopIndex = () => selectionGiven && props.getSelection().selectedStopIndex; + let setSelectedStopIndex = (index) => { + if (!selectionGiven) return; + props.getSelection().selectedStopIndex = index; + }; + let selectedObjectsBounds = selectionGiven && props.getSelection().view._getSelectedObjectsBounds(); return ( diff --git a/src/Editor/Util/ColorPicker/ColorPickerComponents/ColorPickerComponents.jsx b/src/Editor/Util/ColorPicker/ColorPickerComponents/ColorPickerComponents.jsx new file mode 100644 index 000000000..6b86b8ecb --- /dev/null +++ b/src/Editor/Util/ColorPicker/ColorPickerComponents/ColorPickerComponents.jsx @@ -0,0 +1,242 @@ +import React, { Component } from "react"; + +import './_colorpickercomponents.scss'; +import WickCustomSlider from './WickCustomSlider'; +import WickInput from "../../WickInput/WickInput"; + +export const CHECKERBOARD_URL = `url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAPUlEQVR4AeySywkAMAhDH52h+0/YIRoH8IMnD0JyCgSe5gA3sWJfVuCnhWQLYMYNnr4VOdzJDAQR9LUI8AEAAP//ViLpiAAAAAZJREFUAwBk7gjBheCOvgAAAABJRU5ErkJggg==")`; + +function GradientControlStop (props) { + let active = (props.selectedStop === props.stopIndex) ? ' wick-color-picker-gradient-active' : ''; + let colorOpaque = props.color; + if (props.color.includes('rgba')) { + // rgba(R, G, B, A) -> rgb(R, G, B) + colorOpaque.replace('rgba', 'rgb'); + colorOpaque = colorOpaque.substring(0, colorOpaque.lastIndexOf(',')) + ')'; + } + return ( +
+
+
+ +
+ +
+
+ ); +} +export function GradientSlider (props) { + return ( + + ); +} + +function WickControlPointer (props) { + return ( +
+ ); +} +function ColorSlider (props) { + let onMouseMove = offset => props.onChangeIntermediate(props.calculateColor(offset)); + let onMouseUp = offset => props.onChangeComplete(props.calculateColor(offset)); + return ( + + ); +} +export function ColorPickerInput (props) { + let { label, labelBefore, ...otherProps } = props; + return ( + + ); +} + +export class Saturation extends Component { + calculateColor = offset => { + let saturation = offset.x; + let lightness = 1 - offset.y; + return { h: this.props.h, s: saturation, v: lightness, a: this.props.a } + } + renderStyle = () => { + return { + container: { + backgroundColor: `hsl(${this.props.h}, 100%, 50%)`, + backgroundImage: 'linear-gradient(to top, #000, rgba(0, 0, 0, 0)), linear-gradient(to right, #fff, rgba(255, 255, 255, 0))' + } + } + } + render () { + let color = this.props.colorObject.toRgb(); + color = `rgb(${color.r}, ${color.g}, ${color.b})`; + return ( + + ); + } +} +export class Hue extends Component { + calculateColor = offset => { + let hue = offset.x * 360; + return { h: hue, s: this.props.s, v: this.props.v, a: this.props.a } + } + render () { + let color = `hsl(${this.props.h}, 100%, 50%)`; + return ( + + ); + } +} +export class Alpha extends Component { + calculateColor = offset => { + let alpha = offset.x; + return { h: this.props.h, s: this.props.s, v: this.props.v, a: alpha } + } + + renderStyle = () => { + // 8px is pointer radius + let { r, g, b } = this.props.colorObject.toRgb(); + return { + container: { + backgroundColor: '#fff', + backgroundImage: `linear-gradient(to right, rgba(${r}, ${g}, ${b}, 0) 8px, rgb(${r}, ${g}, ${b}) calc(100% - 8px)), + ${CHECKERBOARD_URL}` + } + } + } + render () { + // Mimic transparent against a white background + let color = this.props.colorObject.toHsl(); + let newLight = 1 + color.a * (color.l - 1); + color = `hsl(${color.h}, ${color.s * 100}%, ${newLight * 100}%)`; + return ( + + ); + } +} +export class Fields extends Component { + cleanUpHex = hex => { + let newHex = hex; + if (hex[0] === '#') { + // Accept format #RRGGBB + newHex = hex.substring(1); + } + if (newHex.length > 6) { + // Remove alpha value RRGGBBAA + newHex = newHex.substring(0,6); + } + return newHex; + } + isValidRGB = value => { + if (!/^\d+$/.test(value)) return false; + let newValue = parseInt(value, 10); + return 0 <= newValue && newValue < 256; + } + render () { + const rgba = this.props.colorObject.toRgb(); + return ( +
+ this.props.onChange({ hex })} /> + this.props.onChange({ r })} /> + this.props.onChange({ g })} /> + this.props.onChange({ b })} /> + {!this.props.disableAlpha && this.props.onChange({ a: a / 100 })} />} +
+ ); + } +} + +export function Checkerboard (props) { + let { className, style, color, ...otherProps } = props; + return ( +
+
+ {props.children} +
+ ); +} +export function Swatch (props) { + return ( + (e.key === "Enter" && props.onClick())} + color={props.color} /> + ); +} diff --git a/src/Editor/Util/ColorPicker/ColorPickerComponents/WickCustomSlider.jsx b/src/Editor/Util/ColorPicker/ColorPickerComponents/WickCustomSlider.jsx new file mode 100644 index 000000000..7624137e7 --- /dev/null +++ b/src/Editor/Util/ColorPicker/ColorPickerComponents/WickCustomSlider.jsx @@ -0,0 +1,155 @@ +import React, { Component, createRef } from 'react' + +class WickCustomSlider extends Component { + constructor () { + super(); + this.container = createRef(null); + this.state = { + // Used to re-render the hover pointer + hoverOffset: null + }; + } + componentWillUnmount () { + this.unbindEvents(); + // eslint-disable-next-line react/no-direct-mutation-state + this.state.hoverOffset = null; + } + calculateOffset = (e) => { + if (!this.container.current) return; + const container = this.container.current; + + // e.pageX for MouseEvent, e.touches[0].pageX for TouchEvent + const x = (typeof e.pageX === 'number') ? e.pageX : e.touches[0].pageX; + const left = x - (container.getBoundingClientRect().left + window.pageXOffset); + const y = (typeof e.pageY === 'number') ? e.pageY : e.touches[0].pageY; + const top = y - (container.getBoundingClientRect().top + window.pageYOffset); + + let offsetX = Math.max(Math.min(left / container.clientWidth, 1), 0); + let offsetY = Math.max(Math.min(top / container.clientHeight, 1), 0); + return { x: offsetX, y: offsetY }; + } + + bindEvents = () => { + window.addEventListener('mousemove', this.handleMouseMove); + window.addEventListener('mouseup', this.handleMouseUp); + } + unbindEvents = () => { + window.removeEventListener('mousemove', this.handleMouseMove); + window.removeEventListener('mouseup', this.handleMouseUp); + } + handlePointer = (e) => { + // Don't select text when dragging pointer + e.preventDefault(); + + if (!this.props.onMouseDownPointer) return this.handleContainer(e); + this.props.onMouseDownPointer(e.currentTarget.dataset.wickPointerIndex); + } + handleContainer = (e) => { + // Don't select text when dragging container + e.preventDefault(); + + // Check if one of the pointers were clicked + if (e.target !== e.currentTarget) return; + this.props.onMouseDownContainer(this.calculateOffset(e)); + } + handleMouseMove = (e) => this.props.onMouseMove(this.calculateOffset(e)); + handleMouseUp = (e) => { + this.unbindEvents(); + this.props.onMouseUp(this.calculateOffset(e)); + } + handleTouchEnd = (e) => { + if (e.touches.length > 0) { + return this.props.onMouseUp(this.calculateOffset(e)); + } + else { + return this.props.onMouseUp(this.calculateOffset(e.changedTouches[0])); + } + } + + renderPointers () { + return ( + <> + {this.props.pointers.map((pointerItem, index) => { + let offset = (pointerItem.offset === undefined) ? pointerItem : pointerItem.offset; + let style = {}; + if (typeof offset === 'number' && this.props.pointersDirection) { + if (this.props.pointersDirection.includes('x')) style.left = `${offset * 100}%`; + else style.top = `${offset * 100}%`; + } + else style = { left: `${offset.x * 100}%`, top: `${offset.y * 100}%` }; + style.position = 'absolute'; + + return ( + {this.handlePointer(e); this.bindEvents();}} + onTouchStart={this.handlePointer} + color={pointerItem.color} + pointerType={this.props.pointerType} + style={style} + {...this.props.pointerProps} /> + ); + })} + + ); + } + render () { + return ( +
+
{this.handleContainer(e); this.bindEvents();}} + onTouchStart={this.handleContainer} + onTouchMove={this.handleMouseMove} + onTouchEnd={this.handleTouchEnd} + onTouchCancel={this.handleTouchEnd} + style={this.props.style && this.props.style.background}> + {this.renderPointers()} + {this.props.getHoverColor && this.renderHoverPointer()} +
+
+ ); + } + + handleContainerHover = (e) => { + // Check if hovering over a pointer + if (e.target !== e.currentTarget || e.buttons !== 0) + this.setState({ hoverOffset: null }); + else + this.setState({ hoverOffset: this.calculateOffset(e) }); + } + handleContainerExit = (e) => this.setState({ hoverOffset: null }); + renderHoverPointer () { + if (!this.state.hoverOffset) return (<>); + let offset = this.state.hoverOffset, + style = { position: 'absolute', pointerEvents: 'none' }; + if (this.props.pointersDirection) { + if (this.props.pointersDirection === 'x') { + offset = offset.x; + style.left = `${offset * 100}%`; + } + else { + offset = offset.y; + style.top = `${offset * 100}%`; + } + } + else { + style.left = `${offset.x * 100}%`; + style.top = `${offset.y * 100}%`; + } + return ( + + ); + } +} + +export default WickCustomSlider; diff --git a/src/Editor/Util/ColorPicker/ColorPickerComponents/WickGradient.jsx b/src/Editor/Util/ColorPicker/ColorPickerComponents/WickGradient.jsx new file mode 100644 index 000000000..a272fd1a1 --- /dev/null +++ b/src/Editor/Util/ColorPicker/ColorPickerComponents/WickGradient.jsx @@ -0,0 +1,200 @@ +import React, { Component } from 'react' + +import './_wickgradient.scss'; +import ActionButton from 'Editor/Util/ActionButton/ActionButton'; +import WickColorPicker from './WickSpectrum'; +import { GradientSlider, ColorPickerInput } from 'Editor/Util/ColorPicker/ColorPickerComponents/ColorPickerComponents'; +import tinycolor from 'tinycolor2'; + +class WickGradient extends Component { + componentDidMount () { + this.props.onMount(); + } + componentWillUnmount () { + this.props.onUnmount(); + } + interpolateColor = (offset) => { + const sortedStops = [...this.controlStops]; + sortedStops.sort((objectA, objectB) => objectA.offset - objectB.offset); + if (offset <= sortedStops[0].offset) return sortedStops[0].color || '#000000'; + if (offset >= sortedStops[sortedStops.length - 1].offset) return sortedStops[sortedStops.length - 1].color || '#000000'; + let next = sortedStops.findIndex(stop => (stop.offset > offset)); + let firstStop = sortedStops[next - 1]; + let nextStop = sortedStops[next]; + let percent = (offset - firstStop.offset) / (nextStop.offset - firstStop.offset) * 100; + return tinycolor.mix(firstStop.color || '#000000', nextStop.color || '#000000', percent).toRgbString(); + } + + controlStopMouseDown = (index) => { + // Use onChangeComplete to sync the stops' ordering. + this.onChangeComplete({ stopIndex: parseInt(index) }); + } + containerMouseDown = (offset) => { + let color = this.interpolateColor(offset.x); + // if color is null, use black as default + this.controlStops.push({ color: color || '#000000', offset: offset.x }); + this.onChangeComplete({ stopIndex: this.controlStops.length - 1 }); + } + colorSelectedStop = (color) => { + let offset = this.controlStops[this.props.selectedControlStopIndex].offset; + this.controlStops[this.props.selectedControlStopIndex] = { color, offset }; + } + offsetSelectedStop = (offset) => { + let color = this.controlStops[this.props.selectedControlStopIndex].color || '#000000'; + this.controlStops[this.props.selectedControlStopIndex] = { color, offset }; + } + deleteSelectedStop = () => { + let stopIndex = this.props.selectedControlStopIndex; + if (this.controlStops.length <= 2) { + this.colorSelectedStop(this.controlStops[1 - stopIndex].color); + stopIndex = 1 - stopIndex; + } + else { + this.controlStops.splice(stopIndex, 1); + if (stopIndex >= this.controlStops.length) { + stopIndex = this.controlStops.length - 1; + } + } + this.onChangeComplete({ stopIndex }); + } + gradientObject = () => ({ + stops: this.controlStops, + origin: this.origin, + destination: this.destination, + radial: this.radial + }) + onChangeIntermediate = () => this.props.onChangeIntermediate(this.gradientObject()); + onChangeComplete = (args) => this.props.onChangeComplete(this.gradientObject(), args); + onChangeEndpoint = (endpoint, override) => { + if (typeof override.x === 'number') { + endpoint.x = override.x * this.props.bounds.width + this.props.bounds.left; + } + if (typeof override.y === 'number') { + endpoint.y = override.y * this.props.bounds.height + this.props.bounds.top; + } + this.onChangeComplete(); + } + onChangeRadial = (radial) => { + this.radial = radial; + this.onChangeComplete(); + } + renderHeader = () => { + return ( +
+
+ this.onChangeRadial(false) } + isActive={ () => !this.radial } + icon="linear" /> +
+
+ this.onChangeRadial(true) } + isActive={ () => this.radial } + icon="radial" /> +
+
+ ); + } + renderGradientBackground () { + let linearGradient = 'linear-gradient(to right'; + const sortedControlStops = [...this.controlStops]; + sortedControlStops.sort((objectA, objectB) => objectA.offset - objectB.offset); + sortedControlStops.forEach(controlStopObject => { + linearGradient += `, ${controlStopObject.color || '#000000'} ${controlStopObject.offset * 100}%` + }); + linearGradient += ')'; + return linearGradient; + } + renderGradientInfo () { + // Normalize the gradient endpoints to the selection bounds + let originX = (this.origin.x - this.props.bounds.left) / this.props.bounds.width, + originY = (this.origin.y - this.props.bounds.top) / this.props.bounds.height, + destinationX = (this.destination.x - this.props.bounds.left) / this.props.bounds.width, + destinationY = (this.destination.y - this.props.bounds.top) / this.props.bounds.height; + let selectedStop = this.controlStops[this.props.selectedControlStopIndex], + offset = selectedStop ? selectedStop.offset : 0; + return ( +
+
+ this.onChangeEndpoint(this.origin, { x })} /> + this.onChangeEndpoint(this.origin, { y })} /> +
+
+ this.onChangeEndpoint(this.destination, { x })} /> + this.onChangeEndpoint(this.destination, { y })} /> +
+
+ { + this.offsetSelectedStop(offset); + this.onChangeComplete(); + }} /> + +
+
+ ) + } + + render () { + this.controlStops = [...this.props.color.stops]; + this.origin = {...this.props.color.origin}; + this.destination = {...this.props.color.destination}; + this.radial = this.props.color.radial; + + return ( + <> + {this.renderHeader()} + this.interpolateColor(offset)} + containerDown={this.containerMouseDown} + controlStopDown={this.controlStopMouseDown} + onMouseMove={offset => { this.offsetSelectedStop(offset.x); this.onChangeIntermediate(); }} + onMouseUp={() => this.onChangeComplete()} + stops={this.controlStops} + pointerProps={{ selectedStop: this.props.selectedControlStopIndex }} + background={this.renderGradientBackground()} /> + {this.renderGradientInfo()} + {this.props.colorHeader} + { this.colorSelectedStop(color); this.onChangeIntermediate(); }} + onChangeComplete={color => { this.colorSelectedStop(color); this.onChangeComplete({ stopColor: color }); }} + color={this.controlStops[this.props.selectedControlStopIndex].color || new window.Wick.Color('#000000')} /> + + ); + } +} + +export default WickGradient; diff --git a/src/Editor/Util/ColorPicker/ColorPickerComponents/WickSpectrum.jsx b/src/Editor/Util/ColorPicker/ColorPickerComponents/WickSpectrum.jsx new file mode 100644 index 000000000..505a023dd --- /dev/null +++ b/src/Editor/Util/ColorPicker/ColorPickerComponents/WickSpectrum.jsx @@ -0,0 +1,205 @@ +import React, { Component } from 'react' + +import tinycolor from 'tinycolor2'; +import ActionButton from 'Editor/Util/ActionButton/ActionButton'; + +import './_wickspectrum.scss'; +import WickSwatch from 'Editor/Util/ColorPicker/WickSwatch/WickSwatch'; +import { Saturation, Hue, Alpha, Fields, Checkerboard, Swatch } from 'Editor/Util/ColorPicker/ColorPickerComponents/ColorPickerComponents'; + +class WickColorPicker extends Component { + constructor () { + super(); + + this.values = { + h: 0, + s: 0, + v: 0, + a: 1 + } + this.color = tinycolor(this.values); + } + onChangeIntermediate = (values) => { + this.values = values; + this.color = tinycolor(values); + this.props.onChangeIntermediate(this.color.toRgbString()); + } + onChangeComplete = (values) => { + this.values = values; + this.color = tinycolor(values); + this.props.onChangeComplete(this.color.toRgbString()); + } + onChangeFields = (input) => { + let newInput; + if (input.hex) { + newInput = input.hex; + } + else { + // Input is of the form { r, g, b, a } + newInput = Object.assign(this.color.toRgb(), input); + } + this.color = tinycolor(newInput); + this.values = this.color.toHsv(); + this.props.onChangeComplete(this.color.toRgbString()); + } + + renderSwatchColumn = (colorList, i) => { + return ( +
+ {colorList.map((color,i) => { + return ( + + ); + })} +
+ ); + } + + renderSwatchbook = (colors) => { + return ( +
+ {colors.map((colorList, i) => { + return (this.renderSwatchColumn(colorList, i)); + })} +
+ ); + } + + renderSwatches = () => { + let colors = [ + ["#ff0000","#ffcccc","#ff9999","#ff4d4d","#cc0000","#800000"], + ["#ff8000","#ffe6cc","#ffcc99","#ffa64d","#cc6600","#804000"], + ["#ffff00","#ffffcc","#ffff99","#ffff4d","#cccc00","#808000"], + ["#00ff00","#ccffcc","#99ff99","#4dff4d","#00cc00","#008000"], + ["#00ff80","#ccffe6","#99ffcc","#4dffa6","#00cc66","#008040"], + ["#00ffff","#ccffff","#99ffff","#4dffff","#00cccc","#008080"], + ["#0080ff","#cce6ff","#99ccff","#4da6ff","#0066cc","#004080"], + ["#0000ff","#ccccff","#9999ff","#4d4dff","#0000cc","#000080"], + ["#8000ff","#e6ccff","#cc99ff","#a64dff","#6600cc","#400080"], + ["#ff00ff","#ffccff","#ff99ff","#ff4dff","#cc00cc","#800080"], + ["#ff0080","#ffcce6","#ff99cc","#ff4da6","#cc0066","#800040"], + ["#000000","#ffffff","#cccccc","#999999","#666666","#333333"] + ] + + return ( +
+ {this.renderSwatchbook(colors)} +
+ ); + } + + + + renderHeader () { + return ( +
+
+ {this.props.changeColorPickerType("swatches")}} + isActive={ () => this.props.colorPickerType === "swatches" } + icon="swatches" /> +
+
+ {this.props.changeColorPickerType("spectrum")}} + isActive={ () => this.props.colorPickerType === "spectrum" } + icon="spectrum" /> +
+
+
+ +
+
+
+ ); + } + + + renderSwatchContainer = (colors) => { + return ( +
+ {colors.map((color, i) => { + return ( +
+ {this.props.onChangeComplete(color)}} /> +
+ ); + })} +
+ ); + } + + renderSpectrum = () => { + // let colors = ['#D0021B', '#F8E71C', '#7ED321', '#4A90E2', '#000000', '#4A4A4A', '#FFFFFF', '#FFFFFF00'] + let lastUsedColorsDefaults = ["#000000","#000000","#000000","#000000","#000000","#000000","#000000","#000000"] + let lastColors = this.props.lastColorsUsed || lastUsedColorsDefaults; + return ( +
+ +
+
+ +
+
+ + +
+ +
+ + {/*this.renderSwatchContainer(colors)*/} + {this.renderSwatchContainer(lastColors)} +
+ ); + } + + render () { + let inputColor = tinycolor(this.props.color); + if (!tinycolor.equals(inputColor, this.color)) { + this.values = inputColor.toHsv(); + this.color = inputColor; + } + return (this.props.colorPickerType === "spectrum") ? this.renderSpectrum() : this.renderSwatches(); + } + + openEyedropper = () => { + window.editor.setActiveTool('eyedropper'); + window.editor._onEyedropperPickedColor = this.props.onChangeComplete; + } +} + +export default WickColorPicker; \ No newline at end of file diff --git a/src/Editor/Util/ColorPicker/ColorPickerComponents/_colorpickercomponents.scss b/src/Editor/Util/ColorPicker/ColorPickerComponents/_colorpickercomponents.scss new file mode 100644 index 000000000..6bf6c6b58 --- /dev/null +++ b/src/Editor/Util/ColorPicker/ColorPickerComponents/_colorpickercomponents.scss @@ -0,0 +1,138 @@ +/* + * Copyright I don't know + */ + +@import 'Editor/_wickbrand.scss'; + +$color-picker-stop-width: 24px; +$color-picker-pointer-radius: 8px; +$color-picker-border-active: #01C094; + +.wick-color-picker-gradient-slider { + position: relative; + height: fit-content; + background-color: white; + margin: 0 3px 6px; + border-radius: 2px; + + .wick-custom-slider-background { + height: $color-picker-stop-width * 1.5; + position: relative; + } +} +.wick-color-picker-gradient-stop { + top: 0; + translate: -$color-picker-stop-width / 2; + width: $color-picker-stop-width; + display: flex; + flex-direction: column; + align-items: center; + margin-top: $color-picker-stop-width / 2; +} +.wick-color-picker-gradient-arrow:after { + content: ""; + display: block; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid white; +} +.wick-color-picker-gradient-color { + background-color: white; + width: 100%; + height: $color-picker-stop-width; + padding: 5px; + border-radius: 5px; +} +.wick-color-picker-gradient-arrow.wick-color-picker-gradient-active:after { + border-bottom-color: $color-picker-border-active; +} +.wick-color-picker-gradient-color.wick-color-picker-gradient-active { + background-color: $color-picker-border-active; +} +.wick-color-picker-gradient-checker { + width: 100%; + height: 100%; + border: 1px solid; + border-radius: 2px; + background-color: white; + display: flex; + flex-direction: row-reverse; +} + +.wick-color-picker-pointer { + width: $color-picker-pointer-radius * 2; + height: $color-picker-pointer-radius * 2; + translate: -$color-picker-pointer-radius; + border-radius: $color-picker-pointer-radius; + border: 4px solid white; + background-color: black; +} + +.wick-color-picker-slider .wick-custom-slider-background { + width: 100%; + height: 100%; + position: relative; +} +.wick-color-picker-bar { + height: $color-picker-pointer-radius * 2; + border-radius: $color-picker-pointer-radius; + padding: 0 $color-picker-pointer-radius; + overflow: hidden; + + .wick-color-picker-pointer { + top: 0; + } +} +.wick-color-picker-saturation { + width: 100%; + height: 115px; + border-radius: 2px; + overflow: visible; + + .wick-color-picker-pointer { + translate: (-$color-picker-pointer-radius) (-$color-picker-pointer-radius); + } +} +.wick-color-picker-hue { + background-color: #f00; + + .wick-custom-slider-background { + background-image: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); + } +} + +.wick-color-picker-input-label { + color: $editor-primary-property-text; + font-size: 14px; + font-weight: bold; + display: flex; + align-items: center; + margin-bottom: 0; +} +.wick-color-picker-fields .wick-color-picker-input-label { + flex-direction: column; + flex: 1; +} +.wick-color-picker-input-label.wick-color-picker-field-hex { + flex: 2; + + .wick-color-picker-input-field { + text-transform: uppercase; + } +} + +.wick-color-picker-checkerboard { + overflow: hidden; + background-position: center left; + + div { + width: 100%; + height: 100%; + } +} +.wick-color-picker-swatch-checker { + width: 100%; + height: 100%; + outline: none; + cursor: pointer; +} \ No newline at end of file diff --git a/src/Editor/Util/ColorPicker/ColorPickerComponents/_wickgradient.scss b/src/Editor/Util/ColorPicker/ColorPickerComponents/_wickgradient.scss new file mode 100644 index 000000000..f49295449 --- /dev/null +++ b/src/Editor/Util/ColorPicker/ColorPickerComponents/_wickgradient.scss @@ -0,0 +1,30 @@ +/* + * Copyright I don't know + */ + +@import 'Editor/_wickbrand.scss'; + +.wick-color-picker-gradient-fields { + display: flex; + flex-direction: column; + gap: 7px; + + .wick-color-picker-gradient-fields-row { + display: flex; + justify-content: space-between; + gap: 7px; + + input { + width: 50%; + } + } + label { + justify-content: flex-end; + white-space: nowrap; + gap: 7px; + margin: 0; + } +} +.wick-color-picker-gradient-fields-row > * { + flex: 1; +} \ No newline at end of file diff --git a/src/Editor/Util/ColorPicker/ColorPickerComponents/_wickspectrum.scss b/src/Editor/Util/ColorPicker/ColorPickerComponents/_wickspectrum.scss new file mode 100644 index 000000000..1926ca2b2 --- /dev/null +++ b/src/Editor/Util/ColorPicker/ColorPickerComponents/_wickspectrum.scss @@ -0,0 +1,111 @@ +/* + * Copyright 2020 WICKLETS LLC + * + * This file is part of Wick Editor. + * + * Wick Editor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Wick Editor is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Wick Editor. If not, see . + */ + + @import 'Editor/_wickbrand.scss'; + + $color-picker-padding: 10px; + $color-picker-block-margin: 5px; + $color-picker-pointer-radius: 8px; + +#btn-color-picker-dropper { + width: 25px; + height: 25px; + margin: auto; +} + +.wick-color-picker-control-body { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: stretch; + width: 100%; + margin-top: $color-picker-block-margin; + gap: $color-picker-block-margin; +} + +#wick-color-picker-bar-container { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: stretch; + flex-grow: 1; + gap: $color-picker-block-margin; +} + +.wick-color-picker-fields { + display: flex; + flex-direction: row; + margin-top: $color-picker-block-margin; + gap: $color-picker-block-margin; +} + +.wick-color-picker-color-block-container { + position: relative; + width: 25px; + height: 25px; + background-color: white; + border-radius: 2px; + margin: auto; +} + +.wick-color-picker-swatches-container { + height: fit-content; + display: flex; + justify-content: space-between; + flex-wrap: wrap; + padding: $color-picker-block-margin 0; +} + +.wick-color-picker-small-swatch { + width: 16px; + min-width: 16px; + height: 16px; + border-radius: 4px; + overflow: hidden; + border: 1px solid #222; + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg=="); + background-size: 15px 15px; +} +// Swatchbook styling +.column-swatch { + width: 30px; + height: 20px; +} +.column-swatch:first-child { + margin-bottom: 2px; +} +.wick-swatch-picker-column { + display: flex; + flex-direction: column; + border-radius: 2px; + overflow: hidden; +} +.wick-swatch-picker-book { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + margin: $color-picker-block-margin 0; + gap: 8px; +} + +.wick-swatch-color-picker-body { + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/src/Editor/Util/ColorPicker/WickColorPicker.jsx b/src/Editor/Util/ColorPicker/WickColorPicker.jsx index 7b704f8ad..2ea5a4715 100644 --- a/src/Editor/Util/ColorPicker/WickColorPicker.jsx +++ b/src/Editor/Util/ColorPicker/WickColorPicker.jsx @@ -1,72 +1,153 @@ import React, { Component } from 'react' -import ActionButton from 'Editor/Util/ActionButton/ActionButton'; +import ActionButton from 'Editor/Util/ActionButton/ActionButton'; import './_wickcolorpicker.scss'; -import { CustomPicker } from 'react-color'; -import WickSwatch from 'Editor/Util/ColorPicker/WickSwatch/WickSwatch' +import WickGradient from 'Editor/Util/ColorPicker/ColorPickerComponents/WickGradient'; +import WickSpectrum from 'Editor/Util/ColorPicker/ColorPickerComponents/WickSpectrum'; -var { Saturation, Hue, Alpha, Checkboard, Swatch } = require('react-color/lib/components/common'); -var { SketchFields } = require('react-color/lib/components/sketch/SketchFields'); +class WickGradientColorPicker extends Component { + constructor (props) { + super(props); -class WickColorPicker extends Component { - renderSwatchColumn = (colorList, i) => { - return ( -
- {colorList.map((color,i) => { - return ( - - ); - })} -
- ); + this.state = { + colorOnDrag: null + }; + this.editLastColors = false; + // Used to preserve color when switching solid/gradient + this.lastReceivedColor = null; + this.outOfSync = false; + } + componentWillUnmount () { + this.outOfSync = false; + this.props.onDesyncedChange(this.props.color); } - renderSwatchbook = (colors) => { - return ( -
- {colors.map((colorList, i) => { - return (this.renderSwatchColumn(colorList, i)); - })} -
- ); + onChangeIntermediate = (color) => { + this.props.onChangeIntermediate && this.props.onChangeIntermediate(color); + this.setState({ colorOnDrag: color }); } + onColorChange = (color) => { + this.props.onChangeComplete(color); + if (this.props.updateLastColors) { + this.props.updateLastColors(color, this.editLastColors); + this.editLastColors = true; + } + this.setState({ colorOnDrag: null }); + } + onGradientChange = (color, args) => { + // Set null gradient stops to black + for(let stop of color.stops) { + if(!stop.color) stop.color = '#000000'; + } - renderSwatches = () => { - let colors = [ - ["#ff0000","#ffcccc","#ff9999","#ff4d4d","#cc0000","#800000"], - ["#ff8000","#ffe6cc","#ffcc99","#ffa64d","#cc6600","#804000"], - ["#ffff00","#ffffcc","#ffff99","#ffff4d","#cccc00","#808000"], - ["#00ff00","#ccffcc","#99ff99","#4dff4d","#00cc00","#008000"], - ["#00ff80","#ccffe6","#99ffcc","#4dffa6","#00cc66","#008040"], - ["#00ffff","#ccffff","#99ffff","#4dffff","#00cccc","#008080"], - ["#0080ff","#cce6ff","#99ccff","#4da6ff","#0066cc","#004080"], - ["#0000ff","#ccccff","#9999ff","#4d4dff","#0000cc","#000080"], - ["#8000ff","#e6ccff","#cc99ff","#a64dff","#6600cc","#400080"], - ["#ff00ff","#ffccff","#ff99ff","#ff4dff","#cc00cc","#800080"], - ["#ff0080","#ffcce6","#ff99cc","#ff4da6","#cc0066","#800040"], - ["#000000","#ffffff","#cccccc","#999999","#666666","#333333"] - ] + // Sort color stops, keep the selected stop + let index; + if (args && typeof args.stopIndex === 'number') { + index = args.stopIndex; + } + else { + index = this.props.getSelectedStopIndex(); + } + let selectedStop = color.stops[index]; + color.stops.sort((stop1, stop2) => stop1.offset - stop2.offset); + let newIndex = color.stops.indexOf(selectedStop); + if (newIndex < 0) newIndex = 0; - return ( -
- {this.renderHeader()} -
- {this.renderSwatchbook(colors)} -
-
- ); + this.props.setSelectedStopIndex(newIndex); + this.props.onChangeComplete(color); + + if (args && args.stopColor) { + this.props.updateLastColors(args.stopColor, this.editLastColors); + this.editLastColors = true; + } + this.setState({ + colorOnDrag: null + }); + } + switchSolid = (color) => { + // Exit if color isn't a gradient + if (!color.stops) return; + this.props.onDesyncedChange(color.stops[0].color); + this.outOfSync = true; + } + switchGradient = (color) => { + // Exit if color is a gradient + if (color.stops) return; + if (this.props.color.stops || (this.props.color.gradient && this.props.color.gradient.stops)) { + // If props.color is already a gradient, use that color + this.outOfSync = false; + this.props.onDesyncedChange(this.props.color); + return; + } + + let x = 0, topY = 0, bottomY = 500; + if (this.props.selectedObjectsBounds) { + x = this.props.selectedObjectsBounds.centerX; + topY = this.props.selectedObjectsBounds.top; + bottomY = this.props.selectedObjectsBounds.bottom; + } + + this.props.onDesyncedChange({ + origin: {x, y: topY}, + destination: {x, y: bottomY}, + stops: [{color, offset: 0}, {color, offset: 1}], + radial: false + }); + this.outOfSync = true; } + // Convert paper objects to plain objects that hold the same data + reducePaperColor (color) { + if (!(color instanceof window.paper.Color)) return color; + if (!color.gradient) return color.toCSS(); - renderHeader () { + let stops = color.gradient.stops.map((stop, index, stops) => { + let color = stop.color.toCSS(); + let offset = stop.offset; + if (typeof offset !== 'number') { + offset = index / (stops.length - 1); + } + return { color, offset }; + }); + let { origin, destination } = color; + origin = { x: origin.x, y: origin.y }; + destination = { x: destination.x, y: destination.y }; + return { stops, origin, destination, radial: color.gradient.radial }; + } + reducePaperBounds (bounds) { + if (!(bounds instanceof window.paper.Rectangle)) return bounds; + + let { width, height, left, right, top, bottom } = bounds; + return { width, height, left, right, top, bottom }; + } + + renderGradientHeader (color) { return ( -
+ <> +
+ this.switchSolid(color)} + isActive={ () => !color.stops } + text="Solid" /> +
+
+ this.switchGradient(color)} + isActive={ () => !!color.stops } + text="Gradient" /> +
+ + ); + } + renderColorHeader () { + return ( + <>
this.props.colorPickerType === "spectrum" } icon="spectrum" />
+ + ); + } + renderHeader (color) { + return ( +
+ {this.props.enableGradient ? this.renderGradientHeader(color) : this.renderColorHeader()}
- +
); } + render () { + if (this.props.color !== this.lastReceivedColor) { + this.lastReceivedColor = this.props.color; + this.outOfSync = false; + } + let color = this.outOfSync ? this.props.desyncedColor : this.props.color; + let index = 0; + if (this.state.colorOnDrag !== null) { + color = this.state.colorOnDrag; + } + else { + color = this.reducePaperColor(color); + } + if (color.stops) { + index = this.props.getSelectedStopIndex(); + index = Math.min(index, color.stops.length - 1); + } + let bounds = this.reducePaperBounds(this.props.selectedObjectsBounds); - renderSwatchContainer = (colors) => { - return ( -
- {colors.map((color, i) => { - return ( -
- {this.props.onChangeComplete(color)}} /> -
- ); - })} -
- ); - } - - renderSpectrum = () => { - let styles = { - activeColor: { - position:'absolute', - width: "100%", - height: "100%", - backgroundColor: this.props.color, - } + // Fixes bug where canvas gradient tool disappears on undo + if (color.stops) { + this.props.setGradientActive(index); } - - let colors = ['#D0021B', '#F8E71C', '#7ED321', '#4A90E2', '#000000', '#4A4A4A', '#FFFFFF', '#FFFFFF00'] - let lastUsedColorsDefaults = ["#000000","#000000","#000000","#000000","#000000","#000000","#000000","#000000"] - let lastColors = this.props.lastColorsUsed || lastUsedColorsDefaults; return (
- {this.renderHeader()} -
- -
-
-
- -
-
-
- -
-
- + {this.renderHeader(color)} + {color.stops ? + + {this.renderColorHeader()}
+ } + selectedControlStopIndex={index} + onMount={this.props.setGradientActive} + onUnmount={this.props.setGradientInactive} + onChangeIntermediate={this.onChangeIntermediate} + onChangeComplete={this.onGradientChange} + color={color} + bounds={bounds} /> : + <> + {this.props.enableGradient && +
+ {this.renderColorHeader()}
-
- -
-
-
- - {this.renderSwatchContainer(colors)} - {this.renderSwatchContainer(lastColors)} + } + + + }
); } - - render () { - if (this.props.colorPickerType === "swatches" || !this.props.colorPickerType) { - return this.renderSwatches(); - } else if (this.props.colorPickerType === "spectrum") { - return this.renderSpectrum(); - }; - } - - openEyedropper = () => { - window.editor.setActiveTool('eyedropper'); - window.editor._onEyedropperPickedColor = this.props.onChange; - } } -export default CustomPicker(WickColorPicker); +export default WickGradientColorPicker; diff --git a/src/Editor/Util/ColorPicker/WickSwatch/WickSwatch.jsx b/src/Editor/Util/ColorPicker/WickSwatch/WickSwatch.jsx index 301098947..0e8090550 100644 --- a/src/Editor/Util/ColorPicker/WickSwatch/WickSwatch.jsx +++ b/src/Editor/Util/ColorPicker/WickSwatch/WickSwatch.jsx @@ -1,6 +1,6 @@ import React, { Component } from 'react' +import { Swatch } from '../ColorPickerComponents/ColorPickerComponents'; var tinycolor = require("tinycolor2"); -var { Swatch } = require('react-color/lib/components/common'); class WickSwatch extends Component { constructor (props) { @@ -20,14 +20,9 @@ class WickSwatch extends Component { render () { let colorInfo = tinycolor(this.props.color); let selectedColorInfo = tinycolor(this.props.selectedColor); - let contrastColor = '#CCCCCC' - - let selected = this.props.color === ("#" + selectedColorInfo.toHex()); // TODO clean this check. - - if (colorInfo.isLight()) { - contrastColor = "#333333" - } + let contrastColor = colorInfo.isLight() ? '#333333' : '#CCCCCC'; + let selected = colorInfo.toHex() === selectedColorInfo.toHex(); let selectedStyle = { border: '3px solid' + contrastColor } @@ -54,10 +49,10 @@ class WickSwatch extends Component { style={style}> {this.props.onChangeComplete(color)}} /> + onClick={() => {this.props.onChangeComplete(this.props.color)}} />
); } } -export default WickSwatch \ No newline at end of file +export default WickSwatch diff --git a/src/Editor/Util/ColorPicker/_colorpicker.scss b/src/Editor/Util/ColorPicker/_colorpicker.scss index 081251df9..58c14c225 100644 --- a/src/Editor/Util/ColorPicker/_colorpicker.scss +++ b/src/Editor/Util/ColorPicker/_colorpicker.scss @@ -21,15 +21,39 @@ .btn-color-picker { display: flex; + flex-direction: column-reverse; background: none; width: 100%; height: 100%; border-radius: 16px; border: 4px solid $editor-secondary-text; box-sizing: border-box; + overflow: hidden; + padding: 0; +} +.btn-color-picker-background-opaque { + width: 100%; + flex-basis: 50%; +} +.btn-color-picker-stroke { + overflow: visible; + background-origin: border-box, border-box; + background-clip: border-box, border-box; + border-color: transparent; + mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0); + mask-composite: exclude; +} +.btn-color-picker-stroke .btn-color-picker-background-opaque { + flex-basis: unset; + height: 100%; + // percent-based margin is calculated based on width of an element + // therefore, pill-shaped buttons (e.g. in inspector) need a workaround + margin: 0 -4px; + transform: translateY(50%); // cover bottom half } .color-picker-control-div { display: flex; flex-direction: row; + align-items: stretch; } \ No newline at end of file diff --git a/src/Editor/Util/ColorPicker/_wickcolorpicker.scss b/src/Editor/Util/ColorPicker/_wickcolorpicker.scss index 57784ca0e..3cb807c3f 100644 --- a/src/Editor/Util/ColorPicker/_wickcolorpicker.scss +++ b/src/Editor/Util/ColorPicker/_wickcolorpicker.scss @@ -1,169 +1,44 @@ -/* - * Copyright 2020 WICKLETS LLC - * - * This file is part of Wick Editor. - * - * Wick Editor is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Wick Editor is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Wick Editor. If not, see . +/* + * Copyright I don't know */ - @import 'Editor/_wickbrand.scss'; +@import 'Editor/_wickbrand.scss'; - $color-picker-width: 220px; - $color-picker-height: 300px; - $color-picker-padding: 10px; - $color-picker-bar-height: 25px; - $color-picker-block-margin: 5px; +$color-picker-width: 250px; +$color-picker-bar-height: 25px; +$color-picker-block-margin: 5px; - .wick-color-picker { - background-color: $editor-primary; - width: $color-picker-width; - height: $color-picker-height; - padding: 10px 10px 0px; - border-radius: 4px; - } - - .wick-color-picker-header { - height: $color-picker-bar-height; +.wick-color-picker { + background-color: $editor-primary; + width: $color-picker-width; + height: fit-content; display: flex; - } - - .wick-color-picker-saturation { - width: 100%; - height: 115px; - position: relative; - margin-top: $color-picker-block-margin; - border-radius: 2px; - overflow: hidden; - } - -#btn-color-picker-close { - margin-left: auto; - width:$color-picker-bar-height; - height:$color-picker-bar-height; -} - -#btn-color-picker-dropper { - width:$color-picker-bar-height; - height:$color-picker-bar-height; - margin-right: $color-picker-block-margin; + flex-direction: column; + gap: $color-picker-block-margin; + padding: 10px; + border-radius: 4px; + font-family: 'Nunito Sans'; } .wick-color-picker-action-button { - width:30px; - height:$color-picker-bar-height; - margin-right: 4px + width: 30px; + margin-right: 4px; } .wick-color-picker-action-button.spacer { margin-right: auto; } - -.wick-color-picker-control-body { - display: flex; - flex-direction: row; - width: 100%; - margin-top: $color-picker-block-margin; -} - -.wick-color-picker-control-bar { - width: calc(#{$color-picker-width} - 2 * #{$color-picker-padding} - 2 * #{$color-picker-bar-height} - 2 * #{$color-picker-block-margin}); - height: 45%; - position: relative; - margin-bottom: 2.5%; - background-color: white; +.color-picker-control-div { + margin-left: auto; } -.wick-color-picker-color-block-container { - position: relative; - width: $color-picker-bar-height; +.wick-color-picker-header { height: $color-picker-bar-height; - margin-left: $color-picker-block-margin; - background-color: white; - border-radius: 2px; -} - -.wick-color-picker-swatches-container { - height: 25px; - display: flex; - margin-top: $color-picker-block-margin; - flex-wrap: wrap; - border-top: 1px solid rgba(0,0,0,.1); - padding-top: 8px; - margin: $color-picker-block-margin -10px; - padding: 5px 0px 0px 10px; -} - -.wick-color-picker-small-swatch { - width: 16px; - min-width: 16px; - height: 16px; - border-radius: 4px; - overflow: hidden; - border: 1px solid #222; - margin: 0px 10px 10px 0px; - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg=="); - background-size: 15px 15px; -} - -//Swatchbook styling -.column-swatch { - width: 30px; - height: 20px; -} - -.column-swatch:first-child { - margin-bottom: 2px; -} - -.wick-swatch-picker-column { - display: flex; - flex-direction: column; - margin: 0px 4px 4px 0px; - border-radius: 2px; - overflow: hidden; -} - -.wick-swatch-picker-book { display: flex; - flex-direction: row; - flex-wrap: wrap; - margin: $color-picker-block-margin -10px; - padding: 5px 0px 0px 10px; -} - -.wick-swatch-color-picker-body { - display: flex; - flex-direction: column; -} - -// Fix the styling for the Sketch Inputs. -.wick-color-picker .flexbox-fix { - margin-top: calc(#{$color-picker-block-margin} + 4px) !important; - padding-top: 0px !important; -} - -.wick-color-picker input { - width: 100% !important; - border-radius: 4px; - background-color: $editor-primary !important; - border: $editor-input-border-width solid $editor-primary-outline !important; - color: $editor-primary-property-text !important; - box-shadow: none !important; - padding: 0px 0px 0px 2px !important; + align-items: stretch; } +// Style color picker inputs like the inspector. .wick-color-picker span { - padding-bottom: 0px !important; - color: $editor-primary-property-text !important; -} + color: $editor-primary-property-text; +} \ No newline at end of file diff --git a/src/Editor/Util/ToolIcon/ToolIcon.jsx b/src/Editor/Util/ToolIcon/ToolIcon.jsx index 688088138..1467e624e 100644 --- a/src/Editor/Util/ToolIcon/ToolIcon.jsx +++ b/src/Editor/Util/ToolIcon/ToolIcon.jsx @@ -34,6 +34,8 @@ import iconFillBucket from 'resources/toolbar-icons/fillbucket.svg'; import iconPathCursor from 'resources/toolbar-icons/pathcursor.svg'; import iconSpectrum from 'resources/toolbar-icons/spectrum.svg'; import iconSwatches from 'resources/toolbar-icons/swatches.svg'; +import iconLinear from 'resources/toolbar-icons/linear.svg'; +import iconRadial from 'resources/toolbar-icons/radial.svg'; import iconDelete from 'resources/toolbar-icons/delete.svg'; import iconUndo from 'resources/toolbar-icons/undo.svg'; @@ -290,6 +292,8 @@ const icons = { "layerTween": iconLayerTween, "spectrum": iconSpectrum, "swatches": iconSwatches, + "linear": iconLinear, + "radial": iconRadial, "group": iconGroup, "mascotmark": mascotMark, "mascotmarkdark": mascotMarkDark, diff --git a/src/Editor/Util/WickInput/WickInput.jsx b/src/Editor/Util/WickInput/WickInput.jsx index 5cb2bfaac..10a281f04 100644 --- a/src/Editor/Util/WickInput/WickInput.jsx +++ b/src/Editor/Util/WickInput/WickInput.jsx @@ -181,25 +181,11 @@ class WickInput extends Component { } renderColor = () => { - let wrappedOnChange = (color) => { - let newColor = color; - - // TODO: Check if we can just use HEX here. - if (color.rgb) { - let rgb = color.rgb; - let str = "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + "," + rgb.a + ")"; - newColor = str; - } - - this.props.updateLastColors && this.props.updateLastColors(newColor); - this.props.onChange && this.props.onChange(newColor); - }; - return ( ); } diff --git a/src/Editor/Util/WickInput/WickTextInput/WickTextInput.jsx b/src/Editor/Util/WickInput/WickTextInput/WickTextInput.jsx index ce804d725..c1567ef48 100644 --- a/src/Editor/Util/WickInput/WickTextInput/WickTextInput.jsx +++ b/src/Editor/Util/WickInput/WickTextInput/WickTextInput.jsx @@ -52,7 +52,7 @@ export default function WickTextInput (props) { } if (isValidRegex) { - valid = valid && val.matches(isValidRegex); + valid = valid && isValidRegex.test(val); } return valid; diff --git a/src/resources/toolbar-icons/linear.svg b/src/resources/toolbar-icons/linear.svg new file mode 100644 index 000000000..bc0f41935 --- /dev/null +++ b/src/resources/toolbar-icons/linear.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/resources/toolbar-icons/radial.svg b/src/resources/toolbar-icons/radial.svg new file mode 100644 index 000000000..1839337ae --- /dev/null +++ b/src/resources/toolbar-icons/radial.svg @@ -0,0 +1,9 @@ + + + + + + + + +