From 94b90e104bbb01e381efdd26bb8a7570528760b0 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 26 Oct 2017 20:28:17 -0400 Subject: [PATCH 1/8] Add basic zooming and panning from mousewheel --- src/components/paint-editor/paint-editor.css | 9 ++- src/components/paint-editor/paint-editor.jsx | 41 ++++++++++++ src/components/paint-editor/zoom-in.svg | 15 +++++ src/components/paint-editor/zoom-out.svg | 14 ++++ src/components/paint-editor/zoom-reset.svg | 13 ++++ src/containers/paint-editor.jsx | 13 ++++ src/containers/paper-canvas.jsx | 25 ++++++- src/helper/view.js | 70 ++++++++++++++++++++ 8 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 src/components/paint-editor/zoom-in.svg create mode 100644 src/components/paint-editor/zoom-out.svg create mode 100644 src/components/paint-editor/zoom-reset.svg create mode 100644 src/helper/view.js diff --git a/src/components/paint-editor/paint-editor.css b/src/components/paint-editor/paint-editor.css index 9b7a508f..f10ef7b4 100644 --- a/src/components/paint-editor/paint-editor.css +++ b/src/components/paint-editor/paint-editor.css @@ -19,7 +19,7 @@ } .top-align-row { - display: flex; + display: flex; padding-top: calc(5 * $grid-unit); flex-direction: row; } @@ -29,7 +29,7 @@ } .mod-dashed-border { - border-right: 1px dashed $ui-pane-border; + border-right: 1px dashed $ui-pane-border; padding-right: calc(3 * $grid-unit); } @@ -92,3 +92,8 @@ $border-radius: 0.25rem; align-content: flex-start; justify-content: space-between; } + +.zoom-controls { + display: flex; + flex-direction: row-reverse; +} diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 0f3c115b..7f439239 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -39,6 +39,9 @@ import sendForwardIcon from './send-forward.svg'; import sendFrontIcon from './send-front.svg'; import undoIcon from './undo.svg'; import ungroupIcon from './ungroup.svg'; +import zoomInIcon from './zoom-in.svg'; +import zoomOutIcon from './zoom-out.svg'; +import zoomResetIcon from './zoom-reset.svg'; const BufferedInput = BufferedInputHOC(Input); const messages = defineMessages({ @@ -257,6 +260,41 @@ class PaintEditorComponent extends React.Component { svgId={this.props.svgId} onUpdateSvg={this.props.onUpdateSvg} /> + {/* Zoom controls */} + + + + + + + @@ -279,6 +317,9 @@ PaintEditorComponent.propTypes = { onUngroup: PropTypes.func.isRequired, onUpdateName: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired, + onZoomIn: PropTypes.func.isRequired, + onZoomOut: PropTypes.func.isRequired, + onZoomReset: PropTypes.func.isRequired, rotationCenterX: PropTypes.number, rotationCenterY: PropTypes.number, svg: PropTypes.string, diff --git a/src/components/paint-editor/zoom-in.svg b/src/components/paint-editor/zoom-in.svg new file mode 100644 index 00000000..4b804c1f --- /dev/null +++ b/src/components/paint-editor/zoom-in.svg @@ -0,0 +1,15 @@ + + + + + +zoom-in + + + + + + + diff --git a/src/components/paint-editor/zoom-out.svg b/src/components/paint-editor/zoom-out.svg new file mode 100644 index 00000000..16846628 --- /dev/null +++ b/src/components/paint-editor/zoom-out.svg @@ -0,0 +1,14 @@ + + + + + +zoom-out + + + + + + diff --git a/src/components/paint-editor/zoom-reset.svg b/src/components/paint-editor/zoom-reset.svg new file mode 100644 index 00000000..66d8040c --- /dev/null +++ b/src/components/paint-editor/zoom-reset.svg @@ -0,0 +1,13 @@ + + + + + +zoom-reset + + + + + diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 07a214b2..3fb44843 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -11,6 +11,7 @@ import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRed import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order'; import {groupSelection, ungroupSelection} from '../helper/group'; import {getSelectedLeafItems} from '../helper/selection'; +import {resetZoom, zoomOnSelection} from '../helper/view'; import Modes from '../modes/modes'; import {connect} from 'react-redux'; @@ -91,6 +92,15 @@ class PaintEditor extends React.Component { canRedo () { return shouldShowRedo(this.props.undoState); } + handleZoomIn () { + zoomOnSelection(0.25); + } + handleZoomOut () { + zoomOnSelection(-0.25); + } + handleZoomReset () { + resetZoom(); + } render () { return ( ); } diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 2d84220f..78847954 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -10,6 +10,8 @@ import {undoSnapshot, clearUndoState} from '../reducers/undo'; import {isGroup, ungroupItems} from '../helper/group'; import {setupLayers} from '../helper/layer'; import {deleteSelection, getSelectedLeafItems} from '../helper/selection'; +import {pan, zoomOnFixedPoint} from '../helper/view'; + import {setSelectedItems} from '../reducers/selected-items'; import styles from './paper-canvas.css'; @@ -20,7 +22,8 @@ class PaperCanvas extends React.Component { bindAll(this, [ 'setCanvas', 'importSvg', - 'handleKeyDown' + 'handleKeyDown', + 'handleWheel' ]); } componentDidMount () { @@ -84,7 +87,7 @@ class PaperCanvas extends React.Component { item.clipped = false; mask.remove(); } - + // Reduce single item nested in groups if (item.children && item.children.length === 1) { item = item.reduce(); @@ -114,6 +117,23 @@ class PaperCanvas extends React.Component { this.props.canvasRef(canvas); } } + handleWheel (event) { + if (event.metaKey) { + // Zoom keeping mouse location fixed + const canvasRect = this.canvas.getBoundingClientRect(); + const offsetX = event.clientX - canvasRect.left; + const offsetY = event.clientY - canvasRect.top; + const fixedPoint = paper.project.view.viewToProject( + new paper.Point(offsetX, offsetY) + ); + zoomOnFixedPoint(-event.deltaY / 100, fixedPoint); + } else { + const dx = event.deltaX / paper.project.view.zoom; + const dy = event.deltaY / paper.project.view.zoom; + pan(dx, dy); + } + event.preventDefault(); + } render () { return ( ); } diff --git a/src/helper/view.js b/src/helper/view.js new file mode 100644 index 00000000..6f969ee0 --- /dev/null +++ b/src/helper/view.js @@ -0,0 +1,70 @@ +import paper from '@scratch/paper'; +import {getSelectedRootItems} from './selection'; + +// Zoom keeping the selection center (if any) fixed. +const zoomOnSelection = (deltaZoom) => { + let fixedPoint; + const items = getSelectedRootItems(); + if (items.length > 0) { + let rect = null; + for (const item of items) { + if (rect) { + rect = rect.unite(item.bounds); + } else { + rect = item.bounds; + } + } + fixedPoint = rect.center; + } else { + fixedPoint = paper.project.view.center; + } + zoomOnFixedPoint(deltaZoom, fixedPoint); +}; + +// Zoom keeping a project-space point fixed. +// This article was helpful http://matthiasberth.com/tech/stable-zoom-and-pan-in-paperjs +const zoomOnFixedPoint = (deltaZoom, fixedPoint) => { + const {view} = paper.project; + const preZoomCenter = view.center; + const newZoom = Math.max(1, view.zoom + deltaZoom); + const scaling = view.zoom / newZoom; + const preZoomOffset = fixedPoint.subtract(preZoomCenter); + const postZoomOffset = fixedPoint.subtract(preZoomOffset.multiply(scaling)) + .subtract(preZoomCenter); + view.zoom = newZoom; + view.translate(postZoomOffset.multiply(-1)); + clampViewBounds(); +}; + +const resetZoom = () => { + paper.project.view.zoom = 1; + clampViewBounds(); +}; + +const pan = (dx, dy) => { + paper.project.view.scrollBy(new paper.Point(dx, dy)); + clampViewBounds(); +}; + +const clampViewBounds = () => { + const {left, right, top, bottom} = paper.project.view.bounds; + if (left < 0) { + paper.project.view.scrollBy(new paper.Point(-left, 0)); + } + if (top < 0) { + paper.project.view.scrollBy(new paper.Point(0, -top)); + } + if (bottom > 400) { + paper.project.view.scrollBy(new paper.Point(0, 400 - bottom)); + } + if (right > 500) { + paper.project.view.scrollBy(new paper.Point(500 - right, 0)); + } +}; + +export { + pan, + resetZoom, + zoomOnSelection, + zoomOnFixedPoint +} From da12930e00cd086d7f6528b972020a2788db5d65 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 27 Oct 2017 09:33:06 -0400 Subject: [PATCH 2/8] Fix linting --- src/helper/view.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/helper/view.js b/src/helper/view.js index 6f969ee0..568d81ce 100644 --- a/src/helper/view.js +++ b/src/helper/view.js @@ -1,8 +1,24 @@ import paper from '@scratch/paper'; import {getSelectedRootItems} from './selection'; +const clampViewBounds = () => { + const {left, right, top, bottom} = paper.project.view.bounds; + if (left < 0) { + paper.project.view.scrollBy(new paper.Point(-left, 0)); + } + if (top < 0) { + paper.project.view.scrollBy(new paper.Point(0, -top)); + } + if (bottom > 400) { + paper.project.view.scrollBy(new paper.Point(0, 400 - bottom)); + } + if (right > 500) { + paper.project.view.scrollBy(new paper.Point(500 - right, 0)); + } +}; + // Zoom keeping the selection center (if any) fixed. -const zoomOnSelection = (deltaZoom) => { +const zoomOnSelection = deltaZoom => { let fixedPoint; const items = getSelectedRootItems(); if (items.length > 0) { @@ -46,25 +62,9 @@ const pan = (dx, dy) => { clampViewBounds(); }; -const clampViewBounds = () => { - const {left, right, top, bottom} = paper.project.view.bounds; - if (left < 0) { - paper.project.view.scrollBy(new paper.Point(-left, 0)); - } - if (top < 0) { - paper.project.view.scrollBy(new paper.Point(0, -top)); - } - if (bottom > 400) { - paper.project.view.scrollBy(new paper.Point(0, 400 - bottom)); - } - if (right > 500) { - paper.project.view.scrollBy(new paper.Point(500 - right, 0)); - } -}; - export { pan, resetZoom, zoomOnSelection, zoomOnFixedPoint -} +}; From 9ac6b0b04880622df480275b31305edeae95430b Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 27 Oct 2017 09:59:57 -0400 Subject: [PATCH 3/8] Save and restore project pan and zoom correctly between costume changes --- src/containers/paint-editor.jsx | 9 +++++++++ src/containers/paper-canvas.jsx | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 3fb44843..24ddda7a 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -42,6 +42,11 @@ class PaintEditor extends React.Component { document.removeEventListener('keydown', this.props.onKeyPress); } handleUpdateSvg (skipSnapshot) { + // Store the zoom/pan and restore it after snapshotting + // TODO Only doing this because snapshotting at zoom/pan makes export wrong + const oldZoom = paper.project.view.zoom; + const oldCenter = paper.project.view.center.clone(); + resetZoom(); // Hide guide layer const guideLayer = getGuideLayer(); const backgroundGuideLayer = getBackgroundGuideLayer(); @@ -61,6 +66,10 @@ class PaintEditor extends React.Component { paper.project.addLayer(backgroundGuideLayer); backgroundGuideLayer.sendToBack(); paper.project.addLayer(guideLayer); + // Restore old zoom + paper.project.view.zoom = oldZoom; + paper.project.view.center = oldCenter; + paper.project.view.update(); } handleUndo () { performUndo(this.props.undoState, this.props.onUndo, this.props.setSelectedItems, this.handleUpdateSvg); diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 78847954..9534fedf 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -10,7 +10,7 @@ import {undoSnapshot, clearUndoState} from '../reducers/undo'; import {isGroup, ungroupItems} from '../helper/group'; import {setupLayers} from '../helper/layer'; import {deleteSelection, getSelectedLeafItems} from '../helper/selection'; -import {pan, zoomOnFixedPoint} from '../helper/view'; +import {pan, resetZoom, zoomOnFixedPoint} from '../helper/view'; import {setSelectedItems} from '../reducers/selected-items'; @@ -48,7 +48,14 @@ class PaperCanvas extends React.Component { } this.props.clearUndo(); if (newProps.svg) { + // Store the zoom/pan and restore it after importing a new SVG + const oldZoom = paper.project.view.zoom; + const oldCenter = paper.project.view.center.clone(); + resetZoom(); this.importSvg(newProps.svg, newProps.rotationCenterX, newProps.rotationCenterY); + paper.project.view.zoom = oldZoom; + paper.project.view.center = oldCenter; + paper.project.view.update(); } } componentWillUnmount () { From 62fad0160736f159f7ddf1c8e16a9f691d8064a5 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 27 Oct 2017 10:05:21 -0400 Subject: [PATCH 4/8] Increase zoom button increment --- src/containers/paint-editor.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 24ddda7a..9156b6e6 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -19,6 +19,9 @@ import bindAll from 'lodash.bindall'; import paper from '@scratch/paper'; class PaintEditor extends React.Component { + static get ZOOM_INCREMENT () { + return 0.5; + } constructor (props) { super(props); bindAll(this, [ @@ -102,10 +105,10 @@ class PaintEditor extends React.Component { return shouldShowRedo(this.props.undoState); } handleZoomIn () { - zoomOnSelection(0.25); + zoomOnSelection(PaintEditor.ZOOM_INCREMENT); } handleZoomOut () { - zoomOnSelection(-0.25); + zoomOnSelection(-PaintEditor.ZOOM_INCREMENT); } handleZoomReset () { resetZoom(); From 770a138e2115af66e7e72cf839df2a10f8c12dfc Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 27 Oct 2017 10:06:01 -0400 Subject: [PATCH 5/8] Fix linting --- src/helper/view.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/helper/view.js b/src/helper/view.js index 568d81ce..750bbea1 100644 --- a/src/helper/view.js +++ b/src/helper/view.js @@ -17,6 +17,21 @@ const clampViewBounds = () => { } }; +// Zoom keeping a project-space point fixed. +// This article was helpful http://matthiasberth.com/tech/stable-zoom-and-pan-in-paperjs +const zoomOnFixedPoint = (deltaZoom, fixedPoint) => { + const {view} = paper.project; + const preZoomCenter = view.center; + const newZoom = Math.max(1, view.zoom + deltaZoom); + const scaling = view.zoom / newZoom; + const preZoomOffset = fixedPoint.subtract(preZoomCenter); + const postZoomOffset = fixedPoint.subtract(preZoomOffset.multiply(scaling)) + .subtract(preZoomCenter); + view.zoom = newZoom; + view.translate(postZoomOffset.multiply(-1)); + clampViewBounds(); +}; + // Zoom keeping the selection center (if any) fixed. const zoomOnSelection = deltaZoom => { let fixedPoint; @@ -37,21 +52,6 @@ const zoomOnSelection = deltaZoom => { zoomOnFixedPoint(deltaZoom, fixedPoint); }; -// Zoom keeping a project-space point fixed. -// This article was helpful http://matthiasberth.com/tech/stable-zoom-and-pan-in-paperjs -const zoomOnFixedPoint = (deltaZoom, fixedPoint) => { - const {view} = paper.project; - const preZoomCenter = view.center; - const newZoom = Math.max(1, view.zoom + deltaZoom); - const scaling = view.zoom / newZoom; - const preZoomOffset = fixedPoint.subtract(preZoomCenter); - const postZoomOffset = fixedPoint.subtract(preZoomOffset.multiply(scaling)) - .subtract(preZoomCenter); - view.zoom = newZoom; - view.translate(postZoomOffset.multiply(-1)); - clampViewBounds(); -}; - const resetZoom = () => { paper.project.view.zoom = 1; clampViewBounds(); From d4b28a8817941a9a4a4338453b28d6689756e565 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 27 Oct 2017 10:16:13 -0400 Subject: [PATCH 6/8] Remove scroll action on eraser and brush --- src/containers/brush-mode.jsx | 15 +-------------- src/containers/eraser-mode.jsx | 14 +------------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index f525deab..88a1ac16 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -17,8 +17,7 @@ class BrushMode extends React.Component { super(props); bindAll(this, [ 'activateTool', - 'deactivateTool', - 'onScroll' + 'deactivateTool' ]); this.blob = new Blobbiness( this.props.onUpdateSvg, this.props.clearSelectedItems); @@ -48,9 +47,6 @@ class BrushMode extends React.Component { // TODO: Instead of clearing selection, consider a kind of "draw inside" // analogous to how selection works with eraser clearSelection(this.props.clearSelectedItems); - - // TODO: This is temporary until a component that provides the brush size is hooked up - this.props.canvas.addEventListener('mousewheel', this.onScroll); this.blob.activateTool({ isEraser: false, ...this.props.colorState, @@ -58,17 +54,8 @@ class BrushMode extends React.Component { }); } deactivateTool () { - this.props.canvas.removeEventListener('mousewheel', this.onScroll); this.blob.deactivateTool(); } - onScroll (event) { - if (event.deltaY < 0) { - this.props.changeBrushSize(this.props.brushModeState.brushSize + 1); - } else if (event.deltaY > 0 && this.props.brushModeState.brushSize > 1) { - this.props.changeBrushSize(this.props.brushModeState.brushSize - 1); - } - return true; - } render () { return ( 0 && this.props.eraserModeState.brushSize > 1) { - this.props.changeBrushSize(this.props.eraserModeState.brushSize - 1); - } - } render () { return ( Date: Fri, 27 Oct 2017 10:22:52 -0400 Subject: [PATCH 7/8] Remove wheel listeners from brush and eraser --- src/components/paint-editor/paint-editor.jsx | 4 ---- src/containers/brush-mode.jsx | 2 -- src/containers/eraser-mode.jsx | 2 -- src/containers/line-mode.jsx | 6 ++---- 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 7f439239..8c60c69d 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -225,20 +225,16 @@ class PaintEditorComponent extends React.Component { onUpdateSvg={this.props.onUpdateSvg} /> {/* Text mode will go here */} Date: Fri, 27 Oct 2017 10:57:28 -0400 Subject: [PATCH 8/8] Remove "icon" from alt text --- src/components/paint-editor/paint-editor.jsx | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 8c60c69d..cb262c80 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -100,7 +100,7 @@ class PaintEditorComponent extends React.Component { onClick={this.props.onUndo} > Undo Icon @@ -118,7 +118,7 @@ class PaintEditorComponent extends React.Component { onClick={this.props.onRedo} > Redo Icon @@ -130,14 +130,14 @@ class PaintEditorComponent extends React.Component { Zoom In Icon @@ -274,7 +274,7 @@ class PaintEditorComponent extends React.Component { onClick={this.props.onZoomReset} > Zoom Reset Icon @@ -284,7 +284,7 @@ class PaintEditorComponent extends React.Component { onClick={this.props.onZoomOut} > Zoom Out Icon