mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-23 05:52:42 -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 {updateViewBounds} from '../reducers/view-bounds';
|
||||||
|
|
||||||
import {getRaster, hideGuideLayers, showGuideLayers} from '../helper/layer';
|
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 {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo';
|
||||||
import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
|
import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
|
||||||
import {groupSelection, ungroupSelection} from '../helper/group';
|
import {groupSelection, ungroupSelection} from '../helper/group';
|
||||||
|
@ -39,6 +39,8 @@ class PaintEditor extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'handleUpdateImage',
|
'handleUpdateImage',
|
||||||
|
'handleUpdateBitmap',
|
||||||
|
'handleUpdateVector',
|
||||||
'handleUndo',
|
'handleUndo',
|
||||||
'handleRedo',
|
'handleRedo',
|
||||||
'handleSendBackward',
|
'handleSendBackward',
|
||||||
|
@ -186,36 +188,66 @@ class PaintEditor extends React.Component {
|
||||||
actualFormat = BitmapModes[this.props.mode] ? Formats.BITMAP : Formats.VECTOR;
|
actualFormat = BitmapModes[this.props.mode] ? Formats.BITMAP : Formats.VECTOR;
|
||||||
}
|
}
|
||||||
if (isBitmap(actualFormat)) {
|
if (isBitmap(actualFormat)) {
|
||||||
const rect = getHitBounds(getRaster());
|
this.handleUpdateBitmap(skipSnapshot);
|
||||||
this.props.onUpdateImage(
|
|
||||||
false /* isVector */,
|
|
||||||
getRaster().getImageData(rect),
|
|
||||||
(ART_BOARD_WIDTH / 2) - rect.x,
|
|
||||||
(ART_BOARD_HEIGHT / 2) - rect.y);
|
|
||||||
} else if (isVector(actualFormat)) {
|
} else if (isVector(actualFormat)) {
|
||||||
const guideLayers = hideGuideLayers(true /* includeRaster */);
|
this.handleUpdateVector(skipSnapshot);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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) {
|
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 () {
|
handleUndo () {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import Modes from '../../lib/modes';
|
import Modes from '../../lib/modes';
|
||||||
|
|
||||||
import {createCanvas, getRaster} from '../layer';
|
import {getRaster} from '../layer';
|
||||||
import {fillRect, scaleBitmap} from '../bitmap';
|
import {commitSelectionToBitmap} from '../bitmap';
|
||||||
|
|
||||||
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
||||||
import NudgeTool from '../selection-tools/nudge-tool';
|
import NudgeTool from '../selection-tools/nudge-tool';
|
||||||
|
@ -117,57 +117,10 @@ class SelectTool extends paper.Tool {
|
||||||
commitSelection () {
|
commitSelection () {
|
||||||
if (!this.selection || !this.selection.parent) return;
|
if (!this.selection || !this.selection.parent) return;
|
||||||
|
|
||||||
this.maybeApplyScaleToCanvas(this.selection);
|
commitSelectionToBitmap(this.selection, getRaster());
|
||||||
this.commitArbitraryTransformation(this.selection);
|
this.selection.remove();
|
||||||
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();
|
|
||||||
this.selection = null;
|
this.selection = null;
|
||||||
|
this.onUpdateImage();
|
||||||
}
|
}
|
||||||
deactivateTool () {
|
deactivateTool () {
|
||||||
this.commitSelection();
|
this.commitSelection();
|
||||||
|
|
|
@ -653,7 +653,78 @@ const scaleBitmap = function (canvas, scale) {
|
||||||
return tmpCanvas;
|
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 {
|
export {
|
||||||
|
commitSelectionToBitmap,
|
||||||
convertToBitmap,
|
convertToBitmap,
|
||||||
convertToVector,
|
convertToVector,
|
||||||
fillRect,
|
fillRect,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// modifed from https://github.com/memononen/stylii
|
// modifed from https://github.com/memononen/stylii
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import {hideGuideLayers, showGuideLayers, getRaster} from '../helper/layer';
|
import {hideGuideLayers, showGuideLayers, getRaster} from '../helper/layer';
|
||||||
|
import {getSelectedLeafItems} from '../helper/selection';
|
||||||
import Formats from '../lib/format';
|
import Formats from '../lib/format';
|
||||||
import {isVector, isBitmap} from '../lib/format';
|
import {isVector, isBitmap} from '../lib/format';
|
||||||
import log from '../log/log';
|
import log from '../log/log';
|
||||||
|
@ -23,7 +24,7 @@ const performSnapshot = function (dispatchPerformSnapshot, format) {
|
||||||
showGuideLayers(guideLayers);
|
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--) {
|
for (let i = paper.project.layers.length - 1; i >= 0; i--) {
|
||||||
const layer = paper.project.layers[i];
|
const layer = paper.project.layers[i];
|
||||||
if (!layer.data.isBackgroundGuideLayer) {
|
if (!layer.data.isBackgroundGuideLayer) {
|
||||||
|
@ -32,20 +33,41 @@ const _restore = function (entry, setSelectedItems, onUpdateImage) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
paper.project.importJSON(entry.json);
|
paper.project.importJSON(entry.json);
|
||||||
|
|
||||||
setSelectedItems();
|
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 */);
|
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) {
|
const performUndo = function (undoState, dispatchPerformUndo, setSelectedItems, onUpdateImage) {
|
||||||
if (undoState.pointer > 0) {
|
if (undoState.pointer > 0) {
|
||||||
const state = undoState.stack[undoState.pointer - 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 :
|
const format = isVector(state.paintEditorFormat) ? Formats.VECTOR_SKIP_CONVERT :
|
||||||
isBitmap(state.paintEditorFormat) ? Formats.BITMAP_SKIP_CONVERT : null;
|
isBitmap(state.paintEditorFormat) ? Formats.BITMAP_SKIP_CONVERT : null;
|
||||||
dispatchPerformUndo(format);
|
dispatchPerformUndo(format);
|
||||||
|
@ -56,7 +78,7 @@ const performUndo = function (undoState, dispatchPerformUndo, setSelectedItems,
|
||||||
const performRedo = function (undoState, dispatchPerformRedo, setSelectedItems, onUpdateImage) {
|
const performRedo = function (undoState, dispatchPerformRedo, setSelectedItems, onUpdateImage) {
|
||||||
if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) {
|
if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) {
|
||||||
const state = undoState.stack[undoState.pointer + 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 :
|
const format = isVector(state.paintEditorFormat) ? Formats.VECTOR_SKIP_CONVERT :
|
||||||
isBitmap(state.paintEditorFormat) ? Formats.BITMAP_SKIP_CONVERT : null;
|
isBitmap(state.paintEditorFormat) ? Formats.BITMAP_SKIP_CONVERT : null;
|
||||||
dispatchPerformRedo(format);
|
dispatchPerformRedo(format);
|
||||||
|
|
Loading…
Reference in a new issue