UNPKG

plotboilerplate

Version:

A simple javascript plotting boilerplate for 2d stuff.

1,363 lines (1,270 loc) 77.4 kB
/** * Draws elements into an SVG node. * * Note that this library uses buffers and draw cycles. To draw onto an SVG canvas, do this: * const drawLib = new drawutilssvg( svgNode, ... ); * const fillLib = drawLib.copyInstance(true); * // Begin draw cycle * drawLib.beginDrawCycle(time); * // ... draw or fill your stuff ... * drawLib.endDrawCycle(time); // Here the elements become visible * * @author Ikaros Kappler * @date 2021-01-03 * @modified 2021-01-24 Fixed the `fillShapes` attribute in the copyInstance function. * @modified 2021-01-26 Changed the `isPrimary` (default true) attribute to `isSecondary` (default false). * @modified 2021-02-03 Added the static `createSvg` function. * @modified 2021-02-03 Fixed the currentId='background' bug on the clear() function. * @modified 2021-02-03 Fixed CSSProperty `stroke-width` (was line-width before, which is wrong). * @modified 2021-02-03 Added the static `HEAD_XML` attribute. * @modified 2021-02-19 Added the static helper function `transformPathData(...)` for svg path transformations (scale and translate). * @modified 2021-02-22 Added the static helper function `copyPathData(...)`. * @modified 2021-02-22 Added the `path` drawing function to draw SVG path data. * @modified 2021-03-01 Fixed a bug in the `clear` function (curClassName was not cleared). * @modified 2021-03-29 Fixed a bug in the `text` function (second y param was wrong, used x here). * @modified 2021-03-29 Moved this file from `src/ts/utils/helpers/` to `src/ts/`. * @modified 2021-03-31 Added 'ellipseSector' the the class names. * @modified 2021-03-31 Implemented buffering using a buffer <g> node and the beginDrawCycle and endDrawCycle methods. * @modified 2021-05-31 Added the `setConfiguration` function from `DrawLib`. * @modified 2021-11-15 Adding more parameters tot the `text()` function: fontSize, textAlign, fontFamily, lineHeight. * @modified 2021-11-19 Fixing the `label(text,x,y)` position. * @modified 2021-11-19 Added the `color` param to the `label(...)` function. * @modified 2022-02-03 Added the `lineWidth` param to the `crosshair` function. * @modified 2022-02-03 Added the `cross(...)` function. * @modified 2022-03-26 Added the private `nodeDefs` and `bufferedNodeDefs` attributes. * @modified 2022-03-26 Added the `texturedPoly` function to draw textures polygons. * @modified 2022-07-26 Adding `alpha` to the `image(...)` function. * @modified 2022-11-10 Tweaking some type issues. * @modified 2023-02-04 Fixed a typo in the CSS classname for cubic Bézier paths: cubicBezier (was cubierBezier). * @modified 2023-02-10 The methods `setCurrentClassName` and `setCurrentId` also accept `null` now. * @modified 2023-09-29 Added initialization checks for null parameters. * @modified 2023-09-29 Added a missing implementation to the `drawurilssvg.do(XYCoords,string)` function. Didn't draw anything. * @modified 2023-09-29 Downgrading all `Vertex` param type to the more generic `XYCoords` type in these render functions: line, arrow, texturedPoly, cubicBezier, cubicBezierPath, handle, handleLine, dot, point, circle, circleArc, ellipse, grid, raster. * @modified 2023-09-29 Added the `headLength` parameter to the 'DrawLib.arrow()` function. * @modified 2023-09-29 Added the `arrowHead(...)` function to the 'DrawLib.arrow()` interface. * @modified 2023-09-29 Added the `cubicBezierArrow(...)` function to the 'DrawLib.arrow()` interface. * @modified 2023-10-04 Adding `strokeOptions` param to these draw function: line, arrow, cubicBezierArrow, cubicBezier, cubicBezierPath, circle, circleArc, ellipse, square, rect, polygon, polyline. * @modified 2024-01-30 Fixing an issue with immutable style sets; changes to the global draw config did not reflect here (do now). * @modified 2024-03-10 Fixing some types for Typescript 5 compatibility. * @modified 2024-07-24 Caching custom style defs in a private buffer variable. * @version 1.6.10 **/ import { CircleSector } from "./CircleSector"; import { CubicBezierCurve } from "./CubicBezierCurve"; import { Polygon } from "./Polygon"; import { Vertex } from "./Vertex"; import { DrawConfig, DrawLib, DrawSettings, XYCoords, XYDimension, SVGPathParams, UID, DrawLibConfiguration, FontStyle, FontWeight, StrokeOptions } from "./interfaces"; import { Bounds } from "./Bounds"; import { UIDGenerator } from "./UIDGenerator"; import { Vector } from "./Vector"; const RAD_TO_DEG = 180 / Math.PI; /** * @classdesc A helper class for basic SVG drawing operations. This class should * be compatible to the default 'draw' class. * * @requires CubicBzierCurvce * @requires Polygon * @requires Vertex * @requires XYCoords */ export class drawutilssvg implements DrawLib<void | SVGElement> { static HEAD_XML = [ '<?xml version="1.0" encoding="UTF-8" standalone="no"?>', '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" ', ' "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">', "" ].join("\n"); /** * @member {SVGGElement} * @memberof drawutilssvg * @instance */ svgNode: SVGElement; /** * The root elements container <g> in the svgNode. */ private gNode: SVGGElement; /** * To avoid flickering the lib draws on a buffer, which is replacing the old <g> node at the end of the draw cycle. * @member {SVGGelement} * @memberof drawutilssvg * @instance * @private */ private bufferGNode: SVGGElement; /** * A style node of type `<style>`. * @member {SVGGelement} * @memberof drawutilssvg * @instance * @private */ private nodeStyle: SVGStyleElement; /** * A style node of type `<defs>`. * @member {SVGGelement} * @memberof drawutilssvg * @instance * @private */ private nodeDefs: SVGDefsElement; /** * The buffered nodeDefs. * @member {SVGGelement} * @memberof drawutilssvg * @instance * @private */ private bufferedNodeDefs: SVGDefsElement; /** * @member {Vertex} * @memberof drawutilssvg * @instance */ scale: Vertex; /** * @member {Vertex} * @memberof drawutilssvg * @instance */ offset: Vertex; /** * @member {boolean} * @memberof drawutilssvg * @instance */ fillShapes: boolean; /** * @member {XYDimension} * @memberof drawutilssvg * @instance */ canvasSize: XYDimension; /** * The current drawlib configuration to be used for all upcoming draw operations. * @member {DrawLibConfiguration} * @memberof drawutilssvg * @instance */ drawlibConfiguration: DrawLibConfiguration; /** * The current drawable-ID. This can be any unique ID identifying the following drawn element. * * @member {UID|null} * @memberof drawutilssvg * @instance */ private curId: UID | null; /** * The current drawable-classname. */ private curClassName: string | null; /** * The SVG element cache. On clear() all elements are kept for possible re-use on next draw cycle. */ private cache: Map<UID, SVGElement>; /** * Indicates if this library is the primary or seconday instance (draw an fill share the same DOM nodes). */ private isSecondary: boolean; /** * Keep the initial draw config to rebuild styles on each render loop. */ private drawConfig: DrawConfig; /** * A buffer element for custom style defs (will be re-generated on each draw cycle). */ private customStyleDefs: Map<string, string>; /** * Passed from primary to secondary instance. */ //private nodeStyle: SVGStyleElement; /** * The constructor. * * @constructor * @name drawutilssvg * @param {SVGElement} svgNode - The SVG node to use. * @param {XYCoords} offset - The draw offset to use. * @param {XYCoords} scale - The scale factors to use. * @param {XYDimension} canvasSize - The initial canvas size (use setSize to change). * @param {boolean} fillShapes - Indicates if the constructed drawutils should fill all drawn shapes (if possible). * @param {DrawConfig} drawConfig - The default draw config to use for CSS fallback styles. * @param {boolean=} isSecondary - (optional) Indicates if this is the primary or secondary instance. Only primary instances manage child nodes. * @param {SVGGElement=} gNode - (optional) Primary and seconday instances share the same &lt;g> node. **/ constructor( svgNode: SVGElement, offset: XYCoords, scale: XYCoords, canvasSize: XYDimension, fillShapes: boolean, drawConfig: DrawConfig, isSecondary?: boolean, gNode?: SVGGElement, bufferGNode?: SVGGElement, nodeDefs?: SVGDefsElement, bufferNodeDefs?: SVGDefsElement, nodeStyle?: SVGStyleElement ) { this.svgNode = svgNode; this.offset = new Vertex(0, 0).set(offset); this.scale = new Vertex(1, 1).set(scale); this.fillShapes = fillShapes; this.isSecondary = Boolean(isSecondary); this.drawConfig = drawConfig; this.drawlibConfiguration = {} as DrawLibConfiguration; this.cache = new Map<UID, SVGElement>(); this.setSize(canvasSize); if (isSecondary) { if (!gNode || !bufferGNode || !nodeDefs || !bufferNodeDefs) { throw "Cannot create secondary svg draw lib with undefinde gNode|bufferGNode|nodeDefs|bufferNodeDefs."; } this.gNode = gNode; this.bufferGNode = bufferGNode; this.nodeDefs = nodeDefs; this.bufferedNodeDefs = bufferNodeDefs; if (nodeStyle) { this.nodeStyle = nodeStyle; } } else { this.addStyleDefs(drawConfig); this.addDefsNode(); this.gNode = this.createSVGNode("g") as SVGGElement; this.bufferGNode = this.createSVGNode("g") as SVGGElement; this.svgNode.appendChild(this.gNode); } } /** * Adds a default style defintion based on the passed DrawConfig. * Twaek the draw config to change default colors or line thicknesses. * * @param {DrawConfig} drawConfig */ private addStyleDefs(drawConfig: DrawConfig) { this.nodeStyle = this.createSVGNode("style") as SVGStyleElement; this.svgNode.appendChild(this.nodeStyle); this.rebuildStyleDefs(drawConfig); } /** * This method is required to re-define the global style defs. It is needed * if any value in the DrawConfig changed in the meantime. * @param drawConfig */ private rebuildStyleDefs(drawConfig: DrawConfig) { // Which default styles to add? -> All from the DrawConfig. // Compare with DrawConfig interface const keys = { "bezier": "CubicBezierCurve", //"bezierPath": "BezierPath", // TODO: is this correct? "polygon": "Polygon", "triangle": "Triangle", "ellipse": "Ellipse", "ellipseSector": "EllipseSector", "circle": "Circle", "circleSector": "CircleSector", "vertex": "Vertex", "line": "Line", "vector": "Vector", "image": "Image", "text": "Text" }; // Question: why isn't this working if the svgNode is created dynamically? (nodeStyle.sheet is null) const rules: Array<string> = []; // console.log("drawConfig", drawConfig); for (var k in keys) { const className: string = keys[k as keyof Object] as any as string; const drawSettings: DrawSettings | undefined = drawConfig[k as keyof Object] as any as DrawSettings | undefined; if (drawSettings) { rules.push(`.${className} { fill : none; stroke: ${drawSettings.color}; stroke-width: ${drawSettings.lineWidth}px }`); } else { console.warn(`Warning: your draw config is missing the key '${k}' which is required.`); } } if (this.customStyleDefs) { rules.push("\n/* Custom styles */\n"); this.customStyleDefs.forEach((value: string, key: string) => { rules.push(key + " { " + value + " }"); }); // this.nodeStyle.innerHTML += "\n/* Custom styles */\n" + rules.join("\n"); } this.nodeStyle.innerHTML = rules.join("\n"); } /** * Adds the internal <defs> node. */ private addDefsNode() { this.nodeDefs = this.createSVGNode("defs") as SVGDefsElement; // this.svgNode.appendChild(this.nodeDefs); this.bufferedNodeDefs = this.createSVGNode("defs") as SVGDefsElement; this.svgNode.appendChild(this.nodeDefs); } /** * This is a simple way to include custom CSS class mappings to the style defs of the generated SVG. * * The mapping should be of the form * [style-class] -> [style-def-string] * * Example: * "rect.red" -> "fill: #ff0000; border: 1px solid red" * * @param {Map<string,string>} defs */ addCustomStyleDefs(defs: Map<string, string>) { this.customStyleDefs = defs; } /** * Retieve an old (cached) element. * Only if both – key and nodeName – match, the element will be returned (null otherwise). * * @method findElement * @private * @memberof drawutilssvg * @instance * @param {UID} key - The key of the desired element (used when re-drawing). * @param {string} nodeName - The expected node name. */ private findElement(key: UID | null, nodeName: string): SVGElement | null { if (!key) { return null; } var node: SVGElement | undefined = this.cache.get(key); if (node && node.nodeName.toUpperCase() === nodeName.toUpperCase()) { this.cache.delete(key); return node; } return null; } /** * Create a new DOM node &lt;svg&gt; in the SVG namespace. * * @method createSVGNode * @private * @memberof drawutilssvg * @instance * @param {string} nodeName - The node name (tag-name). * @return {SVGElement} A new element in the SVG namespace with the given node name. */ private createSVGNode(nodeName: string): SVGElement { return document.createElementNS("http://www.w3.org/2000/svg", nodeName); } /** * Make a new SVG node (or recycle an old one) with the given node name (circle, path, line, rect, ...). * * This function is used in draw cycles to re-use old DOM nodes (in hope to boost performance). * * @method makeNode * @private * @instance * @memberof drawutilssvg * @param {string} nodeName - The node name. * @return {SVGElement} The new node, which is not yet added to any document. */ private makeNode(nodeName: string): SVGElement { // Try to find node in current DOM cache. // Unique node keys are strictly necessary. // Try to recycle an old element from cache. var node: SVGElement | null = this.findElement(this.curId, nodeName); if (!node) { // If no such old elements exists (key not found, tag name not matching), // then create a new one. node = this.createSVGNode(nodeName); } if (this.drawlibConfiguration.blendMode) { // node.style["mix-blend-mode"] = this.drawlibConfiguration.blendMode; node.style["mix-blend-mode" as keyof Object](this.drawlibConfiguration.blendMode); } // if (this.lineDashEnabled && this.lineDash && this.lineDash.length > 0 && drawutilssvg.nodeSupportsLineDash(nodeName)) { // node.setAttribute("stroke-dasharray", this.lineDash.join(" ")); // } return node; } /** * This is the final helper function for drawing and filling stuff and binding new * nodes to the SVG document. * It is not intended to be used from the outside. * * When in draw mode it draws the current shape. * When in fill mode it fills the current shape. * * This function is usually only called internally. * * @method _bindFillDraw * @private * @instance * @memberof drawutilssvg * @param {SVGElement} node - The node to draw/fill and bind. * @param {string} className - The class name(s) to use. * @param {string} color - A stroke/fill color to use. * @param {number=1} lineWidth - (optional) A line width to use for drawing (default is 1). * @return {SVGElement} The node itself (for chaining). */ private _bindFillDraw( node: SVGElement, className: string, color?: string | null, lineWidth?: number | null, strokeOptions?: StrokeOptions ): SVGElement { this._configureNode(node, className, this.fillShapes, color, lineWidth, strokeOptions); return this._bindNode(node, undefined); } /** * Bind this given node to a parent. If no parent is passed then the global * node buffer will be used. * * @method _bindNode * @private * @instance * @memberof drawutilssvg * @param {SVGElement} node - The SVG node to bind. * @param {SVGElement=} bindingParent - (optional) You may pass node other than the glober buffer node. * @returns {SVGElement} The passed node itself. */ private _bindNode(node: SVGElement, bindingParent?: SVGElement): SVGElement { if (!node.parentNode) { // Attach to DOM only if not already attached (bindingParent ?? this.bufferGNode).appendChild(node); } return node; } /** * Add custom CSS class names and the globally defined CSS classname to the * given node. * * @method addCSSClasses * @private * @instance * @memberof drawutilssvg * @param {SVGElement} node - The SVG node to bind. * @param {string} className - The additional custom classname to add. * @returns {void} */ private _addCSSClasses(node: SVGElement, className: string) { if (this.curClassName) { node.setAttribute("class", `${className} ${this.curClassName}`); } else { node.setAttribute("class", className); } } private _configureNode( node: SVGElement, className: string, fillMode: boolean, color?: string | null, lineWidth?: number | null, strokeOptions?: StrokeOptions ): SVGElement { this._addCSSClasses(node, className); node.setAttribute("fill", fillMode && color ? color : "none"); node.setAttribute("stroke", fillMode ? "none" : color || "none"); node.setAttribute("stroke-width", `${lineWidth || 1}`); if (this.curId) { node.setAttribute("id", `${this.curId}`); // Maybe React-style 'key' would be better? } this.applyStrokeOpts(node, strokeOptions); return node; } /** * Sets the size and view box of the document. Call this if canvas size changes. * * @method setSize * @instance * @memberof drawutilssvg * @param {XYDimension} canvasSize - The new canvas size. */ setSize(canvasSize: XYDimension) { this.canvasSize = canvasSize; this.svgNode.setAttribute("viewBox", `0 0 ${this.canvasSize.width} ${this.canvasSize.height}`); this.svgNode.setAttribute("width", `${this.canvasSize.width}`); this.svgNode.setAttribute("height", `${this.canvasSize.height}`); } /** * Creates a 'shallow' (non deep) copy of this instance. This implies * that under the hood the same gl context and gl program will be used. */ copyInstance(fillShapes: boolean): drawutilssvg { var copy: drawutilssvg = new drawutilssvg( this.svgNode, this.offset, this.scale, this.canvasSize, fillShapes, this.drawConfig, // null as any as DrawConfig, // no DrawConfig – this will work as long as `isSecondary===true` true, // isSecondary this.gNode, this.bufferGNode, this.nodeDefs, this.bufferedNodeDefs, this.nodeStyle ); return copy; } /** * Set the current drawlib configuration. * * @name setConfiguration * @method * @param {DrawLibConfiguration} configuration - The new configuration settings to use for the next render methods. */ setConfiguration(configuration: DrawLibConfiguration): void { this.drawlibConfiguration = configuration; } /** * This method shouled be called each time the currently drawn `Drawable` changes. * It is used by some libraries for identifying elemente on re-renders. * * @name setCurrentId * @method * @param {UID|null} uid - A UID identifying the currently drawn element(s). * @instance * @memberof drawutilssvg **/ setCurrentId(uid: UID | null): void { this.curId = uid; } /** * This method shouled be called each time the currently drawn `Drawable` changes. * Determine the class name for further usage here. * * @name setCurrentClassName * @method * @param {string|null} className - A class name for further custom use cases. * @instance * @memberof drawutilssvg **/ setCurrentClassName(className: string | null): void { this.curClassName = className; } /** * Called before each draw cycle. * This is required for compatibility with other draw classes in the library. * * @name beginDrawCycle * @method * @param {UID=} uid - (optional) A UID identifying the currently drawn element(s). * @instance * @memberof drawutilssvg **/ beginDrawCycle(renderTime: number) { // Clear non-recycable elements from last draw cycle. this.cache.clear(); // Clearing an SVG is equivalent to removing all its child elements. for (var i = 0; i < this.bufferGNode.childNodes.length; i++) { // Hide all nodes here. Don't throw them away. // We can probably re-use them in the next draw cycle. var child: SVGElement = this.bufferGNode.childNodes[i] as SVGElement; this.cache.set(child.getAttribute("id") as string, child); } this.removeAllChildNodes(); } /** * Called after each draw cycle. * * This is required for compatibility with other draw classes in the library (like drawgl). * * @name endDrawCycle * @method * @param {number} renderTime * @instance **/ endDrawCycle(renderTime: number) { this.rebuildStyleDefs(this.drawConfig); if (!this.isSecondary) { // All elements are drawn into the buffer; they are NOT yet visible, not did the browser perform any // layout updates. // Replace the old <g>-node with the buffer node. // https://stackoverflow.com/questions/27442464/how-to-update-a-svg-image-without-seeing-a-blinking this.svgNode.replaceChild(this.bufferedNodeDefs, this.nodeDefs); this.svgNode.replaceChild(this.bufferGNode, this.gNode); } const tmpGNode: SVGGElement = this.gNode; this.gNode = this.bufferGNode; this.bufferGNode = tmpGNode; const tmpDefsNode: SVGDefsElement = this.nodeDefs; this.nodeDefs = this.bufferedNodeDefs; this.bufferedNodeDefs = tmpDefsNode; } /** * A private helper method to apply stroke options to the current * context. * @param {StrokeOptions=} strokeOptions - */ private applyStrokeOpts(node: SVGElement, strokeOptions?: StrokeOptions) { if ( strokeOptions && strokeOptions.dashArray && strokeOptions.dashArray.length > 0 && drawutilssvg.nodeSupportsLineDash(node.tagName) ) { node.setAttribute( "stroke-dasharray", strokeOptions.dashArray .map((dashArayElem: number) => { return dashArayElem * this.scale.x; }) .join(" ") ); if (strokeOptions.dashOffset) { node.setAttribute("stroke-dashoffset", `${strokeOptions.dashOffset * this.scale.x}`); } } } private _x(x: number): number { return this.offset.x + this.scale.x * x; } private _y(y: number): number { return this.offset.y + this.scale.y * y; } /** * Draw the line between the given two points with the specified (CSS-) color. * * @method line * @param {XYCoords} zA - The start point of the line. * @param {XYCoords} zB - The end point of the line. * @param {string} color - Any valid CSS color string. * @param {number=1} lineWidth? - [optional] The line's width. * @param {StrokeOptions=} strokeOptions - (optional) Stroke settings to use. * * @return {void} * @instance * @memberof drawutilssvg **/ line(zA: XYCoords, zB: XYCoords, color: string, lineWidth?: number, strokeOptions?: StrokeOptions): SVGElement { // const line: SVGElement = this.makeNode("line"); // this.applyStrokeOpts(line, strokeOptions); // line.setAttribute("x1", `${this._x(zA.x)}`); // line.setAttribute("y1", `${this._y(zA.y)}`); // line.setAttribute("x2", `${this._x(zB.x)}`); // line.setAttribute("y2", `${this._y(zB.y)}`); const line = this.makeLineNode(zA, zB, color, lineWidth, strokeOptions); return this._bindFillDraw(line, "line", color, lineWidth || 1, strokeOptions); } /** * Draw a line and an arrow at the end (zB) of the given line with the specified (CSS-) color. * * @method arrow * @param {XYCoords} zA - The start point of the arrow-line. * @param {XYCoords} zB - The end point of the arrow-line. * @param {string} color - Any valid CSS color string. * @param {number=} lineWidth - (optional) The line width to use; default is 1. * @param {headLength=8} headLength - (optional) The length of the arrow head (default is 8 units). * @param {StrokeOptions=} strokeOptions - (optional) Stroke settings to use. * * @return {void} * @instance * @memberof drawutilssvg **/ arrow( zA: XYCoords, zB: XYCoords, color: string, lineWidth?: number, headLength: number = 8, strokeOptions?: StrokeOptions ): SVGElement { const group: SVGElement = this.makeNode("g"); const arrowHeadBasePosition: XYCoords = { x: 0, y: 0 }; // Just create the child nodes, don't bind them to the root node. const arrowHead: SVGElement = this.makeArrowHeadNode(zA, zB, color, lineWidth, headLength, undefined, arrowHeadBasePosition); const line: SVGElement = this.makeLineNode(zA, arrowHeadBasePosition, color, lineWidth, strokeOptions); group.appendChild(line); group.appendChild(arrowHead); this._addCSSClasses(group, "linear-arrow"); this._bindNode(group, undefined); return group; } /** * Draw a cubic Bézier curve and and an arrow at the end (endControlPoint) of the given line width the specified (CSS-) color and arrow size. * * @method cubicBezierArrow * @param {XYCoords} startPoint - The start point of the cubic Bézier curve * @param {XYCoords} endPoint - The end point the cubic Bézier curve. * @param {XYCoords} startControlPoint - The start control point the cubic Bézier curve. * @param {XYCoords} endControlPoint - The end control point the cubic Bézier curve. * @param {string} color - The CSS color to draw the curve with. * @param {number} lineWidth - (optional) The line width to use. * @param {headLength=8} headLength - (optional) The length of the arrow head (default is 8 units). * @param {StrokeOptions=} strokeOptions - (optional) Stroke settings to use. * * @return {void} * @instance * @memberof DrawLib */ cubicBezierArrow( startPoint: XYCoords, endPoint: XYCoords, startControlPoint: XYCoords, endControlPoint: XYCoords, color: string, lineWidth?: number, headLength: number = 8, strokeOptions?: StrokeOptions ): SVGElement { const group: SVGElement = this.makeNode("g"); // Just create the child nodes, don't bind them to the root node. const arrowHeadBasePosition = new Vertex(0, 0); const arrowHead: SVGElement = this.makeArrowHeadNode( endControlPoint, endPoint, color, lineWidth, headLength, undefined, arrowHeadBasePosition ); const diff = arrowHeadBasePosition.difference(endPoint); const bezier: SVGElement = this.makeCubicBezierNode( startPoint, { x: endPoint.x - diff.x, y: endPoint.y - diff.y }, startControlPoint, { x: endControlPoint.x - diff.x, y: endControlPoint.y - diff.y }, color, lineWidth, strokeOptions ); group.appendChild(bezier); group.appendChild(arrowHead); this._addCSSClasses(group, "cubicbezier-arrow"); this._bindNode(group, undefined); return group; } /** * Draw just an arrow head a the end of an imaginary line (zB) of the given line width the specified (CSS-) color and size. * * @method arrow * @param {XYCoords} zA - The start point of the arrow-line. * @param {XYCoords} zB - The end point of the arrow-line. * @param {string} color - Any valid CSS color string. * @param {number=1} lineWidth - (optional) The line width to use; default is 1. * @param {number=8} headLength - (optional) The length of the arrow head (default is 8 pixels). * @param {StrokeOptions=} strokeOptions - (optional) Stroke settings to use. * * @return {void} * @instance * @memberof DrawLib **/ arrowHead( zA: XYCoords, zB: XYCoords, color: string, lineWidth?: number, headLength: number = 8, strokeOptions?: StrokeOptions ): SVGElement { const node: SVGElement = this.makeArrowHeadNode(zA, zB, color, lineWidth, headLength, strokeOptions); return this._bindFillDraw(node, "arrowhead", color, lineWidth || 1, strokeOptions); } /** * Draw an image at the given position with the given size.<br> * <br> * Note: SVG images may have resizing issues at the moment.Draw a line and an arrow at the end (zB) of the given line with the specified (CSS-) color. * * @method image * @param {Image} image - The image object to draw. * @param {XYCoords} position - The position to draw the the upper left corner at. * @param {XYCoords} size - The x/y-size to draw the image with. * @param {number=0.0} alpha - (optional, default=0.0) The transparency (1.0=opaque, 0.0=transparent). * @return {void} * @instance * @memberof drawutilssvg **/ image(image: HTMLImageElement, position: XYCoords, size: XYCoords, alpha: number = 1.0) { const node: SVGElement = this.makeNode("image"); // We need to re-adjust the image if it was not yet fully loaded before. const setImageSize = (image: HTMLImageElement) => { if (image.naturalWidth) { const ratioX = size.x / image.naturalWidth; const ratioY = size.y / image.naturalHeight; node.setAttribute("width", `${image.naturalWidth * this.scale.x}`); node.setAttribute("height", `${image.naturalHeight * this.scale.y}`); node.setAttribute("display", null as any as string); // Dislay when loaded // if (alpha) { node.setAttribute("opacity", `${alpha}`); // } node.setAttribute("transform", `translate(${this._x(position.x)} ${this._y(position.y)}) scale(${ratioX} ${ratioY})`); } }; image.addEventListener("load", event => { setImageSize(image); }); // Safari has a transform-origin bug. // Use x=0, y=0 and translate/scale instead (see above) node.setAttribute("x", `${0}`); node.setAttribute("y", `${0}`); node.setAttribute("display", "none"); // Hide before loaded setImageSize(image); node.setAttribute("href", image.src); return this._bindFillDraw(node, "image", null, null); } /** * Draw an image at the given position with the given size.<br> * <br> * Note: SVG images may have resizing issues at the moment.Draw a line and an arrow at the end (zB) of the given line with the specified (CSS-) color. * * @method texturedPoly * @param {Image} textureImage - The image object to draw. * @param {Bounds} textureSize - The texture size to use; these are the original bounds to map the polygon vertices to. * @param {Polygon} polygon - The polygon to use as clip path. * @param {XYCoords} polygonPosition - The polygon's position (relative), measured at the bounding box's center. * @param {number} rotation - The rotation to use for the polygon (and for the texture). * @return {void} * @instance * @memberof drawutilssvg **/ texturedPoly( textureImage: HTMLImageElement, textureSize: Bounds, polygon: Polygon, polygonPosition: XYCoords, rotation: number ): SVGElement { // const basePolygonBounds: Bounds = polygon.getBounds(); const rotatedScalingOrigin = new Vertex(textureSize.min).clone().rotate(rotation, polygonPosition); // const rotationCenter = polygonPosition.clone().add(rotatedScalingOrigin.difference(textureSize.min).inv()); // Create something like this // ... // <defs> // <clipPath id="shape"> // <path fill="none" d="..."/> // </clipPath> // </defs> // ... // <g clip-path="url(#shape)"> // <g transform="scale(...)"> // <image width="643" height="643" transform="rotate(...)" xlink:href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/222579/beagle400.jpg" > // </g> // </g> // </image> // ... const clipPathNode: SVGClipPathElement = this.makeNode("clipPath") as SVGClipPathElement; const clipPathId: string = `clippath_${UIDGenerator.next()}`; // TODO: use a better UUID generator here? clipPathNode.setAttribute("id", clipPathId); const gNode = this.makeNode("g") as SVGGElement; const imageNode: SVGImageElement = this.makeNode("image") as SVGImageElement; imageNode.setAttribute("x", `${this._x(rotatedScalingOrigin.x)}`); imageNode.setAttribute("y", `${this._y(rotatedScalingOrigin.y)}`); imageNode.setAttribute("width", `${textureSize.width}`); imageNode.setAttribute("height", `${textureSize.height}`); imageNode.setAttribute("href", textureImage.src); // imageNode.setAttribute("opacity", "0.5"); // SVG rotations in degrees imageNode.setAttribute( "transform", `rotate(${rotation * RAD_TO_DEG}, ${this._x(rotatedScalingOrigin.x)}, ${this._y(rotatedScalingOrigin.y)})` ); const pathNode: SVGPathElement = this.makeNode("path") as SVGPathElement; const pathData: string[] = []; if (polygon.vertices.length > 0) { const self = this; pathData.push("M", `${this._x(polygon.vertices[0].x)}`, `${this._y(polygon.vertices[0].y)}`); for (var i = 1; i < polygon.vertices.length; i++) { pathData.push("L", `${this._x(polygon.vertices[i].x)}`, `${this._y(polygon.vertices[i].y)}`); } } pathNode.setAttribute("d", pathData.join(" ")); clipPathNode.appendChild(pathNode); this.bufferedNodeDefs.appendChild(clipPathNode); gNode.appendChild(imageNode); gNode.setAttribute("transform-origin", `${this._x(rotatedScalingOrigin.x)} ${this._y(rotatedScalingOrigin.y)}`); gNode.setAttribute("transform", `scale(${this.scale.x}, ${this.scale.y})`); const clipNode: SVGGElement = this.makeNode("g") as SVGGElement; clipNode.appendChild(gNode); clipNode.setAttribute("clip-path", `url(#${clipPathId})`); // TODO: check if the image class is correct here or if we should use a 'clippedImage' class here this._bindFillDraw(clipNode, "image", null, null); // No color, no lineWidth return clipNode; } /** * Draw the given (cubic) bézier curve. * * @method cubicBezier * @param {XYCoords} startPoint - The start point of the cubic Bézier curve * @param {XYCoords} endPoint - The end point the cubic Bézier curve. * @param {XYCoords} startControlPoint - The start control point the cubic Bézier curve. * @param {XYCoords} endControlPoint - The end control point the cubic Bézier curve. * @param {string} color - The CSS color to draw the curve with. * @param {number} lineWidth - (optional) The line width to use. * @param {StrokeOptions=} strokeOptions - (optional) Stroke settings to use. * * @return {void} * @instance * @memberof drawutilssvg */ cubicBezier( startPoint: XYCoords, endPoint: XYCoords, startControlPoint: XYCoords, endControlPoint: XYCoords, color: string, lineWidth?: number, strokeOptions?: StrokeOptions ): SVGElement { const node: SVGElement = this.makeCubicBezierNode( startPoint, endPoint, startControlPoint, endControlPoint, color, lineWidth, strokeOptions ); return this._bindNode(node, undefined); } /** * Draw the given (cubic) Bézier path. * * The given path must be an array with n*3+1 vertices, where n is the number of * curves in the path: * <pre> [ point1, point1_startControl, point2_endControl, point2, point2_startControl, point3_endControl, point3, ... pointN_endControl, pointN ]</pre> * * @method cubicBezierPath * @param {XYCoords[]} path - The cubic bezier path as described above. * @param {string} color - The CSS colot to draw the path with. * @param {number=1} lineWidth - (optional) The line width to use. * @param {StrokeOptions=} strokeOptions - (optional) Stroke settings to use. * * @return {void} * @instance * @memberof drawutilssvg */ cubicBezierPath(path: Array<XYCoords>, color: string, lineWidth?: number, strokeOptions?: StrokeOptions): SVGElement { const node: SVGElement = this.makeNode("path"); this.applyStrokeOpts(node, strokeOptions); if (!path || path.length == 0) { return node; } // Draw curve const d: Array<string | number> = ["M", this._x(path[0].x), this._y(path[0].y)]; // Draw curve path var endPoint: XYCoords; var startControlPoint: XYCoords; var endControlPoint: XYCoords; for (var i = 1; i < path.length; i += 3) { startControlPoint = path[i]; endControlPoint = path[i + 1]; endPoint = path[i + 2]; d.push( "C", this._x(startControlPoint.x), this._y(startControlPoint.y), this._x(endControlPoint.x), this._y(endControlPoint.y), this._x(endPoint.x), this._y(endPoint.y) ); } node.setAttribute("d", d.join(" ")); return this._bindFillDraw(node, "cubicBezierPath", color, lineWidth || 1); } /** * Draw the given handle and handle point (used to draw interactive Bézier curves). * * The colors for this are fixed and cannot be specified. * * @method handle * @param {Vertex} startPoint - The start of the handle. * @param {Vertex} endPoint - The end point of the handle. * @return {void} * @instance * @memberof drawutilssvg */ handle(startPoint: XYCoords, endPoint: XYCoords): void { // TODO: redefine methods like these into an abstract class? this.point(startPoint, "rgb(0,32,192)"); this.square(endPoint, 5, "rgba(0,128,192,0.5)"); } /** * Draw a handle line (with a light grey). * * @method handleLine * @param {XYCoords} startPoint - The start point to draw the handle at. * @param {XYCoords} endPoint - The end point to draw the handle at. * @return {void} * @instance * @memberof drawutilssvg */ handleLine(startPoint: XYCoords, endPoint: XYCoords): void { this.line(startPoint, endPoint, "rgb(128,128,128,0.5)"); } /** * Draw a 1x1 dot with the specified (CSS-) color. * * @method dot * @param {XYCoords} p - The position to draw the dot at. * @param {string} color - The CSS color to draw the dot with. * @return {void} * @instance * @memberof drawutilssvg */ dot(p: XYCoords, color: string) { const node: SVGElement = this.makeNode("line"); node.setAttribute("x1", `${this._x(p.x)}`); node.setAttribute("y1", `${this._y(p.y)}`); node.setAttribute("x2", `${this._x(p.x)}`); node.setAttribute("y2", `${this._y(p.y)}`); return this._bindFillDraw(node, "dot", color, 1); } /** * Draw the given point with the specified (CSS-) color and radius 3. * * @method point * @param {XYCoords} p - The position to draw the point at. * @param {string} color - The CSS color to draw the point with. * @return {void} * @instance * @memberof drawutilssvg */ point(p: XYCoords, color: string) { var radius: number = 3; const node: SVGElement = this.makeNode("circle"); node.setAttribute("cx", `${this._x(p.x)}`); node.setAttribute("cy", `${this._y(p.y)}`); node.setAttribute("r", `${radius}`); return this._bindFillDraw(node, "point", color, 1); } /** * Draw a circle with the specified (CSS-) color and radius.<br> * <br> * Note that if the x- and y- scales are different the result will be an ellipse rather than a circle. * * @method circle * @param {XYCoords} center - The center of the circle. * @param {number} radius - The radius of the circle. * @param {string} color - The CSS color to draw the circle with. * @param {number=} lineWidth - (optional) The line width to use; default is 1. * @param {StrokeOptions=} strokeOptions - (optional) Stroke settings to use. * * @return {void} * @instance * @memberof drawutilssvg */ circle(center: XYCoords, radius: number, color: string, lineWidth?: number, strokeOptions?: StrokeOptions) { // Todo: draw ellipse when scalex!=scaley const node: SVGElement = this.makeNode("circle"); this.applyStrokeOpts(node, strokeOptions); node.setAttribute("cx", `${this._x(center.x)}`); node.setAttribute("cy", `${this._y(center.y)}`); node.setAttribute("r", `${radius * this.scale.x}`); // y? return this._bindFillDraw(node, "circle", color, lineWidth || 1); } /** * Draw a circular arc (section of a circle) with the given CSS color. * * @method circleArc * @param {XYCoords} center - The center of the circle. * @param {number} radius - The radius of the circle. * @param {number} startAngle - The angle to start at. * @param {number} endAngle - The angle to end at. * @param {string} color - The CSS color to draw the circle with. * @param {StrokeOptions=} strokeOptions - (optional) Stroke settings to use. * * @return {void} * @instance * @memberof drawutilssvg */ circleArc( center: XYCoords, radius: number, startAngle: number, endAngle: number, color: string, lineWidth?: number, strokeOptions?: StrokeOptions ) { const node: SVGElement = this.makeNode("path"); this.applyStrokeOpts(node, strokeOptions); const arcData: SVGPathParams = CircleSector.circleSectorUtils.describeSVGArc( this._x(center.x), this._y(center.y), radius * this.scale.x, // y? startAngle, endAngle ); node.setAttribute("d", arcData.join(" ")); return this._bindFillDraw(node, "circleArc", color, lineWidth || 1); } /** * Draw an ellipse with the specified (CSS-) color and thw two radii. * * @method ellipse * @param {XYCoords} center - The center of the ellipse. * @param {number} radiusX - The radius of the ellipse. * @param {number} radiusY - The radius of the ellipse. * @param {string} color - The CSS color to draw the ellipse with. * @param {number=} lineWidth - (optional) The line width to use; default is 1. * @param {number=} rotation - (optional, default=0) The rotation of the ellipse. * @param {StrokeOptions=} strokeOptions - (optional) Stroke settings to use. * * @return {void} * @instance * @memberof drawutilssvg */ ellipse( center: XYCoords, radiusX: number, radiusY: number, color: string, lineWidth?: number, rotation?: number, strokeOptions?: StrokeOptions ) { if (typeof rotation === "undefined") { rotation = 0.0; } const node: SVGElement = this.makeNode("ellipse"); this.applyStrokeOpts(node, strokeOptions); node.setAttribute("cx", `${this._x(center.x)}`); node.setAttribute("cy", `${this._y(center.y)}`); node.setAttribute("rx", `${radiusX * this.scale.x}`); node.setAttribute("ry", `${radiusY * this.scale.y}`); // node.setAttribute( 'style', `transform: rotate(${rotation} ${center.x} ${center.y})` ); node.setAttribute("transform", `rotate(${(rotation * 180) / Math.PI} ${this._x(center.x)} ${this._y(center.y)})`); return this._bindFillDraw(node, "ellipse", color, lineWidth || 1); } /** * Draw square at the given center, size and with the specified (CSS-) color.<br> * <br> * Note that if the x-scale and the y-scale are different the result will be a rectangle rather than a square. * * @method square * @param {XYCoords} center - The center of the square. * @param {number} size - The size of the square. * @param {string} color - The CSS color to draw the square with. * @param {number=} lineWidth - (optional) The line width to use; default is 1. * @param {StrokeOptions=} strokeOptions - (optional) Stroke settings to use. * * @return {SVGElement} * @instance * @memberof drawutilssvg */ square(center: XYCoords, size: number, color: string, lineWidth?: number, strokeOptions?: StrokeOptions): SVGElement { const node: SVGElement = this.makeNode("rectangle"); this.applyStrokeOpts(node, strokeOptions); node.setAttribute("x", `${this._x(center.x - size / 2.0)}`); node.setAttribute("y", `${this._y(center.y - size / 2.0)}`); node.setAttribute("width", `${size * this.scale.x}`); node.setAttribute("height", `${size * this.scale.y}`); return this._bindFillDraw(node, "square", color, lineWidth || 1); } /** * Draw a rectangle. * * @param {XYCoords} position - The upper left corner of the rectangle. * @param {number} width - The width of the rectangle. * @param {number} height - The height of the rectangle. * @param {string} color - The color to use. * @param {number=1} lineWidth - (optional) The line with to use (default is 1). * @param {StrokeOptions=} strokeOptions - (optional) Stroke settings to use. * * @return {SVGElement} * @instance * @memberof drawutilssvg **/ rect( position: XYCoords, width: number, height: number, color: string, lineWidth?: number, strokeOptions?: StrokeOptions ): SVGElement { const node: SVGElement = this.makeNode("rect"); this.applyStrokeOpts(node, strokeOptions); node.setAttribute("x", `${this._x(position.x)}`); node.setAttribute("y", `${this._y(position.y)}`); node.setAttribute("width", `${width * this.scale.x}`); node.setAttribute("height", `${height * this.scale.y}`); return this._bindFillDraw(node, "rect", color, lineWidth || 1); } /** * Draw a grid of horizontal and vertical lines with the given (CSS-) color. * * @method grid * @param {XYCoords} center - The center of the grid. * @param {number} width - The total width of the grid (width/2 each to the left and to the right). * @param {number} height - The total height of the grid (height/2 each to the top and to the bottom). * @param {number} sizeX - The horizontal grid size. * @param {number} sizeY - The vertical grid size. * @param {string} color - The CSS color to draw the grid with. * @return {void} * @instance * @memberof drawutilssvg */ grid(center: XYCoords, width: number, height: number, sizeX: number, sizeY: number, color: string) { // console.log("grid"); // const node: SVGElement = this.makeNode("pattern"); // var patternId = "pattern_id_" + Math.floor(Math.random() * 65365); // node.setAttribute("id", patternId); // node.setAttribute("viewBox", `0,0,${sizeX},${sizeY}`); // node.setAttribute("width", `${sizeX}`); // node.setAttribute("height", `${sizeX}`); // var pattern: SVGElement = this.makeNode("path"); // const d: SVGPathParams = []; // d.push("M", sizeX / 2.0, 0); // d.push("L", sizeX / 2.0, sizeY); // d.push("M", 0, sizeY / 2.0); // d.push("L", sizeX, sizeY / 2.0); // node.setAttribute("d", d.join(" ")); // this.bufferedNodeDefs.append(pattern); // const fillNode: SVGElement = this.makeNode("rect"); // // For some strange reason SVG rotation transforms use degrees instead of radians // // Note that the background does not scale with the zoom level (always covers full element) // fillNode.setAttribute("x", "0"); // fillNode.setAttribute("y", "0"); // fillNode.setAttribute("width", `${this.canvasSize.width}`); // fillNode.setAttribute("height", `${this.canvasSize.height}`); // fillNode.setAttribute("fill", `url(#${patternId})`); // return this._bindFillDraw(fillNode, "grid", "red", 1); const node: SVGElement = this.makeNode("path"); const d: SVGPathParams = []; var yMin: number = -Math.ceil((height * 0.5) / sizeY) * sizeY; var yMax: number = height / 2; for (var x = -Math.ceil((width * 0.5) / sizeX) * sizeX; x < width / 2; x += sizeX) { d.push("M", this._x(center.x + x), this._y(center.y + yMin)); d.push("L", this._x(center.x + x), this._y(center.y + yMax)); } var xMin: number = -Math.ceil((width * 0.5) / sizeX) * sizeX; var xMax: number = width / 2; for (var y = -Math.ceil((height * 0.5) / sizeY) * sizeY; y < height / 2; y += sizeY) { d.push("M", this._x(center.x + xMin), this._y(center.y + y)); d.push("L", this._x(center.x + xMax), this._y(center.y + y)); } node.setAttribute("d", d.join(" ")); return this._bindFillDraw(node, "grid", color, 1); } /** * Draw a raster of crosshairs in the given grid.<br> * * This works analogue to the grid() function * * @method raster * @param {XYCoords} center - The center of the raster. * @param {number} width - The total width of the raster (width/2 each to the left and to the right). * @param {number} height - The total height of the raster (height/2 each to the top and to the bottom). * @param {number} sizeX - The horizontal raster size. * @param {number} sizeY - The vertical raster size. * @param {string} color - The CSS color to draw the raster with. * @return {void} * @instance * @memberof drawutilssvg */ raster(center: XYCoords, width: number, height: number, sizeX: number, sizeY: number, color: string) { const node: SVGElement = this.makeNode("path"); const d: SVGPathParams = []; for (var x = -Math.ceil((width * 0.5) / sizeX) * sizeX; x < width / 2; x += sizeX) { for (var y = -Math.ceil((height * 0.5) / sizeY) * sizeY; y < height / 2; y += sizeY) { // Draw a crosshair d.push("M", this._x