js-draw
Version:
Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.
365 lines (364 loc) • 15.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.backgroundTypeToClassNameMap = exports.imageBackgroundNonAutomaticSecondaryColorCSSClassName = exports.imageBackgroundGridSizeCSSPrefix = exports.imageBackgroundCSSClassName = exports.BackgroundType = void 0;
const EditorImage_1 = require("../image/EditorImage");
const math_1 = require("@js-draw/math");
const AbstractComponent_1 = __importStar(require("./AbstractComponent"));
const RestylableComponent_1 = require("./RestylableComponent");
const Viewport_1 = __importDefault(require("../Viewport"));
const RenderablePathSpec_1 = require("../rendering/RenderablePathSpec");
var BackgroundType;
(function (BackgroundType) {
BackgroundType[BackgroundType["SolidColor"] = 0] = "SolidColor";
BackgroundType[BackgroundType["Grid"] = 1] = "Grid";
BackgroundType[BackgroundType["None"] = 2] = "None";
})(BackgroundType || (exports.BackgroundType = BackgroundType = {}));
exports.imageBackgroundCSSClassName = 'js-draw-image-background';
// Class name prefix indicating the size of the background's grid cells (if present).
exports.imageBackgroundGridSizeCSSPrefix = 'js-draw-image-background-grid-';
// Flag included in rendered SVGs (etc) that indicates that the secondary color of the
// background has been manually set.
exports.imageBackgroundNonAutomaticSecondaryColorCSSClassName = 'js-draw-image-background-non-automatic-secondary-color';
exports.backgroundTypeToClassNameMap = {
[BackgroundType.Grid]: 'js-draw-image-background-grid',
[BackgroundType.SolidColor]: exports.imageBackgroundCSSClassName,
[BackgroundType.None]: '',
};
// Represents the background of the editor's canvas.
class BackgroundComponent extends AbstractComponent_1.default {
constructor(backgroundType, mainColor) {
super('image-background', 0);
this.backgroundType = backgroundType;
this.mainColor = mainColor;
this.viewportSizeChangeListener = null;
this.autoresizeChangedListener = null;
// Whether the background should grow/shrink to match the screen size,
// rather than being clipped to the image boundaries.
this.fillsScreen = false;
this.gridSize = Viewport_1.default.getGridSize(2);
this.gridStrokeWidth = 0.7;
this.secondaryColor = null;
// eslint-disable-next-line @typescript-eslint/prefer-as-const
this.isRestylableComponent = true;
this.contentBBox = math_1.Rect2.empty;
}
static ofGrid(backgroundColor, gridSize, gridColor, gridStrokeWidth) {
const background = new BackgroundComponent(BackgroundType.Grid, backgroundColor);
if (gridSize !== undefined) {
background.gridSize = gridSize;
}
if (gridColor !== undefined) {
background.secondaryColor = gridColor;
}
if (gridStrokeWidth !== undefined) {
background.gridStrokeWidth = gridStrokeWidth;
}
return background;
}
getBackgroundType() {
return this.backgroundType;
}
// @internal
getMainColor() {
return this.mainColor;
}
// @internal
getSecondaryColor() {
return this.secondaryColor;
}
// @internal
getGridSize() {
return this.gridSize;
}
getStyle() {
let color = this.mainColor;
if (this.backgroundType === BackgroundType.None) {
color = undefined;
}
return {
color,
};
}
updateStyle(style) {
return (0, RestylableComponent_1.createRestyleComponentCommand)(this.getStyle(), style, this);
}
// @internal
forceStyle(style, editor) {
const fill = style.color;
if (!fill) {
return;
}
this.mainColor = fill;
// A solid background and transparent fill is equivalent to no background.
if (fill.eq(math_1.Color4.transparent) && this.backgroundType === BackgroundType.SolidColor) {
this.backgroundType = BackgroundType.None;
}
else if (this.backgroundType === BackgroundType.None) {
this.backgroundType = BackgroundType.SolidColor;
}
if (editor) {
editor.image.queueRerenderOf(this);
editor.queueRerender();
}
}
onAddToImage(image) {
if (this.viewportSizeChangeListener) {
console.warn('onAddToImage called when background is already in an image');
this.onRemoveFromImage();
}
this.viewportSizeChangeListener = image.notifier.on(EditorImage_1.EditorImageEventType.ExportViewportChanged, () => {
this.recomputeBBox(image);
});
this.autoresizeChangedListener = image.notifier.on(EditorImage_1.EditorImageEventType.AutoresizeModeChanged, () => {
this.recomputeBBox(image);
});
this.recomputeBBox(image);
}
onRemoveFromImage() {
this.viewportSizeChangeListener?.remove();
this.autoresizeChangedListener?.remove();
this.viewportSizeChangeListener = null;
this.autoresizeChangedListener = null;
}
recomputeBBox(image) {
const importExportRect = image.getImportExportViewport().visibleRect;
let needsRerender = false;
if (!this.contentBBox.eq(importExportRect)) {
this.contentBBox = importExportRect;
// If the box already fills the screen, rerendering it will have
// no visual effect.
//
// TODO: This decision should be made by queueRerenderOf and not here.
//
needsRerender ||= !this.fillsScreen;
}
const imageAutoresizes = image.getAutoresizeEnabled();
if (imageAutoresizes !== this.fillsScreen) {
this.fillsScreen = imageAutoresizes;
needsRerender = true;
}
if (needsRerender) {
// Re-renders this if already added to the EditorImage.
image.queueRerenderOf(this);
}
}
generateGridPath(visibleRect) {
const contentBBox = this.getFullBoundingBox(visibleRect);
// .grownBy acts on all sides, so we need only grow by strokeWidth / 2 (1 * the stroke radius)
const targetRect = (visibleRect?.intersection(contentBBox) ?? contentBBox).grownBy(this.gridStrokeWidth / 2);
const roundDownToGrid = (coord) => Math.floor(coord / this.gridSize) * this.gridSize;
const roundUpToGrid = (coord) => Math.ceil(coord / this.gridSize) * this.gridSize;
const startY = roundUpToGrid(targetRect.y);
const endY = roundDownToGrid(targetRect.y + targetRect.h);
const startX = roundUpToGrid(targetRect.x);
const endX = roundDownToGrid(targetRect.x + targetRect.w);
const result = [];
// Don't generate grids with a huge number of rows/columns -- such grids
// take a long time to render and are likely invisible due to the number of
// cells.
const rowCount = (endY - startY) / this.gridSize;
const colCount = (endX - startX) / this.gridSize;
const maxGridCols = 1000;
const maxGridRows = 1000;
if (rowCount > maxGridRows || colCount > maxGridCols) {
return math_1.Path.empty;
}
const startPoint = math_1.Vec2.of(targetRect.x, startY);
for (let y = startY; y <= endY; y += this.gridSize) {
result.push({
kind: math_1.PathCommandType.MoveTo,
point: math_1.Vec2.of(targetRect.x, y),
});
result.push({
kind: math_1.PathCommandType.LineTo,
point: math_1.Vec2.of(targetRect.x + targetRect.w, y),
});
}
for (let x = startX; x <= endX; x += this.gridSize) {
result.push({
kind: math_1.PathCommandType.MoveTo,
point: math_1.Vec2.of(x, targetRect.y),
});
result.push({
kind: math_1.PathCommandType.LineTo,
point: math_1.Vec2.of(x, targetRect.y + targetRect.h),
});
}
return new math_1.Path(startPoint, result);
}
/**
* @returns this background's bounding box if the screen size is taken into
* account (which may be necessary if this component is configured to fill the
* entire screen).
*/
getFullBoundingBox(visibleRect) {
return (this.fillsScreen ? visibleRect : this.contentBBox) ?? this.contentBBox;
}
render(canvas, visibleRect) {
if (this.backgroundType === BackgroundType.None) {
return;
}
// If visibleRect is null, components should render everything.
// In that case, a full render is being done.
const mustRender = !visibleRect;
// If this.fillsScreen, the visibleRect needs to be known.
// Use the screen rect.
if (this.fillsScreen) {
visibleRect ??= canvas.getVisibleRect();
}
const clip = this.backgroundType === BackgroundType.Grid;
const contentBBox = this.getFullBoundingBox(visibleRect);
canvas.startObject(contentBBox, clip);
if (this.backgroundType === BackgroundType.SolidColor ||
this.backgroundType === BackgroundType.Grid) {
// If the rectangle for this region contains the visible rect,
// we can fill the entire visible rectangle (which may be more efficient than
// filling the entire region for this.)
const intersection = visibleRect?.intersection(contentBBox);
if (intersection) {
canvas.fillRect(intersection, this.mainColor);
}
else if (mustRender) {
canvas.fillRect(contentBBox, this.mainColor);
}
}
if (this.backgroundType === BackgroundType.Grid) {
let gridColor = this.secondaryColor;
gridColor ??= math_1.Color4.ofRGBA(1 - this.mainColor.r, 1 - this.mainColor.g, 1 - this.mainColor.b, 0.2);
// If the background fill is completely transparent, ensure visibility on otherwise light
// and dark backgrounds.
if (this.mainColor.a === 0) {
gridColor = math_1.Color4.ofRGBA(0.5, 0.5, 0.5, 0.2);
}
const style = {
fill: math_1.Color4.transparent,
stroke: { width: this.gridStrokeWidth, color: gridColor },
};
canvas.drawPath((0, RenderablePathSpec_1.pathToRenderable)(this.generateGridPath(visibleRect), style));
}
const backgroundTypeCSSClass = exports.backgroundTypeToClassNameMap[this.backgroundType];
const classNames = [exports.imageBackgroundCSSClassName];
if (backgroundTypeCSSClass !== exports.imageBackgroundCSSClassName) {
classNames.push(backgroundTypeCSSClass);
const gridSizeStr = (0, math_1.toRoundedString)(this.gridSize).replace(/[.]/g, 'p');
classNames.push(exports.imageBackgroundGridSizeCSSPrefix + gridSizeStr);
}
if (this.secondaryColor !== null) {
classNames.push(exports.imageBackgroundNonAutomaticSecondaryColorCSSClassName);
}
canvas.endObject(this.getLoadSaveData(), classNames);
}
intersects(lineSegment) {
return this.contentBBox.getEdges().some((edge) => edge.intersects(lineSegment));
}
isSelectable() {
return false;
}
isBackground() {
return true;
}
getSizingMode() {
return this.fillsScreen ? AbstractComponent_1.ComponentSizingMode.FillScreen : AbstractComponent_1.ComponentSizingMode.BoundingBox;
}
serializeToJSON() {
return {
mainColor: this.mainColor.toHexString(),
secondaryColor: this.secondaryColor?.toHexString(),
backgroundType: this.backgroundType,
gridSize: this.gridSize,
gridStrokeWidth: this.gridStrokeWidth,
};
}
applyTransformation(_affineTransfm) {
// Do nothing — it doesn't make sense to transform the background.
}
description(localizationTable) {
if (this.backgroundType === BackgroundType.SolidColor) {
return localizationTable.filledBackgroundWithColor(this.mainColor.toString());
}
else if (this.backgroundType === BackgroundType.None) {
return localizationTable.emptyBackground;
}
else if (this.backgroundType === BackgroundType.Grid) {
return localizationTable.gridBackground;
}
else {
const exhaustivenessCheck = this.backgroundType;
return exhaustivenessCheck;
}
}
createClone() {
return new BackgroundComponent(this.backgroundType, this.mainColor);
}
// @internal
static deserializeFromJSON(json) {
if (typeof json === 'string') {
json = JSON.parse(json);
}
if (typeof json.mainColor !== 'string') {
throw new Error('Error deserializing — mainColor must be of type string.');
}
let backgroundType;
const jsonBackgroundType = json.backgroundType;
if (jsonBackgroundType === BackgroundType.None ||
jsonBackgroundType === BackgroundType.Grid ||
jsonBackgroundType === BackgroundType.SolidColor) {
backgroundType = jsonBackgroundType;
}
else {
const exhaustivenessCheck = jsonBackgroundType;
return exhaustivenessCheck;
}
const mainColor = math_1.Color4.fromHex(json.mainColor);
const secondaryColor = json.secondaryColor ? math_1.Color4.fromHex(json.secondaryColor) : null;
const gridSize = json.gridSize ?? undefined;
const gridStrokeWidth = json.gridStrokeWidth ?? undefined;
const result = new BackgroundComponent(backgroundType, mainColor);
result.secondaryColor = secondaryColor;
if (gridSize) {
result.gridSize = gridSize;
}
if (gridStrokeWidth) {
result.gridStrokeWidth = gridStrokeWidth;
}
return result;
}
}
exports.default = BackgroundComponent;
AbstractComponent_1.default.registerComponent('image-background', BackgroundComponent.deserializeFromJSON);