mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
Save bitmap selection (#569)
This commit is contained in:
parent
f7ca2c7e43
commit
df88d56d1b
4 changed files with 164 additions and 86 deletions
|
@ -15,7 +15,7 @@ import {setTextEditTarget} from '../reducers/text-edit-target';
|
|||
import {updateViewBounds} from '../reducers/view-bounds';
|
||||
|
||||
import {getRaster, hideGuideLayers, showGuideLayers} from '../helper/layer';
|
||||
import {convertToBitmap, convertToVector, getHitBounds} from '../helper/bitmap';
|
||||
import {commitSelectionToBitmap, convertToBitmap, convertToVector, getHitBounds} from '../helper/bitmap';
|
||||
import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo';
|
||||
import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
|
||||
import {groupSelection, ungroupSelection} from '../helper/group';
|
||||
|
@ -39,6 +39,8 @@ class PaintEditor extends React.Component {
|
|||
super(props);
|
||||
bindAll(this, [
|
||||
'handleUpdateImage',
|
||||
'handleUpdateBitmap',
|
||||
'handleUpdateVector',
|
||||
'handleUndo',
|
||||
'handleRedo',
|
||||
'handleSendBackward',
|
||||
|
@ -186,36 +188,66 @@ class PaintEditor extends React.Component {
|
|||
actualFormat = BitmapModes[this.props.mode] ? Formats.BITMAP : Formats.VECTOR;
|
||||
}
|
||||
if (isBitmap(actualFormat)) {
|
||||
const rect = getHitBounds(getRaster());
|
||||
this.props.onUpdateImage(
|
||||
false /* isVector */,
|
||||
getRaster().getImageData(rect),
|
||||
(ART_BOARD_WIDTH / 2) - rect.x,
|
||||
(ART_BOARD_HEIGHT / 2) - rect.y);
|
||||
this.handleUpdateBitmap(skipSnapshot);
|
||||
} else if (isVector(actualFormat)) {
|
||||
const guideLayers = hideGuideLayers(true /* includeRaster */);
|
||||
|
||||
// Export at 0.5x
|
||||
scaleWithStrokes(paper.project.activeLayer, .5, new paper.Point());
|
||||
const bounds = paper.project.activeLayer.bounds;
|
||||
// @todo generate view box
|
||||
this.props.onUpdateImage(
|
||||
true /* isVector */,
|
||||
paper.project.exportSVG({
|
||||
asString: true,
|
||||
bounds: 'content',
|
||||
matrix: new paper.Matrix().translate(-bounds.x, -bounds.y)
|
||||
}),
|
||||
(SVG_ART_BOARD_WIDTH / 2) - bounds.x,
|
||||
(SVG_ART_BOARD_HEIGHT / 2) - bounds.y);
|
||||
scaleWithStrokes(paper.project.activeLayer, 2, new paper.Point());
|
||||
paper.project.activeLayer.applyMatrix = true;
|
||||
|
||||
showGuideLayers(guideLayers);
|
||||
this.handleUpdateVector(skipSnapshot);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
// Plaster the selection onto the raster layer before exporting, if there is a selection.
|
||||
const plasteredRaster = getRaster().getSubRaster(getRaster().bounds);
|
||||
plasteredRaster.remove(); // Don't insert
|
||||
const selectedItems = getSelectedLeafItems();
|
||||
if (selectedItems.length === 1 && selectedItems[0] instanceof paper.Raster) {
|
||||
if (!selectedItems[0].loaded ||
|
||||
(selectedItems[0].data && selectedItems[0].data.expanded && !selectedItems[0].data.expanded.loaded)) {
|
||||
log.warn('Bitmap layer should be loaded before calling updateImage.');
|
||||
return;
|
||||
}
|
||||
commitSelectionToBitmap(selectedItems[0], plasteredRaster);
|
||||
}
|
||||
const rect = getHitBounds(plasteredRaster);
|
||||
this.props.onUpdateImage(
|
||||
false /* isVector */,
|
||||
plasteredRaster.getImageData(rect),
|
||||
(ART_BOARD_WIDTH / 2) - rect.x,
|
||||
(ART_BOARD_HEIGHT / 2) - rect.y);
|
||||
|
||||
if (!skipSnapshot) {
|
||||
performSnapshot(this.props.undoSnapshot, actualFormat);
|
||||
performSnapshot(this.props.undoSnapshot, Formats.BITMAP);
|
||||
}
|
||||
}
|
||||
handleUpdateVector (skipSnapshot) {
|
||||
const guideLayers = hideGuideLayers(true /* includeRaster */);
|
||||
|
||||
// Export at 0.5x
|
||||
scaleWithStrokes(paper.project.activeLayer, .5, new paper.Point());
|
||||
const bounds = paper.project.activeLayer.bounds;
|
||||
// @todo generate view box
|
||||
this.props.onUpdateImage(
|
||||
true /* isVector */,
|
||||
paper.project.exportSVG({
|
||||
asString: true,
|
||||
bounds: 'content',
|
||||
matrix: new paper.Matrix().translate(-bounds.x, -bounds.y)
|
||||
}),
|
||||
(SVG_ART_BOARD_WIDTH / 2) - bounds.x,
|
||||
(SVG_ART_BOARD_HEIGHT / 2) - bounds.y);
|
||||
scaleWithStrokes(paper.project.activeLayer, 2, new paper.Point());
|
||||
paper.project.activeLayer.applyMatrix = true;
|
||||
|
||||
showGuideLayers(guideLayers);
|
||||
|
||||
if (!skipSnapshot) {
|
||||
performSnapshot(this.props.undoSnapshot, Formats.VECTOR);
|
||||
}
|
||||
}
|
||||
handleUndo () {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import paper from '@scratch/paper';
|
||||
import Modes from '../../lib/modes';
|
||||
|
||||
import {createCanvas, getRaster} from '../layer';
|
||||
import {fillRect, scaleBitmap} from '../bitmap';
|
||||
import {getRaster} from '../layer';
|
||||
import {commitSelectionToBitmap} from '../bitmap';
|
||||
|
||||
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
||||
import NudgeTool from '../selection-tools/nudge-tool';
|
||||
|
@ -117,57 +117,10 @@ class SelectTool extends paper.Tool {
|
|||
commitSelection () {
|
||||
if (!this.selection || !this.selection.parent) return;
|
||||
|
||||
this.maybeApplyScaleToCanvas(this.selection);
|
||||
this.commitArbitraryTransformation(this.selection);
|
||||
this.onUpdateImage();
|
||||
}
|
||||
maybeApplyScaleToCanvas (item) {
|
||||
if (!item.matrix.isInvertible()) {
|
||||
item.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// context.drawImage will anti-alias the image if both width and height are reduced.
|
||||
// However, it will preserve pixel colors if only one or the other is reduced, and
|
||||
// imageSmoothingEnabled is set to false. Therefore, we can avoid aliasing by scaling
|
||||
// down images in a 2 step process.
|
||||
const decomposed = item.matrix.decompose(); // Decomposition order: translate, rotate, scale, skew
|
||||
if (Math.abs(decomposed.scaling.x) < 1 && Math.abs(decomposed.scaling.y) < 1 &&
|
||||
decomposed.scaling.x !== 0 && decomposed.scaling.y !== 0) {
|
||||
item.canvas = scaleBitmap(item.canvas, decomposed.scaling);
|
||||
if (item.data && item.data.expanded) {
|
||||
item.data.expanded.canvas = scaleBitmap(item.data.expanded.canvas, decomposed.scaling);
|
||||
}
|
||||
// Remove the scale from the item's matrix
|
||||
item.matrix.append(
|
||||
new paper.Matrix().scale(new paper.Point(1 / decomposed.scaling.x, 1 / decomposed.scaling.y)));
|
||||
}
|
||||
}
|
||||
commitArbitraryTransformation (item) {
|
||||
// Create a canvas to perform masking
|
||||
const tmpCanvas = createCanvas();
|
||||
const context = tmpCanvas.getContext('2d');
|
||||
// Draw mask
|
||||
const rect = new paper.Shape.Rectangle(new paper.Point(), item.size);
|
||||
rect.matrix = item.matrix;
|
||||
fillRect(rect, context);
|
||||
rect.remove();
|
||||
context.globalCompositeOperation = 'source-in';
|
||||
|
||||
// Draw image onto mask
|
||||
const m = item.matrix;
|
||||
context.transform(m.a, m.b, m.c, m.d, m.tx, m.ty);
|
||||
let canvas = item.canvas;
|
||||
if (item.data && item.data.expanded) {
|
||||
canvas = item.data.expanded.canvas;
|
||||
}
|
||||
context.transform(1, 0, 0, 1, -canvas.width / 2, -canvas.height / 2);
|
||||
context.drawImage(canvas, 0, 0);
|
||||
|
||||
// Draw temp canvas onto raster layer
|
||||
getRaster().drawImage(tmpCanvas, new paper.Point());
|
||||
item.remove();
|
||||
commitSelectionToBitmap(this.selection, getRaster());
|
||||
this.selection.remove();
|
||||
this.selection = null;
|
||||
this.onUpdateImage();
|
||||
}
|
||||
deactivateTool () {
|
||||
this.commitSelection();
|
||||
|
|
|
@ -653,7 +653,78 @@ const scaleBitmap = function (canvas, scale) {
|
|||
return tmpCanvas;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a raster, take the scale on the transform and apply it to the raster's canvas, then remove
|
||||
* the scale from the item's transform matrix. Do this only if scale.x or scale.y is less than 1.
|
||||
* @param {paper.Raster} item raster to change
|
||||
*/
|
||||
const maybeApplyScaleToCanvas_ = function (item) {
|
||||
if (!item.matrix.isInvertible()) {
|
||||
item.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// context.drawImage will anti-alias the image if both width and height are reduced.
|
||||
// However, it will preserve pixel colors if only one or the other is reduced, and
|
||||
// imageSmoothingEnabled is set to false. Therefore, we can avoid aliasing by scaling
|
||||
// down images in a 2 step process.
|
||||
const decomposed = item.matrix.decompose(); // Decomposition order: translate, rotate, scale, skew
|
||||
if (Math.abs(decomposed.scaling.x) < 1 && Math.abs(decomposed.scaling.y) < 1 &&
|
||||
decomposed.scaling.x !== 0 && decomposed.scaling.y !== 0) {
|
||||
item.canvas = scaleBitmap(item.canvas, decomposed.scaling);
|
||||
if (item.data && item.data.expanded) {
|
||||
item.data.expanded.canvas = scaleBitmap(item.data.expanded.canvas, decomposed.scaling);
|
||||
}
|
||||
// Remove the scale from the item's matrix
|
||||
item.matrix.append(
|
||||
new paper.Matrix().scale(new paper.Point(1 / decomposed.scaling.x, 1 / decomposed.scaling.y)));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a raster, apply its transformation matrix to its canvas. Call maybeApplyScaleToCanvas_ first
|
||||
* to avoid introducing anti-aliasing to scaled-down rasters.
|
||||
* @param {paper.Raster} item raster to resolve transform of
|
||||
* @param {paper.Raster} destination raster to draw selection to
|
||||
*/
|
||||
const commitArbitraryTransformation_ = function (item, destination) {
|
||||
// Create a canvas to perform masking
|
||||
const tmpCanvas = createCanvas();
|
||||
const context = tmpCanvas.getContext('2d');
|
||||
// Draw mask
|
||||
const rect = new paper.Shape.Rectangle(new paper.Point(), item.size);
|
||||
rect.matrix = item.matrix;
|
||||
fillRect(rect, context);
|
||||
rect.remove();
|
||||
context.globalCompositeOperation = 'source-in';
|
||||
|
||||
// Draw image onto mask
|
||||
const m = item.matrix;
|
||||
context.transform(m.a, m.b, m.c, m.d, m.tx, m.ty);
|
||||
let canvas = item.canvas;
|
||||
if (item.data && item.data.expanded) {
|
||||
canvas = item.data.expanded.canvas;
|
||||
}
|
||||
context.transform(1, 0, 0, 1, -canvas.width / 2, -canvas.height / 2);
|
||||
context.drawImage(canvas, 0, 0);
|
||||
|
||||
// Draw temp canvas onto raster layer
|
||||
destination.drawImage(tmpCanvas, new paper.Point());
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a raster item, take its transform matrix and apply it to its canvas. Try to avoid
|
||||
* introducing anti-aliasing.
|
||||
* @param {paper.Raster} selection raster to resolve transform of
|
||||
* @param {paper.Raster} bitmap raster to draw selection to
|
||||
*/
|
||||
const commitSelectionToBitmap = function (selection, bitmap) {
|
||||
maybeApplyScaleToCanvas_(selection);
|
||||
commitArbitraryTransformation_(selection, bitmap);
|
||||
};
|
||||
|
||||
export {
|
||||
commitSelectionToBitmap,
|
||||
convertToBitmap,
|
||||
convertToVector,
|
||||
fillRect,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// modifed from https://github.com/memononen/stylii
|
||||
import paper from '@scratch/paper';
|
||||
import {hideGuideLayers, showGuideLayers, getRaster} from '../helper/layer';
|
||||
import {getSelectedLeafItems} from '../helper/selection';
|
||||
import Formats from '../lib/format';
|
||||
import {isVector, isBitmap} from '../lib/format';
|
||||
import log from '../log/log';
|
||||
|
@ -23,7 +24,7 @@ const performSnapshot = function (dispatchPerformSnapshot, format) {
|
|||
showGuideLayers(guideLayers);
|
||||
};
|
||||
|
||||
const _restore = function (entry, setSelectedItems, onUpdateImage) {
|
||||
const _restore = function (entry, setSelectedItems, onUpdateImage, isBitmapMode) {
|
||||
for (let i = paper.project.layers.length - 1; i >= 0; i--) {
|
||||
const layer = paper.project.layers[i];
|
||||
if (!layer.data.isBackgroundGuideLayer) {
|
||||
|
@ -32,20 +33,41 @@ const _restore = function (entry, setSelectedItems, onUpdateImage) {
|
|||
}
|
||||
}
|
||||
paper.project.importJSON(entry.json);
|
||||
|
||||
setSelectedItems();
|
||||
getRaster().onLoad = function () {
|
||||
|
||||
// Ensure that all rasters are loaded before updating storage with new image data.
|
||||
const rastersThatNeedToLoad = [];
|
||||
const onLoad = () => {
|
||||
if (!getRaster().loaded) return;
|
||||
for (const raster of rastersThatNeedToLoad) {
|
||||
if (!raster.loaded) return;
|
||||
}
|
||||
onUpdateImage(true /* skipSnapshot */);
|
||||
};
|
||||
if (getRaster().loaded) {
|
||||
getRaster().onLoad();
|
||||
|
||||
// Bitmap mode should have at most 1 selected item
|
||||
if (isBitmapMode) {
|
||||
const selectedItems = getSelectedLeafItems();
|
||||
if (selectedItems.length === 1 && selectedItems[0] instanceof paper.Raster) {
|
||||
rastersThatNeedToLoad.push(selectedItems[0]);
|
||||
if (selectedItems[0].data && selectedItems[0].data.expanded instanceof paper.Raster) {
|
||||
rastersThatNeedToLoad.push(selectedItems[0].data.expanded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRaster().onLoad = onLoad;
|
||||
if (getRaster().loaded) getRaster().onLoad();
|
||||
for (const raster of rastersThatNeedToLoad) {
|
||||
raster.onLoad = onLoad;
|
||||
if (raster.loaded) raster.onLoad();
|
||||
}
|
||||
};
|
||||
|
||||
const performUndo = function (undoState, dispatchPerformUndo, setSelectedItems, onUpdateImage) {
|
||||
if (undoState.pointer > 0) {
|
||||
const state = undoState.stack[undoState.pointer - 1];
|
||||
_restore(state, setSelectedItems, onUpdateImage);
|
||||
_restore(state, setSelectedItems, onUpdateImage, isBitmap(state.paintEditorFormat));
|
||||
const format = isVector(state.paintEditorFormat) ? Formats.VECTOR_SKIP_CONVERT :
|
||||
isBitmap(state.paintEditorFormat) ? Formats.BITMAP_SKIP_CONVERT : null;
|
||||
dispatchPerformUndo(format);
|
||||
|
@ -56,7 +78,7 @@ const performUndo = function (undoState, dispatchPerformUndo, setSelectedItems,
|
|||
const performRedo = function (undoState, dispatchPerformRedo, setSelectedItems, onUpdateImage) {
|
||||
if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) {
|
||||
const state = undoState.stack[undoState.pointer + 1];
|
||||
_restore(state, setSelectedItems, onUpdateImage);
|
||||
_restore(state, setSelectedItems, onUpdateImage, isBitmap(state.paintEditorFormat));
|
||||
const format = isVector(state.paintEditorFormat) ? Formats.VECTOR_SKIP_CONVERT :
|
||||
isBitmap(state.paintEditorFormat) ? Formats.BITMAP_SKIP_CONVERT : null;
|
||||
dispatchPerformRedo(format);
|
||||
|
|
Loading…
Reference in a new issue