From ca76a99fb48908fb6b085b84512b000ea7ba1544 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 21 Apr 2020 12:31:06 -0400 Subject: [PATCH 1/8] Add more space around the scrollbars --- src/components/scrollable-canvas/scrollable-canvas.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/scrollable-canvas/scrollable-canvas.css b/src/components/scrollable-canvas/scrollable-canvas.css index 58155447..c3a3f312 100644 --- a/src/components/scrollable-canvas/scrollable-canvas.css +++ b/src/components/scrollable-canvas/scrollable-canvas.css @@ -1,5 +1,5 @@ $scrollbar-size: 8px; -$scrollbar-padding: 1px; +$scrollbar-padding: 4px; .vertical-scrollbar, .horizontal-scrollbar { position: absolute; @@ -10,17 +10,17 @@ $scrollbar-padding: 1px; .vertical-scrollbar-wrapper { position: absolute; width: $scrollbar-size; - right: 0; + right: $scrollbar-padding; top: $scrollbar-padding; - height: calc(100% - $scrollbar-size - $scrollbar-padding); + height: calc(100% - $scrollbar-size - 2 * $scrollbar-padding); } .horizontal-scrollbar-wrapper { position: absolute; height: $scrollbar-size; left: $scrollbar-padding; - bottom: 0; - width: calc(100% - $scrollbar-size - $scrollbar-padding); + bottom: $scrollbar-padding; + width: calc(100% - $scrollbar-size - 2 * $scrollbar-padding); } .vertical-scrollbar { From eda582363a3bcf8202be6dde58f0de44678a1b2f Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 21 Apr 2020 13:11:27 -0400 Subject: [PATCH 2/8] Pass mode through to more selection tools. Correct the mode type of some tools that were misreporting it before. --- src/containers/fill-color-indicator.jsx | 1 + src/helper/bit-tools/oval-tool.js | 2 +- src/helper/bit-tools/rect-tool.js | 2 +- src/helper/bit-tools/select-tool.js | 6 +++--- src/helper/selection-tools/bounding-box-tool.js | 2 +- src/helper/selection-tools/nudge-tool.js | 5 ++++- src/helper/selection-tools/scale-tool.js | 5 ++++- src/helper/selection-tools/select-tool.js | 2 +- src/helper/tools/oval-tool.js | 2 +- src/helper/tools/rect-tool.js | 2 +- src/helper/tools/text-tool.js | 5 +++-- 11 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index b3848428..abbebabf 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -130,6 +130,7 @@ const mapStateToProps = state => ({ shouldShowGradientTools: state.scratchPaint.mode === Modes.SELECT || state.scratchPaint.mode === Modes.RESHAPE || state.scratchPaint.mode === Modes.FILL || + state.scratchPaint.mode === Modes.BIT_SELECT || state.scratchPaint.mode === Modes.BIT_FILL, textEditTarget: state.scratchPaint.textEditTarget }); diff --git a/src/helper/bit-tools/oval-tool.js b/src/helper/bit-tools/oval-tool.js index 194f5ac5..9873ecd9 100644 --- a/src/helper/bit-tools/oval-tool.js +++ b/src/helper/bit-tools/oval-tool.js @@ -32,7 +32,7 @@ class OvalTool extends paper.Tool { setCursor, onUpdateImage ); - const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); + const nudgeTool = new NudgeTool(Modes.BIT_OVAL, this.boundingBoxTool, onUpdateImage); // We have to set these functions instead of just declaring them because // paper.js tools hook up the listeners in the setter functions. diff --git a/src/helper/bit-tools/rect-tool.js b/src/helper/bit-tools/rect-tool.js index 50362c7b..8b3b84c5 100644 --- a/src/helper/bit-tools/rect-tool.js +++ b/src/helper/bit-tools/rect-tool.js @@ -32,7 +32,7 @@ class RectTool extends paper.Tool { setCursor, onUpdateImage ); - const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); + const nudgeTool = new NudgeTool(Modes.BIT_RECT, this.boundingBoxTool, onUpdateImage); // We have to set these functions instead of just declaring them because // paper.js tools hook up the listeners in the setter functions. diff --git a/src/helper/bit-tools/select-tool.js b/src/helper/bit-tools/select-tool.js index 6b242d73..b9ec8151 100644 --- a/src/helper/bit-tools/select-tool.js +++ b/src/helper/bit-tools/select-tool.js @@ -30,14 +30,14 @@ class SelectTool extends paper.Tool { super(); this.onUpdateImage = onUpdateImage; this.boundingBoxTool = new BoundingBoxTool( - Modes.SELECT, + Modes.BIT_SELECT, setSelectedItems, clearSelectedItems, setCursor, onUpdateImage ); - const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); - this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT, setSelectedItems, clearSelectedItems); + const nudgeTool = new NudgeTool(Modes.BIT_SELECT, this.boundingBoxTool, onUpdateImage); + this.selectionBoxTool = new SelectionBoxTool(Modes.BIT_SELECT, setSelectedItems, clearSelectedItems); this.selectionBoxMode = false; this.selection = null; this.active = false; diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index dc86e4d7..6922b74f 100644 --- a/src/helper/selection-tools/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -49,7 +49,7 @@ class BoundingBoxTool { this.boundsScaleHandles = []; this.boundsRotHandles = []; this._modeMap = {}; - this._modeMap[BoundingBoxModes.SCALE] = new ScaleTool(onUpdateImage); + this._modeMap[BoundingBoxModes.SCALE] = new ScaleTool(mode, onUpdateImage); this._modeMap[BoundingBoxModes.ROTATE] = new RotateTool(onUpdateImage); this._modeMap[BoundingBoxModes.MOVE] = new MoveTool(mode, setSelectedItems, clearSelectedItems, onUpdateImage, switchToTextTool); diff --git a/src/helper/selection-tools/nudge-tool.js b/src/helper/selection-tools/nudge-tool.js index d336e1c7..5b2b9ddd 100644 --- a/src/helper/selection-tools/nudge-tool.js +++ b/src/helper/selection-tools/nudge-tool.js @@ -1,6 +1,7 @@ import paper from '@scratch/paper'; import {getSelectedRootItems} from '../selection'; import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view'; +import {BitmapModes} from '../../lib/modes'; const NUDGE_MORE_MULTIPLIER = 15; @@ -10,12 +11,14 @@ const NUDGE_MORE_MULTIPLIER = 15; */ class NudgeTool { /** + * @param {Mode} mode Paint editor mode * @param {function} boundingBoxTool to control the bounding box * @param {!function} onUpdateImage A callback to call when the image visibly changes */ - constructor (boundingBoxTool, onUpdateImage) { + constructor (mode, boundingBoxTool, onUpdateImage) { this.boundingBoxTool = boundingBoxTool; this.onUpdateImage = onUpdateImage; + this.boundingBoxTool.isBitmap = mode in BitmapModes; } onKeyDown (event) { if (event.event.target instanceof HTMLInputElement) { diff --git a/src/helper/selection-tools/scale-tool.js b/src/helper/selection-tools/scale-tool.js index c9f9daac..23e232f5 100644 --- a/src/helper/selection-tools/scale-tool.js +++ b/src/helper/selection-tools/scale-tool.js @@ -1,6 +1,7 @@ import paper from '@scratch/paper'; import {getItems} from '../selection'; import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view'; +import {BitmapModes} from '../../lib/modes'; /** * Tool to handle scaling items by pulling on the handles around the edges of the bounding @@ -8,9 +9,11 @@ import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view'; */ class ScaleTool { /** + * @param {Mode} mode Paint editor mode * @param {!function} onUpdateImage A callback to call when the image visibly changes */ - constructor (onUpdateImage) { + constructor (mode, onUpdateImage) { + this.isBitmap = mode in BitmapModes; this.active = false; this.boundsPath = null; this.pivot = null; diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js index 5a0824d7..5bce77d7 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -46,7 +46,7 @@ class SelectTool extends paper.Tool { onUpdateImage, switchToTextTool ); - const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); + const nudgeTool = new NudgeTool(Modes.SELECT, this.boundingBoxTool, onUpdateImage); this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT, setSelectedItems, clearSelectedItems); this.selectionBoxMode = false; this.prevHoveredItemId = null; diff --git a/src/helper/tools/oval-tool.js b/src/helper/tools/oval-tool.js index 68be2286..1246a705 100644 --- a/src/helper/tools/oval-tool.js +++ b/src/helper/tools/oval-tool.js @@ -31,7 +31,7 @@ class OvalTool extends paper.Tool { setCursor, onUpdateImage ); - const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); + const nudgeTool = new NudgeTool(Modes.OVAL, this.boundingBoxTool, onUpdateImage); // We have to set these functions instead of just declaring them because // paper.js tools hook up the listeners in the setter functions. diff --git a/src/helper/tools/rect-tool.js b/src/helper/tools/rect-tool.js index df7bb03d..3ac9f9fa 100644 --- a/src/helper/tools/rect-tool.js +++ b/src/helper/tools/rect-tool.js @@ -31,7 +31,7 @@ class RectTool extends paper.Tool { setCursor, onUpdateImage ); - const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); + const nudgeTool = new NudgeTool(Modes.RECT, this.boundingBoxTool, onUpdateImage); // We have to set these functions instead of just declaring them because // paper.js tools hook up the listeners in the setter functions. diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index b034862a..3c20bc72 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -50,14 +50,15 @@ class TextTool extends paper.Tool { this.onUpdateImage = onUpdateImage; this.setTextEditTarget = setTextEditTarget; this.changeFont = changeFont; + const paintMode = isBitmap ? Modes.BIT_TEXT : Modes.TEXT; this.boundingBoxTool = new BoundingBoxTool( - Modes.TEXT, + paintMode, setSelectedItems, clearSelectedItems, setCursor, onUpdateImage ); - this.nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); + this.nudgeTool = new NudgeTool(paintMode, this.boundingBoxTool, onUpdateImage); this.isBitmap = isBitmap; // We have to set these functions instead of just declaring them because From 8865291d0425cd8463a69791aa8a97efa5e0b874 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 21 Apr 2020 17:59:56 -0400 Subject: [PATCH 3/8] Add new zoom level --- src/helper/view.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/helper/view.js b/src/helper/view.js index c79f2a9c..c6bf6e8b 100644 --- a/src/helper/view.js +++ b/src/helper/view.js @@ -47,7 +47,14 @@ const resizeCrosshair = () => { const zoomOnFixedPoint = (deltaZoom, fixedPoint) => { const view = paper.view; const preZoomCenter = view.center; - const newZoom = Math.max(0.5, view.zoom + deltaZoom); + let newZoom; + if (view.zoom <= 0.5 && deltaZoom < 0) { + newZoom = 0.333; + } else if (view.zoom <= 0.333 && deltaZoom > 0) { + newZoom = 0.5; + } else { + newZoom = Math.max(0.5, view.zoom + deltaZoom); + } const scaling = view.zoom / newZoom; const preZoomOffset = fixedPoint.subtract(preZoomCenter); const postZoomOffset = fixedPoint.subtract(preZoomOffset.multiply(scaling)) From 337f12b836275f89ca83e6045f0ec46f2d7e25b5 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 21 Apr 2020 18:04:26 -0400 Subject: [PATCH 4/8] Fill the available space in the costume tab --- src/components/button-group/button-group.css | 3 +- src/components/dropdown/dropdown.css | 2 - src/components/forms/label.css | 7 +- src/components/input-group/input-group.css | 10 +- src/components/paint-editor/paint-editor.css | 39 ++++-- src/components/paint-editor/paint-editor.jsx | 2 +- src/containers/paper-canvas.css | 8 +- src/containers/paper-canvas.jsx | 92 ++++++++++--- src/containers/scrollable-canvas.jsx | 11 +- src/helper/bitmap.js | 22 ++- src/helper/blob-tools/blob.js | 3 +- src/helper/layer.js | 91 +++++++++++-- src/helper/selection-tools/move-tool.js | 13 +- src/helper/selection-tools/nudge-tool.js | 15 ++- src/helper/selection-tools/point-tool.js | 7 +- src/helper/selection-tools/scale-tool.js | 7 +- .../selection-tools/selection-box-tool.js | 11 +- src/helper/undo.js | 4 +- src/helper/view.js | 127 +++++++++++++++--- src/hocs/update-image-hoc.jsx | 32 ++++- src/playground/index.ejs | 5 - src/playground/playground.css | 15 +++ src/playground/playground.jsx | 2 + 23 files changed, 414 insertions(+), 114 deletions(-) create mode 100644 src/playground/playground.css diff --git a/src/components/button-group/button-group.css b/src/components/button-group/button-group.css index 65634efe..bdd7e295 100644 --- a/src/components/button-group/button-group.css +++ b/src/components/button-group/button-group.css @@ -1,6 +1,7 @@ @import "../../css/units"; .button-group { - display: flex; + display: inline-flex; + flex-direction: row; padding: 0 $grid-unit; } diff --git a/src/components/dropdown/dropdown.css b/src/components/dropdown/dropdown.css index 6ea54003..6219da4c 100644 --- a/src/components/dropdown/dropdown.css +++ b/src/components/dropdown/dropdown.css @@ -9,8 +9,6 @@ $arrow-border-width: 14px; min-width: 3.5rem; color: $motion-primary; padding: .5rem; - display: flex; - align-items: center; } .mod-open { diff --git a/src/components/forms/label.css b/src/components/forms/label.css index 5c43af82..e55c4231 100644 --- a/src/components/forms/label.css +++ b/src/components/forms/label.css @@ -4,12 +4,7 @@ See https://github.com/LLK/scratch-paint/issues/13 */ @import "../../css/units.css"; @import "../../css/colors.css"; - -.input-group { - display: inline-flex; - flex-direction: row; - align-items: center; -} +@import "../input-group/input-group.css"; .input-label, .input-label-secondary { font-size: 0.625rem; diff --git a/src/components/input-group/input-group.css b/src/components/input-group/input-group.css index 25eb16d3..6ec77247 100644 --- a/src/components/input-group/input-group.css +++ b/src/components/input-group/input-group.css @@ -1,5 +1,11 @@ @import '../../css/units.css'; +.input-group { + display: inline-flex; + flex-direction: row; + align-items: center; +} + [dir="ltr"] .input-group + .input-group { margin-left: calc(2 * $grid-unit); } @@ -8,10 +14,6 @@ margin-right: calc(2 * $grid-unit); } -.input-group { - display:flex; -} - .disabled { opacity: 0.3; /* Prevent any user actions */ diff --git a/src/components/paint-editor/paint-editor.css b/src/components/paint-editor/paint-editor.css index b18fa99f..77e3ef67 100644 --- a/src/components/paint-editor/paint-editor.css +++ b/src/components/paint-editor/paint-editor.css @@ -2,6 +2,8 @@ @import "../../css/units.css"; .editor-container { + width: 100%; + height: 100%; display: flex; flex-direction: column; padding: calc(3 * $grid-unit); @@ -20,8 +22,10 @@ .top-align-row { display: flex; - padding-top: calc(5 * $grid-unit); flex-direction: row; + height: 100%; + padding-top: calc(5 * $grid-unit); + min-width: 524px; } .row + .row { @@ -114,9 +118,19 @@ $border-radius: 0.25rem; margin-left: calc(2 * $grid-unit); } +.controls-container { + width: 100%; + display: flex; + flex-flow: column; + flex-grow: 1; + margin-left: calc(2 * $grid-unit); + margin-right: calc(2 * $grid-unit); +} + .canvas-container { - width: 480px; - height: 360px; + width: 100%; + flex-grow: 1; + min-width: 402px; /* Leave room for the border */ box-sizing: content-box; border: 1px solid #e8edf1; border-radius: .25rem; @@ -126,7 +140,7 @@ $border-radius: 0.25rem; .mode-selector { display: flex; - max-width: 6rem; + max-width: 7.5rem; flex-direction: row; flex-wrap: wrap; align-items: flex-start; @@ -134,14 +148,6 @@ $border-radius: 0.25rem; justify-content: space-between; } -[dir="ltr"] .mode-selector { - margin-right: calc(2 * $grid-unit); -} - -[dir="rtl"] .mode-selector { - margin-left: calc(2 * $grid-unit); -} - .zoom-controls { display: flex; flex-direction: row-reverse; @@ -158,7 +164,8 @@ $border-radius: 0.25rem; .canvas-controls { display: flex; - margin-top: .25rem; + height: 36px; + margin-top: $grid-unit; justify-content: space-between; } @@ -188,10 +195,14 @@ $border-radius: 0.25rem; } .mode-selector { - margin-right: $grid-unit; flex-direction: column; justify-content: flex-start; } + + .controls-container { + margin-right: $grid-unit; + margin-left: $grid-unit; + } } .text-area { diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 4f238312..b4a2844d 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -196,7 +196,7 @@ const PaintEditorComponent = props => ( ) : null} -
+
{/* Canvas */} { @@ -134,8 +156,10 @@ class PaperCanvas extends React.Component { imgElement, (ART_BOARD_WIDTH / 2) - rotationCenterX, (ART_BOARD_HEIGHT / 2) - rotationCenterY); + this.maybeZoomToFit(true /* isBitmap */); performSnapshot(this.props.undoSnapshot, Formats.BITMAP_SKIP_CONVERT); + this.recalibrateSize(); }; imgElement.src = image; } else if (format === 'svg') { @@ -145,6 +169,7 @@ class PaperCanvas extends React.Component { log.error(`Didn't recognize format: ${format}. Use 'jpg', 'png' or 'svg'.`); this.props.changeFormat(Formats.VECTOR_SKIP_CONVERT); performSnapshot(this.props.undoSnapshot, Formats.VECTOR_SKIP_CONVERT); + this.recalibrateSize(); } } maybeZoomToFit (isBitmapMode) { @@ -154,9 +179,10 @@ class PaperCanvas extends React.Component { resizeCrosshair(); } else if (this.shouldZoomToFit === true) { zoomToFit(isBitmapMode); - this.props.updateViewBounds(paper.view.matrix); } this.shouldZoomToFit = false; + setWorkspaceBounds(); + this.props.updateViewBounds(paper.view.matrix); } importSvg (svg, rotationCenterX, rotationCenterY) { const paperCanvas = this; @@ -200,6 +226,10 @@ class PaperCanvas extends React.Component { // positioned incorrectly paperCanvas.queuedImport = window.setTimeout(() => { + // Detached + if (!paper.view) return; + paper.view.setViewSize(paper.DomElement.getSize(paper.view.element)); + paperCanvas.props.updateViewBounds(paper.view.matrix); paperCanvas.initializeSvg(item, rotationCenterX, rotationCenterY, viewBox); }, 0); } @@ -210,18 +240,28 @@ class PaperCanvas extends React.Component { const itemWidth = item.bounds.width; const itemHeight = item.bounds.height; - // Remove viewbox + // Get reference to viewbox + let mask; if (item.clipped) { - let mask; for (const child of item.children) { if (child.isClipMask()) { mask = child; break; } } - item.clipped = false; - mask.remove(); + mask.clipMask = false; + } else { + mask = new paper.Shape.Rectangle(item.bounds); } + mask.guide = true; + mask.locked = true; + mask.matrix = new paper.Matrix(); // Identity + // Set the artwork to get clipped at the max costume size + mask.size.height = MAX_WORKSPACE_BOUNDS.height; + mask.size.width = MAX_WORKSPACE_BOUNDS.width; + mask.setPosition(CENTER); + paper.project.activeLayer.addChild(mask); + mask.clipMask = true; // Reduce single item nested in groups if (item instanceof paper.Group && item.children.length === 1) { @@ -231,18 +271,18 @@ class PaperCanvas extends React.Component { ensureClockwise(item); scaleWithStrokes(item, 2, new paper.Point()); // Import at 2x + // Apply rotation center if (typeof rotationCenterX !== 'undefined' && typeof rotationCenterY !== 'undefined') { let rotationPoint = new paper.Point(rotationCenterX, rotationCenterY); if (viewBox && viewBox.length >= 2 && !isNaN(viewBox[0]) && !isNaN(viewBox[1])) { rotationPoint = rotationPoint.subtract(viewBox[0], viewBox[1]); } - item.translate(new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2) - .subtract(rotationPoint.multiply(2))); + item.translate(CENTER.subtract(rotationPoint.multiply(2))); } else { // Center - item.translate(new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2) - .subtract(itemWidth, itemHeight)); + item.translate(CENTER.subtract(itemWidth, itemHeight)); } + paper.project.activeLayer.insertChild(0, item); if (isGroup(item)) { // Fixes an issue where we may export empty groups @@ -253,9 +293,25 @@ class PaperCanvas extends React.Component { } ungroupItems([item]); } + performSnapshot(this.props.undoSnapshot, Formats.VECTOR_SKIP_CONVERT); this.maybeZoomToFit(); } + onViewResize () { + setWorkspaceBounds(true /* clipEmpty */); + clampViewBounds(); + // Fix incorrect paper canvas scale on browser zoom reset + this.recalibrateSize(); + this.props.updateViewBounds(paper.view.matrix); + } + recalibrateSize () { + // Sets the size that Paper thinks the canvas is to the size the canvas element actually is. + // When these are out of sync, the mouse events in the paint editor don't line up correctly. + window.setTimeout(() => { + if (!paper.view) return; + paper.view.setViewSize(paper.DomElement.getSize(paper.view.element)); + }); + } setCanvas (canvas) { this.canvas = canvas; if (this.props.canvasRef) { @@ -266,10 +322,9 @@ class PaperCanvas extends React.Component { return ( ); } @@ -283,6 +338,7 @@ PaperCanvas.propTypes = { clearSelectedItems: PropTypes.func.isRequired, clearUndo: PropTypes.func.isRequired, cursor: PropTypes.string, + format: PropTypes.oneOf(Object.keys(Formats)), image: PropTypes.oneOfType([ PropTypes.string, PropTypes.instanceOf(HTMLImageElement) diff --git a/src/containers/scrollable-canvas.jsx b/src/containers/scrollable-canvas.jsx index b8fb2191..2989798b 100644 --- a/src/containers/scrollable-canvas.jsx +++ b/src/containers/scrollable-canvas.jsx @@ -5,7 +5,7 @@ import React from 'react'; import {connect} from 'react-redux'; import ScrollableCanvasComponent from '../components/scrollable-canvas/scrollable-canvas.jsx'; -import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, clampViewBounds, pan, zoomOnFixedPoint} from '../helper/view'; +import {clampViewBounds, pan, zoomOnFixedPoint, getWorkspaceBounds} from '../helper/view'; import {updateViewBounds} from '../reducers/view-bounds'; import {redrawSelectionBox} from '../reducers/selected-items'; @@ -132,11 +132,12 @@ class ScrollableCanvas extends React.Component { let topPercent = 0; let leftPercent = 0; if (paper.project) { + const bounds = getWorkspaceBounds(); const {x, y, width, height} = paper.view.bounds; - widthPercent = Math.min(100, 100 * width / ART_BOARD_WIDTH); - heightPercent = Math.min(100, 100 * height / ART_BOARD_HEIGHT); - const centerX = (x + (width / 2)) / ART_BOARD_WIDTH; - const centerY = (y + (height / 2)) / ART_BOARD_HEIGHT; + widthPercent = Math.min(100, 100 * width / bounds.width); + heightPercent = Math.min(100, 100 * height / bounds.height); + const centerX = (x + (width / 2) - bounds.x) / bounds.width; + const centerY = (y + (height / 2) - bounds.y) / bounds.height; topPercent = Math.max(0, (100 * centerY) - (heightPercent / 2)); leftPercent = Math.max(0, (100 * centerX) - (widthPercent / 2)); } diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js index 01128020..93ff589c 100644 --- a/src/helper/bitmap.js +++ b/src/helper/bitmap.js @@ -2,6 +2,7 @@ import paper from '@scratch/paper'; import {createCanvas, clearRaster, getRaster, hideGuideLayers, showGuideLayers} from './layer'; import {getGuideColor} from './guides'; import {clearSelection} from './selection'; +import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, CENTER, MAX_WORKSPACE_BOUNDS} from './view'; import {inlineSvgFonts} from 'scratch-svg-renderer'; import Formats from '../lib/format'; @@ -399,7 +400,17 @@ const convertToBitmap = function (clearSelectedItems, onUpdateImage) { img, new paper.Point(Math.floor(bounds.topLeft.x), Math.floor(bounds.topLeft.y))); } - paper.project.activeLayer.removeChildren(); + for (let i = paper.project.activeLayer.children.length - 1; i >= 0; i--) { + const item = paper.project.activeLayer.children[i]; + if (item.clipMask === false) { + item.remove(); + } else { + // Resize mask for bitmap bounds + item.size.height = ART_BOARD_HEIGHT; + item.size.width = ART_BOARD_WIDTH; + item.setPosition(CENTER); + } + } onUpdateImage(false /* skipSnapshot */, Formats.BITMAP /* formatOverride */); }; img.onerror = () => { @@ -420,7 +431,16 @@ const convertToBitmap = function (clearSelectedItems, onUpdateImage) { const convertToVector = function (clearSelectedItems, onUpdateImage) { clearSelection(clearSelectedItems); + for (const item of paper.project.activeLayer.children) { + if (item.clipMask === true) { + // Resize mask for vector bounds + item.size.height = MAX_WORKSPACE_BOUNDS.height; + item.size.width = MAX_WORKSPACE_BOUNDS.width; + item.setPosition(CENTER); + } + } getTrimmedRaster(true /* shouldInsert */); + clearRaster(); onUpdateImage(false /* skipSnapshot */, Formats.VECTOR /* formatOverride */); }; diff --git a/src/helper/blob-tools/blob.js b/src/helper/blob-tools/blob.js index 60615f85..4133d5cb 100644 --- a/src/helper/blob-tools/blob.js +++ b/src/helper/blob-tools/blob.js @@ -4,7 +4,7 @@ import BroadBrushHelper from './broad-brush-helper'; import SegmentBrushHelper from './segment-brush-helper'; import {MIXED, styleCursorPreview} from '../../helper/style-path'; import {clearSelection, getItems} from '../../helper/selection'; -import {getGuideLayer} from '../../helper/layer'; +import {getGuideLayer, setGuideItem} from '../../helper/layer'; /** * Shared code for the brush and eraser mode. Adds functions on the paper tool object @@ -181,6 +181,7 @@ class Blobbiness { }); this.cursorPreview.parent = getGuideLayer(); this.cursorPreview.data.isHelperItem = true; + setGuideItem(this.cursorPreview); } this.cursorPreview.position = this.cursorPreviewLastPoint; this.cursorPreview.radius = this.options.brushSize / 2; diff --git a/src/helper/layer.js b/src/helper/layer.js index a3928818..f8a1b738 100644 --- a/src/helper/layer.js +++ b/src/helper/layer.js @@ -1,7 +1,8 @@ import paper from '@scratch/paper'; import log from '../log/log'; -import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, CENTER} from './view'; +import {ART_BOARD_BOUNDS, ART_BOARD_WIDTH, ART_BOARD_HEIGHT, CENTER, MAX_WORKSPACE_BOUNDS} from './view'; import {isGroupItem} from './item'; +import {isBitmap, isVector} from '../lib/format'; const CROSSHAIR_SIZE = 16; const CROSSHAIR_FULL_OPACITY = 0.75; @@ -42,7 +43,7 @@ const clearRaster = function () { raster.parent = layer; raster.guide = true; raster.locked = true; - raster.position = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2); + raster.position = CENTER; }; const getRaster = function () { @@ -62,6 +63,20 @@ const getBackgroundGuideLayer = function () { return _getLayer('isBackgroundGuideLayer'); }; +const _convertLayer = function (layer, format) { + if (isVector(format)) { + layer.vectorBackground.visible = true; + layer.bitmapBackground.visible = false; + } else if (isBitmap(format)) { + layer.bitmapBackground.visible = true; + layer.vectorBackground.visible = false; + } +}; + +const convertBackgroundGuideLayer = function (format) { + _convertLayer(getBackgroundGuideLayer(), format); +}; + const _makeGuideLayer = function () { const guideLayer = new paper.Layer(); guideLayer.data.isGuideLayer = true; @@ -95,8 +110,10 @@ const setGuideItem = function (item) { const hideGuideLayers = function (includeRaster) { const backgroundGuideLayer = getBackgroundGuideLayer(); const dragCrosshairLayer = getDragCrosshairLayer(); + const outlineLayer = _getLayer('isOutlineLayer'); const guideLayer = getGuideLayer(); dragCrosshairLayer.remove(); + outlineLayer.remove(); guideLayer.remove(); backgroundGuideLayer.remove(); let rasterLayer; @@ -106,6 +123,7 @@ const hideGuideLayers = function (includeRaster) { } return { dragCrosshairLayer: dragCrosshairLayer, + outlineLayer: outlineLayer, guideLayer: guideLayer, backgroundGuideLayer: backgroundGuideLayer, rasterLayer: rasterLayer @@ -120,6 +138,7 @@ const hideGuideLayers = function (includeRaster) { const showGuideLayers = function (guideLayers) { const backgroundGuideLayer = guideLayers.backgroundGuideLayer; const dragCrosshairLayer = guideLayers.dragCrosshairLayer; + const outlineLayer = guideLayers.outlineLayer; const guideLayer = guideLayers.guideLayer; const rasterLayer = guideLayers.rasterLayer; if (rasterLayer && !rasterLayer.index) { @@ -134,6 +153,10 @@ const showGuideLayers = function (guideLayers) { paper.project.addLayer(dragCrosshairLayer); dragCrosshairLayer.bringToFront(); } + if (!outlineLayer.index) { + paper.project.addLayer(outlineLayer); + outlineLayer.bringToFront(); + } if (!guideLayer.index) { paper.project.addLayer(guideLayer); guideLayer.bringToFront(); @@ -157,7 +180,7 @@ const _makeRasterLayer = function () { return rasterLayer; }; -const _makeBackgroundPaper = function (width, height, color) { +const _makeBackgroundPaper = function (width, height, color, opacity) { // creates a checkerboard path of width * height squares in color on white let x = 0; let y = 0; @@ -180,12 +203,22 @@ const _makeBackgroundPaper = function (width, height, color) { vRect.fillColor = '#fff'; vRect.guide = true; vRect.locked = true; + vRect.position = CENTER; const vPath = new paper.Path(pathPoints); vPath.fillRule = 'evenodd'; vPath.fillColor = color; + vPath.opacity = opacity; vPath.guide = true; vPath.locked = true; - const vGroup = new paper.Group([vRect, vPath]); + vPath.position = CENTER; + const mask = new paper.Shape.Rectangle(MAX_WORKSPACE_BOUNDS); + mask.position = CENTER; + mask.guide = true; + mask.locked = true; + mask.scale(1 / 8); + const vGroup = new paper.Group([vRect, vPath, mask]); + mask.clipMask = true; + return vGroup; }; @@ -230,7 +263,6 @@ const _makeCrosshair = function (opacity, parent) { crosshair.applyMatrix = false; parent.dragCrosshair = crosshair; crosshair.scale(CROSSHAIR_SIZE / crosshair.bounds.width / paper.view.zoom); - }; const _makeDragCrosshairLayer = function () { @@ -241,30 +273,64 @@ const _makeDragCrosshairLayer = function () { return dragCrosshairLayer; }; -const _makeBackgroundGuideLayer = function () { +const _makeOutlineLayer = function () { + const outlineLayer = new paper.Layer(); + const whiteRect = new paper.Shape.Rectangle(ART_BOARD_BOUNDS.expand(1)); + whiteRect.strokeWidth = 2; + whiteRect.strokeColor = 'white'; + setGuideItem(whiteRect); + const blueRect = new paper.Shape.Rectangle(ART_BOARD_BOUNDS.expand(5)); + blueRect.strokeWidth = 2; + blueRect.strokeColor = '#4280D7'; + blueRect.opacity = 0.25; + setGuideItem(blueRect); + outlineLayer.data.isOutlineLayer = true; + return outlineLayer; +}; + +const _makeBackgroundGuideLayer = function (format) { const guideLayer = new paper.Layer(); guideLayer.locked = true; + + const vWorkspaceBounds = new paper.Shape.Rectangle(MAX_WORKSPACE_BOUNDS); + vWorkspaceBounds.fillColor = '#ECF1F9'; + vWorkspaceBounds.position = CENTER; - const vBackground = _makeBackgroundPaper(120, 90, '#E5E5E5'); - vBackground.position = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2); + const vBackground = _makeBackgroundPaper(180, 136, '#0062ff', 0.05); + vBackground.position = CENTER; vBackground.scaling = new paper.Point(8, 8); - vBackground.guide = true; - vBackground.locked = true; + const vectorBackground = new paper.Group(); + vectorBackground.addChild(vWorkspaceBounds); + vectorBackground.addChild(vBackground); + setGuideItem(vectorBackground); + guideLayer.vectorBackground = vectorBackground; + + const bitmapBackground = _makeBackgroundPaper(120, 90, '#0062ff', 0.05); + bitmapBackground.position = CENTER; + bitmapBackground.scaling = new paper.Point(8, 8); + bitmapBackground.guide = true; + bitmapBackground.locked = true; + guideLayer.bitmapBackground = bitmapBackground; + + _convertLayer(guideLayer, format); + _makeCrosshair(0.16, guideLayer); guideLayer.data.isBackgroundGuideLayer = true; return guideLayer; }; -const setupLayers = function () { - const backgroundGuideLayer = _makeBackgroundGuideLayer(); +const setupLayers = function (format) { + const backgroundGuideLayer = _makeBackgroundGuideLayer(format); _makeRasterLayer(); const paintLayer = _makePaintingLayer(); const dragCrosshairLayer = _makeDragCrosshairLayer(); + const outlineLayer = _makeOutlineLayer(); const guideLayer = _makeGuideLayer(); backgroundGuideLayer.sendToBack(); dragCrosshairLayer.bringToFront(); + outlineLayer.bringToFront(); guideLayer.bringToFront(); paintLayer.activate(); }; @@ -278,6 +344,7 @@ export { getDragCrosshairLayer, getGuideLayer, getBackgroundGuideLayer, + convertBackgroundGuideLayer, clearRaster, getRaster, setGuideItem, diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js index dc5e0fc7..410aa700 100644 --- a/src/helper/selection-tools/move-tool.js +++ b/src/helper/selection-tools/move-tool.js @@ -1,9 +1,10 @@ import paper from '@scratch/paper'; import Modes from '../../lib/modes'; +import {BitmapModes} from '../../lib/modes'; import {isGroup} from '../group'; import {isCompoundPathItem, getRootItem} from '../item'; import {checkPointsClose, snapDeltaToAngle} from '../math'; -import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, CENTER} from '../view'; +import {getActionBounds, CENTER} from '../view'; import {clearSelection, cloneSelection, getSelectedLeafItems, getSelectedRootItems, setItemSelection} from '../selection'; import {getDragCrosshairLayer, CROSSHAIR_FULL_OPACITY} from '../layer'; @@ -123,8 +124,10 @@ class MoveTool { } onMouseDrag (event) { const point = event.point; - point.x = Math.max(0, Math.min(point.x, ART_BOARD_WIDTH)); - point.y = Math.max(0, Math.min(point.y, ART_BOARD_HEIGHT)); + const actionBounds = getActionBounds(this.mode in BitmapModes); + + point.x = Math.max(actionBounds.left, Math.min(point.x, actionBounds.right)); + point.y = Math.max(actionBounds.top, Math.min(point.y, actionBounds.bottom)); const dragVector = point.subtract(event.downPoint); let snapVector; @@ -135,7 +138,7 @@ class MoveTool { this.selectionCenter.add(dragVector), CENTER, SNAPPING_THRESHOLD / paper.view.zoom /* threshold */)) { - + snapVector = CENTER.subtract(this.selectionCenter); } } @@ -180,6 +183,7 @@ class MoveTool { (CENTER.y > bounds.bottom && CENTER.x < bounds.left) || (CENTER.y < bounds.top && CENTER.x > bounds.right) || (CENTER.y > bounds.bottom && CENTER.x > bounds.right)) { + // rotation center is to one of the 4 corners of the selection bounding box const distX = Math.max(CENTER.x - bounds.right, bounds.left - CENTER.x); const distY = Math.max(CENTER.y - bounds.bottom, bounds.top - CENTER.y); @@ -196,7 +200,6 @@ class MoveTool { (1 - ((Math.abs(CENTER.x - newCenter.x) - (bounds.width / 2)) / (FADE_DISTANCE / paper.view.zoom)))); } // else the rotation center is within selection bounds, always show drag crosshair at full opacity getDragCrosshairLayer().opacity = CROSSHAIR_FULL_OPACITY * opacityMultiplier; - } onMouseUp () { this.firstDrag = false; diff --git a/src/helper/selection-tools/nudge-tool.js b/src/helper/selection-tools/nudge-tool.js index 5b2b9ddd..96c453bd 100644 --- a/src/helper/selection-tools/nudge-tool.js +++ b/src/helper/selection-tools/nudge-tool.js @@ -1,6 +1,6 @@ import paper from '@scratch/paper'; import {getSelectedRootItems} from '../selection'; -import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view'; +import {getActionBounds} from '../view'; import {BitmapModes} from '../../lib/modes'; const NUDGE_MORE_MULTIPLIER = 15; @@ -41,16 +41,21 @@ class NudgeTool { rect = item.bounds; } } + const bounds = getActionBounds(this.boundingBoxTool.isBitmap); + const bottom = bounds.bottom - rect.top - 1; + const top = bounds.top - rect.bottom + 1; + const left = bounds.left - rect.right + 1; + const right = bounds.right - rect.left - 1; let translation; if (event.key === 'up') { - translation = new paper.Point(0, -Math.min(nudgeAmount, rect.bottom - 1)); + translation = new paper.Point(0, Math.min(bottom, Math.max(-nudgeAmount, top))); } else if (event.key === 'down') { - translation = new paper.Point(0, Math.min(nudgeAmount, ART_BOARD_HEIGHT - rect.top - 1)); + translation = new paper.Point(0, Math.max(top, Math.min(nudgeAmount, bottom))); } else if (event.key === 'left') { - translation = new paper.Point(-Math.min(nudgeAmount, rect.right - 1), 0); + translation = new paper.Point(Math.min(right, Math.max(-nudgeAmount, left)), 0); } else if (event.key === 'right') { - translation = new paper.Point(Math.min(nudgeAmount, ART_BOARD_WIDTH - rect.left - 1), 0); + translation = new paper.Point(Math.max(left, Math.min(nudgeAmount, right)), 0); } if (translation) { diff --git a/src/helper/selection-tools/point-tool.js b/src/helper/selection-tools/point-tool.js index 5b141d32..1c0bafb3 100644 --- a/src/helper/selection-tools/point-tool.js +++ b/src/helper/selection-tools/point-tool.js @@ -1,6 +1,6 @@ import paper from '@scratch/paper'; import {HANDLE_RATIO, snapDeltaToAngle} from '../math'; -import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view'; +import {getActionBounds} from '../view'; import {clearSelection, getSelectedLeafItems, getSelectedSegments} from '../selection'; /** Subtool of ReshapeTool for moving control points. */ @@ -146,8 +146,9 @@ class PointTool { this.deleteOnMouseUp = null; const point = event.point; - point.x = Math.max(0, Math.min(point.x, ART_BOARD_WIDTH)); - point.y = Math.max(0, Math.min(point.y, ART_BOARD_HEIGHT)); + const bounds = getActionBounds(); + point.x = Math.max(bounds.left, Math.min(point.x, bounds.right)); + point.y = Math.max(bounds.top, Math.min(point.y, bounds.bottom)); if (!this.lastPoint) this.lastPoint = event.lastPoint; const dragVector = point.subtract(event.downPoint); diff --git a/src/helper/selection-tools/scale-tool.js b/src/helper/selection-tools/scale-tool.js index 23e232f5..21df70dd 100644 --- a/src/helper/selection-tools/scale-tool.js +++ b/src/helper/selection-tools/scale-tool.js @@ -1,6 +1,6 @@ import paper from '@scratch/paper'; import {getItems} from '../selection'; -import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view'; +import {getActionBounds} from '../view'; import {BitmapModes} from '../../lib/modes'; /** @@ -74,8 +74,9 @@ class ScaleTool { onMouseDrag (event) { if (!this.active) return; const point = event.point; - point.x = Math.max(0, Math.min(point.x, ART_BOARD_WIDTH)); - point.y = Math.max(0, Math.min(point.y, ART_BOARD_HEIGHT)); + const bounds = getActionBounds(this.isBitmap); + point.x = Math.max(bounds.left, Math.min(point.x, bounds.right)); + point.y = Math.max(bounds.top, Math.min(point.y, bounds.bottom)); if (!this.lastPoint) this.lastPoint = event.lastPoint; const delta = point.subtract(this.lastPoint); diff --git a/src/helper/selection-tools/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js index e88d79d6..76bf084d 100644 --- a/src/helper/selection-tools/selection-box-tool.js +++ b/src/helper/selection-tools/selection-box-tool.js @@ -44,7 +44,7 @@ class SelectionBoxTool { } onMouseUpBitmap (event) { if (event.event.button > 0) return; // only first mouse button - if (this.selectionRect) { + if (this.selectionRect && this.selectionRect.bounds.intersects(getRaster().bounds)) { const rect = new paper.Rectangle({ from: new paper.Point( Math.max(0, Math.round(this.selectionRect.bounds.topLeft.x)), @@ -54,10 +54,6 @@ class SelectionBoxTool { Math.min(ART_BOARD_HEIGHT, Math.round(this.selectionRect.bounds.bottomRight.y))) }); - // Remove dotted rectangle - this.selectionRect.remove(); - this.selectionRect = null; - if (rect.area) { // Pull selected raster to active layer const raster = getRaster().getSubRaster(rect); @@ -75,6 +71,11 @@ class SelectionBoxTool { this.setSelectedItems(); } } + if (this.selectionRect) { + // Remove dotted rectangle + this.selectionRect.remove(); + this.selectionRect = null; + } } } diff --git a/src/helper/undo.js b/src/helper/undo.js index ed36e9e9..9961a16e 100644 --- a/src/helper/undo.js +++ b/src/helper/undo.js @@ -27,7 +27,9 @@ const performSnapshot = function (dispatchPerformSnapshot, format) { const _restore = function (entry, setSelectedItems, onUpdateImage, isBitmapMode) { for (let i = paper.project.layers.length - 1; i >= 0; i--) { const layer = paper.project.layers[i]; - if (!layer.data.isBackgroundGuideLayer && !layer.data.isDragCrosshairLayer) { + if (!layer.data.isBackgroundGuideLayer && + !layer.data.isDragCrosshairLayer && + !layer.data.isOutlineLayer) { layer.removeChildren(); layer.remove(); } diff --git a/src/helper/view.js b/src/helper/view.js index c6bf6e8b..d622ccd1 100644 --- a/src/helper/view.js +++ b/src/helper/view.js @@ -1,34 +1,95 @@ import paper from '@scratch/paper'; -import {getSelectedRootItems} from './selection'; import {CROSSHAIR_SIZE, getBackgroundGuideLayer, getDragCrosshairLayer, getRaster} from './layer'; +import {getAllRootItems, getSelectedRootItems} from './selection'; import {getHitBounds} from './bitmap'; +import log from '../log/log'; // Vectors are imported and exported at SVG_ART_BOARD size. // Once they are imported however, both SVGs and bitmaps are on // canvases of ART_BOARD size. +// (This is for backwards compatibility, to handle both assets +// designed for 480 x 360, and bitmap resolution 2 bitmaps) const SVG_ART_BOARD_WIDTH = 480; const SVG_ART_BOARD_HEIGHT = 360; -const ART_BOARD_WIDTH = 480 * 2; -const ART_BOARD_HEIGHT = 360 * 2; +const ART_BOARD_WIDTH = SVG_ART_BOARD_WIDTH * 2; +const ART_BOARD_HEIGHT = SVG_ART_BOARD_HEIGHT * 2; const CENTER = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2); const PADDING_PERCENT = 25; // Padding as a percent of the max of width/height of the sprite +const BUFFER = 50; // Number of pixels of allowance around objects at the edges of the workspace const MIN_RATIO = .125; // Zoom in to at least 1/8 of the screen. This way you don't end up incredibly -// zoomed in for tiny costumes. +// zoomed in for tiny costumes. +const ART_BOARD_BOUNDS = new paper.Rectangle(0, 0, ART_BOARD_WIDTH, ART_BOARD_HEIGHT); +const MAX_WORKSPACE_BOUNDS = new paper.Rectangle( + -ART_BOARD_WIDTH / 4, + -ART_BOARD_HEIGHT / 4, + ART_BOARD_WIDTH * 1.5, + ART_BOARD_HEIGHT * 1.5); + +let _workspaceBounds = ART_BOARD_BOUNDS; + +const getWorkspaceBounds = () => _workspaceBounds; + +/** +* The workspace bounds define the areas that the scroll bars can access. +* They include at minimum the artboard, and extend to a bit beyond the +* farthest item off tne edge in any given direction (so items can't be +* "lost" off the edge) +* +* @param {boolean} clipEmpty Clip empty space from bounds, even if it +* means discontinuously jumping the viewport. This should probably be +* false unless the viewport is going to move discontinuously anyway +* (such as in a zoom button click) +*/ +const setWorkspaceBounds = clipEmpty => { + const items = getAllRootItems(); + // Include the artboard and what's visible in the viewport + let bounds = ART_BOARD_BOUNDS; + if (!clipEmpty) { + bounds = bounds.unite(paper.view.bounds); + } + // Include everything the user has drawn and a buffer around it + for (const item of items) { + bounds = bounds.unite(item.bounds.expand(BUFFER)); + } + // Limit to max workspace bounds + bounds = bounds.intersect(MAX_WORKSPACE_BOUNDS.expand(BUFFER)); + let top = bounds.top; + let left = bounds.left; + let bottom = bounds.bottom; + let right = bounds.right; + + // Center in view if viewport is larger than workspace + let hDiff = 0; + let vDiff = 0; + if (bounds.width < paper.view.bounds.width) { + hDiff = (paper.view.bounds.width - bounds.width) / 2; + left -= hDiff; + right += hDiff; + } + if (bounds.height < paper.view.bounds.height) { + vDiff = (paper.view.bounds.height - bounds.height) / 2; + top -= vDiff; + bottom += vDiff; + } + + _workspaceBounds = new paper.Rectangle(left, top, right - left, bottom - top); +}; const clampViewBounds = () => { const {left, right, top, bottom} = paper.project.view.bounds; - if (left < 0) { - paper.project.view.scrollBy(new paper.Point(-left, 0)); + if (left < _workspaceBounds.left) { + paper.project.view.scrollBy(new paper.Point(_workspaceBounds.left - left, 0)); } - if (top < 0) { - paper.project.view.scrollBy(new paper.Point(0, -top)); + if (top < _workspaceBounds.top) { + paper.project.view.scrollBy(new paper.Point(0, _workspaceBounds.top - top)); } - if (bottom > ART_BOARD_HEIGHT) { - paper.project.view.scrollBy(new paper.Point(0, ART_BOARD_HEIGHT - bottom)); + if (bottom > _workspaceBounds.bottom) { + paper.project.view.scrollBy(new paper.Point(0, _workspaceBounds.bottom - bottom)); } - if (right > ART_BOARD_WIDTH) { - paper.project.view.scrollBy(new paper.Point(ART_BOARD_WIDTH - right, 0)); + if (right > _workspaceBounds.right) { + paper.project.view.scrollBy(new paper.Point(_workspaceBounds.right - right, 0)); } + setWorkspaceBounds(); }; const resizeCrosshair = () => { @@ -61,6 +122,8 @@ const zoomOnFixedPoint = (deltaZoom, fixedPoint) => { .subtract(preZoomCenter); view.zoom = newZoom; view.translate(postZoomOffset.multiply(-1)); + + setWorkspaceBounds(true /* clipEmpty */); clampViewBounds(); resizeCrosshair(); }; @@ -87,6 +150,7 @@ const zoomOnSelection = deltaZoom => { const resetZoom = () => { paper.project.view.zoom = .5; + setWorkspaceBounds(true /* clipEmpty */); resizeCrosshair(); clampViewBounds(); }; @@ -96,18 +160,40 @@ const pan = (dx, dy) => { clampViewBounds(); }; +/** + * Mouse actions are clamped to action bounds + * @param {boolean} isBitmap True if the editor is in bitmap mode, false if it is in vector mode + * @returns {paper.Rectangle} the bounds within which mouse events should work in the paint editor + */ +const getActionBounds = isBitmap => { + if (isBitmap) { + return ART_BOARD_BOUNDS; + } + return paper.view.bounds.unite(ART_BOARD_BOUNDS).intersect(MAX_WORKSPACE_BOUNDS); +}; + const zoomToFit = isBitmap => { resetZoom(); let bounds; if (isBitmap) { - bounds = getHitBounds(getRaster()); + bounds = getHitBounds(getRaster()).expand(BUFFER); } else { - bounds = paper.project.activeLayer.bounds; + const items = getAllRootItems(); + for (const item of items) { + if (bounds) { + bounds = bounds.unite(item.bounds); + } else { + bounds = item.bounds; + } + } } if (bounds && bounds.width && bounds.height) { - // Ratio of (sprite length plus padding on all sides) to art board length. - let ratio = Math.max(bounds.width * (1 + (2 * PADDING_PERCENT / 100)) / ART_BOARD_WIDTH, - bounds.height * (1 + (2 * PADDING_PERCENT / 100)) / ART_BOARD_HEIGHT); + const canvas = paper.view.element; + // Ratio of (sprite length plus padding on all sides) to viewport length. + let ratio = paper.view.zoom * + Math.max( + bounds.width * (1 + (2 * PADDING_PERCENT / 100)) / canvas.clientWidth, + bounds.height * (1 + (2 * PADDING_PERCENT / 100)) / canvas.clientHeight); // Clamp ratio ratio = Math.max(Math.min(1, ratio), MIN_RATIO); if (ratio < 1) { @@ -116,18 +202,25 @@ const zoomToFit = isBitmap => { resizeCrosshair(); clampViewBounds(); } + } else { + log.warn('No bounds!'); } }; export { + ART_BOARD_BOUNDS, ART_BOARD_HEIGHT, ART_BOARD_WIDTH, CENTER, SVG_ART_BOARD_WIDTH, SVG_ART_BOARD_HEIGHT, + MAX_WORKSPACE_BOUNDS, clampViewBounds, + getActionBounds, pan, resetZoom, + setWorkspaceBounds, + getWorkspaceBounds, resizeCrosshair, zoomOnSelection, zoomOnFixedPoint, diff --git a/src/hocs/update-image-hoc.jsx b/src/hocs/update-image-hoc.jsx index ed3fea08..f966aeff 100644 --- a/src/hocs/update-image-hoc.jsx +++ b/src/hocs/update-image-hoc.jsx @@ -8,13 +8,16 @@ import {connect} from 'react-redux'; import {undoSnapshot} from '../reducers/undo'; import {setSelectedItems} from '../reducers/selected-items'; +import {updateViewBounds} from '../reducers/view-bounds'; import {getSelectedLeafItems} from '../helper/selection'; import {getRaster, hideGuideLayers, showGuideLayers} from '../helper/layer'; import {commitRectToBitmap, commitOvalToBitmap, commitSelectionToBitmap, getHitBounds} from '../helper/bitmap'; import {performSnapshot} from '../helper/undo'; import {scaleWithStrokes} from '../helper/math'; + import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, SVG_ART_BOARD_WIDTH, SVG_ART_BOARD_HEIGHT} from '../helper/view'; +import {setWorkspaceBounds} from '../helper/view'; import Modes from '../lib/modes'; import {BitmapModes} from '../lib/modes'; @@ -47,6 +50,9 @@ const UpdateImageHOC = function (WrappedComponent) { } else if (isVector(actualFormat)) { this.handleUpdateVector(skipSnapshot); } + // Any time an image update is made, recalculate the bounds of the artwork + setWorkspaceBounds(); + this.props.updateViewBounds(paper.view.matrix); } handleUpdateBitmap (skipSnapshot) { if (!getRaster().loaded) { @@ -110,12 +116,24 @@ const UpdateImageHOC = function (WrappedComponent) { } } handleUpdateVector (skipSnapshot) { + // Remove viewbox (this would make it export at MAX_WORKSPACE_BOUNDS) + let workspaceMask; + if (paper.project.activeLayer.clipped) { + for (const child of paper.project.activeLayer.children) { + if (child.isClipMask()) { + workspaceMask = child; + break; + } + } + paper.project.activeLayer.clipped = false; + workspaceMask.remove(); + } const guideLayers = hideGuideLayers(true /* includeRaster */); // Export at 0.5x scaleWithStrokes(paper.project.activeLayer, .5, new paper.Point()); + const bounds = paper.project.activeLayer.drawnBounds; - // @todo (https://github.com/LLK/scratch-paint/issues/445) generate view box this.props.onUpdateImage( true /* isVector */, paper.project.exportSVG({ @@ -130,6 +148,12 @@ const UpdateImageHOC = function (WrappedComponent) { showGuideLayers(guideLayers); + // Add back viewbox + if (workspaceMask) { + paper.project.activeLayer.addChild(workspaceMask); + workspaceMask.clipMask = true; + } + if (!skipSnapshot) { performSnapshot(this.props.undoSnapshot, Formats.VECTOR); } @@ -153,7 +177,8 @@ const UpdateImageHOC = function (WrappedComponent) { format: PropTypes.oneOf(Object.keys(Formats)), mode: PropTypes.oneOf(Object.keys(Modes)).isRequired, onUpdateImage: PropTypes.func.isRequired, - undoSnapshot: PropTypes.func.isRequired + undoSnapshot: PropTypes.func.isRequired, + updateViewBounds: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -167,6 +192,9 @@ const UpdateImageHOC = function (WrappedComponent) { }, undoSnapshot: snapshot => { dispatch(undoSnapshot(snapshot)); + }, + updateViewBounds: matrix => { + dispatch(updateViewBounds(matrix)); } }); diff --git a/src/playground/index.ejs b/src/playground/index.ejs index 1192488f..5aae5994 100644 --- a/src/playground/index.ejs +++ b/src/playground/index.ejs @@ -4,11 +4,6 @@ <%= htmlWebpackPlugin.options.title %> - diff --git a/src/playground/playground.css b/src/playground/playground.css new file mode 100644 index 00000000..f6144dfe --- /dev/null +++ b/src/playground/playground.css @@ -0,0 +1,15 @@ + +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: 0px; +} + +body, html { + height: 100% +} + +.playgroundContainer{ + height: 90%; + width: 90%; + margin: auto; +} \ No newline at end of file diff --git a/src/playground/playground.jsx b/src/playground/playground.jsx index 2b73c550..8ccc8684 100644 --- a/src/playground/playground.jsx +++ b/src/playground/playground.jsx @@ -6,8 +6,10 @@ import {Provider} from 'react-redux'; import {createStore} from 'redux'; import reducer from './reducers/combine-reducers'; import {intlInitialState, IntlProvider} from './reducers/intl.js'; +import styles from './playground.css'; const appTarget = document.createElement('div'); +appTarget.setAttribute('class', styles.playgroundContainer); document.body.appendChild(appTarget); const store = createStore( reducer, From 0d63987fea53dd74bd067453d5703d3362d0ac5a Mon Sep 17 00:00:00 2001 From: DD Liu Date: Wed, 6 May 2020 22:45:42 -0400 Subject: [PATCH 5/8] Move magic numbers to constants in layer.js --- src/helper/layer.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/helper/layer.js b/src/helper/layer.js index f8a1b738..a9661cf1 100644 --- a/src/helper/layer.js +++ b/src/helper/layer.js @@ -4,6 +4,7 @@ import {ART_BOARD_BOUNDS, ART_BOARD_WIDTH, ART_BOARD_HEIGHT, CENTER, MAX_WORKSPA import {isGroupItem} from './item'; import {isBitmap, isVector} from '../lib/format'; +const CHECKERBOARD_SIZE = 8; const CROSSHAIR_SIZE = 16; const CROSSHAIR_FULL_OPACITY = 0.75; @@ -199,7 +200,9 @@ const _makeBackgroundPaper = function (width, height, color, opacity) { pathPoints.push(new paper.Point(x, y)); y--; } - const vRect = new paper.Shape.Rectangle(new paper.Point(0, 0), new paper.Point(120, 90)); + const vRect = new paper.Shape.Rectangle( + new paper.Point(0, 0), + new paper.Point(ART_BOARD_WIDTH / CHECKERBOARD_SIZE, ART_BOARD_HEIGHT / CHECKERBOARD_SIZE)); vRect.fillColor = '#fff'; vRect.guide = true; vRect.locked = true; @@ -215,7 +218,7 @@ const _makeBackgroundPaper = function (width, height, color, opacity) { mask.position = CENTER; mask.guide = true; mask.locked = true; - mask.scale(1 / 8); + mask.scale(1 / CHECKERBOARD_SIZE); const vGroup = new paper.Group([vRect, vPath, mask]); mask.clipMask = true; @@ -296,9 +299,14 @@ const _makeBackgroundGuideLayer = function (format) { vWorkspaceBounds.fillColor = '#ECF1F9'; vWorkspaceBounds.position = CENTER; - const vBackground = _makeBackgroundPaper(180, 136, '#0062ff', 0.05); + // Add 1 to the height because it's an odd number otherwise, and we want it to be even + // so the corner of the checkerboard to line up with the center crosshair + const vBackground = _makeBackgroundPaper( + (MAX_WORKSPACE_BOUNDS.height / CHECKERBOARD_SIZE) + 1, + MAX_WORKSPACE_BOUNDS.width / CHECKERBOARD_SIZE, + '#0062ff', 0.05); vBackground.position = CENTER; - vBackground.scaling = new paper.Point(8, 8); + vBackground.scaling = new paper.Point(CHECKERBOARD_SIZE, CHECKERBOARD_SIZE); const vectorBackground = new paper.Group(); vectorBackground.addChild(vWorkspaceBounds); @@ -306,9 +314,12 @@ const _makeBackgroundGuideLayer = function (format) { setGuideItem(vectorBackground); guideLayer.vectorBackground = vectorBackground; - const bitmapBackground = _makeBackgroundPaper(120, 90, '#0062ff', 0.05); + const bitmapBackground = _makeBackgroundPaper( + ART_BOARD_WIDTH / CHECKERBOARD_SIZE, + ART_BOARD_HEIGHT / CHECKERBOARD_SIZE, + '#0062ff', 0.05); bitmapBackground.position = CENTER; - bitmapBackground.scaling = new paper.Point(8, 8); + bitmapBackground.scaling = new paper.Point(CHECKERBOARD_SIZE, CHECKERBOARD_SIZE); bitmapBackground.guide = true; bitmapBackground.locked = true; guideLayer.bitmapBackground = bitmapBackground; From c6d5d892cfaa3c05de346b35009fef04e814966e Mon Sep 17 00:00:00 2001 From: DD Liu Date: Wed, 6 May 2020 22:57:11 -0400 Subject: [PATCH 6/8] Bring in adroitwhiz's changes --- src/containers/paper-canvas.css | 4 ++-- src/containers/paper-canvas.jsx | 7 ++++++- src/helper/layer.js | 9 ++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/containers/paper-canvas.css b/src/containers/paper-canvas.css index 99ef54e0..36dead46 100644 --- a/src/containers/paper-canvas.css +++ b/src/containers/paper-canvas.css @@ -1,6 +1,6 @@ .paper-canvas { - top: 1px; /* leave room for the border*/ - left: 1px; + top: 1px; /* leave room for the border */ + left: 1px; width: calc(100% - 2px); height: calc(100% - 2px); margin: auto; diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index a6858b0d..8a6db731 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -228,7 +228,12 @@ class PaperCanvas extends React.Component { window.setTimeout(() => { // Detached if (!paper.view) return; - paper.view.setViewSize(paper.DomElement.getSize(paper.view.element)); + // Prevent blurriness caused if the "CSS size" of the element is a float-- + // setting canvas dimensions to floats floors them, but we need to round instead + const elemSize = paper.DomElement.getSize(paper.view.element); + elemSize.width = Math.round(elemSize.width); + elemSize.height = Math.round(elemSize.height); + paper.view.setViewSize(elemSize); paperCanvas.props.updateViewBounds(paper.view.matrix); paperCanvas.initializeSvg(item, rotationCenterX, rotationCenterY, viewBox); }, 0); diff --git a/src/helper/layer.js b/src/helper/layer.js index a9661cf1..8f1c100b 100644 --- a/src/helper/layer.js +++ b/src/helper/layer.js @@ -65,13 +65,8 @@ const getBackgroundGuideLayer = function () { }; const _convertLayer = function (layer, format) { - if (isVector(format)) { - layer.vectorBackground.visible = true; - layer.bitmapBackground.visible = false; - } else if (isBitmap(format)) { - layer.bitmapBackground.visible = true; - layer.vectorBackground.visible = false; - } + layer.bitmapBackground.visible = isBitmap(format); + layer.vectorBackground.visible = isVector(format); }; const convertBackgroundGuideLayer = function (format) { From 5230e97903d93a509f8a2f784295bd5d74080bd1 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Mon, 11 May 2020 17:56:22 -0400 Subject: [PATCH 7/8] Fix width/height switched --- src/helper/layer.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/helper/layer.js b/src/helper/layer.js index 8f1c100b..fbf29ace 100644 --- a/src/helper/layer.js +++ b/src/helper/layer.js @@ -216,7 +216,6 @@ const _makeBackgroundPaper = function (width, height, color, opacity) { mask.scale(1 / CHECKERBOARD_SIZE); const vGroup = new paper.Group([vRect, vPath, mask]); mask.clipMask = true; - return vGroup; }; @@ -297,8 +296,8 @@ const _makeBackgroundGuideLayer = function (format) { // Add 1 to the height because it's an odd number otherwise, and we want it to be even // so the corner of the checkerboard to line up with the center crosshair const vBackground = _makeBackgroundPaper( - (MAX_WORKSPACE_BOUNDS.height / CHECKERBOARD_SIZE) + 1, MAX_WORKSPACE_BOUNDS.width / CHECKERBOARD_SIZE, + (MAX_WORKSPACE_BOUNDS.height / CHECKERBOARD_SIZE) + 1, '#0062ff', 0.05); vBackground.position = CENTER; vBackground.scaling = new paper.Point(CHECKERBOARD_SIZE, CHECKERBOARD_SIZE); From 0e20cd023477cd93ec5d721750c576f11612f1ba Mon Sep 17 00:00:00 2001 From: DD Liu Date: Mon, 11 May 2020 18:42:52 -0400 Subject: [PATCH 8/8] fix that zooming from .5 to .333 by scroll wheel is not smooth --- src/containers/paint-editor.jsx | 10 ++++++++-- src/helper/view.js | 11 +++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 8c3fa325..23658563 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -20,7 +20,7 @@ import {setLayout} from '../reducers/layout'; import {getSelectedLeafItems} from '../helper/selection'; import {convertToBitmap, convertToVector} from '../helper/bitmap'; -import {resetZoom, zoomOnSelection} from '../helper/view'; +import {resetZoom, zoomOnSelection, OUTERMOST_ZOOM_LEVEL} from '../helper/view'; import EyeDropperTool from '../helper/tools/eye-dropper'; import Modes from '../lib/modes'; @@ -211,7 +211,13 @@ class PaintEditor extends React.Component { } } handleZoomIn () { - zoomOnSelection(PaintEditor.ZOOM_INCREMENT); + // Make the "next step" after the outermost zoom level be the default + // zoom level (0.5) + let zoomIncrement = PaintEditor.ZOOM_INCREMENT; + if (paper.view.zoom === OUTERMOST_ZOOM_LEVEL) { + zoomIncrement = 0.5 - OUTERMOST_ZOOM_LEVEL; + } + zoomOnSelection(zoomIncrement); this.props.updateViewBounds(paper.view.matrix); this.handleSetSelectedItems(); } diff --git a/src/helper/view.js b/src/helper/view.js index d622ccd1..36affd81 100644 --- a/src/helper/view.js +++ b/src/helper/view.js @@ -18,6 +18,7 @@ const PADDING_PERCENT = 25; // Padding as a percent of the max of width/height o const BUFFER = 50; // Number of pixels of allowance around objects at the edges of the workspace const MIN_RATIO = .125; // Zoom in to at least 1/8 of the screen. This way you don't end up incredibly // zoomed in for tiny costumes. +const OUTERMOST_ZOOM_LEVEL = 0.333; const ART_BOARD_BOUNDS = new paper.Rectangle(0, 0, ART_BOARD_WIDTH, ART_BOARD_HEIGHT); const MAX_WORKSPACE_BOUNDS = new paper.Rectangle( -ART_BOARD_WIDTH / 4, @@ -108,14 +109,7 @@ const resizeCrosshair = () => { const zoomOnFixedPoint = (deltaZoom, fixedPoint) => { const view = paper.view; const preZoomCenter = view.center; - let newZoom; - if (view.zoom <= 0.5 && deltaZoom < 0) { - newZoom = 0.333; - } else if (view.zoom <= 0.333 && deltaZoom > 0) { - newZoom = 0.5; - } else { - newZoom = Math.max(0.5, view.zoom + deltaZoom); - } + const newZoom = Math.max(OUTERMOST_ZOOM_LEVEL, view.zoom + deltaZoom); const scaling = view.zoom / newZoom; const preZoomOffset = fixedPoint.subtract(preZoomCenter); const postZoomOffset = fixedPoint.subtract(preZoomOffset.multiply(scaling)) @@ -212,6 +206,7 @@ export { ART_BOARD_HEIGHT, ART_BOARD_WIDTH, CENTER, + OUTERMOST_ZOOM_LEVEL, SVG_ART_BOARD_WIDTH, SVG_ART_BOARD_HEIGHT, MAX_WORKSPACE_BOUNDS,