UNPKG

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