mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-01-09 22:22:23 -05:00
357 lines
12 KiB
JavaScript
357 lines
12 KiB
JavaScript
import paper from '@scratch/paper';
|
|
import log from '../log/log';
|
|
import {ART_BOARD_BOUNDS, ART_BOARD_WIDTH, ART_BOARD_HEIGHT, CENTER, MAX_WORKSPACE_BOUNDS} from './view';
|
|
import {isGroupItem} from './item';
|
|
import {isBitmap, isVector} from '../lib/format';
|
|
|
|
const CHECKERBOARD_SIZE = 8;
|
|
const CROSSHAIR_SIZE = 16;
|
|
const CROSSHAIR_FULL_OPACITY = 0.75;
|
|
|
|
const _getLayer = function (layerString) {
|
|
for (const layer of paper.project.layers) {
|
|
if (layer.data && layer.data[layerString]) {
|
|
return layer;
|
|
}
|
|
}
|
|
};
|
|
|
|
const _getPaintingLayer = function () {
|
|
return _getLayer('isPaintingLayer');
|
|
};
|
|
|
|
/**
|
|
* Creates a canvas with width and height matching the art board size.
|
|
* @param {?number} width Width of the canvas. Defaults to ART_BOARD_WIDTH.
|
|
* @param {?number} height Height of the canvas. Defaults to ART_BOARD_HEIGHT.
|
|
* @return {HTMLCanvasElement} the canvas
|
|
*/
|
|
const createCanvas = function (width, height) {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = width ? width : ART_BOARD_WIDTH;
|
|
canvas.height = height ? height : ART_BOARD_HEIGHT;
|
|
canvas.getContext('2d').imageSmoothingEnabled = false;
|
|
return canvas;
|
|
};
|
|
|
|
const clearRaster = function () {
|
|
const layer = _getLayer('isRasterLayer');
|
|
layer.removeChildren();
|
|
|
|
// Generate blank raster
|
|
const raster = new paper.Raster(createCanvas());
|
|
raster.canvas.getContext('2d').imageSmoothingEnabled = false;
|
|
raster.parent = layer;
|
|
raster.guide = true;
|
|
raster.locked = true;
|
|
raster.position = CENTER;
|
|
};
|
|
|
|
const getRaster = function () {
|
|
const layer = _getLayer('isRasterLayer');
|
|
// Generate blank raster
|
|
if (layer.children.length === 0) {
|
|
clearRaster();
|
|
}
|
|
return _getLayer('isRasterLayer').children[0];
|
|
};
|
|
|
|
const getDragCrosshairLayer = function () {
|
|
return _getLayer('isDragCrosshairLayer');
|
|
};
|
|
|
|
const getBackgroundGuideLayer = function () {
|
|
return _getLayer('isBackgroundGuideLayer');
|
|
};
|
|
|
|
const _convertLayer = function (layer, format) {
|
|
layer.bitmapBackground.visible = isBitmap(format);
|
|
layer.vectorBackground.visible = isVector(format);
|
|
};
|
|
|
|
const convertBackgroundGuideLayer = function (format) {
|
|
_convertLayer(getBackgroundGuideLayer(), format);
|
|
};
|
|
|
|
const _makeGuideLayer = function () {
|
|
const guideLayer = new paper.Layer();
|
|
guideLayer.data.isGuideLayer = true;
|
|
return guideLayer;
|
|
};
|
|
|
|
const getGuideLayer = function () {
|
|
let layer = _getLayer('isGuideLayer');
|
|
if (!layer) {
|
|
layer = _makeGuideLayer();
|
|
_getPaintingLayer().activate();
|
|
}
|
|
return layer;
|
|
};
|
|
|
|
const setGuideItem = function (item) {
|
|
item.locked = true;
|
|
item.guide = true;
|
|
if (isGroupItem(item)) {
|
|
for (let i = 0; i < item.children.length; i++) {
|
|
setGuideItem(item.children[i]);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Removes the guide layers, e.g. for purposes of exporting the image. Must call showGuideLayers to re-add them.
|
|
* @param {boolean} includeRaster true if the raster layer should also be hidden
|
|
* @return {object} an object of the removed layers, which should be passed to showGuideLayers to re-add them.
|
|
*/
|
|
const hideGuideLayers = function (includeRaster) {
|
|
const backgroundGuideLayer = getBackgroundGuideLayer();
|
|
const dragCrosshairLayer = getDragCrosshairLayer();
|
|
const outlineLayer = _getLayer('isOutlineLayer');
|
|
const guideLayer = getGuideLayer();
|
|
dragCrosshairLayer.remove();
|
|
outlineLayer.remove();
|
|
guideLayer.remove();
|
|
backgroundGuideLayer.remove();
|
|
let rasterLayer;
|
|
if (includeRaster) {
|
|
rasterLayer = _getLayer('isRasterLayer');
|
|
rasterLayer.remove();
|
|
}
|
|
return {
|
|
dragCrosshairLayer: dragCrosshairLayer,
|
|
outlineLayer: outlineLayer,
|
|
guideLayer: guideLayer,
|
|
backgroundGuideLayer: backgroundGuideLayer,
|
|
rasterLayer: rasterLayer
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Add back the guide layers removed by calling hideGuideLayers. This must be done before any editing operations are
|
|
* taken in the paint editor.
|
|
* @param {!object} guideLayers object of the removed layers, which was returned by hideGuideLayers
|
|
*/
|
|
const showGuideLayers = function (guideLayers) {
|
|
const backgroundGuideLayer = guideLayers.backgroundGuideLayer;
|
|
const dragCrosshairLayer = guideLayers.dragCrosshairLayer;
|
|
const outlineLayer = guideLayers.outlineLayer;
|
|
const guideLayer = guideLayers.guideLayer;
|
|
const rasterLayer = guideLayers.rasterLayer;
|
|
if (rasterLayer && !rasterLayer.index) {
|
|
paper.project.addLayer(rasterLayer);
|
|
rasterLayer.sendToBack();
|
|
}
|
|
if (!backgroundGuideLayer.index) {
|
|
paper.project.addLayer(backgroundGuideLayer);
|
|
backgroundGuideLayer.sendToBack();
|
|
}
|
|
if (!dragCrosshairLayer.index) {
|
|
paper.project.addLayer(dragCrosshairLayer);
|
|
dragCrosshairLayer.bringToFront();
|
|
}
|
|
if (!outlineLayer.index) {
|
|
paper.project.addLayer(outlineLayer);
|
|
outlineLayer.bringToFront();
|
|
}
|
|
if (!guideLayer.index) {
|
|
paper.project.addLayer(guideLayer);
|
|
guideLayer.bringToFront();
|
|
}
|
|
if (paper.project.activeLayer !== _getPaintingLayer()) {
|
|
log.error(`Wrong active layer`);
|
|
log.error(paper.project.activeLayer.data);
|
|
}
|
|
};
|
|
|
|
const _makePaintingLayer = function () {
|
|
const paintingLayer = new paper.Layer();
|
|
paintingLayer.data.isPaintingLayer = true;
|
|
return paintingLayer;
|
|
};
|
|
|
|
const _makeRasterLayer = function () {
|
|
const rasterLayer = new paper.Layer();
|
|
rasterLayer.data.isRasterLayer = true;
|
|
clearRaster();
|
|
return rasterLayer;
|
|
};
|
|
|
|
const _makeBackgroundPaper = function (width, height, color, opacity) {
|
|
// creates a checkerboard path of width * height squares in color on white
|
|
let x = 0;
|
|
let y = 0;
|
|
const pathPoints = [];
|
|
while (x < width) {
|
|
pathPoints.push(new paper.Point(x, y));
|
|
x++;
|
|
pathPoints.push(new paper.Point(x, y));
|
|
y = y === 0 ? height : 0;
|
|
}
|
|
y = height - 1;
|
|
x = width;
|
|
while (y > 0) {
|
|
pathPoints.push(new paper.Point(x, y));
|
|
x = (x === 0 ? width : 0);
|
|
pathPoints.push(new paper.Point(x, y));
|
|
y--;
|
|
}
|
|
const vRect = new paper.Shape.Rectangle(
|
|
new paper.Point(0, 0),
|
|
new paper.Point(ART_BOARD_WIDTH / CHECKERBOARD_SIZE, ART_BOARD_HEIGHT / CHECKERBOARD_SIZE));
|
|
vRect.fillColor = '#fff';
|
|
vRect.guide = true;
|
|
vRect.locked = true;
|
|
vRect.position = CENTER;
|
|
const vPath = new paper.Path(pathPoints);
|
|
vPath.fillRule = 'evenodd';
|
|
vPath.fillColor = color;
|
|
vPath.opacity = opacity;
|
|
vPath.guide = true;
|
|
vPath.locked = true;
|
|
vPath.position = CENTER;
|
|
const mask = new paper.Shape.Rectangle(MAX_WORKSPACE_BOUNDS);
|
|
mask.position = CENTER;
|
|
mask.guide = true;
|
|
mask.locked = true;
|
|
mask.scale(1 / CHECKERBOARD_SIZE);
|
|
const vGroup = new paper.Group([vRect, vPath, mask]);
|
|
mask.clipMask = true;
|
|
return vGroup;
|
|
};
|
|
|
|
// Helper function for drawing a crosshair
|
|
const _makeCrosshair = function (opacity, parent) {
|
|
const crosshair = new paper.Group();
|
|
|
|
const vLine2 = new paper.Path.Line(new paper.Point(0, -7), new paper.Point(0, 7));
|
|
vLine2.strokeWidth = 6;
|
|
vLine2.strokeColor = 'white';
|
|
vLine2.strokeCap = 'round';
|
|
crosshair.addChild(vLine2);
|
|
const hLine2 = new paper.Path.Line(new paper.Point(-7, 0), new paper.Point(7, 0));
|
|
hLine2.strokeWidth = 6;
|
|
hLine2.strokeColor = 'white';
|
|
hLine2.strokeCap = 'round';
|
|
crosshair.addChild(hLine2);
|
|
const circle2 = new paper.Shape.Circle(new paper.Point(0, 0), 5.5);
|
|
circle2.strokeWidth = 6;
|
|
circle2.strokeColor = 'white';
|
|
crosshair.addChild(circle2);
|
|
|
|
const vLine = new paper.Path.Line(new paper.Point(0, -7), new paper.Point(0, 7));
|
|
vLine.strokeWidth = 2;
|
|
vLine.strokeColor = 'black';
|
|
vLine.strokeCap = 'round';
|
|
crosshair.addChild(vLine);
|
|
const hLine = new paper.Path.Line(new paper.Point(-7, 0), new paper.Point(7, 0));
|
|
hLine.strokeWidth = 2;
|
|
hLine.strokeColor = 'black';
|
|
hLine.strokeCap = 'round';
|
|
crosshair.addChild(hLine);
|
|
const circle = new paper.Shape.Circle(new paper.Point(0, 0), 5.5);
|
|
circle.strokeWidth = 2;
|
|
circle.strokeColor = 'black';
|
|
crosshair.addChild(circle);
|
|
|
|
setGuideItem(crosshair);
|
|
crosshair.position = CENTER;
|
|
crosshair.opacity = opacity;
|
|
crosshair.parent = parent;
|
|
crosshair.applyMatrix = false;
|
|
parent.dragCrosshair = crosshair;
|
|
crosshair.scale(CROSSHAIR_SIZE / crosshair.bounds.width / paper.view.zoom);
|
|
};
|
|
|
|
const _makeDragCrosshairLayer = function () {
|
|
const dragCrosshairLayer = new paper.Layer();
|
|
_makeCrosshair(CROSSHAIR_FULL_OPACITY, dragCrosshairLayer);
|
|
dragCrosshairLayer.data.isDragCrosshairLayer = true;
|
|
dragCrosshairLayer.visible = false;
|
|
return dragCrosshairLayer;
|
|
};
|
|
|
|
const _makeOutlineLayer = function () {
|
|
const outlineLayer = new paper.Layer();
|
|
const whiteRect = new paper.Shape.Rectangle(ART_BOARD_BOUNDS.expand(1));
|
|
whiteRect.strokeWidth = 2;
|
|
whiteRect.strokeColor = 'white';
|
|
setGuideItem(whiteRect);
|
|
const blueRect = new paper.Shape.Rectangle(ART_BOARD_BOUNDS.expand(5));
|
|
blueRect.strokeWidth = 2;
|
|
blueRect.strokeColor = '#4280D7';
|
|
blueRect.opacity = 0.25;
|
|
setGuideItem(blueRect);
|
|
outlineLayer.data.isOutlineLayer = true;
|
|
return outlineLayer;
|
|
};
|
|
|
|
const _makeBackgroundGuideLayer = function (format) {
|
|
const guideLayer = new paper.Layer();
|
|
guideLayer.locked = true;
|
|
|
|
const vWorkspaceBounds = new paper.Shape.Rectangle(MAX_WORKSPACE_BOUNDS);
|
|
vWorkspaceBounds.fillColor = '#ECF1F9';
|
|
vWorkspaceBounds.position = CENTER;
|
|
|
|
// Add 1 to the height because it's an odd number otherwise, and we want it to be even
|
|
// so the corner of the checkerboard to line up with the center crosshair
|
|
const vBackground = _makeBackgroundPaper(
|
|
MAX_WORKSPACE_BOUNDS.width / CHECKERBOARD_SIZE,
|
|
(MAX_WORKSPACE_BOUNDS.height / CHECKERBOARD_SIZE) + 1,
|
|
'#D9E3F2', 0.55);
|
|
vBackground.position = CENTER;
|
|
vBackground.scaling = new paper.Point(CHECKERBOARD_SIZE, CHECKERBOARD_SIZE);
|
|
|
|
const vectorBackground = new paper.Group();
|
|
vectorBackground.addChild(vWorkspaceBounds);
|
|
vectorBackground.addChild(vBackground);
|
|
setGuideItem(vectorBackground);
|
|
guideLayer.vectorBackground = vectorBackground;
|
|
|
|
const bitmapBackground = _makeBackgroundPaper(
|
|
ART_BOARD_WIDTH / CHECKERBOARD_SIZE,
|
|
ART_BOARD_HEIGHT / CHECKERBOARD_SIZE,
|
|
'#D9E3F2', 0.55);
|
|
bitmapBackground.position = CENTER;
|
|
bitmapBackground.scaling = new paper.Point(CHECKERBOARD_SIZE, CHECKERBOARD_SIZE);
|
|
bitmapBackground.guide = true;
|
|
bitmapBackground.locked = true;
|
|
guideLayer.bitmapBackground = bitmapBackground;
|
|
|
|
_convertLayer(guideLayer, format);
|
|
|
|
_makeCrosshair(0.16, guideLayer);
|
|
|
|
guideLayer.data.isBackgroundGuideLayer = true;
|
|
return guideLayer;
|
|
};
|
|
|
|
const setupLayers = function (format) {
|
|
const backgroundGuideLayer = _makeBackgroundGuideLayer(format);
|
|
_makeRasterLayer();
|
|
const paintLayer = _makePaintingLayer();
|
|
const dragCrosshairLayer = _makeDragCrosshairLayer();
|
|
const outlineLayer = _makeOutlineLayer();
|
|
const guideLayer = _makeGuideLayer();
|
|
backgroundGuideLayer.sendToBack();
|
|
dragCrosshairLayer.bringToFront();
|
|
outlineLayer.bringToFront();
|
|
guideLayer.bringToFront();
|
|
paintLayer.activate();
|
|
};
|
|
|
|
export {
|
|
CROSSHAIR_SIZE,
|
|
CROSSHAIR_FULL_OPACITY,
|
|
createCanvas,
|
|
hideGuideLayers,
|
|
showGuideLayers,
|
|
getDragCrosshairLayer,
|
|
getGuideLayer,
|
|
getBackgroundGuideLayer,
|
|
convertBackgroundGuideLayer,
|
|
clearRaster,
|
|
getRaster,
|
|
setGuideItem,
|
|
setupLayers
|
|
};
|