@pixi-essentials/svg
Version:
Native SVG Renderer on top of PixiJS
1,565 lines (1,321 loc) • 83.9 kB
JavaScript
/* 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
*/
import { Sprite, Matrix, Point, Graphics, Texture, CanvasTextMetrics, TextStyle, fontStringFromTextStyle, Container, Bounds, Rectangle, RenderTexture } from 'pixi.js';
import color from 'tinycolor2';
import { GradientFactory } from '@pixi-essentials/gradients';
import dPathParser from 'd-path-parser';
import { CanvasTextureAllocator } from '@pixi-essentials/texture-allocator';
/**
* @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 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 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 <use /> 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(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 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 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 Matrix();
const tempPoint$1 = new Point();
/**
* This node can be used to directly embed the following elements:
*
* | Interface | Element |
* | ------------------- | ------------------ |
* | SVGGElement | <g /> |
* | SVGCircleElement | <circle /> |
* | SVGLineElement | <line /> |
* | SVGPolylineElement | <polyline /> |
* | SVGPolygonElement | <polygon /> |
* | SVGRectElement | <rect /> |
*
* It also provides an implementation for dashed stroking, by adding the `dashArray` and `dashOffset` properties
* to `LineStyle`.
*
* @public
*/
class SVGGraphicsNode extends 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 Matrix();
/**
* Draws SVG <image /> 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 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 = 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 Point();
/**
* Draws SVG <path /> 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(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 <canvas /> 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 Sprite
{
constructor()
{
super(Texture.EMPTY);SVGTextEngineImpl.prototype.__init.call(this);
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d');
this.texture = 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 = CanvasTextMetrics.measureText(content, new 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 = CanvasTextMetrics.measureText(content, new 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 = CanvasTextMetrics.measureText(content, new TextStyle(style), this.canvas, false);
const textStyle = new TextStyle(style);
this.context.fillStyle = typeof textStyle.fill === 'string' ? textStyle.fill : 'black';
this.context.font = 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 <text /> elements.
*
* @public
*/
class SVGTextNode extends 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.
*/
/**
* The current text position, where the next glyph will be placed.
*/
constructor()
{
super();
this.currentTextPosition = { x: 0, y: 0 };
this.engine = new (SVGTextNode.defaultEngine)();
this.addChild(this.engine);
// Listen to nodetransformdirty on the engine so bounds are updated
// when the text is rendered.
this.engine.on(NODE_TRANSFORM_D