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..cb262c80 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({ @@ -97,7 +100,7 @@ class PaintEditorComponent extends React.Component { onClick={this.props.onUndo} > Undo Icon @@ -115,7 +118,7 @@ class PaintEditorComponent extends React.Component { onClick={this.props.onRedo} > Redo Icon @@ -127,14 +130,14 @@ class PaintEditorComponent extends React.Component { {/* Text mode will go here */} + {/* Zoom controls */} + + + + + + + @@ -279,6 +313,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/brush-mode.jsx b/src/containers/brush-mode.jsx index a9401f38..adf0f603 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -22,8 +22,7 @@ class BrushMode extends React.Component { super(props); bindAll(this, [ 'activateTool', - 'deactivateTool', - 'onScroll' + 'deactivateTool' ]); this.blob = new Blobbiness( this.props.onUpdateSvg, this.props.clearSelectedItems); @@ -53,15 +52,11 @@ 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); - // Force the default brush color if fill is MIXED or transparent const {fillColor} = this.props.colorState; if (fillColor === MIXED || fillColor === null) { this.props.onChangeFillColor(BrushMode.DEFAULT_COLOR); } - - // 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, @@ -69,17 +64,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 ( ); } diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 2d84220f..9534fedf 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, resetZoom, 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 () { @@ -45,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 () { @@ -84,7 +94,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 +124,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..750bbea1 --- /dev/null +++ b/src/helper/view.js @@ -0,0 +1,70 @@ +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 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; + 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); +}; + +const resetZoom = () => { + paper.project.view.zoom = 1; + clampViewBounds(); +}; + +const pan = (dx, dy) => { + paper.project.view.scrollBy(new paper.Point(dx, dy)); + clampViewBounds(); +}; + +export { + pan, + resetZoom, + zoomOnSelection, + zoomOnFixedPoint +};