scratch-paint/src/containers/paper-canvas.jsx

237 lines
8.1 KiB
React
Raw Normal View History

2017-07-27 16:41:41 -04:00
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
2017-07-17 12:04:12 -04:00
import React from 'react';
2017-10-05 15:41:22 -04:00
import {connect} from 'react-redux';
2017-10-12 18:35:30 -04:00
import paper from '@scratch/paper';
2017-11-07 14:02:39 -05:00
import Modes from '../lib/modes';
2017-11-30 17:56:10 -05:00
import log from '../log/log';
2017-07-17 12:04:12 -04:00
2017-10-05 15:41:22 -04:00
import {performSnapshot} from '../helper/undo';
import {undoSnapshot, clearUndoState} from '../reducers/undo';
import {isGroup, ungroupItems} from '../helper/group';
2017-10-24 13:21:57 -04:00
import {setupLayers} from '../helper/layer';
import {deleteSelection, getSelectedLeafItems} from '../helper/selection';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {pan, resetZoom, zoomOnFixedPoint} from '../helper/view';
import {clearHoveredItem} from '../reducers/hover';
import {clearPasteOffset} from '../reducers/clipboard';
2017-10-05 15:41:22 -04:00
2017-09-06 18:01:49 -04:00
import styles from './paper-canvas.css';
2017-07-17 12:04:12 -04:00
class PaperCanvas extends React.Component {
2017-07-27 16:41:41 -04:00
constructor (props) {
super(props);
bindAll(this, [
2017-08-30 10:50:05 -04:00
'setCanvas',
'importSvg',
'handleKeyDown',
'handleWheel',
'_ensureClockwise'
2017-07-27 16:41:41 -04:00
]);
}
2017-07-17 12:04:12 -04:00
componentDidMount () {
document.addEventListener('keydown', this.handleKeyDown);
2017-07-27 00:34:33 -04:00
paper.setup(this.canvas);
2017-09-18 11:28:34 -04:00
// Don't show handles by default
paper.settings.handleSize = 0;
2017-10-24 13:21:57 -04:00
// Make layers.
setupLayers();
2017-08-30 10:50:05 -04:00
if (this.props.svg) {
this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY);
} else {
performSnapshot(this.props.undoSnapshot);
2017-08-30 10:50:05 -04:00
}
}
componentWillReceiveProps (newProps) {
if (this.props.svgId === newProps.svgId) return;
for (const layer of paper.project.layers) {
2017-10-24 13:21:57 -04:00
if (!layer.data.isBackgroundGuideLayer) {
layer.removeChildren();
}
}
this.props.clearUndo();
this.props.clearSelectedItems();
this.props.clearHoveredItem();
this.props.clearPasteOffset();
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);
paper.project.view.zoom = oldZoom;
paper.project.view.center = oldCenter;
paper.project.view.update();
}
2017-07-17 12:04:12 -04:00
}
componentWillUnmount () {
2017-07-27 00:34:33 -04:00
paper.remove();
document.removeEventListener('keydown', this.handleKeyDown);
}
handleKeyDown (event) {
if (event.target instanceof HTMLInputElement) {
// Ignore delete if a text input field is focused
return;
}
// Backspace, delete
2017-10-26 12:05:54 -04:00
if (event.key === 'Delete' || event.key === 'Backspace') {
if (deleteSelection(this.props.mode, this.props.onUpdateSvg)) {
this.props.setSelectedItems();
}
}
2017-07-17 12:04:12 -04:00
}
importSvg (svg, rotationCenterX, rotationCenterY) {
const paperCanvas = this;
// Pre-process SVG to prevent parsing errors (discussion from #213)
// 1. Remove newlines and tab characters, chrome will not load urls with them.
// https://www.chromestatus.com/feature/5735596811091968
svg = svg.split(/[\n|\r|\t]/).join('');
// 2. Remove svg: namespace on elements.
svg = svg.split(/<\s*svg:/).join('<');
svg = svg.split(/<\/\s*svg:/).join('</');
// 3. Add root svg namespace if it does not exist.
const svgAttrs = svg.match(/<svg [^>]*>/);
if (svgAttrs && svgAttrs[0].indexOf('xmlns=') === -1) {
svg = svg.replace(
'<svg ', '<svg xmlns="http://www.w3.org/2000/svg" ');
}
paper.project.importSVG(svg, {
expandShapes: true,
onLoad: function (item) {
2017-11-30 17:56:10 -05:00
if (!item) {
log.error('SVG import failed:');
log.info(svg);
performSnapshot(paperCanvas.props.undoSnapshot);
return;
}
const itemWidth = item.bounds.width;
const itemHeight = item.bounds.height;
// Remove viewbox
if (item.clipped) {
let mask;
for (const child of item.children) {
if (child.isClipMask()) {
mask = child;
break;
2017-09-21 18:20:44 -04:00
}
2017-08-30 10:50:05 -04:00
}
item.clipped = false;
mask.remove();
}
// Reduce single item nested in groups
if (item.children && item.children.length === 1) {
item = item.reduce();
}
paperCanvas._ensureClockwise(item);
if (typeof rotationCenterX !== 'undefined' && typeof rotationCenterY !== 'undefined') {
item.position =
paper.project.view.center
.add(itemWidth / 2, itemHeight / 2)
.subtract(rotationCenterX, rotationCenterY);
} else {
// Center
item.position = paper.project.view.center;
2017-08-30 10:50:05 -04:00
}
if (isGroup(item)) {
ungroupItems([item]);
}
performSnapshot(paperCanvas.props.undoSnapshot);
paper.project.view.update();
}
});
2017-08-30 10:50:05 -04:00
}
_ensureClockwise (item) {
if (item instanceof paper.Group) {
for (const child of item.children) {
this._ensureClockwise(child);
}
} else if (item instanceof paper.PathItem) {
item.clockwise = true;
}
}
2017-07-27 16:41:41 -04:00
setCanvas (canvas) {
this.canvas = canvas;
if (this.props.canvasRef) {
this.props.canvasRef(canvas);
}
}
handleWheel (event) {
2017-10-27 11:18:21 -04:00
if (event.metaKey || event.ctrlKey) {
// 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();
}
2017-07-17 12:04:12 -04:00
render () {
return (
2017-07-27 00:34:33 -04:00
<canvas
2017-09-06 18:01:49 -04:00
className={styles.paperCanvas}
height="360px"
2017-07-27 16:41:41 -04:00
ref={this.setCanvas}
width="480px"
onWheel={this.handleWheel}
2017-07-27 00:34:33 -04:00
/>
2017-07-17 12:04:12 -04:00
);
}
}
PaperCanvas.propTypes = {
2017-08-30 10:50:05 -04:00
canvasRef: PropTypes.func,
clearHoveredItem: PropTypes.func.isRequired,
clearPasteOffset: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
clearUndo: PropTypes.func.isRequired,
mode: PropTypes.oneOf(Object.keys(Modes)),
onUpdateSvg: PropTypes.func.isRequired,
rotationCenterX: PropTypes.number,
rotationCenterY: PropTypes.number,
setSelectedItems: PropTypes.func.isRequired,
2017-10-05 15:41:22 -04:00
svg: PropTypes.string,
svgId: PropTypes.string,
2017-10-05 15:41:22 -04:00
undoSnapshot: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
mode: state.scratchPaint.mode
});
2017-10-05 15:41:22 -04:00
const mapDispatchToProps = dispatch => ({
undoSnapshot: snapshot => {
dispatch(undoSnapshot(snapshot));
},
clearUndo: () => {
dispatch(clearUndoState());
},
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems()));
},
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
clearHoveredItem: () => {
dispatch(clearHoveredItem());
},
clearPasteOffset: () => {
dispatch(clearPasteOffset());
2017-10-05 15:41:22 -04:00
}
});
2017-07-17 12:04:12 -04:00
2017-10-05 15:41:22 -04:00
export default connect(
mapStateToProps,
2017-10-05 15:41:22 -04:00
mapDispatchToProps
)(PaperCanvas);