- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436
- 437
- 438
- 439
- 440
- 441
- 442
- 443
import { DashedLineStyle } from './style/DashedLineStyle';
import { EllipticArcUtils } from './utils/EllipticArcUtils';
import { Matrix } from '@pixi/math';
import { LINE_CAP, LINE_JOIN } from '@pixi/graphics';
import { GRAPHICS_CURVES, Graphics } from '@pixi/graphics';
import { SVGGraphicsGeometry } from './SVGGraphicsGeometry';
import { Texture } from '@pixi/core';
import type { PaintServer } from './paint/PaintServer';
import type { Renderer } from '@pixi/core';
import type { SVGSceneContext } from './SVGSceneContext';
/**
* @public
* @ignore
*/
export interface ILineStyleOptions {
color?: number;
alpha?: number;
texture?: Texture;
matrix?: Matrix;
width?: number;
alignment?: number;
native?: boolean;
cap?: LINE_CAP;
join?: LINE_JOIN;
miterLimit?: number;
// additions
dashArray?: number[];
dashOffset?: number;
}
const tempMatrix = new Matrix();
const _segmentsCount: (length: number, defaultSegments?: number) => number
= (GRAPHICS_CURVES as any)._segmentsCount.bind(GRAPHICS_CURVES);
/**
* 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
*/
export class SVGGraphicsNode extends Graphics
{
paintServers: PaintServer[];
protected context: SVGSceneContext;
constructor(context: SVGSceneContext)
{
super();
this.context = context;
(this as any)._geometry = new SVGGraphicsGeometry();
(this as any)._geometry.refCount++;
this._lineStyle = new DashedLineStyle();
this.paintServers = [];
}
public lineTextureStyle(options: ILineStyleOptions): this
{
// Apply defaults
options = Object.assign({
width: 0,
texture: Texture.WHITE,
color: (options && options.texture) ? 0xFFFFFF : 0x0,
alpha: 1,
matrix: null,
alignment: 0.5,
native: false,
cap: LINE_CAP.BUTT,
join: LINE_JOIN.MITER,
miterLimit: 10,
dashArray: null,
dashOffset: 0,
}, options);
if (this.currentPath)
{
this.startPoly();
}
const visible = options.width > 0 && options.alpha > 0;
if (!visible)
{
this._lineStyle.reset();
}
else
{
if (options.matrix)
{
options.matrix = options.matrix.clone();
options.matrix.invert();
}
Object.assign(this._lineStyle, { visible }, options);
}
return this;
}
/**
* 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: number,
cy: number,
rx: number,
ry: number,
startAngle: number,
endAngle: number,
xAxisRotation = 0,
anticlockwise = false): this
{
const sweepAngle = endAngle - startAngle;
const n = GRAPHICS_CURVES.adaptive
? _segmentsCount(EllipticArcUtils.calculateArcLength(rx, ry, startAngle, endAngle - startAngle)) * 4
: 20;
const delta = (anticlockwise ? -1 : 1) * Math.abs(sweepAngle) / (n - 1);
tempMatrix.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.apply({ x: xr, y: yr }) : { x: xr, y: yr };
if (i === 0)
{
this._initCurve(x, y);
continue;
}
this.currentPath.points.push(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: number,
endY: number,
rx: number,
ry: number,
xAxisRotation = 0,
anticlockwise = false,
largeArc = false,
): this
{
if (rx === 0 || ry === 0)
{
return this.lineTo(endX, endY) as this;
}
// See https://www.w3.org/TR/SVG2/implnote.html#ArcImplementationNotes
const points = this.currentPath.points;
const startX = points[points.length - 2];
const startY = points[points.length - 1];
const midX = (startX + endX) / 2;
const midY = (startY + endY) / 2;
// Transform into a rotated frame with the origin at the midpoint.
const matrix = tempMatrix
.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: SVGCircleElement): void
{
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: SVGEllipseElement): void
{
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: SVGLineElement): void
{
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: SVGRectElement): void
{
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: SVGPolygonElement): void
{
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: SVGPolylineElement): void
{
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: Renderer): void
{
const paintServers = this.paintServers;
// Ensure paint servers are updated
for (let i = 0, j = paintServers.length; i < j; i++)
{
paintServers[i].resolvePaint(renderer);
}
super.render(renderer);
}
}