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}
>
@@ -115,7 +118,7 @@ class PaintEditorComponent extends React.Component {
onClick={this.props.onRedo}
>
@@ -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 @@
+
+
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 @@
+
+
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 @@
+
+
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
+};