UNPKG

@pixi-essentials/svg

Version:
1,560 lines (1,315 loc) 84.8 kB
/* eslint-disable */ /*! * @pixi-essentials/svg - v3.0.0 * Compiled Tue, 08 Oct 2024 01:10:18 UTC * * @pixi-essentials/svg is licensed under the MIT License. * http://www.opensource.org/licenses/mit-license * * Copyright 2019-2020, Shukant K. Pal <shukantpal@outlook.com>, All Rights Reserved */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var pixi_js = require('pixi.js'); var color = require('tinycolor2'); var gradients = require('@pixi-essentials/gradients'); var dPathParser = require('d-path-parser'); var textureAllocator = require('@pixi-essentials/texture-allocator'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var color__default = /*#__PURE__*/_interopDefaultLegacy(color); var dPathParser__default = /*#__PURE__*/_interopDefaultLegacy(dPathParser); /** * @internal * @ignore */ const _SVG_DOCUMENT_CACHE = new Map(); /** * @internal * @ignore */ async function _load(href) { const url = new URL(href, document.baseURI); const id = url.host + url.pathname; let doc = _SVG_DOCUMENT_CACHE.get(id); if (!doc) { doc = await fetch(url.toString()) .then((res) => res.text()) .then((text) => new DOMParser().parseFromString(text, 'image/svg+xml').documentElement ); _SVG_DOCUMENT_CACHE.set(id, doc); } return doc; } /** * Get information on the internal cache of the SVG loading mechanism. * * @public * @returns A view on the cache - clear() method and a size property. */ function getLoaderCache() { return { clear() { _SVG_DOCUMENT_CACHE.clear(); }, size: _SVG_DOCUMENT_CACHE.size, }; } // const tempSourceFrame = new Rectangle(); // const tempDestinationFrame = new Rectangle(); /** * A sprite that does not render anything. It can be used as a mask whose bounds can be updated by adding it * as a child of the mask-target. * * @public * @see MaskServer.createMask * @ignore */ class MaskSprite extends pixi_js.Sprite { // eslint-disable-next-line @typescript-eslint/no-unused-vars render(_) { // NOTHING } } /** * A `MaskServer` will lazily render its content's luminance into its render-texture's alpha * channel using the luminance-alpha filter. The `dirtyId` flag can be used to make it re-render its * contents. It is intended to be used as a sprite-mask, where black pixels are invisible and white * pixels are visible (i.e. black pixels are filtered to alpha = 0, while white pixels are filtered * to alpha = 1. The rest are filtered to an alpha such that 0 < alpha < 1.). This is in compliance * with [CSS Masking Module Level 1](https://www.w3.org/TR/css-masking-1/#MaskElement). * * **Note: This functionality is disabled in PixiJS 8's renderer** * * @public * @ignore */ class MaskServer extends pixi_js.Sprite { /** * Flags when re-renders are required due to content updates. */ /** * Flags when the content is re-rendered and should be equal to `this.dirtyId` when the texture * is update-to-date. */ /** * @param texture - The render-texture that will cache the contents. */ constructor(texture) { super(texture); this.dirtyId = 0; this.updateId = -1; } /** * @override */ // render(renderer: Renderer): void // { // if (this.dirtyId !== this.updateId) // { // // Update texture resolution, without changing screen-space resolution // this.texture.baseTexture.setSize(this.texture.width, this.texture.height, renderer.resolution); // // renderer.batch.flush(); // // const renderTarget = renderer.renderTexture.current; // const sourceFrame = tempSourceFrame.copyFrom(renderer.renderTexture.sourceFrame); // const destinationFrame = tempDestinationFrame.copyFrom(renderer.renderTexture.destinationFrame); // // const localBounds = (this as Sprite).getLocalBounds(null); // const children: ContainerChild[] = this.children; // // renderer.renderTexture.bind(this.texture as RenderTexture, localBounds); // renderer.renderTexture.clear(); // renderer.filter.push({ filterArea: localBounds, getBounds: () => localBounds }, [l2rFilter]); // // for (let i = 0, j = children.length; i < j; i++) // { // const child = children[i]; // // child.enableTempParent(); // child.updateTransform(); // (children[i] as Container).render(renderer); // child.disableTempParent(this); // } // // renderer.batch.flush(); // renderer.filter.pop(); // // renderer.renderTexture.bind(renderTarget, sourceFrame, destinationFrame); // // this.updateId = this.dirtyId; // // this.getBounds(); // } // } /** * Create a mask that will overlay on top of the given display-object using the texture of this * mask server. * * @param displayObject - The mask target. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars createMask(_) { return new MaskSprite(this.texture); } } /** * Inherited paint, used for &lt;use /&gt; elements. The properties used on the inherited paint do not * override those on the parent. * * @public */ class InheritedPaintProvider { /** * Composes a `Paint` that will inherit properties from the `parent` if the `provider` does not * define them. * * @param parent * @param provider */ constructor(parent, provider) { this.parent = parent; this.provider = provider; } get dirtyId() { return this.parent.dirtyId + this.provider.dirtyId; } get fill() { return this.provider.fill !== null ? this.provider.fill : this.parent.fill; } get opacity() { return (typeof this.provider.opacity === 'number') ? this.provider.opacity : this.parent.opacity; } get stroke() { return this.provider.stroke !== null ? this.provider.stroke : this.parent.stroke; } get strokeDashArray() { return Array.isArray(this.provider.strokeDashArray) ? this.provider.strokeDashArray : this.parent.strokeDashArray; } get strokeDashOffset() { return typeof this.provider.strokeDashOffset === 'number' ? this.provider.strokeDashOffset : this.parent.strokeDashOffset; } get strokeLineCap() { return typeof this.provider.strokeLineCap === 'string' ? this.provider.strokeLineCap : this.parent.strokeLineCap; } get strokeLineJoin() { return typeof this.provider.strokeLineJoin === 'string' ? this.provider.strokeLineJoin : this.parent.strokeLineJoin; } get strokeMiterLimit() { return typeof this.provider.strokeMiterLimit === 'number' ? this.provider.strokeMiterLimit : this.parent.strokeMiterLimit; } get strokeWidth() { return typeof this.provider.strokeWidth === 'number' ? this.provider.strokeWidth : this.parent.strokeWidth; } } function _optionalChain$2(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } /** * Provides the `Paint` for an `SVGElement`. It will also respond to changes in the attributes of the element * (not implemented). * * @public */ class PaintProvider { __init() {this.dirtyId = 0;} /** * @param element - The element whose paint is to be provided. */ constructor(element) {PaintProvider.prototype.__init.call(this); this.element = element; const fill = element.getAttribute('fill'); const opacity = element.getAttribute('opacity'); const stroke = element.getAttribute('stroke'); const strokeDashArray = element.getAttribute('stroke-dasharray'); const strokeDashOffset = element.getAttribute('stroke-dashoffset'); const strokeLineCap = element.getAttribute('stroke-linecap'); const strokeLineJoin = element.getAttribute('stroke-linejoin'); const strokeMiterLimit = element.getAttribute('stroke-miterlimit'); const strokeWidth = element.getAttribute('stroke-width'); /* eslint-disable-next-line no-nested-ternary */ this.fill = fill !== null ? (fill === 'none' ? 'none' : PaintProvider.parseColor(fill)) : null; this.opacity = opacity && parseFloat(opacity); this.stroke = stroke && PaintProvider.parseColor(element.getAttribute('stroke')); this.strokeDashArray = strokeDashArray && _optionalChain$2([strokeDashArray , 'optionalAccess', _ => _.split, 'call', _2 => _2(/[, ]+/g) , 'access', _3 => _3.map, 'call', _4 => _4((num) => parseFloat(num.trim()))]); this.strokeDashOffset = strokeDashOffset && parseFloat(strokeDashOffset); this.strokeLineCap = strokeLineCap ; this.strokeLineJoin = strokeLineJoin ; this.strokeMiterLimit = strokeMiterLimit && parseFloat(strokeMiterLimit); this.strokeWidth = strokeWidth && parseFloat(strokeWidth); } /** * Parses the color attribute into an RGBA hexadecimal equivalent, if encoded. If the `colorString` is `none` or * is a `url(#id)` reference, it is returned as is. * * @param colorString * @see https://github.com/bigtimebuddy/pixi-svg/blob/89e4ab834fa4ef05b64741596516c732eae34daa/src/SVG.js#L106 */ static parseColor(colorString) { /* Modifications have been made. */ /* Copyright (C) Matt Karl. */ if (!colorString) { return 0; } if (colorString === 'none' || colorString.startsWith('url')) { return colorString; } if (colorString[0] === '#') { // Remove the hash colorString = colorString.substr(1); // Convert shortcolors fc9 to ffcc99 if (colorString.length === 3) { colorString = colorString.replace(/([a-f0-9])/ig, '$1$1'); } return parseInt(colorString, 16); } const { r, g, b } = color__default["default"](colorString).toRgb(); return (r << 16) + (g << 8) + b; } } /** * Converts the linear gradient's x1, x2, y1, y2 attributes into percentage units. * * @param linearGradient - The linear gradient element whose attributes are to be converted. */ function convertLinearGradientAxis(linearGradient) { if (linearGradient.x1.baseVal.unitType !== SVGLength.SVG_LENGTHTYPE_PERCENTAGE) { linearGradient.x1.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PERCENTAGE); } if (linearGradient.y1.baseVal.unitType !== SVGLength.SVG_LENGTHTYPE_PERCENTAGE) { linearGradient.y1.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PERCENTAGE); } if (linearGradient.x2.baseVal.unitType !== SVGLength.SVG_LENGTHTYPE_PERCENTAGE) { linearGradient.x2.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PERCENTAGE); } if (linearGradient.y2.baseVal.unitType !== SVGLength.SVG_LENGTHTYPE_PERCENTAGE) { linearGradient.y2.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PERCENTAGE); } } /** * [Paint Servers]{@link https://svgwg.org/svg-next/pservers.html} are implemented as textures. This class is a lazy * wrapper around paint textures, which can only be generated using the `renderer` drawing to the screen. * * @public */ class PaintServer { /** * Creates a `PaintServer` wrapper. * * @param paintServer * @param paintTexture */ constructor(paintServer, paintTexture) { this.paintServer = paintServer; this.paintTexture = paintTexture; this.paintContexts = new Map(); this.dirtyId = 0; } /** * Ensures the paint texture is updated for the renderer's WebGL context. This should be called before using the * paint texture to render anything. * * @param renderer - The renderer that will use the paint texture. */ resolvePaint(renderer) { const contextDirtyId = this.paintContexts.get(renderer); const dirtyId = this.dirtyId; if (contextDirtyId === undefined || contextDirtyId < dirtyId) { this.updatePaint(renderer); this.paintContexts.set(renderer, dirtyId); } } /** * Calculates the optimal texture dimensions for the paint texture, given the bounding box of the * object applying it. The paint texture is resized accordingly. * * If the paint texture is sized smaller than the bounding box, then it is expected that it will * be scaled up to fit it. * * @param bbox - The bounding box of the object applying the paint texture. */ resolvePaintDimensions(bbox) { const bwidth = Math.ceil(bbox.width); const bheight = Math.ceil(bbox.height); const baspectRatio = bwidth / bheight; const paintServer = this.paintServer; const paintTexture = this.paintTexture; if (paintServer instanceof SVGLinearGradientElement) { convertLinearGradientAxis(paintServer); const colorStops = paintServer.children; const x1 = paintServer.x1.baseVal.valueInSpecifiedUnits; const y1 = paintServer.y1.baseVal.valueInSpecifiedUnits; const x2 = paintServer.x2.baseVal.valueInSpecifiedUnits; const y2 = paintServer.y2.baseVal.valueInSpecifiedUnits; const mainAxisAngle = Math.atan2(y2 - y1, x2 - x1); const mainAxisLength = colorStops.length === 1 ? 2 : 64; let width = Math.max(1, mainAxisLength * Math.cos(mainAxisAngle)); let height = Math.max(1, mainAxisLength * Math.sin(mainAxisAngle)); if (width < bwidth && height < bheight) { // If the gradient is not parallel to x- or y- axis, then ensure that the texture's aspect ratio // matches that of the bounding box. This will ensure scaling is equal along both axes, and the // angle is not skewed due to scaling. if (Math.abs(mainAxisAngle) > 1e-2 && Math.abs(mainAxisAngle) % (Math.PI / 2) > 1e-2) { const aspectRatio = width / height; if (aspectRatio > baspectRatio) { height = width / baspectRatio; } else { width = baspectRatio * height; } } paintTexture.resize(width, height); return; } } paintTexture.resize(bwidth, bheight); } /** * Renders the paint texture using the renderer immediately. * * @param renderer - The renderer to use for rendering to the paint texture. */ updatePaint(renderer) { if (this.paintServer instanceof SVGLinearGradientElement) { this.linearGradient(renderer); } else if (this.paintServer instanceof SVGRadialGradientElement) { this.radialGradient(renderer); } } /** * Renders `this.paintServer` as a `SVGLinearGradientElement`. * * @param renderer - The renderer being used to render the paint texture. */ linearGradient(renderer) { const linearGradient = this.paintServer ; const paintTexture = this.paintTexture; convertLinearGradientAxis(linearGradient); return gradients.GradientFactory.createLinearGradient( renderer, paintTexture, { x0: linearGradient.x1.baseVal.valueInSpecifiedUnits * paintTexture.width / 100, y0: linearGradient.y1.baseVal.valueInSpecifiedUnits * paintTexture.height / 100, x1: linearGradient.x2.baseVal.valueInSpecifiedUnits * paintTexture.width / 100, y1: linearGradient.y2.baseVal.valueInSpecifiedUnits * paintTexture.height / 100, colorStops: this.createColorStops(linearGradient.children), }, ); } /** * Renders `this.paintServer` as a `SVGRadialGradientElement`. * * @param renderer - The renderer being used to render the paint texture. */ radialGradient(renderer) { const radialGradient = this.paintServer ; const paintTexture = this.paintTexture; radialGradient.fx.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_NUMBER); radialGradient.fy.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_NUMBER); radialGradient.cx.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_NUMBER); radialGradient.cy.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_NUMBER); return gradients.GradientFactory.createRadialGradient( renderer, paintTexture, { x0: radialGradient.fx.baseVal.valueInSpecifiedUnits * paintTexture.width / 100, y0: radialGradient.fy.baseVal.valueInSpecifiedUnits * paintTexture.height / 100, r0: radialGradient.fr.baseVal.valueInSpecifiedUnits * paintTexture.width / 100, x1: radialGradient.cx.baseVal.valueInSpecifiedUnits * paintTexture.width / 100, y1: radialGradient.cy.baseVal.valueInSpecifiedUnits * paintTexture.height / 100, r1: radialGradient.r.baseVal.valueInSpecifiedUnits * paintTexture.width / 100, colorStops: this.createColorStops(radialGradient.children), }, ); } /** * Extracts the color-stops from the children of a `SVGGradientElement`. * * @param stopElements - The children of a `SVGGradientElement`. You can get it via `element.children`. * @return The color stops that can be fed into {@link GradientFactory}. */ createColorStops(stopElements) { const colorStops = []; for (let i = 0, j = stopElements.length; i < j; i++) { const stopElement = stopElements.item(i) ; colorStops.push({ offset: stopElement.offset.baseVal, color: PaintProvider.parseColor(stopElement.getAttribute('stop-color')) , }); } return colorStops; } } const tempMatrix$2 = new pixi_js.Matrix(); const tempPoint$1 = new pixi_js.Point(); /** * This node can be used to directly embed the following elements: * * | Interface | Element | * | ------------------- | ------------------ | * | SVGGElement | &lt;g /&gt; | * | SVGCircleElement | &lt;circle /&gt; | * | SVGLineElement | &lt;line /&gt; | * | SVGPolylineElement | &lt;polyline /&gt; | * | SVGPolygonElement | &lt;polygon /&gt; | * | SVGRectElement | &lt;rect /&gt; | * * It also provides an implementation for dashed stroking, by adding the `dashArray` and `dashOffset` properties * to `LineStyle`. * * @public */ class SVGGraphicsNode extends pixi_js.Graphics { constructor(context) { super(); this._sceneContext = context; this.paintServers = []; } /** * Draws an elliptical arc. * * @param cx - The x-coordinate of the center of the ellipse. * @param cy - The y-coordinate of the center of the ellipse. * @param rx - The radius along the x-axis. * @param ry - The radius along the y-axis. * @param startAngle - The starting eccentric angle, in radians (0 is at the 3 o'clock position of the arc's circle). * @param endAngle - The ending eccentric angle, in radians. * @param xAxisRotation - The angle of the whole ellipse w.r.t. x-axis. * @param anticlockwise - Specifies whether the drawing should be counterclockwise or clockwise. * @return This Graphics object. Good for chaining method calls. */ ellipticArc( cx, cy, rx, ry, startAngle, endAngle, xAxisRotation = 0, anticlockwise = false) { const sweepAngle = endAngle - startAngle; // Choose a number of segments such that the maximum absolute deviation from the circle is approximately 0.029 const n = (window.devicePixelRatio || 1) * Math.ceil(2.3 * Math.sqrt(rx + ry)); const delta = (anticlockwise ? -1 : 1) * Math.abs(sweepAngle) / (n - 1); tempMatrix$2.identity() .translate(-cx, -cy) .rotate(xAxisRotation) .translate(cx, cy); for (let i = 0; i < n; i++) { const eccentricAngle = startAngle + (i * delta); const xr = cx + (rx * Math.cos(eccentricAngle)); const yr = cy + (ry * Math.sin(eccentricAngle)); const { x, y } = xAxisRotation !== 0 ? tempMatrix$2.apply({ x: xr, y: yr }) : { x: xr, y: yr }; if (i === 0) { this.moveTo(x, y); continue; } this.lineTo(x, y); } return this; } /** * Draws an elliptical arc to the specified point. * * If rx = 0 or ry = 0, then a line is drawn. If the radii provided are too small to draw the arc, then * they are scaled up appropriately. * * @param endX - the x-coordinate of the ending point. * @param endY - the y-coordinate of the ending point. * @param rx - The radius along the x-axis. * @param ry - The radius along the y-axis. * @param xAxisRotation - The angle of the ellipse as a whole w.r.t/ x-axis. * @param anticlockwise - Specifies whether the arc should be drawn counterclockwise or clockwise. * @param largeArc - Specifies whether the larger arc of two possible should be choosen. * @return This Graphics object. Good for chaining method calls. * @see https://svgwg.org/svg2-draft/paths.html#PathDataEllipticalArcCommands * @see https://www.w3.org/TR/SVG2/implnote.html#ArcImplementationNotes */ ellipticArcTo( endX, endY, rx, ry, xAxisRotation = 0, anticlockwise = false, largeArc = false, ) { if (rx === 0 || ry === 0) { return this.lineTo(endX, endY) ; } // See https://www.w3.org/TR/SVG2/implnote.html#ArcImplementationNotes /* eslint-disable dot-notation */ const activePath = this.context['_activePath'] ; activePath.shapePath['_ensurePoly'](); activePath.getLastPoint(tempPoint$1); /* eslint-enable dot-notation */ const startX = tempPoint$1.x; const startY = tempPoint$1.y; const midX = (startX + endX) / 2; const midY = (startY + endY) / 2; // Transform into a rotated frame with the origin at the midpoint. const matrix = tempMatrix$2 .identity() .translate(-midX, -midY) .rotate(-xAxisRotation); const { x: xRotated, y: yRotated } = matrix.apply({ x: startX, y: startY }); const a = Math.pow(xRotated / rx, 2) + Math.pow(yRotated / ry, 2); if (a > 1) { // Ensure radii are large enough to connect start to end point. rx = Math.sqrt(a) * rx; ry = Math.sqrt(a) * ry; } const rx2 = rx * rx; const ry2 = ry * ry; // Calculate the center of the ellipse in this rotated space. // See implementation notes for the equations: https://svgwg.org/svg2-draft/implnote.html#ArcImplementationNotes const sgn = (anticlockwise === largeArc) ? 1 : -1; const coef = sgn * Math.sqrt( // use Math.abs to prevent numerical imprecision from creating very small -ve // values (which should be zero instead). Otherwise, NaNs are possible Math.abs((rx2 * ry2) - (rx2 * yRotated * yRotated) - (ry2 * xRotated * xRotated)) / ((rx2 * yRotated * yRotated) + (ry2 * xRotated * xRotated)), ); const cxRotated = coef * (rx * yRotated / ry); const cyRotated = -coef * (ry * xRotated / rx); // Calculate the center of the ellipse back in local space. const { x: cx, y: cy } = matrix.applyInverse({ x: cxRotated, y: cyRotated }); // Calculate startAngle const x1Norm = (xRotated - cxRotated) / rx; const y1Norm = (yRotated - cyRotated) / ry; const dist1Norm = Math.sqrt((x1Norm ** 2) + (y1Norm ** 2)); const startAngle = (y1Norm >= 0 ? 1 : -1) * Math.acos(x1Norm / dist1Norm); // Calculate endAngle const x2Norm = (-xRotated - cxRotated) / rx; const y2Norm = (-yRotated - cyRotated) / ry; const dist2Norm = Math.sqrt((x2Norm ** 2) + (y2Norm ** 2)); let endAngle = (y2Norm >= 0 ? 1 : -1) * Math.acos(x2Norm / dist2Norm); // Ensure endAngle is on the correct side of startAngle if (endAngle > startAngle && anticlockwise) { endAngle -= Math.PI * 2; } else if (startAngle > endAngle && !anticlockwise) { endAngle += Math.PI * 2; } // Draw the ellipse! this.ellipticArc( cx, cy, rx, ry, startAngle, endAngle, xAxisRotation, anticlockwise, ); return this; } /** * Embeds the `SVGCircleElement` into this node. * * @param element - The circle element to draw. */ embedCircle(element) { element.cx.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.cy.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.r.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); const cx = element.cx.baseVal.valueInSpecifiedUnits; const cy = element.cy.baseVal.valueInSpecifiedUnits; const r = element.r.baseVal.valueInSpecifiedUnits; this.drawCircle(cx, cy, r); } /** * Embeds the `SVGEllipseElement` into this node. * * @param element - The ellipse element to draw. */ embedEllipse(element) { element.cx.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.cy.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.rx.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.ry.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); const cx = element.cx.baseVal.valueInSpecifiedUnits; const cy = element.cy.baseVal.valueInSpecifiedUnits; const rx = element.rx.baseVal.valueInSpecifiedUnits; const ry = element.ry.baseVal.valueInSpecifiedUnits; this.ellipticArc( cx, cy, rx, ry, 0, 2 * Math.PI, ); } /** * Embeds the `SVGLineElement` into this node. * * @param element - The line element to draw. */ embedLine(element) { element.x1.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.y1.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.x2.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.y2.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); const x1 = element.x1.baseVal.valueInSpecifiedUnits; const y1 = element.y1.baseVal.valueInSpecifiedUnits; const x2 = element.x2.baseVal.valueInSpecifiedUnits; const y2 = element.y2.baseVal.valueInSpecifiedUnits; this.moveTo(x1, y1); this.lineTo(x2, y2); } /** * Embeds the `SVGRectElement` into this node. * * @param element - The rectangle element to draw. */ embedRect(element) { element.x.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.y.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.width.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.height.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.rx.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.ry.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); const x = element.x.baseVal.valueInSpecifiedUnits; const y = element.y.baseVal.valueInSpecifiedUnits; const width = element.width.baseVal.valueInSpecifiedUnits; const height = element.height.baseVal.valueInSpecifiedUnits; const rx = element.rx.baseVal.valueInSpecifiedUnits; const ry = element.ry.baseVal.valueInSpecifiedUnits || rx; if (rx === 0 || ry === 0) { this.drawRect(x, y, width, height); } else { this.moveTo(x, y + ry); this.ellipticArcTo(x + rx, y, rx, ry, 0, false, false); this.lineTo(x + width - rx, y); this.ellipticArcTo(x + width, y + ry, rx, ry, 0, false, false); this.lineTo(x + width, y + height - ry); this.ellipticArcTo(x + width - rx, y + height, rx, ry, 0, false, false); this.lineTo(x + rx, y + height); this.ellipticArcTo(x, y + height - ry, rx, ry, 0, false, false); this.closePath(); } } /** * Embeds the `SVGPolygonElement` element into this node. * * @param element - The polygon element to draw. */ embedPolygon(element) { const points = element.getAttribute('points') .split(/[ ,]/g) .map((p) => parseInt(p, 10)); this.moveTo(points[0], points[1]); for (let i = 2; i < points.length; i += 2) { this.lineTo(points[i], points[i + 1]); } this.closePath(); } /** * Embeds the `SVGPolylineElement` element into this node. * * @param element - The polyline element to draw. */ embedPolyline(element) { const points = element.getAttribute('points') .split(/[ ,]/g) .map((p) => parseInt(p, 10)); this.moveTo(points[0], points[1]); for (let i = 2; i < points.length; i += 2) { this.lineTo(points[i], points[i + 1]); } } /** * @override */ render(renderer) { const paintServers = this.paintServers; // Ensure paint servers are updated for (let i = 0, j = paintServers.length; i < j; i++) { paintServers[i].resolvePaint(renderer); } // TODO: Fix rendering } } function _optionalChain$1(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } const tempMatrix$1 = new pixi_js.Matrix(); /** * Draws SVG &lt;image /&gt; elements. * * @public */ class SVGImageNode extends SVGGraphicsNode { /** * The canvas used into which the `SVGImageElement` is drawn. This is because WebGL does not support * using `SVGImageElement` as an `ImageSource` for textures. */ /** * The Canvas 2D context for `this._canvas`. */ /** * A texture backed by `this._canvas`. */ /** * Embeds the given SVG image element into this node. * * @param element - The SVG image element to embed. */ embedImage(element) { element.x.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.y.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.width.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); element.height.baseVal.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); // Image frame const x = element.x.baseVal.valueInSpecifiedUnits; const y = element.y.baseVal.valueInSpecifiedUnits; const width = element.width.baseVal.valueInSpecifiedUnits; const height = element.height.baseVal.valueInSpecifiedUnits; const opacity = Number.parseFloat(element.getAttribute('opacity') || '1'); // Calculate scale. If the <image /> element is scaled down, then the texture can be rendered at a lower // resolution to save graphics memory. const transform = element instanceof SVGGraphicsElement ? element.transform.baseVal.consolidate() : null; const transformMatrix = transform ? transform.matrix : tempMatrix$1.identity(); const { a, b, c, d } = transformMatrix; const sx = Math.min(1, Math.sqrt((a * a) + (b * b))); const sy = Math.min(1, Math.sqrt((c * c) + (d * d))); const twidth = Math.ceil(width * sx); const theight = Math.ceil(height * sy); // Initialize the texture & canvas this.initTexture(twidth, theight); // Load the image element /* eslint-disable-next-line no-undef */ const baseURL = _optionalChain$1([globalThis, 'optionalAccess', _ => _.location, 'access', _2 => _2.href]); const imageURL = element.getAttribute('href') || element.getAttribute('xlink:href'); const imageOrigin = new URL(imageURL, document.baseURI).origin; let imageElement = element; if (imageOrigin && imageOrigin !== baseURL) { imageElement = document.createElement('img'); imageElement.crossOrigin = 'anonymous'; imageElement.src = imageURL; } // Draw the image when it loads imageElement.onload = () => { this.drawTexture(imageElement); }; this.rect(x, y, width, height); this.fill({ texture: this._texture, alpha: opacity, matrix: new pixi_js.Matrix().scale(1 / sx, 1 / sy).translate(x, y), }); } /** * Initializes {@code this._texture} by allocating it from the atlas. It is expected the texture size requested * is less than the atlas's slab dimensions. * * @param width * @param height */ initTexture(width, height) { // If the texture already exists, nothing much to do. if (this._texture) { if (this._texture.width <= this._sceneContext.atlas.maxWidth && this._texture.height <= this._sceneContext.atlas.maxHeight) { this._sceneContext.atlas.free(this._texture); } else { // TODO: This does destroy it, right? this._texture.destroy(); } } this._texture = null; this._texture = this._sceneContext.atlas.allocate(width, height); if (this._texture) { this._canvas = (this._texture.source ).resource ; this._canvasContext = this._canvas.getContext('2d'); } else // Allocation fails if the texture is too large. If so, create a standalone texture. { this._canvas = document.createElement('canvas'); this._canvas.width = width; this._canvas.height = height; this._canvasContext = this._canvas.getContext('2d'); this._texture = pixi_js.Texture.from(this._canvas); } } /** * Draws the image into this node's texture. * * @param image - The image element holding the image. */ drawTexture(image) { const destinationFrame = this._texture.frame; this._canvasContext.clearRect( destinationFrame.x, destinationFrame.y, destinationFrame.width, destinationFrame.height, ); this._canvasContext.drawImage( image, destinationFrame.x, destinationFrame.y, destinationFrame.width, destinationFrame.height, ); this._texture.update(); this._texture.source.update(); } } const tempPoint = new pixi_js.Point(); /** * Draws SVG &lt;path /&gt; elements. * * @public */ class SVGPathNode extends SVGGraphicsNode { /** * Embeds the `SVGPathElement` into this node. * * @param element - the path to draw */ embedPath(element) { const d = element.getAttribute('d'); // Parse path commands using d-path-parser. This is an inefficient solution that causes excess memory allocation // and should be optimized in the future. const commands = dPathParser__default["default"](d.trim()); // Current point let x = 0; let y = 0; for (let i = 0, j = commands.length; i < j; i++) { const lastCommand = commands[i - 1]; const command = commands[i]; if (isNaN(x) || isNaN(y)) { throw new Error('Data corruption'); } // Taken from: https://github.com/bigtimebuddy/pixi-svg/blob/main/src/SVG.js // Copyright Matt Karl switch (command.code) { case 'm': { this.moveTo( x += command.end.x, y += command.end.y, ); break; } case 'M': { this.moveTo( x = command.end.x, y = command.end.y, ); break; } case 'H': { this.lineTo(x = command.value, y); break; } case 'h': { this.lineTo(x += command.value, y); break; } case 'V': { this.lineTo(x, y = command.value); break; } case 'v': { this.lineTo(x, y += command.value); break; } case 'z': case 'Z': { // eslint-disable-next-line dot-notation const activePath = this.context['_activePath'] ; activePath.getLastPoint(tempPoint); x = tempPoint.x; y = tempPoint.y; this.closePath(); break; } case 'L': { this.lineTo( x = command.end.x, y = command.end.y, ); break; } case 'l': { this.lineTo( x += command.end.x, y += command.end.y, ); break; } case 'C': { this.bezierCurveTo( command.cp1.x, command.cp1.y, command.cp2.x, command.cp2.y, x = command.end.x, y = command.end.y, ); break; } case 'c': { const currX = x; const currY = y; this.bezierCurveTo( currX + command.cp1.x, currY + command.cp1.y, currX + command.cp2.x, currY + command.cp2.y, x += command.end.x, y += command.end.y, ); break; } case 's': case 'S': { const cp1 = { x, y }; const lastCode = commands[i - 1] ? commands[i - 1].code : null; if (i > 0 && (lastCode === 's' || lastCode === 'S' || lastCode === 'c' || lastCode === 'C')) { const lastCommand = commands[i - 1]; const lastCp2 = { ...(lastCommand.cp2 || lastCommand.cp) }; if (commands[i - 1].relative) { lastCp2.x += (x - lastCommand.end.x); lastCp2.y += (y - lastCommand.end.y); } cp1.x = (2 * x) - lastCp2.x; cp1.y = (2 * y) - lastCp2.y; } const cp2 = { x: command.cp.x, y: command.cp.y }; if (command.relative) { cp2.x += x; cp2.y += y; x += command.end.x; y += command.end.y; } else { x = command.end.x; y = command.end.y; } this.bezierCurveTo( cp1.x, cp1.y, cp2.x, cp2.y, x, y, ); break; } case 'q': { const currX = x; const currY = y; this.quadraticCurveTo( currX + command.cp.x, currY + command.cp.y, x += command.end.x, y += command.end.y, ); break; } case 'Q': { this.quadraticCurveTo( command.cp.x, command.cp.y, x = command.end.x, y = command.end.y, ); break; } case 'A': this.ellipticArcTo( x = command.end.x, y = command.end.y, command.radii.x, command.radii.y, (command.rotation || 0) * Math.PI / 180, !command.clockwise, command.large, ); break; case 'a': this.ellipticArcTo( x += command.end.x, y += command.end.y, command.radii.x, command.radii.y, (command.rotation || 0) * Math.PI / 180, !command.clockwise, command.large, ); break; case 't': case 'T': { let cx; let cy; if (lastCommand && lastCommand.cp) { let lcx = lastCommand.cp.x; let lcy = lastCommand.cp.y; if (lastCommand.relative) { const lx = x - lastCommand.end.x; const ly = y - lastCommand.end.y; lcx += lx; lcy += ly; } cx = (2 * x) - lcx; cy = (2 * y) - lcy; } else { cx = x; cy = y; } if (command.code === 't') { this.quadraticCurveTo( cx, cy, x += command.end.x, y += command.end.y, ); } else { this.quadraticCurveTo( cx, cy, x = command.end.x, y = command.end.y, ); } break; } default: { console.warn('[PIXI.SVG] Draw command not supported:', command.code, command); break; } } } return this; } } const NODE_TRANSFORM_DIRTY = 'nodetransformdirty'; const TRANSFORM_DIRTY = 'transformdirty'; /** * `SVGTextEngineImpl` is the default implementation for {@link SVGTextEngine}. It is inspired by {@link PIXI.Text} that * is provided by @pixi/text. It uses a &lt;canvas /&gt; to draw and cache the text. This may cause blurring issues when * the SVG is viewed at highly zoomed-in scales because it is rasterized. * * @public */ class SVGTextEngineImpl extends pixi_js.Sprite { constructor() { super(pixi_js.Texture.EMPTY);SVGTextEngineImpl.prototype.__init.call(this); this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext('2d'); this.texture = pixi_js.Texture.from(this.canvas); this.contentList = new Map(); this.dirtyId = 0; this.updateId = 0; } async clear() { this.contentList.clear(); this.dirtyId++; this.position.set(0, 0); } async put( id, position, content, style, matrix, ) { this.contentList.set(id, { position, content, style, matrix, }); const textMetrics = pixi_js.CanvasTextMetrics.measureText(content, new pixi_js.TextStyle(style), this.canvas, false); this.dirtyId++; return { x: position.x + textMetrics.width, y: position.y, }; } updateText() { let w = 0; let h = 0; this.contentList.forEach(({ position, content, style }) => { const textMetrics = pixi_js.CanvasTextMetrics.measureText(content, new pixi_js.TextStyle(style), this.canvas, false); w = Math.max(w, position.x + textMetrics.width); h = Math.max(h, position.y + textMetrics.height + textMetrics.fontProperties.descent); }); const resolution = window.devicePixelRatio || 1; this.texture.source.resize(w, h, resolution); this.texture.update(); this.context.clearRect(0, 0, w * resolution, h * resolution); this.context.setTransform(1, 0, 0, 1, 0, 0); this.context.scale(resolution, resolution); let i = 0; for (const [, { position, content, style }] of this.contentList) { const textMetrics = pixi_js.CanvasTextMetrics.measureText(content, new pixi_js.TextStyle(style), this.canvas, false); const textStyle = new pixi_js.TextStyle(style); this.context.fillStyle = typeof textStyle.fill === 'string' ? textStyle.fill : 'black'; this.context.font = pixi_js.fontStringFromTextStyle(textStyle); this.context.fillText(content, position.x, position.y + textMetrics.height); if (i === 0) { this.y -= textMetrics.height; } i++; } this.updateId = this.dirtyId; // Ensure the SVG scene updates its bounds after the text is rendered. this.emit(NODE_TRANSFORM_DIRTY); } __init() {this.onRender = () => { if (this.updateId !== this.dirtyId) { this.updateText(); } };} } /** * Parses font measurements, e.g. '14px', '.5em' * @ignore */ function parseMeasurement(mes, fontSize = 16) { if (!mes) { return 0; } // TODO: Handle non-px/em units // Handle em if (mes.includes('em')) { return parseFloat(mes) * fontSize; } return parseFloat(mes); } /** * Draws SVG &lt;text /&gt; elements. * * @public */ class SVGTextNode extends pixi_js.Container { /** * The SVG text rendering engine to be used by default in `SVGTextNode`. This API is not stable and * can change anytime. * * @alpha */ static __initStatic() {this.defaultEngine = SVGTextEngineImpl;} /** * An instance of a SVG text engine used to layout and render text. */ /**