Merge pull request #116 from paulkaplan/wheel-pan-zoom

Add basic zooming and panning
This commit is contained in:
Paul Kaplan 2017-10-27 11:12:21 -04:00 committed by GitHub
commit 5bae62e7d1
11 changed files with 226 additions and 51 deletions

View file

@ -19,7 +19,7 @@
} }
.top-align-row { .top-align-row {
display: flex; display: flex;
padding-top: calc(5 * $grid-unit); padding-top: calc(5 * $grid-unit);
flex-direction: row; flex-direction: row;
} }
@ -29,7 +29,7 @@
} }
.mod-dashed-border { .mod-dashed-border {
border-right: 1px dashed $ui-pane-border; border-right: 1px dashed $ui-pane-border;
padding-right: calc(3 * $grid-unit); padding-right: calc(3 * $grid-unit);
} }
@ -92,3 +92,8 @@ $border-radius: 0.25rem;
align-content: flex-start; align-content: flex-start;
justify-content: space-between; justify-content: space-between;
} }
.zoom-controls {
display: flex;
flex-direction: row-reverse;
}

View file

@ -39,6 +39,9 @@ import sendForwardIcon from './send-forward.svg';
import sendFrontIcon from './send-front.svg'; import sendFrontIcon from './send-front.svg';
import undoIcon from './undo.svg'; import undoIcon from './undo.svg';
import ungroupIcon from './ungroup.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 BufferedInput = BufferedInputHOC(Input);
const messages = defineMessages({ const messages = defineMessages({
@ -97,7 +100,7 @@ class PaintEditorComponent extends React.Component {
onClick={this.props.onUndo} onClick={this.props.onUndo}
> >
<img <img
alt="Undo Icon" alt="Undo"
className={styles.buttonGroupButtonIcon} className={styles.buttonGroupButtonIcon}
src={undoIcon} src={undoIcon}
/> />
@ -115,7 +118,7 @@ class PaintEditorComponent extends React.Component {
onClick={this.props.onRedo} onClick={this.props.onRedo}
> >
<img <img
alt="Redo Icon" alt="Redo"
className={styles.buttonGroupButtonIcon} className={styles.buttonGroupButtonIcon}
src={redoIcon} src={redoIcon}
/> />
@ -127,14 +130,14 @@ class PaintEditorComponent extends React.Component {
<InputGroup className={styles.modDashedBorder}> <InputGroup className={styles.modDashedBorder}>
<LabeledIconButton <LabeledIconButton
disabled={!shouldShowGroup()} disabled={!shouldShowGroup()}
imgAlt="Group Icon" imgAlt="Group"
imgSrc={groupIcon} imgSrc={groupIcon}
title="Group" title="Group"
onClick={this.props.onGroup} onClick={this.props.onGroup}
/> />
<LabeledIconButton <LabeledIconButton
disabled={!shouldShowUngroup()} disabled={!shouldShowUngroup()}
imgAlt="Ungroup Icon" imgAlt="Ungroup"
imgSrc={ungroupIcon} imgSrc={ungroupIcon}
title="Ungroup" title="Ungroup"
onClick={this.props.onUngroup} onClick={this.props.onUngroup}
@ -145,14 +148,14 @@ class PaintEditorComponent extends React.Component {
<InputGroup className={styles.modDashedBorder}> <InputGroup className={styles.modDashedBorder}>
<LabeledIconButton <LabeledIconButton
disabled={!shouldShowBringForward()} disabled={!shouldShowBringForward()}
imgAlt="Send Forward Icon" imgAlt="Send Forward"
imgSrc={sendForwardIcon} imgSrc={sendForwardIcon}
title="Forward" title="Forward"
onClick={this.props.onSendForward} onClick={this.props.onSendForward}
/> />
<LabeledIconButton <LabeledIconButton
disabled={!shouldShowSendBackward()} disabled={!shouldShowSendBackward()}
imgAlt="Send Backward Icon" imgAlt="Send Backward"
imgSrc={sendBackwardIcon} imgSrc={sendBackwardIcon}
title="Backward" title="Backward"
onClick={this.props.onSendBackward} onClick={this.props.onSendBackward}
@ -163,14 +166,14 @@ class PaintEditorComponent extends React.Component {
<InputGroup> <InputGroup>
<LabeledIconButton <LabeledIconButton
disabled={!shouldShowBringForward()} disabled={!shouldShowBringForward()}
imgAlt="Send to Front Icon" imgAlt="Send to Front"
imgSrc={sendFrontIcon} imgSrc={sendFrontIcon}
title="Front" title="Front"
onClick={this.props.onSendToFront} onClick={this.props.onSendToFront}
/> />
<LabeledIconButton <LabeledIconButton
disabled={!shouldShowSendBackward()} disabled={!shouldShowSendBackward()}
imgAlt="Send to Back Icon" imgAlt="Send to Back"
imgSrc={sendBackIcon} imgSrc={sendBackIcon}
title="Back" title="Back"
onClick={this.props.onSendToBack} onClick={this.props.onSendToBack}
@ -180,7 +183,7 @@ class PaintEditorComponent extends React.Component {
{/* To be rotation point */} {/* To be rotation point */}
{/* <InputGroup> {/* <InputGroup>
<LabeledIconButton <LabeledIconButton
imgAlt="Rotation Point Icon" imgAlt="Rotation Point"
imgSrc={rotationPointIcon} imgSrc={rotationPointIcon}
title="Rotation Point" title="Rotation Point"
onClick={function () {}} onClick={function () {}}
@ -222,20 +225,16 @@ class PaintEditorComponent extends React.Component {
onUpdateSvg={this.props.onUpdateSvg} onUpdateSvg={this.props.onUpdateSvg}
/> />
<BrushMode <BrushMode
canvas={this.state.canvas}
onUpdateSvg={this.props.onUpdateSvg} onUpdateSvg={this.props.onUpdateSvg}
/> />
<EraserMode <EraserMode
canvas={this.state.canvas}
onUpdateSvg={this.props.onUpdateSvg} onUpdateSvg={this.props.onUpdateSvg}
/> />
<PenMode <PenMode
canvas={this.state.canvas}
onUpdateSvg={this.props.onUpdateSvg} onUpdateSvg={this.props.onUpdateSvg}
/> />
{/* Text mode will go here */} {/* Text mode will go here */}
<LineMode <LineMode
canvas={this.state.canvas}
onUpdateSvg={this.props.onUpdateSvg} onUpdateSvg={this.props.onUpdateSvg}
/> />
<OvalMode <OvalMode
@ -257,6 +256,41 @@ class PaintEditorComponent extends React.Component {
svgId={this.props.svgId} svgId={this.props.svgId}
onUpdateSvg={this.props.onUpdateSvg} onUpdateSvg={this.props.onUpdateSvg}
/> />
{/* Zoom controls */}
<InputGroup className={styles.zoomControls}>
<ButtonGroup>
<Button
className={styles.buttonGroupButton}
onClick={this.props.onZoomIn}
>
<img
alt="Zoom In"
className={styles.buttonGroupButtonIcon}
src={zoomInIcon}
/>
</Button>
<Button
className={styles.buttonGroupButton}
onClick={this.props.onZoomReset}
>
<img
alt="Zoom Reset"
className={styles.buttonGroupButtonIcon}
src={zoomResetIcon}
/>
</Button>
<Button
className={styles.buttonGroupButton}
onClick={this.props.onZoomOut}
>
<img
alt="Zoom Out"
className={styles.buttonGroupButtonIcon}
src={zoomOutIcon}
/>
</Button>
</ButtonGroup>
</InputGroup>
</div> </div>
</div> </div>
</div> </div>
@ -279,6 +313,9 @@ PaintEditorComponent.propTypes = {
onUngroup: PropTypes.func.isRequired, onUngroup: PropTypes.func.isRequired,
onUpdateName: PropTypes.func.isRequired, onUpdateName: PropTypes.func.isRequired,
onUpdateSvg: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired,
onZoomIn: PropTypes.func.isRequired,
onZoomOut: PropTypes.func.isRequired,
onZoomReset: PropTypes.func.isRequired,
rotationCenterX: PropTypes.number, rotationCenterX: PropTypes.number,
rotationCenterY: PropTypes.number, rotationCenterY: PropTypes.number,
svg: PropTypes.string, svg: PropTypes.string,

View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="6 6 24 24">
<defs>
<style>
.cls-4{fill:none;stroke:#575e75;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}
</style>
</defs>
<title>zoom-in</title>
<g class="cls-3">
<circle class="cls-4" cx="18" cy="18" r="7"/>
<line class="cls-4" x1="23" y1="23" x2="26" y2="26"/>
<line class="cls-4" x1="16" y1="18" x2="20" y2="18"/>
<line class="cls-4" x1="18" y1="16" x2="18" y2="20"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 537 B

View file

@ -0,0 +1,14 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="6 6 24 24">
<defs>
<style>
.cls-4{fill:none;stroke:#575e75;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}
</style>
</defs>
<title>zoom-out</title>
<g class="cls-3">
<circle class="cls-4" cx="18" cy="18" r="7"/>
<line class="cls-4" x1="23" y1="23" x2="26" y2="26"/>
<line class="cls-4" x1="16" y1="18" x2="20" y2="18"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 480 B

View file

@ -0,0 +1,13 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="6 6 24 24">
<defs>
<style>
.cls-4{fill:#575e75;}
</style>
</defs>
<title>zoom-reset</title>
<g class="cls-3">
<rect class="cls-4" x="13" y="14" width="10" height="2" rx="1" ry="1"/>
<rect class="cls-4" x="13" y="20" width="10" height="2" rx="1" ry="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 394 B

View file

@ -22,8 +22,7 @@ class BrushMode extends React.Component {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'activateTool', 'activateTool',
'deactivateTool', 'deactivateTool'
'onScroll'
]); ]);
this.blob = new Blobbiness( this.blob = new Blobbiness(
this.props.onUpdateSvg, this.props.clearSelectedItems); 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" // TODO: Instead of clearing selection, consider a kind of "draw inside"
// analogous to how selection works with eraser // analogous to how selection works with eraser
clearSelection(this.props.clearSelectedItems); clearSelection(this.props.clearSelectedItems);
// Force the default brush color if fill is MIXED or transparent // Force the default brush color if fill is MIXED or transparent
const {fillColor} = this.props.colorState; const {fillColor} = this.props.colorState;
if (fillColor === MIXED || fillColor === null) { if (fillColor === MIXED || fillColor === null) {
this.props.onChangeFillColor(BrushMode.DEFAULT_COLOR); 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({ this.blob.activateTool({
isEraser: false, isEraser: false,
...this.props.colorState, ...this.props.colorState,
@ -69,17 +64,8 @@ class BrushMode extends React.Component {
}); });
} }
deactivateTool () { deactivateTool () {
this.props.canvas.removeEventListener('mousewheel', this.onScroll);
this.blob.deactivateTool(); 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 () { render () {
return ( return (
<BrushModeComponent <BrushModeComponent
@ -94,8 +80,6 @@ BrushMode.propTypes = {
brushModeState: PropTypes.shape({ brushModeState: PropTypes.shape({
brushSize: PropTypes.number.isRequired brushSize: PropTypes.number.isRequired
}), }),
canvas: PropTypes.instanceOf(Element).isRequired,
changeBrushSize: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired,
colorState: PropTypes.shape({ colorState: PropTypes.shape({
fillColor: PropTypes.string, fillColor: PropTypes.string,

View file

@ -14,8 +14,7 @@ class EraserMode extends React.Component {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'activateTool', 'activateTool',
'deactivateTool', 'deactivateTool'
'onScroll'
]); ]);
this.blob = new Blobbiness( this.blob = new Blobbiness(
this.props.onUpdateSvg, this.props.clearSelectedItems); this.props.onUpdateSvg, this.props.clearSelectedItems);
@ -41,22 +40,11 @@ class EraserMode extends React.Component {
return nextProps.isEraserModeActive !== this.props.isEraserModeActive; return nextProps.isEraserModeActive !== this.props.isEraserModeActive;
} }
activateTool () { activateTool () {
this.props.canvas.addEventListener('mousewheel', this.onScroll);
this.blob.activateTool({isEraser: true, ...this.props.eraserModeState}); this.blob.activateTool({isEraser: true, ...this.props.eraserModeState});
} }
deactivateTool () { deactivateTool () {
this.props.canvas.removeEventListener('mousewheel', this.onScroll);
this.blob.deactivateTool(); this.blob.deactivateTool();
} }
onScroll (event) {
event.preventDefault();
if (event.deltaY < 0) {
this.props.changeBrushSize(this.props.eraserModeState.brushSize + 1);
} else if (event.deltaY > 0 && this.props.eraserModeState.brushSize > 1) {
this.props.changeBrushSize(this.props.eraserModeState.brushSize - 1);
}
}
render () { render () {
return ( return (
<EraserModeComponent <EraserModeComponent
@ -68,8 +56,6 @@ class EraserMode extends React.Component {
} }
EraserMode.propTypes = { EraserMode.propTypes = {
canvas: PropTypes.instanceOf(Element).isRequired,
changeBrushSize: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired,
eraserModeState: PropTypes.shape({ eraserModeState: PropTypes.shape({
brushSize: PropTypes.number.isRequired brushSize: PropTypes.number.isRequired

View file

@ -205,7 +205,6 @@ class LineMode extends React.Component {
} }
} }
deactivateTool () { deactivateTool () {
this.props.canvas.removeEventListener('mousewheel', this.onScroll);
this.tool.remove(); this.tool.remove();
this.tool = null; this.tool = null;
if (this.hitResult) { if (this.hitResult) {
@ -227,7 +226,6 @@ class LineMode extends React.Component {
} }
LineMode.propTypes = { LineMode.propTypes = {
canvas: PropTypes.instanceOf(Element).isRequired,
clearSelectedItems: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired,
colorState: PropTypes.shape({ colorState: PropTypes.shape({
fillColor: PropTypes.string, fillColor: PropTypes.string,

View file

@ -11,6 +11,7 @@ import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRed
import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order'; import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
import {groupSelection, ungroupSelection} from '../helper/group'; import {groupSelection, ungroupSelection} from '../helper/group';
import {getSelectedLeafItems} from '../helper/selection'; import {getSelectedLeafItems} from '../helper/selection';
import {resetZoom, zoomOnSelection} from '../helper/view';
import Modes from '../modes/modes'; import Modes from '../modes/modes';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
@ -18,6 +19,9 @@ import bindAll from 'lodash.bindall';
import paper from '@scratch/paper'; import paper from '@scratch/paper';
class PaintEditor extends React.Component { class PaintEditor extends React.Component {
static get ZOOM_INCREMENT () {
return 0.5;
}
constructor (props) { constructor (props) {
super(props); super(props);
bindAll(this, [ bindAll(this, [
@ -41,6 +45,11 @@ class PaintEditor extends React.Component {
document.removeEventListener('keydown', this.props.onKeyPress); document.removeEventListener('keydown', this.props.onKeyPress);
} }
handleUpdateSvg (skipSnapshot) { 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 // Hide guide layer
const guideLayer = getGuideLayer(); const guideLayer = getGuideLayer();
const backgroundGuideLayer = getBackgroundGuideLayer(); const backgroundGuideLayer = getBackgroundGuideLayer();
@ -60,6 +69,10 @@ class PaintEditor extends React.Component {
paper.project.addLayer(backgroundGuideLayer); paper.project.addLayer(backgroundGuideLayer);
backgroundGuideLayer.sendToBack(); backgroundGuideLayer.sendToBack();
paper.project.addLayer(guideLayer); paper.project.addLayer(guideLayer);
// Restore old zoom
paper.project.view.zoom = oldZoom;
paper.project.view.center = oldCenter;
paper.project.view.update();
} }
handleUndo () { handleUndo () {
performUndo(this.props.undoState, this.props.onUndo, this.props.setSelectedItems, this.handleUpdateSvg); performUndo(this.props.undoState, this.props.onUndo, this.props.setSelectedItems, this.handleUpdateSvg);
@ -91,6 +104,15 @@ class PaintEditor extends React.Component {
canRedo () { canRedo () {
return shouldShowRedo(this.props.undoState); return shouldShowRedo(this.props.undoState);
} }
handleZoomIn () {
zoomOnSelection(PaintEditor.ZOOM_INCREMENT);
}
handleZoomOut () {
zoomOnSelection(-PaintEditor.ZOOM_INCREMENT);
}
handleZoomReset () {
resetZoom();
}
render () { render () {
return ( return (
<PaintEditorComponent <PaintEditorComponent
@ -111,6 +133,9 @@ class PaintEditor extends React.Component {
onUngroup={this.handleUngroup} onUngroup={this.handleUngroup}
onUpdateName={this.props.onUpdateName} onUpdateName={this.props.onUpdateName}
onUpdateSvg={this.handleUpdateSvg} onUpdateSvg={this.handleUpdateSvg}
onZoomIn={this.handleZoomIn}
onZoomOut={this.handleZoomOut}
onZoomReset={this.handleZoomReset}
/> />
); );
} }

View file

@ -10,6 +10,8 @@ import {undoSnapshot, clearUndoState} from '../reducers/undo';
import {isGroup, ungroupItems} from '../helper/group'; import {isGroup, ungroupItems} from '../helper/group';
import {setupLayers} from '../helper/layer'; import {setupLayers} from '../helper/layer';
import {deleteSelection, getSelectedLeafItems} from '../helper/selection'; import {deleteSelection, getSelectedLeafItems} from '../helper/selection';
import {pan, resetZoom, zoomOnFixedPoint} from '../helper/view';
import {setSelectedItems} from '../reducers/selected-items'; import {setSelectedItems} from '../reducers/selected-items';
import styles from './paper-canvas.css'; import styles from './paper-canvas.css';
@ -20,7 +22,8 @@ class PaperCanvas extends React.Component {
bindAll(this, [ bindAll(this, [
'setCanvas', 'setCanvas',
'importSvg', 'importSvg',
'handleKeyDown' 'handleKeyDown',
'handleWheel'
]); ]);
} }
componentDidMount () { componentDidMount () {
@ -45,7 +48,14 @@ class PaperCanvas extends React.Component {
} }
this.props.clearUndo(); this.props.clearUndo();
if (newProps.svg) { 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); this.importSvg(newProps.svg, newProps.rotationCenterX, newProps.rotationCenterY);
paper.project.view.zoom = oldZoom;
paper.project.view.center = oldCenter;
paper.project.view.update();
} }
} }
componentWillUnmount () { componentWillUnmount () {
@ -84,7 +94,7 @@ class PaperCanvas extends React.Component {
item.clipped = false; item.clipped = false;
mask.remove(); mask.remove();
} }
// Reduce single item nested in groups // Reduce single item nested in groups
if (item.children && item.children.length === 1) { if (item.children && item.children.length === 1) {
item = item.reduce(); item = item.reduce();
@ -114,6 +124,23 @@ class PaperCanvas extends React.Component {
this.props.canvasRef(canvas); 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 () { render () {
return ( return (
<canvas <canvas
@ -121,6 +148,7 @@ class PaperCanvas extends React.Component {
height="400px" height="400px"
ref={this.setCanvas} ref={this.setCanvas}
width="500px" width="500px"
onWheel={this.handleWheel}
/> />
); );
} }

70
src/helper/view.js Normal file
View file

@ -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
};