Save bitmap selection (#569)

This commit is contained in:
DD Liu 2018-07-25 19:07:35 -04:00 committed by GitHub
parent f7ca2c7e43
commit df88d56d1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 164 additions and 86 deletions

View file

@ -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,13 +188,44 @@ 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);
} else if (isVector(actualFormat)) {
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( this.props.onUpdateImage(
false /* isVector */, false /* isVector */,
getRaster().getImageData(rect), plasteredRaster.getImageData(rect),
(ART_BOARD_WIDTH / 2) - rect.x, (ART_BOARD_WIDTH / 2) - rect.x,
(ART_BOARD_HEIGHT / 2) - rect.y); (ART_BOARD_HEIGHT / 2) - rect.y);
} else if (isVector(actualFormat)) {
if (!skipSnapshot) {
performSnapshot(this.props.undoSnapshot, Formats.BITMAP);
}
}
handleUpdateVector (skipSnapshot) {
const guideLayers = hideGuideLayers(true /* includeRaster */); const guideLayers = hideGuideLayers(true /* includeRaster */);
// Export at 0.5x // Export at 0.5x
@ -212,10 +245,9 @@ class PaintEditor extends React.Component {
paper.project.activeLayer.applyMatrix = true; paper.project.activeLayer.applyMatrix = true;
showGuideLayers(guideLayers); showGuideLayers(guideLayers);
}
if (!skipSnapshot) { if (!skipSnapshot) {
performSnapshot(this.props.undoSnapshot, actualFormat); performSnapshot(this.props.undoSnapshot, Formats.VECTOR);
} }
} }
handleUndo () { handleUndo () {

View file

@ -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();

View file

@ -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,

View file

@ -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);