mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-24 22:42:28 -05:00
215 lines
9.2 KiB
JavaScript
215 lines
9.2 KiB
JavaScript
import paper from '@scratch/paper';
|
|
import PropTypes from 'prop-types';
|
|
import log from '../log/log';
|
|
import bindAll from 'lodash.bindall';
|
|
import React from 'react';
|
|
import omit from 'lodash.omit';
|
|
import {connect} from 'react-redux';
|
|
|
|
import {undoSnapshot} from '../reducers/undo';
|
|
import {setSelectedItems} from '../reducers/selected-items';
|
|
import {updateViewBounds} from '../reducers/view-bounds';
|
|
|
|
import {getSelectedLeafItems} from '../helper/selection';
|
|
import {getRaster, hideGuideLayers, showGuideLayers} from '../helper/layer';
|
|
import {commitRectToBitmap, commitOvalToBitmap, commitSelectionToBitmap, getHitBounds} from '../helper/bitmap';
|
|
import {performSnapshot} from '../helper/undo';
|
|
import {scaleWithStrokes} from '../helper/math';
|
|
|
|
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, SVG_ART_BOARD_WIDTH, SVG_ART_BOARD_HEIGHT} from '../helper/view';
|
|
import {setWorkspaceBounds} from '../helper/view';
|
|
|
|
import Modes from '../lib/modes';
|
|
import {BitmapModes} from '../lib/modes';
|
|
import Formats from '../lib/format';
|
|
import {isBitmap, isVector} from '../lib/format';
|
|
|
|
const UpdateImageHOC = function (WrappedComponent) {
|
|
class UpdateImageWrapper extends React.Component {
|
|
constructor (props) {
|
|
super(props);
|
|
bindAll(this, [
|
|
'handleUpdateImage',
|
|
'handleUpdateBitmap',
|
|
'handleUpdateVector'
|
|
]);
|
|
}
|
|
/**
|
|
* @param {?boolean} skipSnapshot True if the call to update image should not trigger saving
|
|
* an undo state. For instance after calling undo.
|
|
* @param {?Formats} formatOverride Normally the mode is used to determine the format of the image,
|
|
* but the format used can be overridden here. In particular when converting between formats,
|
|
* the does not accurately represent the format.
|
|
*/
|
|
handleUpdateImage (skipSnapshot, formatOverride) {
|
|
// If in the middle of switching formats, rely on the current mode instead of format.
|
|
const actualFormat = formatOverride ? formatOverride :
|
|
BitmapModes[this.props.mode] ? Formats.BITMAP : Formats.VECTOR;
|
|
if (isBitmap(actualFormat)) {
|
|
this.handleUpdateBitmap(skipSnapshot);
|
|
} else if (isVector(actualFormat)) {
|
|
this.handleUpdateVector(skipSnapshot);
|
|
}
|
|
// Any time an image update is made, recalculate the bounds of the artwork
|
|
setWorkspaceBounds();
|
|
this.props.updateViewBounds(paper.view.matrix);
|
|
}
|
|
handleUpdateBitmap (skipSnapshot) {
|
|
if (!getRaster().loaded) {
|
|
// In general, callers of updateImage should wait for getRaster().loaded = true before
|
|
// calling updateImage.
|
|
// However, this may happen if the user is rapidly undoing/redoing. In this case it's safe
|
|
// to skip the update.
|
|
log.warn('Bitmap layer should be loaded before calling updateImage.');
|
|
return;
|
|
}
|
|
// Anything that is selected is on the vector layer waiting to be committed to the bitmap layer.
|
|
// Plaster the selection onto the raster layer before exporting, if there is a selection.
|
|
const plasteredRaster = getRaster().getSubRaster(getRaster().bounds); // Clone the raster layer
|
|
plasteredRaster.remove(); // Don't insert
|
|
const selectedItems = getSelectedLeafItems();
|
|
if (selectedItems.length === 1) {
|
|
const item = selectedItems[0];
|
|
if (item instanceof paper.Raster) {
|
|
if (!item.loaded ||
|
|
(item.data &&
|
|
item.data.expanded &&
|
|
!item.data.expanded.loaded)) {
|
|
// This may get logged when rapidly undoing/redoing or changing costumes,
|
|
// in which case the warning is not relevant.
|
|
log.warn('Bitmap layer should be loaded before calling updateImage.');
|
|
return;
|
|
}
|
|
commitSelectionToBitmap(item, plasteredRaster);
|
|
} else if (item instanceof paper.Shape && item.type === 'rectangle') {
|
|
commitRectToBitmap(item, plasteredRaster);
|
|
} else if (item instanceof paper.Shape && item.type === 'ellipse') {
|
|
commitOvalToBitmap(item, plasteredRaster);
|
|
} else if (item instanceof paper.PointText) {
|
|
const bounds = item.drawnBounds;
|
|
const textRaster = item.rasterize(72, false /* insert */, bounds);
|
|
plasteredRaster.drawImage(
|
|
textRaster.canvas,
|
|
new paper.Point(Math.floor(bounds.x), Math.floor(bounds.y))
|
|
);
|
|
}
|
|
}
|
|
const rect = getHitBounds(plasteredRaster);
|
|
|
|
// Use 1x1 instead of 0x0 for getting imageData since paper.js automagically
|
|
// returns the full artboard in the case of getImageData(0x0).
|
|
// Bitmaps need a non-zero width/height in order to be saved as PNG.
|
|
if (rect.width === 0 || rect.height === 0) {
|
|
rect.width = rect.height = 1;
|
|
}
|
|
|
|
const imageData = plasteredRaster.getImageData(rect);
|
|
|
|
this.props.onUpdateImage(
|
|
false /* isVector */,
|
|
imageData,
|
|
(ART_BOARD_WIDTH / 2) - rect.x,
|
|
(ART_BOARD_HEIGHT / 2) - rect.y);
|
|
|
|
if (!skipSnapshot) {
|
|
performSnapshot(this.props.undoSnapshot, Formats.BITMAP);
|
|
}
|
|
}
|
|
handleUpdateVector (skipSnapshot) {
|
|
// Remove viewbox (this would make it export at MAX_WORKSPACE_BOUNDS)
|
|
let workspaceMask;
|
|
if (paper.project.activeLayer.clipped) {
|
|
for (const child of paper.project.activeLayer.children) {
|
|
if (child.isClipMask()) {
|
|
workspaceMask = child;
|
|
break;
|
|
}
|
|
}
|
|
paper.project.activeLayer.clipped = false;
|
|
workspaceMask.remove();
|
|
}
|
|
const guideLayers = hideGuideLayers(true /* includeRaster */);
|
|
|
|
// Export at 0.5x
|
|
scaleWithStrokes(paper.project.activeLayer, .5, new paper.Point());
|
|
|
|
const bounds = paper.project.activeLayer.drawnBounds;
|
|
|
|
// `bounds.x` and `bounds.y` are relative to the top left corner,
|
|
// but if there is no content in the active layer, they default to 0,
|
|
// making the "Scratch space" rotation center ((SVG_ART_BOARD_WIDTH / 2), (SVG_ART_BOARD_HEIGHT / 2)),
|
|
// aka the upper left corner. Special-case this to be (0, 0), which is the center of the art board.
|
|
const centerX = bounds.width === 0 ? 0 : (SVG_ART_BOARD_WIDTH / 2) - bounds.x;
|
|
const centerY = bounds.height === 0 ? 0 : (SVG_ART_BOARD_HEIGHT / 2) - bounds.y;
|
|
|
|
this.props.onUpdateImage(
|
|
true /* isVector */,
|
|
paper.project.exportSVG({
|
|
asString: true,
|
|
bounds: 'content',
|
|
matrix: new paper.Matrix().translate(-bounds.x, -bounds.y)
|
|
}),
|
|
centerX,
|
|
centerY);
|
|
scaleWithStrokes(paper.project.activeLayer, 2, new paper.Point());
|
|
paper.project.activeLayer.applyMatrix = true;
|
|
|
|
showGuideLayers(guideLayers);
|
|
|
|
// Add back viewbox
|
|
if (workspaceMask) {
|
|
paper.project.activeLayer.addChild(workspaceMask);
|
|
workspaceMask.clipMask = true;
|
|
}
|
|
|
|
if (!skipSnapshot) {
|
|
performSnapshot(this.props.undoSnapshot, Formats.VECTOR);
|
|
}
|
|
}
|
|
render () {
|
|
const componentProps = omit(this.props, [
|
|
'format',
|
|
'onUpdateImage',
|
|
'undoSnapshot'
|
|
]);
|
|
return (
|
|
<WrappedComponent
|
|
onUpdateImage={this.handleUpdateImage}
|
|
{...componentProps}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
UpdateImageWrapper.propTypes = {
|
|
format: PropTypes.oneOf(Object.keys(Formats)),
|
|
mode: PropTypes.oneOf(Object.keys(Modes)).isRequired,
|
|
onUpdateImage: PropTypes.func.isRequired,
|
|
undoSnapshot: PropTypes.func.isRequired,
|
|
updateViewBounds: PropTypes.func.isRequired
|
|
};
|
|
|
|
const mapStateToProps = state => ({
|
|
format: state.scratchPaint.format,
|
|
mode: state.scratchPaint.mode,
|
|
undoState: state.scratchPaint.undo
|
|
});
|
|
const mapDispatchToProps = dispatch => ({
|
|
setSelectedItems: format => {
|
|
dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format)));
|
|
},
|
|
undoSnapshot: snapshot => {
|
|
dispatch(undoSnapshot(snapshot));
|
|
},
|
|
updateViewBounds: matrix => {
|
|
dispatch(updateViewBounds(matrix));
|
|
}
|
|
});
|
|
|
|
return connect(
|
|
mapStateToProps,
|
|
mapDispatchToProps
|
|
)(UpdateImageWrapper);
|
|
};
|
|
|
|
export default UpdateImageHOC;
|