import PropTypes from 'prop-types'; import React from 'react'; import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx'; import {changeMode} from '../reducers/modes'; import {undo, redo, undoSnapshot} from '../reducers/undo'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {incrementPasteOffset, setClipboardItems} from '../reducers/clipboard'; import {deactivateEyeDropper} from '../reducers/eye-dropper'; import {hideGuideLayers, showGuideLayers} from '../helper/layer'; import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo'; import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order'; import {groupSelection, ungroupSelection} from '../helper/group'; import {clearSelection, getSelectedLeafItems, getSelectedRootItems} from '../helper/selection'; import {resetZoom, zoomOnSelection} from '../helper/view'; import EyeDropperTool from '../helper/eye-dropper'; import Modes from '../lib/modes'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import paper from '@scratch/paper'; class PaintEditor extends React.Component { static get ZOOM_INCREMENT () { return 0.5; } constructor (props) { super(props); bindAll(this, [ 'handleUpdateSvg', 'handleUndo', 'handleRedo', 'handleSendBackward', 'handleSendForward', 'handleSendToBack', 'handleSendToFront', 'handleGroup', 'handleUngroup', 'canRedo', 'canUndo', 'handleCopyToClipboard', 'handlePasteFromClipboard', 'setPaintEditor', 'onMouseDown', 'startEyeDroppingLoop', 'stopEyeDroppingLoop' ]); this.state = { colorInfo: null }; } componentDidMount () { document.addEventListener('keydown', this.props.onKeyPress); } shouldComponentUpdate (nextProps, nextState) { return this.props.isEyeDropping !== nextProps.isEyeDropping || this.state.colorInfo !== nextState.colorInfo || this.props.clipboardItems !== nextProps.clipboardItems || this.props.pasteOffset !== nextProps.pasteOffset || this.props.selectedItems !== nextProps.selectedItems || this.props.undoState !== nextProps.undoState; } componentDidUpdate (prevProps) { if (this.props.isEyeDropping && !prevProps.isEyeDropping) { this.startEyeDroppingLoop(); } else if (!this.props.isEyeDropping && prevProps.isEyeDropping) { this.stopEyeDroppingLoop(); } } componentWillUnmount () { document.removeEventListener('keydown', this.props.onKeyPress); this.stopEyeDroppingLoop(); } 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(); const guideLayers = hideGuideLayers(); const bounds = paper.project.activeLayer.bounds; this.props.onUpdateSvg( paper.project.exportSVG({ asString: true, matrix: new paper.Matrix().translate(-bounds.x, -bounds.y) }), paper.project.view.center.x - bounds.x, paper.project.view.center.y - bounds.y); showGuideLayers(guideLayers); if (!skipSnapshot) { performSnapshot(this.props.undoSnapshot); } // Restore old zoom paper.project.view.zoom = oldZoom; paper.project.view.center = oldCenter; paper.project.view.update(); } handleUndo () { performUndo(this.props.undoState, this.props.onUndo, this.props.setSelectedItems, this.handleUpdateSvg); } handleRedo () { performRedo(this.props.undoState, this.props.onRedo, this.props.setSelectedItems, this.handleUpdateSvg); } handleGroup () { groupSelection(this.props.clearSelectedItems, this.props.setSelectedItems, this.handleUpdateSvg); } handleUngroup () { ungroupSelection(this.props.clearSelectedItems, this.props.setSelectedItems, this.handleUpdateSvg); } handleSendBackward () { sendBackward(this.handleUpdateSvg); } handleSendForward () { bringForward(this.handleUpdateSvg); } handleSendToBack () { sendToBack(this.handleUpdateSvg); } handleSendToFront () { bringToFront(this.handleUpdateSvg); } handleCopyToClipboard () { const selectedItems = getSelectedRootItems(); if (selectedItems.length > 0) { const clipboardItems = []; for (let i = 0; i < selectedItems.length; i++) { const jsonItem = selectedItems[i].exportJSON({asString: false}); clipboardItems.push(jsonItem); } this.props.setClipboardItems(clipboardItems); } } handlePasteFromClipboard () { clearSelection(this.props.clearSelectedItems); if (this.props.clipboardItems.length > 0) { for (let i = 0; i < this.props.clipboardItems.length; i++) { const item = paper.Base.importJSON(this.props.clipboardItems[i]); if (item) { item.selected = true; } const placedItem = paper.project.getActiveLayer().addChild(item); placedItem.position.x += 10 * this.props.pasteOffset; placedItem.position.y += 10 * this.props.pasteOffset; } this.props.incrementPasteOffset(); this.props.setSelectedItems(); paper.project.view.update(); this.handleUpdateSvg(); } } canUndo () { return shouldShowUndo(this.props.undoState); } canRedo () { return shouldShowRedo(this.props.undoState); } handleZoomIn () { zoomOnSelection(PaintEditor.ZOOM_INCREMENT); } handleZoomOut () { zoomOnSelection(-PaintEditor.ZOOM_INCREMENT); } handleZoomReset () { resetZoom(); } setPaintEditor (paintEditor) { this.paintEditor = paintEditor; } onMouseDown () { if (this.props.isEyeDropping) { const colorString = this.eyeDropper.colorString; const callback = this.props.changeColorToEyeDropper; this.props.onDeactivateEyeDropper(this.props.previousMode); this.stopEyeDroppingLoop(); if (!this.eyeDropper.hideLoupe) { // If not hide loupe, that means the click is inside the canvas, // so apply the new color callback(colorString); } this.setState({colorInfo: null}); } } startEyeDroppingLoop () { const canvas = this.paintEditor.getWrappedInstance().canvas; this.eyeDropper = new EyeDropperTool(canvas); this.eyeDropper.activate(); // document listeners used to detect if a mouse is down outside of the // canvas, and should therefore stop the eye dropper document.addEventListener('mousedown', this.onMouseDown); document.addEventListener('touchstart', this.onMouseDown); this.intervalId = setInterval(() => { this.setState({ colorInfo: this.eyeDropper.getColorInfo( this.eyeDropper.pickX, this.eyeDropper.pickY, this.eyeDropper.hideLoupe ) }); }, 30); } stopEyeDroppingLoop () { clearInterval(this.intervalId); document.removeEventListener('mousedown', this.onMouseDown); document.removeEventListener('touchstart', this.onMouseDown); } render () { return ( ); } } PaintEditor.propTypes = { changeColorToEyeDropper: PropTypes.func, clearSelectedItems: PropTypes.func.isRequired, clipboardItems: PropTypes.arrayOf(PropTypes.array), incrementPasteOffset: PropTypes.func.isRequired, isEyeDropping: PropTypes.bool, name: PropTypes.string, onDeactivateEyeDropper: PropTypes.func.isRequired, onKeyPress: PropTypes.func.isRequired, onRedo: PropTypes.func.isRequired, onUndo: PropTypes.func.isRequired, onUpdateName: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired, pasteOffset: PropTypes.number, previousMode: PropTypes.string, rotationCenterX: PropTypes.number, rotationCenterY: PropTypes.number, selectedItems: PropTypes.arrayOf(PropTypes.object), setClipboardItems: PropTypes.func.isRequired, setSelectedItems: PropTypes.func.isRequired, svg: PropTypes.string, svgId: PropTypes.string, undoSnapshot: PropTypes.func.isRequired, undoState: PropTypes.shape({ stack: PropTypes.arrayOf(PropTypes.object).isRequired, pointer: PropTypes.number.isRequired }) }; const mapStateToProps = state => ({ changeColorToEyeDropper: state.scratchPaint.color.eyeDropper.callback, clipboardItems: state.scratchPaint.clipboard.items, isEyeDropping: state.scratchPaint.color.eyeDropper.active, pasteOffset: state.scratchPaint.clipboard.pasteOffset, previousItems: state.scratchPaint.color.eyeDropper.previousItems, previousMode: state.scratchPaint.color.eyeDropper.previousMode, selectedItems: state.scratchPaint.selectedItems, undoState: state.scratchPaint.undo }); const mapDispatchToProps = dispatch => ({ onKeyPress: event => { if (event.key === 'e') { dispatch(changeMode(Modes.ERASER)); } else if (event.key === 'b') { dispatch(changeMode(Modes.BRUSH)); } else if (event.key === 'l') { dispatch(changeMode(Modes.LINE)); } else if (event.key === 's') { dispatch(changeMode(Modes.SELECT)); } }, clearSelectedItems: () => { dispatch(clearSelectedItems()); }, setSelectedItems: () => { dispatch(setSelectedItems(getSelectedLeafItems())); }, onUndo: () => { dispatch(undo()); }, onRedo: () => { dispatch(redo()); }, undoSnapshot: snapshot => { dispatch(undoSnapshot(snapshot)); }, setClipboardItems: items => { dispatch(setClipboardItems(items)); }, incrementPasteOffset: () => { dispatch(incrementPasteOffset()); }, onDeactivateEyeDropper: previousMode => { // deactivate the eye dropper, reset to previously selected mode dispatch(deactivateEyeDropper()); dispatch(changeMode(previousMode)); } }); export default connect( mapStateToProps, mapDispatchToProps )(PaintEditor);