UNPKG

plotboilerplate

Version:

A simple javascript plotting boilerplate for 2d stuff.

1,245 lines (1,182 loc) 100 kB
/** * @author Ikaros Kappler * @date 2018-10-23 * @modified 2018-11-19 Added multi-select and multi-drag. * @modified 2018-12-04 Added basic SVG export. * @modified 2018-12-09 Extended the constructor (canvas). * @modified 2018-12-18 Added the config.redrawOnResize param. * @modified 2018-12-18 Added the config.defaultCanvas{Width,Height} params. * @modified 2018-12-19 Added CSS scaling. * @modified 2018-12-28 Removed the unused 'drawLabel' param. Added the 'enableMouse' and 'enableKeys' params. * @modified 2018-12-29 Added the 'drawOrigin' param. * @modified 2018-12-29 Renamed the 'autoCenterOffset' param to 'autoAdjustOffset'. Added the params 'offsetAdjustXPercent' and 'offsetAdjustYPercent'. * @modified 2019-01-14 Added params 'drawBezierHandleLines' and 'drawBezierHandlePoints'. Added the 'redraw' praam to the add() function. * @modified 2019-01-16 Added params 'drawHandleLines' and 'drawHandlePoints'. Added the new params to the dat.gui interface. * @modified 2019-01-30 Added the 'Vector' type (extending the Line class). * @modified 2019-01-30 Added the 'PBImage' type (a wrapper for images). * @modified 2019-02-02 Added the 'canvasWidthFactor' and 'canvasHeightFactor' params. * @modified 2019-02-03 Removed the drawBackgroundImage() function, with had no purpose at all. Just add an image to the drawables-list. * @modified 2019-02-06 Vertices (instace of Vertex) can now be added. Added the 'draggable' attribute to the vertex attributes. * @modified 2019-02-10 Fixed a draggable-bug in PBImage handling (scaling was not possible). * @modified 2019-02-10 Added the 'enableTouch' option (default is true). * @modified 2019-02-14 Added the console for debugging (setConsole(object)). * @modified 2019-02-19 Added two new constants: DEFAULT_CLICK_TOLERANCE and DEFAULT_TOUCH_TOLERANCE. * @modified 2019-02-19 Added the second param to the locatePointNear(Vertex,Number) function. * @modified 2019-02-20 Removed the 'loadFile' entry from the GUI as it was experimental and never in use. * @modified 2019-02-23 Removed the 'rebuild' function as it had no purpose. * @modified 2019-02-23 Added scaling of the click-/touch-tolerance with the CSS scale. * @modified 2019-03-23 Added JSDoc tags. Changed the default value of config.drawOrigin to false. * @modified 2019-04-03 Fixed the touch-drag position detection for canvas elements that are not located at document position (0,0). * @modified 2019-04-03 Tweaked the fit-to-parent function to work with paddings and borders. * @modified 2019-04-28 Added the preClear callback param (called before the canvas was cleared on redraw and before any elements are drawn). * @modified 2019-09-18 Added basics for WebGL support (strictly experimental). * @modified 2019-10-03 Added the .beginDrawCycle call in the redraw function. * @modified 2019-11-06 Added fetch.num, fetch.val, fetch.bool, fetch.func functions. * @modified 2019-11-13 Fixed an issue with the mouse-sensitive area around vertices (were affected by zoom). * @modified 2019-11-13 Added the 'enableMouseWheel' param. * @modified 2019-11-18 Added the Triangle class as a regular drawable element. * @modified 2019-11-18 The add function now works with arrays, too. * @modified 2019-11-18 Added the _handleColor helper function to determine the render color of non-draggable vertices. * @modified 2019-11-19 Fixed a bug in the resizeCanvas function; retina resolution was not possible. * @modified 2019-12-04 Added relative positioned zooming. * @modified 2019-12-04 Added offsetX and offsetY params. * @modified 2019-12-04 Added an 'Set to fullsize retina' button to the GUI config. * @modified 2019-12-07 Added the drawConfig for lines, polygons, ellipse, triangles, bezier curves and image control lines. * @modified 2019-12-08 Fixed a css scale bug in the viewport() function. * @modified 2019-12-08 Added the drawconfig UI panel (line colors and line widths). * @modified 2020-02-06 Added handling for the end- and end-control-points of non-cirular Bézier paths (was still missing). * @modified 2020-02-06 Fixed a drag-amount bug in the move handling of end points of Bezier paths (control points was not properly moved when non circular). * @modified 2020-03-28 Ported this class from vanilla-JS to Typescript. * @modified 2020-03-29 Fixed the enableSVGExport flag (read enableEport before). * @modified 2020-05-09 Included the Cirlcle class. * @modified 2020-06-22 Added the rasterScaleX and rasterScaleY config params. * @modified 2020-06-03 Fixed the selectedVerticesOnPolyon(Polygon) function: non-selectable vertices were selected too, before. * @modified 2020-07-06 Replacing Touchy.js by AlloyFinger.js * @modified 2020-07-27 Added the getVertexNear(XYCoords,number) function * @modified 2020-07-27 Extended the remove(Drawable) function: vertices are now removed, too. * @modified 2020-07-28 Added PlotBoilerplate.revertMousePosition(number,number) – the inverse function of transformMousePosition(...). * @modified 2020-07-31 Added PlotBoilerplate.getDraggedElementCount() to check wether any elements are currently being dragged. * @modified 2020-08-19 Added the VertexAttributes.visible attribute to make vertices invisible. * @modified 2020-11-17 Added pure click handling (no dragEnd and !wasMoved jiggliny any more) to the PlotBoilerplate. * @modified 2020-12-11 Added the `removeAll(boolean)` function. * @modified 2020-12-17 Added the `CircleSector` drawable. * @modified 2021-01-04 Avoiding multiple redraw call on adding multiple Drawables (array). * @modified 2021-01-08 Added param `draw:DraLib<void>` to the methods `drawVertices`, `drawGrid` and `drawSelectPolygon`. * @modified 2021-01-08 Added the customizable `drawAll(...)` function. * @modified 2021-01-09 Added the `drawDrawable(...)` function. * @modified 2021-01-10 Added the `eventCatcher` element (used to track mouse events on SVGs). * @modified 2021-01-26 Fixed SVG resizing. * @modified 2021-01-26 Replaced the old SVGBuilder by the new `drawutilssvg` library. * @modified 2021-02-08 Fixed a lot of es2015 compatibility issues. * @modified 2021-02-18 Adding `adjustOffset(boolean)` function. * @modified 2021-03-01 Updated the `PlotBoilerplate.draw(...)` function: ellipses are now rotate-able. * @modified 2021-03-03 Added the `VEllipseSector` drawable. * @modified 2021-03-29 Clearing `currentClassName` and `currentId` after drawing each drawable. * @modified 2021-04-25 Extending `remove` to accept arrays of drawables. * @modified 2021-11-16 Adding the `PBText` drawable. * @modified 2022-08-01 Added `title` to the params. * @modified 2022-10-25 Added the `origin` to the default draw config. * @modified 2022-11-06 Adding an XML declaration to the SVG export routine. * @modified 2022-11-23 Added the `drawRaster` (default=true) option to the config/drawconfig. * @modified 2023-02-04 Fixed a bug in the `drawDrawable` function; fill's current classname was not set. * @modified 2023-02-10 Fixing an issue of the `style.position` setting when `fitToParent=true` from `absolute` to `static` (default). * @modified 2023-02-10 Cleaning up most type errors in the main class (mostly null checks). * @modified 2023-02-10 Adding `enableZoom` and `enablePan` (both default true) to have the option to disable these functions. * @modified 2023-09-29 Adding proper dicionary key and value types to the params of `PlotBoilerplate.utils.safeMergeByKeys` (was `object` before). * @modified 2024-07-08 Adding `PlotBoilerplate.getGUI()` to retrieve the GUI instance. * @modified 2024-08-25 Extending main class `PlotBoilerplate` optional param `isBackdropFiltersEnabled`. * @modified 2024-12-02 Adding the `triggerRedraw` to the `removeAll` method. * * @version 1.20.0 * * @file PlotBoilerplate * @fileoverview The main class. * @public **/ import AlloyFinger, { TouchMoveEvent, TouchPinchEvent } from "alloyfinger-typescript"; import { GUI } from "dat.gui"; import { drawutils } from "./draw"; import { drawutilsgl } from "./drawgl"; import { drawutilssvg } from "./drawutilssvg"; import { BezierPath } from "./BezierPath"; import { Bounds } from "./Bounds"; import { Circle } from "./Circle"; import { CircleSector } from "./CircleSector"; import { Grid } from "./Grid"; import { KeyHandler } from "./KeyHandler"; import { Line } from "./Line"; import { MouseHandler, XMouseEvent, XWheelEvent } from "./MouseHandler"; import { PBImage } from "./PBImage"; import { Polygon } from "./Polygon"; import { Triangle } from "./Triangle"; import { VEllipse } from "./VEllipse"; import { VEllipseSector } from "./VEllipseSector"; import { Vector } from "./Vector"; import { Vertex } from "./Vertex"; import { VertexAttr } from "./VertexAttr"; import { VertEvent } from "./VertexListeners"; import { IDraggable, Config, DrawLib, Drawable, DrawConfig, IHooks, PBParams, SVGPathParams, XYCoords, XYDimension, DatGuiProps, // DEPRECATED. Please do not use any more. LilGuiProps } from "./interfaces"; import { PBText } from "./PBText"; import { AlloyFingerOptions } from "alloyfinger-typescript/src/cjs/alloy_finger"; /** * @classdesc The main class of the PlotBoilerplate. * * @requires Vertex * @requires Line * @requires Vector * @requires Polygon * @requires PBImage * @requires VEllipse * @requires Circle * @requires MouseHandler * @requires KeyHandler * @requires VertexAttr * @requires CubicBezierCurve * @requires BezierPath * @requires Drawable * @requires DrawConfig * @requires IHooks * @requires PBParams * @requires Triangle * @requires drawutils * @requires drawutilsgl * @requires SVGSerializable * @requires XYCoords * @requires XYDimension */ export class PlotBoilerplate { /** @constant {number} */ static readonly DEFAULT_CANVAS_WIDTH: number = 1024; /** @constant {number} */ static readonly DEFAULT_CANVAS_HEIGHT: number = 768; /** @constant {number} */ static readonly DEFAULT_CLICK_TOLERANCE: number = 8; /** @constant {number} */ static readonly DEFAULT_TOUCH_TOLERANCE: number = 32; /** * A wrapper class for draggable items (mostly vertices). * @private **/ static Draggable = class { static VERTEX: string = "vertex"; item: any; typeName: string; vindex: number; // Vertex-index pindex: number; // Point-index (on path) pid: number; // Point-ID (on curve) cindex: number; // Curve-index constructor(item: any, typeName: string) { this.item = item; this.typeName = typeName; } isVertex() { return this.typeName == PlotBoilerplate.Draggable.VERTEX; } setVIndex(vindex: number): IDraggable { this.vindex = vindex; return this; } }; /** * @member {HTMLCanvasElement} * @memberof PlotBoilerplate * @instance */ canvas: HTMLCanvasElement | SVGElement; /** * The event catcher might be a different element positioned over the actual canvas. * * @member {HTMLElement} * @memberof PlotBoilerplate * @instance */ eventCatcher: HTMLElement; /** * @member {Config} * @memberof PlotBoilerplate * @instance */ config: Config; /** * @member {CanvasRenderingContext2D|WebGLRenderingContext} * @memberof PlotBoilerplate * @deprecated * @instance */ // @DEPRECATED Will be removed in version 2 ctx: CanvasRenderingContext2D | WebGLRenderingContext | undefined; /** * @member {drawutils|drawutilsgl|drawutilssvg} * @memberof PlotBoilerplate * @instance */ draw: DrawLib<void>; /** * @member {drawutils|drawutilsgl|drawutilssvg} * @memberof PlotBoilerplate * @instance */ fill: DrawLib<void>; /** * @member {DrawConfig} * @memberof PlotBoilerplate * @instance */ drawConfig: DrawConfig; /** * @member {Grid} * @memberof PlotBoilerplate * @instance */ grid: Grid; /** * @member {XYDimension} * @memberof PlotBoilerplate * @instance */ canvasSize: XYDimension; /** * @member {Array<Vertex>} * @memberof PlotBoilerplate * @instance */ vertices: Array<Vertex>; /** * @member {Array<BezierPath>} * @memberof PlotBoilerplate * @instance */ paths: Array<BezierPath>; /** * @member {Poylgon} * @memberof PlotBoilerplate * @instance */ selectPolygon: Polygon | null; /** * @member {Array<IDraggable>} * @memberof PlotBoilerplate * @instance */ draggedElements: Array<IDraggable>; /** * @member {Array<Drawable>} * @memberof PlotBoilerplate * @instance */ drawables: Array<Drawable>; /** * @member {Console} * @memberof PlotBoilerplate * @instance */ console: Console; /** * @member {IHooks} * @memberof PlotBoilerplate * @instance */ hooks: IHooks; /** * @member {KeyHandler|undefined} * @memberof PlotBoilerplate * @instance */ private keyHandler: KeyHandler | undefined; /** * A discrete timestamp to identify single render cycles. * Note that using system time milliseconds is not a safe way to identify render frames, as on modern powerful machines * multiple frames might be rendered within each millisecond. * @member {number} * @memberof plotboilerplate * @instance * @private */ private renderTime: number = 0; /** * A storage variable for retrieving the GUI instance once it was created. */ private _gui: GUI | null = null; /** * The constructor. * * @constructor * @name PlotBoilerplate * @public * @param {object} config={} - The configuration. * @param {HTMLCanvasElement} config.canvas - Your canvas element in the DOM (required). * @param {boolean=} [config.fullSize=true] - If set to true the canvas will gain full window size. * @param {boolean=} [config.fitToParent=true] - If set to true the canvas will gain the size of its parent container (overrides fullSize). * @param {number=} [config.scaleX=1.0] - The initial x-zoom. Default is 1.0. * @param {number=} [config.scaleY=1.0] - The initial y-zoom. Default is 1.0. * @param {number=} [config.offsetX=1.0] - The initial x-offset. Default is 0.0. Note that autoAdjustOffset=true overrides these values. * @param {number=} [config.offsetY=1.0] - The initial y-offset. Default is 0.0. Note that autoAdjustOffset=true overrides these values. * @param {boolean=} [config.rasterGrid=true] - If set to true the background grid will be drawn rastered. * @param {boolean=} [config.rasterScaleX=1.0] - Define the default horizontal raster scale (default=1.0). * @param {boolean=} [config.rasterScaleY=1.0] - Define the default vertical raster scale (default=1.0). * @param {number=} [config.rasterAdjustFactor=1.0] - The exponential limit for wrapping down the grid. (2.0 means: halve the grid each 2.0*n zoom step). * @param {boolean=} [config.drawOrigin=false] - Draw a crosshair at (0,0). * @param {boolean=} [config.autoAdjustOffset=true] - When set to true then the origin of the XY plane will * be re-adjusted automatically (see the params * offsetAdjust{X,Y}Percent for more). * @param {number=} [config.offsetAdjustXPercent=50] - The x-fallback position for the origin after * resizing the canvas. * @param {number=} [config.offsetAdjustYPercent=50] - The y-fallback position for the origin after * resizing the canvas. * @param {number=} [config.defaultCanvasWidth=1024] - The canvas size fallback (width) if no automatic resizing * is switched on. * @param {number=} [config.defaultCanvasHeight=768] - The canvas size fallback (height) if no automatic resizing * is switched on. * @param {number=} [config.canvasWidthFactor=1.0] - Scaling factor (width) upon the canvas size. * In combination with cssScale{X,Y} this can be used to obtain * sub pixel resolutions for retina displays. * @param {number=} [config.canvasHeightFactor=1.0] - Scaling factor (height) upon the canvas size. * In combination with cssScale{X,Y} this can be used to obtain * sub pixel resolutions for retina displays. * @param {number=} [config.cssScaleX=1.0] - Visually resize the canvas (horizontally) using CSS transforms (scale). * @param {number=} [config.cssScaleY=1.0] - Visually resize the canvas (vertically) using CSS transforms (scale). * @param {boolan=} [config.cssUniformScale=true] - CSS scale x and y obtaining aspect ratio. * @param {boolean=} [config.autoDetectRetina=true] - When set to true (default) the canvas will try to use the display's pixel ratio. * @param {string=} [config.backgroundColor=#ffffff] - The backround color. * @param {boolean=} [config.redrawOnResize=true] - Switch auto-redrawing on resize on/off (some applications * might want to prevent automatic redrawing to avoid data loss from the draw buffer). * @param {boolean=} [config.drawBezierHandleLines=true] - Indicates if Bézier curve handles should be drawn (used for * editors, no required in pure visualizations). * @param {boolean=} [config.drawBezierHandlePoints=true] - Indicates if Bézier curve handle points should be drawn. * @param {function=} [config.preClear=null] - A callback function that will be triggered just before the * draw function clears the canvas (before anything else was drawn). * @param {function=} [config.preDraw=null] - A callback function that will be triggered just before the draw * function starts. * @param {function=} [config.postDraw=null] - A callback function that will be triggered right after the drawing * process finished. * @param {boolean=} [config.enableMouse=true] - Indicates if the application should handle mouse events for you. * @param {boolean=} [config.enableTouch=true] - Indicates if the application should handle touch events for you. * @param {boolean=} [config.enableKeys=true] - Indicates if the application should handle key events for you. * @param {boolean=} [config.enableMouseWheel=true] - Indicates if the application should handle mouse wheel events for you. * @param {boolean=} [config.enablePan=true] - (default true) Set to false if you want to disable panning completely. * @param {boolean=} [config.enableZoom=true] - (default true) Set to false if you want to disable zooming completely. * @param {boolean=} [config.enableGL=false] - Indicates if the application should use the experimental WebGL features (not recommended). * @param {boolean=} [config.enableSVGExport=true] - Indicates if the SVG export should be enabled (default is true). * Note that changes from the postDraw hook might not be visible in the export. * @param {string=} [config.title=null] - Specify any hover tile here. It will be attached as a `title` attribute to the most elevated element. */ constructor(config: PBParams, drawConfig: DrawConfig) { // This should be in some static block ... VertexAttr.model = { bezierAutoAdjust: false, renderTime: 0, selectable: true, isSelected: false, draggable: true, visible: true }; if (typeof config.canvas === "undefined") { throw "No canvas specified."; } /** * A global config that's attached to the dat.gui control interface. * * @member {Object} * @memberof PlotBoilerplate * @instance */ const f = PlotBoilerplate.utils.fetch; this.config = { canvas: config.canvas, fullSize: f.val(config, "fullSize", true), fitToParent: f.bool(config, "fitToParent", true), scaleX: f.num(config, "scaleX", 1.0), scaleY: f.num(config, "scaleY", 1.0), offsetX: f.num(config, "offsetX", 0.0), offsetY: f.num(config, "offsetY", 0.0), rasterGrid: f.bool(config, "rasterGrid", true), drawRaster: f.bool(config, "drawRaster", true), rasterScaleX: f.num(config, "rasterScaleX", 1.0), rasterScaleY: f.num(config, "rasterScaleY", 1.0), rasterAdjustFactor: f.num(config, "rasterAdjustdFactror", 2.0), drawOrigin: f.bool(config, "drawOrigin", false), autoAdjustOffset: f.val(config, "autoAdjustOffset", true), offsetAdjustXPercent: f.num(config, "offsetAdjustXPercent", 50), offsetAdjustYPercent: f.num(config, "offsetAdjustYPercent", 50), backgroundColor: config.backgroundColor || "#ffffff", redrawOnResize: f.bool(config, "redrawOnResize", true), defaultCanvasWidth: f.num(config, "defaultCanvasWidth", PlotBoilerplate.DEFAULT_CANVAS_WIDTH), defaultCanvasHeight: f.num(config, "defaultCanvasHeight", PlotBoilerplate.DEFAULT_CANVAS_HEIGHT), canvasWidthFactor: f.num(config, "canvasWidthFactor", 1.0), canvasHeightFactor: f.num(config, "canvasHeightFactor", 1.0), cssScaleX: f.num(config, "cssScaleX", 1.0), cssScaleY: f.num(config, "cssScaleY", 1.0), cssUniformScale: f.bool(config, "cssUniformScale", true), saveFile: () => { _self.hooks.saveFile(_self); }, setToRetina: () => { _self._setToRetina(); }, autoDetectRetina: f.bool(config, "autoDetectRetina", true), enableSVGExport: f.bool(config, "enableSVGExport", true), // Listeners/observers preClear: f.func(config, "preClear", null), preDraw: f.func(config, "preDraw", null), postDraw: f.func(config, "postDraw", null), // Interaction enableMouse: f.bool(config, "enableMouse", true), enableTouch: f.bool(config, "enableTouch", true), enableKeys: f.bool(config, "enableKeys", true), enableMouseWheel: f.bool(config, "enableMouseWheel", true), enableZoom: f.bool(config, "enableZoom", true), // default=true enablePan: f.bool(config, "enablePan", true), // default=true // Experimental (and unfinished) enableGL: f.bool(config, "enableGL", false), isBackdropFiltersEnabled: f.bool(config, "isBackdropFiltersEnabled", true) }; // END confog /** * Configuration for drawing things. * * @member {Object} * @memberof PlotBoilerplate * @instance */ this.drawConfig = { drawVertices: true, drawBezierHandleLines: f.bool(config, "drawBezierHandleLines", true), drawBezierHandlePoints: f.bool(config, "drawBezierHandlePoints", true), drawHandleLines: f.bool(config, "drawHandleLines", true), drawHandlePoints: f.bool(config, "drawHandlePoints", true), drawGrid: f.bool(config, "drawGrid", true), drawRaster: f.bool(config, "drawRaster", true), bezier: { color: "#00a822", lineWidth: 2, handleLine: { color: "rgba(180,180,180,0.5)", lineWidth: 1 }, pathVertex: { color: "#B400FF", lineWidth: 1, fill: true }, controlVertex: { color: "#B8D438", lineWidth: 1, fill: true } }, // bezierPath: { // color: "#0022a8", // lineWidth: 1 // }, polygon: { color: "#0022a8", lineWidth: 1 }, triangle: { color: "#6600ff", lineWidth: 1 }, ellipse: { color: "#2222a8", lineWidth: 1 }, ellipseSector: { color: "#a822a8", lineWidth: 2 }, circle: { color: "#22a8a8", lineWidth: 2 }, circleSector: { color: "#2280a8", lineWidth: 1 }, vertex: { color: "#a8a8a8", lineWidth: 1 }, selectedVertex: { color: "#c08000", lineWidth: 2 }, line: { color: "#a844a8", lineWidth: 1 }, vector: { color: "#ff44a8", lineWidth: 1 }, image: { color: "#a8a8a8", lineWidth: 1 }, text: { color: "rgba(192,0,128,0.5)", lineWidth: 1, fill: true, anchor: true }, origin: { color: "#000000" } }; // END drawConfig // +--------------------------------------------------------------------------------- // | Object members. // +------------------------------- this.grid = new Grid(new Vertex(0, 0), new Vertex(50, 50)); this.canvasSize = { width: PlotBoilerplate.DEFAULT_CANVAS_WIDTH, height: PlotBoilerplate.DEFAULT_CANVAS_HEIGHT }; const canvasElement: Element = typeof config.canvas === "string" ? (document.querySelector(config.canvas) as Element) : config.canvas; if (typeof canvasElement === "undefined") { throw `Cannot initialize PlotBoilerplate with a null canvas (element "${config.canvas} not found).`; } // Which renderer to use: Canvas2D, WebGL (experimental) or SVG? if (canvasElement.tagName.toLowerCase() === "canvas") { this.canvas = canvasElement as HTMLCanvasElement; this.eventCatcher = this.canvas; if (this.config.enableGL && typeof drawutilsgl === "undefined") { console.warn( `Cannot use webgl. Package was compiled without experimental gl support. Please use plotboilerplate-glsupport.min.js instead.` ); console.warn(`Disabling GL and falling back to Canvas2D.`); this.config.enableGL = false; } if (this.config.enableGL) { // Override the case 'null' here. If GL is not supported, well then nothing works. const ctx: WebGLRenderingContext = this.canvas.getContext("webgl") as WebGLRenderingContext; // webgl-experimental? this.draw = new drawutilsgl(ctx, false); // PROBLEM: same instance of fill and draw when using WebGL. // Shader program cannot be duplicated on the same context. this.fill = (this.draw as drawutilsgl).copyInstance(true); console.warn("Initialized with experimental mode enableGL=true. Note that this is not yet fully implemented."); } else { // Override the case 'null' here. If context creation is not supported, well then nothing works. const ctx: CanvasRenderingContext2D = this.canvas.getContext("2d") as CanvasRenderingContext2D; this.draw = new drawutils(ctx, false); this.fill = new drawutils(ctx, true); } } else if (canvasElement.tagName.toLowerCase() === "svg") { if (typeof drawutilssvg === "undefined") throw `The svg draw library is not yet integrated part of PlotBoilerplate. Please include ./src/js/utils/helpers/drawutils.svg into your document.`; this.canvas = canvasElement as SVGElement; this.draw = new drawutilssvg( this.canvas as SVGElement, new Vertex(), // offset new Vertex(), // scale this.canvasSize, false, // fillShapes=false this.drawConfig, false // isSecondary=false ); this.fill = (this.draw as drawutilssvg).copyInstance(true); // fillShapes=true if (this.canvas.parentElement) { this.eventCatcher = document.createElement("div"); this.eventCatcher.style.position = "absolute"; this.eventCatcher.style.left = "0"; this.eventCatcher.style.top = "0"; this.eventCatcher.style.cursor = "pointer"; this.canvas.parentElement.style.position = "relative"; this.canvas.parentElement.appendChild(this.eventCatcher); } else { this.eventCatcher = document.body; } } else { throw "Element is neither a canvas nor an svg element."; } // At this point the event cacher element is deinfed and located at highest elevation. // Set `title` attribut? if (config.title) { this.eventCatcher.setAttribute("title", config.title); } this.draw.scale.set(this.config.scaleX ?? 1.0, this.config.scaleY); this.fill.scale.set(this.config.scaleX ?? 1.0, this.config.scaleY); this.vertices = []; this.selectPolygon = null; this.draggedElements = []; this.drawables = []; this.console = console; this.hooks = { // This is changable from the outside saveFile: PlotBoilerplate._saveFile }; var _self = this; globalThis.addEventListener("resize", () => _self.resizeCanvas()); this.resizeCanvas(); if (config.autoDetectRetina) { this._setToRetina(); } this.installInputListeners(); // Apply the configured CSS scale. this.updateCSSscale(); // Init this.redraw(); // Gain focus this.canvas.focus(); } // END constructor /** * This function opens a save-as file dialog and – once an output file is * selected – stores the current canvas contents as an SVG image. * * It is the default hook for saving files and can be overwritten. * * @method _saveFile * @instance * @memberof PlotBoilerplate * @return {void} * @private **/ private static _saveFile(pb: PlotBoilerplate) { // Create fake SVG node const svgNode: SVGElement = document.createElementNS("http://www.w3.org/2000/svg", "svg"); // Draw everything to fake node. var tosvgDraw = new drawutilssvg( svgNode, pb.draw.offset, pb.draw.scale, pb.canvasSize, false, // fillShapes=false pb.drawConfig ); var tosvgFill = tosvgDraw.copyInstance(true); // fillShapes=true tosvgDraw.beginDrawCycle(0); tosvgFill.beginDrawCycle(0); if (pb.config.preClear) { pb.config.preClear(); } tosvgDraw.clear(pb.config.backgroundColor || "white"); if (pb.config.preDraw) { pb.config.preDraw(tosvgDraw, tosvgFill); } pb.drawAll(0, tosvgDraw, tosvgFill); pb.drawVertices(0, tosvgDraw); if (pb.config.postDraw) pb.config.postDraw(tosvgDraw, tosvgFill); tosvgDraw.endDrawCycle(0); tosvgFill.endDrawCycle(0); // Full support in all browsers \o/ // https://caniuse.com/xml-serializer var serializer = new XMLSerializer(); var svgCode = serializer.serializeToString(svgNode); // Add: '<?xml version="1.0" encoding="utf-8"?>\n' ? var blob = new Blob(['<?xml version="1.0" encoding="utf-8"?>\n' + svgCode], { type: "image/svg;charset=utf-8" }); // See documentation for FileSaver.js for usage. // https://github.com/eligrey/FileSaver.js if (typeof globalThis["saveAs" as keyof Object] !== "function") { throw "Cannot save file; did you load the ./utils/savefile helper function and the eligrey/SaveFile library?"; } var _saveAs: (blob: Blob, filename: string) => void = globalThis["saveAs" as keyof Object] as ( blob: Blob, filename: string ) => void; _saveAs(blob, "plotboilerplate.svg"); } /** * This function sets the canvas resolution to factor 2.0 (or the preferred pixel ratio of your device) for retina displays. * Please not that in non-GL mode this might result in very slow rendering as the canvas buffer size may increase. * * @method _setToRetina * @instance * @memberof PlotBoilerplate * @return {void} * @private **/ private _setToRetina(): void { this.config.autoDetectRetina = true; const pixelRatio: number = globalThis.devicePixelRatio || 1; this.config.cssScaleX = this.config.cssScaleY = 1.0 / pixelRatio; this.config.canvasWidthFactor = this.config.canvasHeightFactor = pixelRatio; this.resizeCanvas(); this.updateCSSscale(); } /** * Set the current zoom and draw offset to fit the given bounds. * * This method currently restores the aspect zoom ratio. * **/ fitToView(bounds: Bounds): void { const canvasCenter: Vertex = new Vertex(this.canvasSize.width / 2.0, this.canvasSize.height / 2.0); const canvasRatio: number = this.canvasSize.width / this.canvasSize.height; const ratio: number = bounds.width / bounds.height; // Find the new draw offset const center: Vertex = new Vertex(bounds.max.x - bounds.width / 2.0, bounds.max.y - bounds.height / 2.0) .inv() .addXY(this.canvasSize.width / 2.0, this.canvasSize.height / 2.0); this.setOffset(center); if (canvasRatio < ratio) { const newUniformZoom: number = this.canvasSize.width / bounds.width; this.setZoom(newUniformZoom, newUniformZoom, canvasCenter); } else { const newUniformZoom: number = this.canvasSize.height / bounds.height; this.setZoom(newUniformZoom, newUniformZoom, canvasCenter); } this.redraw(); } /** * Set the console for this instance. * * @method setConsole * @param {Console} con - The new console object (default is globalThis.console). * @instance * @memberof PlotBoilerplate * @return {void} **/ setConsole(con: Console): void { this.console = con; } /** * Update the CSS scale for the canvas depending onf the cssScale{X,Y} settings.<br> * <br> * This function is usually only used inernally. * * @method updateCSSscale * @instance * @memberof PlotBoilerplate * @return {void} * @private **/ private updateCSSscale() { if (this.config.cssUniformScale) { PlotBoilerplate.utils.setCSSscale(this.canvas, this.config.cssScaleX ?? 1.0, this.config.cssScaleX ?? 1.0); } else { PlotBoilerplate.utils.setCSSscale(this.canvas, this.config.cssScaleX ?? 1.0, this.config.cssScaleY ?? 1.0); } } /** * Add a drawable object.<br> * <br> * This must be either:<br> * <pre> * * a Vertex * * a Line * * a Vector * * a VEllipse * * a VEllipseSector * * a Circle * * a Polygon * * a Triangle * * a BezierPath * * a BPImage * </pre> * * @param {Drawable|Drawable[]} drawable - The drawable (of one of the allowed class instance) to add. * @param {boolean} [redraw=true] - If true the function will trigger redraw after the drawable(s) was/were added. * @method add * @instance * @memberof PlotBoilerplate * @return {void} **/ add(drawable: Drawable | Array<Drawable>, redraw?: boolean) { if (Array.isArray(drawable)) { const arr: Array<Drawable> = drawable as Array<Drawable>; for (var i = 0; i < arr.length; i++) { this.add(arr[i], false); } } else if (drawable instanceof Vertex) { this.drawables.push(drawable); this.vertices.push(drawable); } else if (drawable instanceof Line) { // Add some lines this.drawables.push(drawable); this.vertices.push(drawable.a); this.vertices.push(drawable.b); } else if (drawable instanceof Vector) { this.drawables.push(drawable); this.vertices.push(drawable.a); this.vertices.push(drawable.b); } else if (drawable instanceof VEllipse) { this.vertices.push(drawable.center); this.vertices.push(drawable.axis); this.drawables.push(drawable); drawable.center.listeners.addDragListener((event: VertEvent) => { drawable.axis.add(event.params.dragAmount); }); } else if (drawable instanceof VEllipseSector) { this.vertices.push(drawable.ellipse.center); this.vertices.push(drawable.ellipse.axis); this.drawables.push(drawable); drawable.ellipse.center.listeners.addDragListener((event: VertEvent) => { drawable.ellipse.axis.add(event.params.dragAmount); }); } else if (drawable instanceof Circle) { this.vertices.push(drawable.center); this.drawables.push(drawable); } else if (drawable instanceof CircleSector) { this.vertices.push(drawable.circle.center); this.drawables.push(drawable); } else if (drawable instanceof Polygon) { this.drawables.push(drawable); for (var i = 0; i < drawable.vertices.length; i++) { this.vertices.push(drawable.vertices[i]); } } else if (drawable instanceof Triangle) { this.drawables.push(drawable); this.vertices.push(drawable.a); this.vertices.push(drawable.b); this.vertices.push(drawable.c); } else if (drawable instanceof BezierPath) { this.drawables.push(drawable); const bezierPath: BezierPath = drawable as BezierPath; for (var i = 0; i < bezierPath.bezierCurves.length; i++) { if (!drawable.adjustCircular && i == 0) { this.vertices.push(bezierPath.bezierCurves[i].startPoint); } this.vertices.push(bezierPath.bezierCurves[i].endPoint); this.vertices.push(bezierPath.bezierCurves[i].startControlPoint); this.vertices.push(bezierPath.bezierCurves[i].endControlPoint); bezierPath.bezierCurves[i].startControlPoint.attr.selectable = false; bezierPath.bezierCurves[i].endControlPoint.attr.selectable = false; } PlotBoilerplate.utils.enableBezierPathAutoAdjust(drawable); } else if (drawable instanceof PBImage) { this.vertices.push(drawable.upperLeft); this.vertices.push(drawable.lowerRight); this.drawables.push(drawable); // Todo: think about a IDragEvent interface drawable.upperLeft.listeners.addDragListener((e: VertEvent) => { drawable.lowerRight.add(e.params.dragAmount); }); drawable.lowerRight.attr.selectable = false; } else if (drawable instanceof PBText) { this.vertices.push(drawable.anchor); this.drawables.push(drawable); drawable.anchor.attr.selectable = false; } else { throw "Cannot add drawable of unrecognized type: " + typeof drawable + "."; } // This is a workaround for backwards compatibility when the 'redraw' param was not yet present. if (redraw || typeof redraw == "undefined") this.redraw(); } /** * Remove a drawable object.<br> * <br> * This must be either:<br> * <pre> * * a Vertex * * a Line * * a Vector * * a VEllipse * * a Circle * * a Polygon * * a BezierPath * * a BPImage * * a Triangle * </pre> * * @param {Drawable|Array<Drawable>} drawable - The drawable (of one of the allowed class instance) to remove. * @param {boolean} [redraw=false] * @method remove * @instance * @memberof PlotBoilerplate * @return {void} **/ remove(drawable: Drawable | Array<Drawable>, redraw?: boolean, removeWithVertices?: boolean): void { if (Array.isArray(drawable)) { for (var i = 0; i < drawable.length; i++) { this.remove(drawable[i], false, removeWithVertices); } if (redraw) { this.redraw(); } return; } if (drawable instanceof Vertex) { this.removeVertex(drawable, false); if (redraw) { this.redraw(); } } for (var i = 0; i < this.drawables.length; i++) { if (this.drawables[i] === drawable || this.drawables[i].uid === drawable.uid) { this.drawables.splice(i, 1); if (removeWithVertices) { // Check if some listeners need to be removed if (drawable instanceof Line) { // Add some lines this.removeVertex(drawable.a, false); this.removeVertex(drawable.b, false); } else if (drawable instanceof Vector) { this.removeVertex(drawable.a, false); this.removeVertex(drawable.b, false); } else if (drawable instanceof VEllipse) { this.removeVertex(drawable.center, false); this.removeVertex(drawable.axis, false); } else if (drawable instanceof VEllipseSector) { this.removeVertex(drawable.ellipse.center); this.removeVertex(drawable.ellipse.axis); } else if (drawable instanceof Circle) { this.removeVertex(drawable.center, false); } else if (drawable instanceof CircleSector) { this.removeVertex(drawable.circle.center, false); } else if (drawable instanceof Polygon) { // for( var i in drawable.vertices ) for (var i = 0; i < drawable.vertices.length; i++) this.removeVertex(drawable.vertices[i], false); } else if (drawable instanceof Triangle) { this.removeVertex(drawable.a, false); this.removeVertex(drawable.b, false); this.removeVertex(drawable.c, false); } else if (drawable instanceof BezierPath) { for (var i = 0; i < drawable.bezierCurves.length; i++) { this.removeVertex(drawable.bezierCurves[i].startPoint, false); this.removeVertex(drawable.bezierCurves[i].startControlPoint, false); this.removeVertex(drawable.bezierCurves[i].endControlPoint, false); if (i + 1 == drawable.bezierCurves.length) { this.removeVertex(drawable.bezierCurves[i].endPoint, false); } } } else if (drawable instanceof PBImage) { this.removeVertex(drawable.upperLeft, false); this.removeVertex(drawable.lowerRight, false); } else if (drawable instanceof PBText) { this.removeVertex(drawable.anchor, false); } } // END removeWithVertices if (redraw) { this.redraw(); } } } } /** * Remove a vertex from the vertex list.<br> * * @param {Vertex} vert - The vertex to remove. * @param {boolean} [redraw=false] * @method removeVertex * @instance * @memberof PlotBoilerplate * @return {void} **/ removeVertex(vert: Vertex, redraw?: boolean): void { for (var i = 0; i < this.vertices.length; i++) { if (this.vertices[i] === vert) { this.vertices.splice(i, 1); if (redraw) { this.redraw(); } return; } } } /** * Remove all elements. * * If you want to keep the vertices, pass `true`. * * @method removeAll * @param {boolean=false} keepVertices * @param {boolean=true} triggerRedraw - By default this method triggers the redraw routine; passing `false` will suppress redrawing. * @instance * @memberof PlotBoilerplate * @return {void} */ removeAll(keepVertices?: boolean, triggerRedraw?: boolean) { this.drawables = []; if (!Boolean(keepVertices)) { this.vertices = []; } if (triggerRedraw || typeof triggerRedraw === "undefined") { this.redraw(); } } /** * Find the vertex near the given position. * * The position is the absolute vertex position, not the x-y-coordinates on the canvas. * * @param {XYCoords} position - The position of the vertex to search for. * @param {number} pixelTolerance - A radius around the position to include into the search. * Note that the tolerance will be scaled up/down when zoomed. * @return The vertex near the given position or undefined if none was found there. **/ getVertexNear(pixelPosition: XYCoords, pixelTolerance: number): Vertex | undefined { const p: IDraggable | null = this.locatePointNear( this.transformMousePosition(pixelPosition.x, pixelPosition.y), pixelTolerance / Math.min(this.config.cssScaleX ?? 1.0, this.config.cssScaleY ?? 1.0) ); if (p && p.typeName == "vertex") { return this.vertices[p.vindex]; } return undefined; } /** * Draw the grid with the current config settings.<br> * * This function is usually only used internally. * * @method drawGrid * @param {DrawLib} draw - The drawing library to use to draw lines. * @private * @instance * @memberof PlotBoilerplate * @return {void} **/ drawGrid(draw: DrawLib<any>) { if (typeof draw === "undefined") { draw = this.draw; } const gScale: XYCoords = { x: (Grid.utils.mapRasterScale(this.config.rasterAdjustFactor, this.draw.scale.x) * this.config.rasterScaleX) / this.config.cssScaleX, y: (Grid.utils.mapRasterScale(this.config.rasterAdjustFactor, this.draw.scale.y) * this.config.rasterScaleY) / this.config.cssScaleY }; var gSize: XYDimension = { width: this.grid.size.x * gScale.x, height: this.grid.size.y * gScale.y }; var cs: XYDimension = { width: this.canvasSize.width / 2, height: this.canvasSize.height / 2 }; var offset: Vertex = this.draw.offset.clone().inv(); // console.log( "drawGrid", gScale, gSize, cs, offset ); offset.x = ((Math.round(offset.x + cs.width) / Math.round(gSize.width)) * gSize.width) / this.draw.scale.x + (((this.draw.offset.x - cs.width) / this.draw.scale.x) % gSize.width); offset.y = ((Math.round(offset.y + cs.height) / Math.round(gSize.height)) * gSize.height) / this.draw.scale.y + (((this.draw.offset.y - cs.height) / this.draw.scale.x) % gSize.height); if (this.drawConfig.drawGrid) { draw.setCurrentClassName(null); if (this.config.rasterGrid) { // TODO: move config member to drawConfig draw.setCurrentId("raster"); draw.raster( offset, this.canvasSize.width / this.draw.scale.x, this.canvasSize.height / this.draw.scale.y, gSize.width, gSize.height, "rgba(0,128,255,0.125)" ); } else { draw.setCurrentId("grid"); draw.grid( offset, this.canvasSize.width / this.draw.scale.x, this.canvasSize.height / this.draw.scale.y, gSize.width, gSize.height, "rgba(0,128,255,0.095)" ); } } } /** * Draw the origin with the current config settings.<br> * * This function is usually only used internally. * * @method drawOrigin * @param {DrawLib} draw - The drawing library to use to draw lines. * @private * @instance * @memberof PlotBoilerplate * @return {void} **/ drawOrigin(draw: DrawLib<any>) { // Add a crosshair to mark the origin draw.setCurrentId("origin"); draw.crosshair({ x: 0, y: 0 }, 10, this.drawConfig.origin.color); } /** * This is just a tiny helper function to determine the render color of vertices. **/ private _handleColor(h: Vertex, color: string) { return h.attr.isSelected ? this.drawConfig.selectedVertex.color : h.attr.draggable ? color : "rgba(128,128,128,0.5)"; } /** * Draw all drawables. * * This function is usually only used internally. * * @method drawDrawables * @param {number} renderTime - The current render time. It will be used to distinct * already draw vertices from non-draw-yet vertices. * @param {DrawLib} draw - The drawing library to use to draw lines. * @param {DrawLib} fill - The drawing library to use to fill areas. * @instance * @memberof PlotBoilerplate * @return {void} **/ drawDrawables(renderTime: number, draw: DrawLib<any>, fill: DrawLib<any>) { for (var i in this.drawables) { var d: Drawable = this.drawables[i]; this.draw.setCurrentId(d.uid); this.fill.setCurrentId(d.uid); this.draw.setCurrentClassName(d.className); this.fill.setCurrentClassName(d.className); this.drawDrawable(d, renderTime, draw, fill); } } /** * Draw the given drawable. * * This function is usually only used internally. * * @method drawDrawable * @param {Drawable} d - The drawable to draw. * @param {number} renderTime - The current render time. It will be used to distinct * already draw vertices from non-draw-yet vertices. * @param {DrawLib} draw - The drawing library to use to draw lines. * @param {DrawLib} fill - The drawing library to use to fill areas. * @instance * @memberof PlotBoilerplate * @return {void} **/ drawDrawable(d: Drawable, renderTime: number, draw: DrawLib<any>, fill: DrawLib<any>) { if (d instanceof BezierPath) { var curveIndex = 0; for (var c in d.bezierCurves) { // Restore these settings again in each loop (will be overwritten) this.draw.setCurrentId(`${d.uid}-${curveIndex}`); this.fill.setCurrentId(`${d.uid}-${curveIndex}`); this.draw.setCurrentClassName(d.className); this.fill.setCurrentClassName(d.className); draw.cubicBezier( d.bezierCurves[c].startPoint, d.bezierCurves[c].endPoint, d.bezierCurves[c].startControlPoint, d.bezierCurves[c].endControlPoint, this.drawConfig.bezier.color, this.drawConfig.bezier.lineWidth ); if (this.drawConfig.drawBezierHandlePoints && this.drawConfig.drawHandlePoints) { if (d.bezierCurves[c].startPoint.attr.visible) { const df = this.drawConfig.bezier.pathVertex.fill ? fill : draw; df.setCurrentId(`${d.uid}_h0`); df.setCurrentClassName(`${d.className}-start-handle`); if (d.bezierCurves[c].startPoint.attr.bezierAutoAdjust) { df.squareHandle( d.bezierCurves[c].startPoint, 5, this._handleColor(d.bezierCurves[c].startPoint, this.drawConfig.bezier.pathVertex.color) ); } else { df.diamondHandle( d.bezierCurves[c].startPoint, 7, this._handleColor(d.bezierCurves[c].startPoint, this.drawConfig.bezier.pathVertex.color) ); } } d.bezierCurves[c].startPoint.attr.renderTime = renderTime; if (d.bezierCurves[c].endPoint.attr.visible) { const df = this.drawConfig.bezier.pathVertex.fill ? fill : draw; df.setCurrentId(`${d.uid}_h0`); df.setCurrentClassName(`${d.className}-start-handle`); if (d.bezierCurves[c].endPoint.attr.bezierAutoAdjust) { df.squareHandle( d.bezierCurves[c].endPoint, 5, this._handleColor(d.bezierCu