mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-23 05:52:42 -05:00
Merge pull request #116 from paulkaplan/wheel-pan-zoom
Add basic zooming and panning
This commit is contained in:
commit
5bae62e7d1
11 changed files with 226 additions and 51 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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 |
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
70
src/helper/view.js
Normal 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
|
||||||
|
};
|
Loading…
Reference in a new issue