mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 13:32:28 -05:00
parent
20a98db397
commit
516f6eb714
7 changed files with 206 additions and 19 deletions
31
README.md
31
README.md
|
@ -69,11 +69,6 @@ npm install --save scratch-paint
|
||||||
```
|
```
|
||||||
|
|
||||||
For an example of how to use scratch-paint as a library, check out the `scratch-paint/src/playground` directory.
|
For an example of how to use scratch-paint as a library, check out the `scratch-paint/src/playground` directory.
|
||||||
In `playground.jsx`, you can change the image that is passed in (which may either be nothing, an SVG string or a base64 data URI) and edit the handler `onUpdateImage`, which is called with the new image (either an SVG string or an ImageData) each time the vector drawing is edited.
|
|
||||||
|
|
||||||
If the `imageId` parameter changes, then the paint editor will be cleared, the undo stack reset, and the image re-imported.
|
|
||||||
|
|
||||||
SVGs of up to size 480 x 360 will fit into the view window of the paint editor, while bitmaps of size up to 960 x 720 will fit into the paint editor. One unit of an SVG will appear twice as tall and wide as one unit of a bitmap. This quirky import behavior comes from needing to support legacy projects in Scratch.
|
|
||||||
|
|
||||||
In your parent component:
|
In your parent component:
|
||||||
```
|
```
|
||||||
|
@ -82,13 +77,33 @@ import PaintEditor from 'scratch-paint';
|
||||||
<PaintEditor
|
<PaintEditor
|
||||||
image={optionalImage}
|
image={optionalImage}
|
||||||
imageId={optionalId}
|
imageId={optionalId}
|
||||||
imageFormat='svg' // 'svg', 'png', or 'jpg'
|
imageFormat='svg'
|
||||||
rotationCenterX={optionalCenterPointXRelativeToTopLeft}
|
rotationCenterX={optionalCenterPointX}
|
||||||
rotationCenterY={optionalCenterPointYRelativeToTopLeft}
|
rotationCenterY={optionalCenterPointY}
|
||||||
|
rtl={true|false}
|
||||||
onUpdateImage={handleUpdateImageFunction}
|
onUpdateImage={handleUpdateImageFunction}
|
||||||
|
zoomLevelId={optionalZoomLevelId}
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`image`: may either be nothing, an SVG string or a base64 data URI)
|
||||||
|
SVGs of up to size 480 x 360 will fit into the view window of the paint editor, while bitmaps of size up to 960 x 720 will fit into the paint editor. One unit of an SVG will appear twice as tall and wide as one unit of a bitmap. This quirky import behavior comes from needing to support legacy projects in Scratch.
|
||||||
|
|
||||||
|
`imageId`: If this parameter changes, then the paint editor will be cleared, the undo stack reset, and the image re-imported.
|
||||||
|
|
||||||
|
`imageFormat`: 'svg', 'png', or 'jpg'. Other formats are currently not supported.
|
||||||
|
|
||||||
|
`rotationCenterX`: x coordinate relative to the top left corner of the sprite of the point that should be centered.
|
||||||
|
|
||||||
|
`rotationCenterY`: y coordinate relative to the top left corner of the sprite of the point that should be centered.
|
||||||
|
|
||||||
|
`rtl`: True if the paint editor should be laid out right to left (meant for right to left languages)
|
||||||
|
|
||||||
|
`onUpdateImage`: A handler called with the new image (either an SVG string or an ImageData) each time the drawing is edited.
|
||||||
|
|
||||||
|
`zoomLevelId`: All costumes with the same zoom level ID will share the same saved zoom level. When a new zoom level ID is encountered, the paint editor will zoom to fit the current costume comfortably. Leave undefined to perform no zoom to fit.
|
||||||
|
|
||||||
|
|
||||||
In the top-level combineReducers function:
|
In the top-level combineReducers function:
|
||||||
```
|
```
|
||||||
import {ScratchPaintReducer} from 'scratch-paint';
|
import {ScratchPaintReducer} from 'scratch-paint';
|
||||||
|
|
|
@ -210,6 +210,7 @@ const PaintEditorComponent = props => (
|
||||||
imageId={props.imageId}
|
imageId={props.imageId}
|
||||||
rotationCenterX={props.rotationCenterX}
|
rotationCenterX={props.rotationCenterX}
|
||||||
rotationCenterY={props.rotationCenterY}
|
rotationCenterY={props.rotationCenterY}
|
||||||
|
zoomLevelId={props.zoomLevelId}
|
||||||
onUpdateImage={props.onUpdateImage}
|
onUpdateImage={props.onUpdateImage}
|
||||||
/>
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
|
@ -332,7 +333,8 @@ PaintEditorComponent.propTypes = {
|
||||||
rtl: PropTypes.bool,
|
rtl: PropTypes.bool,
|
||||||
setCanvas: PropTypes.func.isRequired,
|
setCanvas: PropTypes.func.isRequired,
|
||||||
setTextArea: PropTypes.func.isRequired,
|
setTextArea: PropTypes.func.isRequired,
|
||||||
textArea: PropTypes.instanceOf(Element)
|
textArea: PropTypes.instanceOf(Element),
|
||||||
|
zoomLevelId: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(PaintEditorComponent);
|
export default injectIntl(PaintEditorComponent);
|
||||||
|
|
|
@ -28,6 +28,47 @@ import Formats from '../lib/format';
|
||||||
import {isBitmap, isVector} from '../lib/format';
|
import {isBitmap, isVector} from '../lib/format';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top-level paint editor component. See README for more details on usage.
|
||||||
|
*
|
||||||
|
* <PaintEditor
|
||||||
|
* image={optionalImage}
|
||||||
|
* imageId={optionalId}
|
||||||
|
* imageFormat='svg'
|
||||||
|
* rotationCenterX={optionalCenterPointX}
|
||||||
|
* rotationCenterY={optionalCenterPointY}
|
||||||
|
* rtl={true|false}
|
||||||
|
* onUpdateImage={handleUpdateImageFunction}
|
||||||
|
* zoomLevelId={optionalZoomLevelId}
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* `image`: may either be nothing, an SVG string or a base64 data URI)
|
||||||
|
* SVGs of up to size 480 x 360 will fit into the view window of the paint editor,
|
||||||
|
* while bitmaps of size up to 960 x 720 will fit into the paint editor. One unit
|
||||||
|
* of an SVG will appear twice as tall and wide as one unit of a bitmap. This quirky
|
||||||
|
* import behavior comes from needing to support legacy projects in Scratch.
|
||||||
|
*
|
||||||
|
* `imageId`: If this parameter changes, then the paint editor will be cleared, the
|
||||||
|
* undo stack reset, and the image re-imported.
|
||||||
|
*
|
||||||
|
* `imageFormat`: 'svg', 'png', or 'jpg'. Other formats are currently not supported.
|
||||||
|
*
|
||||||
|
* `rotationCenterX`: x coordinate relative to the top left corner of the sprite of
|
||||||
|
* the point that should be centered.
|
||||||
|
*
|
||||||
|
* `rotationCenterY`: y coordinate relative to the top left corner of the sprite of
|
||||||
|
* the point that should be centered.
|
||||||
|
*
|
||||||
|
* `rtl`: True if the paint editor should be laid out right to left (meant for right
|
||||||
|
* to left languages)
|
||||||
|
*
|
||||||
|
* `onUpdateImage`: A handler called with the new image (either an SVG string or an
|
||||||
|
* ImageData) each time the drawing is edited.
|
||||||
|
*
|
||||||
|
* `zoomLevelId`: All costumes with the same zoom level ID will share the same saved
|
||||||
|
* zoom level. When a new zoom level ID is encountered, the paint editor will zoom to
|
||||||
|
* fit the current costume comfortably. Leave undefined to perform no zoom to fit.
|
||||||
|
*/
|
||||||
class PaintEditor extends React.Component {
|
class PaintEditor extends React.Component {
|
||||||
static get ZOOM_INCREMENT () {
|
static get ZOOM_INCREMENT () {
|
||||||
return 0.5;
|
return 0.5;
|
||||||
|
@ -265,6 +306,7 @@ class PaintEditor extends React.Component {
|
||||||
setCanvas={this.setCanvas}
|
setCanvas={this.setCanvas}
|
||||||
setTextArea={this.setTextArea}
|
setTextArea={this.setTextArea}
|
||||||
textArea={this.state.textArea}
|
textArea={this.state.textArea}
|
||||||
|
zoomLevelId={this.props.zoomLevelId}
|
||||||
onRedo={this.props.onRedo}
|
onRedo={this.props.onRedo}
|
||||||
onSwitchToBitmap={this.props.handleSwitchToBitmap}
|
onSwitchToBitmap={this.props.handleSwitchToBitmap}
|
||||||
onSwitchToVector={this.props.handleSwitchToVector}
|
onSwitchToVector={this.props.handleSwitchToVector}
|
||||||
|
@ -314,7 +356,8 @@ PaintEditor.propTypes = {
|
||||||
shouldShowRedo: PropTypes.func.isRequired,
|
shouldShowRedo: PropTypes.func.isRequired,
|
||||||
shouldShowUndo: PropTypes.func.isRequired,
|
shouldShowUndo: PropTypes.func.isRequired,
|
||||||
updateViewBounds: PropTypes.func.isRequired,
|
updateViewBounds: PropTypes.func.isRequired,
|
||||||
viewBounds: PropTypes.instanceOf(paper.Matrix).isRequired
|
viewBounds: PropTypes.instanceOf(paper.Matrix).isRequired,
|
||||||
|
zoomLevelId: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
|
|
@ -11,12 +11,13 @@ import {undoSnapshot, clearUndoState} from '../reducers/undo';
|
||||||
import {isGroup, ungroupItems} from '../helper/group';
|
import {isGroup, ungroupItems} from '../helper/group';
|
||||||
import {clearRaster, getRaster, setupLayers} from '../helper/layer';
|
import {clearRaster, getRaster, setupLayers} from '../helper/layer';
|
||||||
import {clearSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems} from '../reducers/selected-items';
|
||||||
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, resetZoom} from '../helper/view';
|
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, resetZoom, zoomToFit} from '../helper/view';
|
||||||
import {ensureClockwise, scaleWithStrokes} from '../helper/math';
|
import {ensureClockwise, scaleWithStrokes} from '../helper/math';
|
||||||
import {clearHoveredItem} from '../reducers/hover';
|
import {clearHoveredItem} from '../reducers/hover';
|
||||||
import {clearPasteOffset} from '../reducers/clipboard';
|
import {clearPasteOffset} from '../reducers/clipboard';
|
||||||
import {changeFormat} from '../reducers/format';
|
import {changeFormat} from '../reducers/format';
|
||||||
import {updateViewBounds} from '../reducers/view-bounds';
|
import {updateViewBounds} from '../reducers/view-bounds';
|
||||||
|
import {saveZoomLevel, setZoomLevelId} from '../reducers/zoom-levels';
|
||||||
import styles from './paper-canvas.css';
|
import styles from './paper-canvas.css';
|
||||||
|
|
||||||
class PaperCanvas extends React.Component {
|
class PaperCanvas extends React.Component {
|
||||||
|
@ -25,6 +26,7 @@ class PaperCanvas extends React.Component {
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'setCanvas',
|
'setCanvas',
|
||||||
'importSvg',
|
'importSvg',
|
||||||
|
'maybeZoomToFit',
|
||||||
'switchCostume'
|
'switchCostume'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -32,6 +34,16 @@ class PaperCanvas extends React.Component {
|
||||||
paper.setup(this.canvas);
|
paper.setup(this.canvas);
|
||||||
resetZoom();
|
resetZoom();
|
||||||
this.props.updateViewBounds(paper.view.matrix);
|
this.props.updateViewBounds(paper.view.matrix);
|
||||||
|
if (this.props.zoomLevelId) {
|
||||||
|
this.props.setZoomLevelId(this.props.zoomLevelId);
|
||||||
|
if (this.props.zoomLevels[this.props.zoomLevelId]) {
|
||||||
|
// This is the matrix that the view should be zoomed to after image import
|
||||||
|
this.shouldZoomToFit = this.props.zoomLevels[this.props.zoomLevelId];
|
||||||
|
} else {
|
||||||
|
// Zoom to fit true means find a comfortable zoom level for viewing the costume
|
||||||
|
this.shouldZoomToFit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const context = this.canvas.getContext('2d');
|
const context = this.canvas.getContext('2d');
|
||||||
context.webkitImageSmoothingEnabled = false;
|
context.webkitImageSmoothingEnabled = false;
|
||||||
|
@ -46,14 +58,27 @@ class PaperCanvas extends React.Component {
|
||||||
}
|
}
|
||||||
componentWillReceiveProps (newProps) {
|
componentWillReceiveProps (newProps) {
|
||||||
if (this.props.imageId !== newProps.imageId) {
|
if (this.props.imageId !== newProps.imageId) {
|
||||||
this.switchCostume(
|
this.switchCostume(newProps.imageFormat, newProps.image,
|
||||||
newProps.imageFormat, newProps.image, newProps.rotationCenterX, newProps.rotationCenterY);
|
newProps.rotationCenterX, newProps.rotationCenterY,
|
||||||
|
this.props.zoomLevelId, newProps.zoomLevelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
this.props.saveZoomLevel();
|
||||||
paper.remove();
|
paper.remove();
|
||||||
}
|
}
|
||||||
switchCostume (format, image, rotationCenterX, rotationCenterY) {
|
switchCostume (format, image, rotationCenterX, rotationCenterY, oldZoomLevelId, newZoomLevelId) {
|
||||||
|
if (oldZoomLevelId && oldZoomLevelId !== newZoomLevelId) {
|
||||||
|
this.props.saveZoomLevel();
|
||||||
|
}
|
||||||
|
if (newZoomLevelId && oldZoomLevelId !== newZoomLevelId) {
|
||||||
|
if (this.props.zoomLevels[newZoomLevelId]) {
|
||||||
|
this.shouldZoomToFit = this.props.zoomLevels[newZoomLevelId];
|
||||||
|
} else {
|
||||||
|
this.shouldZoomToFit = true;
|
||||||
|
}
|
||||||
|
this.props.setZoomLevelId(newZoomLevelId);
|
||||||
|
}
|
||||||
for (const layer of paper.project.layers) {
|
for (const layer of paper.project.layers) {
|
||||||
if (layer.data.isRasterLayer) {
|
if (layer.data.isRasterLayer) {
|
||||||
clearRaster();
|
clearRaster();
|
||||||
|
@ -87,18 +112,28 @@ class PaperCanvas extends React.Component {
|
||||||
imgElement,
|
imgElement,
|
||||||
(ART_BOARD_WIDTH / 2) - rotationCenterX,
|
(ART_BOARD_WIDTH / 2) - rotationCenterX,
|
||||||
(ART_BOARD_HEIGHT / 2) - rotationCenterY);
|
(ART_BOARD_HEIGHT / 2) - rotationCenterY);
|
||||||
|
this.maybeZoomToFit(true /* isBitmap */);
|
||||||
performSnapshot(this.props.undoSnapshot, Formats.BITMAP_SKIP_CONVERT);
|
performSnapshot(this.props.undoSnapshot, Formats.BITMAP_SKIP_CONVERT);
|
||||||
};
|
};
|
||||||
imgElement.src = image;
|
imgElement.src = image;
|
||||||
} else if (format === 'svg') {
|
} else if (format === 'svg') {
|
||||||
this.props.changeFormat(Formats.VECTOR_SKIP_CONVERT);
|
this.props.changeFormat(Formats.VECTOR_SKIP_CONVERT);
|
||||||
this.importSvg(image, rotationCenterX, rotationCenterY);
|
this.importSvg(image, rotationCenterX, rotationCenterY);
|
||||||
|
this.maybeZoomToFit();
|
||||||
} else {
|
} else {
|
||||||
log.error(`Didn't recognize format: ${format}. Use 'jpg', 'png' or 'svg'.`);
|
log.error(`Didn't recognize format: ${format}. Use 'jpg', 'png' or 'svg'.`);
|
||||||
this.props.changeFormat(Formats.VECTOR_SKIP_CONVERT);
|
this.props.changeFormat(Formats.VECTOR_SKIP_CONVERT);
|
||||||
performSnapshot(this.props.undoSnapshot, Formats.VECTOR_SKIP_CONVERT);
|
performSnapshot(this.props.undoSnapshot, Formats.VECTOR_SKIP_CONVERT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
maybeZoomToFit (isBitmapMode) {
|
||||||
|
if (this.shouldZoomToFit instanceof paper.Matrix) {
|
||||||
|
paper.view.matrix = this.shouldZoomToFit;
|
||||||
|
} else if (this.shouldZoomToFit === true) {
|
||||||
|
zoomToFit(isBitmapMode);
|
||||||
|
}
|
||||||
|
this.shouldZoomToFit = false;
|
||||||
|
}
|
||||||
importSvg (svg, rotationCenterX, rotationCenterY) {
|
importSvg (svg, rotationCenterX, rotationCenterY) {
|
||||||
const paperCanvas = this;
|
const paperCanvas = this;
|
||||||
// Pre-process SVG to prevent parsing errors (discussion from #213)
|
// Pre-process SVG to prevent parsing errors (discussion from #213)
|
||||||
|
@ -221,12 +256,19 @@ PaperCanvas.propTypes = {
|
||||||
imageId: PropTypes.string,
|
imageId: PropTypes.string,
|
||||||
rotationCenterX: PropTypes.number,
|
rotationCenterX: PropTypes.number,
|
||||||
rotationCenterY: PropTypes.number,
|
rotationCenterY: PropTypes.number,
|
||||||
|
saveZoomLevel: PropTypes.func.isRequired,
|
||||||
|
setZoomLevelId: PropTypes.func.isRequired,
|
||||||
undoSnapshot: PropTypes.func.isRequired,
|
undoSnapshot: PropTypes.func.isRequired,
|
||||||
updateViewBounds: PropTypes.func.isRequired
|
updateViewBounds: PropTypes.func.isRequired,
|
||||||
|
zoomLevelId: PropTypes.string,
|
||||||
|
zoomLevels: PropTypes.shape({
|
||||||
|
currentZoomLevelId: PropTypes.string
|
||||||
|
})
|
||||||
};
|
};
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
mode: state.scratchPaint.mode,
|
mode: state.scratchPaint.mode,
|
||||||
format: state.scratchPaint.format
|
format: state.scratchPaint.format,
|
||||||
|
zoomLevels: state.scratchPaint.zoomLevels
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
undoSnapshot: snapshot => {
|
undoSnapshot: snapshot => {
|
||||||
|
@ -247,6 +289,12 @@ const mapDispatchToProps = dispatch => ({
|
||||||
changeFormat: format => {
|
changeFormat: format => {
|
||||||
dispatch(changeFormat(format));
|
dispatch(changeFormat(format));
|
||||||
},
|
},
|
||||||
|
saveZoomLevel: () => {
|
||||||
|
dispatch(saveZoomLevel(paper.view.matrix));
|
||||||
|
},
|
||||||
|
setZoomLevelId: zoomLevelId => {
|
||||||
|
dispatch(setZoomLevelId(zoomLevelId));
|
||||||
|
},
|
||||||
updateViewBounds: matrix => {
|
updateViewBounds: matrix => {
|
||||||
dispatch(updateViewBounds(matrix));
|
dispatch(updateViewBounds(matrix));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import {getSelectedRootItems} from './selection';
|
import {getSelectedRootItems} from './selection';
|
||||||
|
import {getRaster} from './layer';
|
||||||
|
import {getHitBounds} from './bitmap';
|
||||||
|
|
||||||
// Vectors are imported and exported at SVG_ART_BOARD size.
|
// Vectors are imported and exported at SVG_ART_BOARD size.
|
||||||
// Once they are imported however, both SVGs and bitmaps are on
|
// Once they are imported however, both SVGs and bitmaps are on
|
||||||
|
@ -8,6 +10,9 @@ const SVG_ART_BOARD_WIDTH = 480;
|
||||||
const SVG_ART_BOARD_HEIGHT = 360;
|
const SVG_ART_BOARD_HEIGHT = 360;
|
||||||
const ART_BOARD_WIDTH = 480 * 2;
|
const ART_BOARD_WIDTH = 480 * 2;
|
||||||
const ART_BOARD_HEIGHT = 360 * 2;
|
const ART_BOARD_HEIGHT = 360 * 2;
|
||||||
|
const PADDING_PERCENT = 25; // Padding as a percent of the max of width/height of the sprite
|
||||||
|
const MIN_RATIO = .125; // Zoom in to at least 1/8 of the screen. This way you don't end up incredibly
|
||||||
|
// zoomed in for tiny costumes.
|
||||||
|
|
||||||
const clampViewBounds = () => {
|
const clampViewBounds = () => {
|
||||||
const {left, right, top, bottom} = paper.project.view.bounds;
|
const {left, right, top, bottom} = paper.project.view.bounds;
|
||||||
|
@ -28,7 +33,7 @@ const clampViewBounds = () => {
|
||||||
// Zoom keeping a project-space point fixed.
|
// Zoom keeping a project-space point fixed.
|
||||||
// This article was helpful http://matthiasberth.com/tech/stable-zoom-and-pan-in-paperjs
|
// This article was helpful http://matthiasberth.com/tech/stable-zoom-and-pan-in-paperjs
|
||||||
const zoomOnFixedPoint = (deltaZoom, fixedPoint) => {
|
const zoomOnFixedPoint = (deltaZoom, fixedPoint) => {
|
||||||
const {view} = paper.project;
|
const view = paper.view;
|
||||||
const preZoomCenter = view.center;
|
const preZoomCenter = view.center;
|
||||||
const newZoom = Math.max(0.5, view.zoom + deltaZoom);
|
const newZoom = Math.max(0.5, view.zoom + deltaZoom);
|
||||||
const scaling = view.zoom / newZoom;
|
const scaling = view.zoom / newZoom;
|
||||||
|
@ -70,6 +75,28 @@ const pan = (dx, dy) => {
|
||||||
clampViewBounds();
|
clampViewBounds();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const zoomToFit = isBitmap => {
|
||||||
|
resetZoom();
|
||||||
|
let bounds;
|
||||||
|
if (isBitmap) {
|
||||||
|
bounds = getHitBounds(getRaster());
|
||||||
|
} else {
|
||||||
|
bounds = paper.project.activeLayer.bounds;
|
||||||
|
}
|
||||||
|
if (bounds && bounds.width && bounds.height) {
|
||||||
|
// Ratio of (sprite length plus padding on all sides) to art board length.
|
||||||
|
let ratio = Math.max(bounds.width * (1 + (2 * PADDING_PERCENT / 100)) / ART_BOARD_WIDTH,
|
||||||
|
bounds.height * (1 + (2 * PADDING_PERCENT / 100)) / ART_BOARD_HEIGHT);
|
||||||
|
// Clamp ratio
|
||||||
|
ratio = Math.max(Math.min(1, ratio), MIN_RATIO);
|
||||||
|
if (ratio < 1) {
|
||||||
|
paper.view.center = bounds.center;
|
||||||
|
paper.view.zoom = paper.view.zoom / ratio;
|
||||||
|
clampViewBounds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ART_BOARD_HEIGHT,
|
ART_BOARD_HEIGHT,
|
||||||
ART_BOARD_WIDTH,
|
ART_BOARD_WIDTH,
|
||||||
|
@ -79,5 +106,6 @@ export {
|
||||||
pan,
|
pan,
|
||||||
resetZoom,
|
resetZoom,
|
||||||
zoomOnSelection,
|
zoomOnSelection,
|
||||||
zoomOnFixedPoint
|
zoomOnFixedPoint,
|
||||||
|
zoomToFit
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,7 @@ import selectedItemReducer from './selected-items';
|
||||||
import textEditTargetReducer from './text-edit-target';
|
import textEditTargetReducer from './text-edit-target';
|
||||||
import viewBoundsReducer from './view-bounds';
|
import viewBoundsReducer from './view-bounds';
|
||||||
import undoReducer from './undo';
|
import undoReducer from './undo';
|
||||||
|
import zoomLevelsReducer from './zoom-levels';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
mode: modeReducer,
|
mode: modeReducer,
|
||||||
|
@ -36,5 +37,6 @@ export default combineReducers({
|
||||||
selectedItems: selectedItemReducer,
|
selectedItems: selectedItemReducer,
|
||||||
textEditTarget: textEditTargetReducer,
|
textEditTarget: textEditTargetReducer,
|
||||||
undo: undoReducer,
|
undo: undoReducer,
|
||||||
viewBounds: viewBoundsReducer
|
viewBounds: viewBoundsReducer,
|
||||||
|
zoomLevels: zoomLevelsReducer
|
||||||
});
|
});
|
||||||
|
|
49
src/reducers/zoom-levels.js
Normal file
49
src/reducers/zoom-levels.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import paper from '@scratch/paper';
|
||||||
|
import log from '../log/log';
|
||||||
|
|
||||||
|
const SAVE_ZOOM_LEVEL = 'scratch-paint/zoom-levels/SAVE_ZOOM_LEVEL';
|
||||||
|
const SET_ZOOM_LEVEL_ID = 'scratch-paint/zoom-levels/SET_ZOOM_LEVEL_ID';
|
||||||
|
const initialState = {};
|
||||||
|
|
||||||
|
const reducer = function (state, action) {
|
||||||
|
if (typeof state === 'undefined') state = initialState;
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_ZOOM_LEVEL_ID:
|
||||||
|
if (action.zoomLevelId === 'currentZoomLevelId') {
|
||||||
|
log.warn(`currentZoomLevelId is an invalid string for zoomLevel`);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
currentZoomLevelId: action.zoomLevelId
|
||||||
|
});
|
||||||
|
case SAVE_ZOOM_LEVEL:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[state.currentZoomLevelId]: action.zoomLevel
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action creators ==================================
|
||||||
|
const saveZoomLevel = function (zoomLevel) {
|
||||||
|
if (!(zoomLevel instanceof paper.Matrix)) {
|
||||||
|
log.warn(`Not a matrix: ${zoomLevel}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: SAVE_ZOOM_LEVEL,
|
||||||
|
zoomLevel: new paper.Matrix(zoomLevel)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const setZoomLevelId = function (zoomLevelId) {
|
||||||
|
return {
|
||||||
|
type: SET_ZOOM_LEVEL_ID,
|
||||||
|
zoomLevelId: zoomLevelId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
reducer as default,
|
||||||
|
saveZoomLevel,
|
||||||
|
setZoomLevelId
|
||||||
|
};
|
Loading…
Reference in a new issue