plotboilerplate
Version:
A simple javascript plotting boilerplate for 2d stuff.
1,245 lines (1,182 loc) • 100 kB
text/typescript
/**
* @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