Add basic zooming and panning from mousewheel

This commit is contained in:
Paul Kaplan 2017-10-26 20:28:17 -04:00
parent c67459da0a
commit 94b90e104b
8 changed files with 196 additions and 4 deletions

View file

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

View file

@ -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 */}
<InputGroup className={styles.zoomControls}>
<ButtonGroup>
<Button
className={styles.buttonGroupButton}
onClick={this.props.onZoomIn}
>
<img
alt="Zoom In Icon"
className={styles.buttonGroupButtonIcon}
src={zoomInIcon}
/>
</Button>
<Button
className={styles.buttonGroupButton}
onClick={this.props.onZoomReset}
>
<img
alt="Zoom Reset Icon"
className={styles.buttonGroupButtonIcon}
src={zoomResetIcon}
/>
</Button>
<Button
className={styles.buttonGroupButton}
onClick={this.props.onZoomOut}
>
<img
alt="Zoom Out Icon"
className={styles.buttonGroupButtonIcon}
src={zoomOutIcon}
/>
</Button>
</ButtonGroup>
</InputGroup>
</div>
</div>
</div>
@ -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,

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

@ -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 (
<PaintEditorComponent
@ -111,6 +121,9 @@ class PaintEditor extends React.Component {
onUngroup={this.handleUngroup}
onUpdateName={this.props.onUpdateName}
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 {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 (
<canvas
@ -121,6 +141,7 @@ class PaperCanvas extends React.Component {
height="400px"
ref={this.setCanvas}
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';
// 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
}