mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-01-08 13:42:00 -05:00
Add basic zooming and panning from mousewheel
This commit is contained in:
parent
c67459da0a
commit
94b90e104b
8 changed files with 196 additions and 4 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
15
src/components/paint-editor/zoom-in.svg
Normal file
15
src/components/paint-editor/zoom-in.svg
Normal 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 |
14
src/components/paint-editor/zoom-out.svg
Normal file
14
src/components/paint-editor/zoom-out.svg
Normal 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 |
13
src/components/paint-editor/zoom-reset.svg
Normal file
13
src/components/paint-editor/zoom-reset.svg
Normal 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 |
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
70
src/helper/view.js
Normal 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
|
||||
}
|
Loading…
Reference in a new issue