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 {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 () {

View file

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

View file

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

View file

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