From 94b90e104bbb01e381efdd26bb8a7570528760b0 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 26 Oct 2017 20:28:17 -0400 Subject: [PATCH] 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 +}