UNPKG

192 kBJavaScriptView Raw
1import requestAnimationFrame from 'raf';
2import RGBColor from 'rgbcolor';
3import { SVGPathData } from 'svg-pathdata';
4import { canvasRGBA } from 'stackblur-canvas';
5
6/**
7 * HTML-safe compress white-spaces.
8 * @param str - String to compress.
9 */
10function compressSpaces(str) {
11 return str.replace(/(?!\u3000)\s+/gm, ' ');
12}
13/**
14 * HTML-safe left trim.
15 * @param str - String to trim.
16 */
17function trimLeft(str) {
18 return str.replace(/^[\n \t]+/, '');
19}
20/**
21 * HTML-safe right trim.
22 * @param str - String to trim.
23 */
24function trimRight(str) {
25 return str.replace(/[\n \t]+$/, '');
26}
27/**
28 * String to numbers array.
29 * @param str - Numbers string.
30 */
31function toNumbers(str) {
32 const matches = (str || '').match(/-?(\d+(?:\.\d*(?:[eE][+-]?\d+)?)?|\.\d+)(?=\D|$)/gm) || [];
33 return matches.map(parseFloat);
34}
35// Microsoft Edge fix
36const allUppercase = /^[A-Z-]+$/;
37/**
38 * Normalize attribute name.
39 * @param name - Attribute name.
40 */
41function normalizeAttributeName(name) {
42 if (allUppercase.test(name)) {
43 return name.toLowerCase();
44 }
45 return name;
46}
47/**
48 * Parse external URL.
49 * @param url - CSS url string.
50 */
51function parseExternalUrl(url) {
52 // single quotes [2]
53 // v double quotes [3]
54 // v v no quotes [4]
55 // v v v
56 const urlMatch = url.match(/url\(('([^']+)'|"([^"]+)"|([^'"\)]+))\)/) || [];
57 return urlMatch[2] || urlMatch[3] || urlMatch[4];
58}
59/**
60 * Transform floats to integers in rgb colors.
61 * @param color - Color to normalize.
62 */
63function normalizeColor(color) {
64 if (!color.startsWith('rgb')) {
65 return color;
66 }
67 let rgbParts = 3;
68 const normalizedColor = color.replace(/\d+(\.\d+)?/g, (num, isFloat) => rgbParts-- && isFloat
69 ? String(Math.round(parseFloat(num)))
70 : num);
71 return normalizedColor;
72}
73
74// slightly modified version of https://github.com/keeganstreet/specificity/blob/master/specificity.js
75const attributeRegex = /(\[[^\]]+\])/g;
76const idRegex = /(#[^\s\+>~\.\[:]+)/g;
77const classRegex = /(\.[^\s\+>~\.\[:]+)/g;
78const pseudoElementRegex = /(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi;
79const pseudoClassWithBracketsRegex = /(:[\w-]+\([^\)]*\))/gi;
80const pseudoClassRegex = /(:[^\s\+>~\.\[:]+)/g;
81const elementRegex = /([^\s\+>~\.\[:]+)/g;
82function findSelectorMatch(selector, regex) {
83 const matches = selector.match(regex);
84 if (!matches) {
85 return [
86 selector,
87 0
88 ];
89 }
90 return [
91 selector.replace(regex, ' '),
92 matches.length
93 ];
94}
95/**
96 * Measure selector specificity.
97 * @param selector - Selector to measure.
98 */
99function getSelectorSpecificity(selector) {
100 const specificity = [0, 0, 0];
101 let currentSelector = selector
102 .replace(/:not\(([^\)]*)\)/g, ' $1 ')
103 .replace(/{[\s\S]*/gm, ' ');
104 let delta = 0;
105 [currentSelector, delta] = findSelectorMatch(currentSelector, attributeRegex);
106 specificity[1] += delta;
107 [currentSelector, delta] = findSelectorMatch(currentSelector, idRegex);
108 specificity[0] += delta;
109 [currentSelector, delta] = findSelectorMatch(currentSelector, classRegex);
110 specificity[1] += delta;
111 [currentSelector, delta] = findSelectorMatch(currentSelector, pseudoElementRegex);
112 specificity[2] += delta;
113 [currentSelector, delta] = findSelectorMatch(currentSelector, pseudoClassWithBracketsRegex);
114 specificity[1] += delta;
115 [currentSelector, delta] = findSelectorMatch(currentSelector, pseudoClassRegex);
116 specificity[1] += delta;
117 currentSelector = currentSelector
118 .replace(/[\*\s\+>~]/g, ' ')
119 .replace(/[#\.]/g, ' ');
120 [currentSelector, delta] = findSelectorMatch(currentSelector, elementRegex); // lgtm [js/useless-assignment-to-local]
121 specificity[2] += delta;
122 return specificity.join('');
123}
124
125const PSEUDO_ZERO = .00000001;
126/**
127 * Vector magnitude.
128 */
129function vectorMagnitude(v) {
130 return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2));
131}
132/**
133 * Ratio between two vectors.
134 */
135function vectorsRatio(u, v) {
136 return (u[0] * v[0] + u[1] * v[1]) / (vectorMagnitude(u) * vectorMagnitude(v));
137}
138/**
139 * Angle between two vectors.
140 */
141function vectorsAngle(u, v) {
142 return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vectorsRatio(u, v));
143}
144function CB1(t) {
145 return t * t * t;
146}
147function CB2(t) {
148 return 3 * t * t * (1 - t);
149}
150function CB3(t) {
151 return 3 * t * (1 - t) * (1 - t);
152}
153function CB4(t) {
154 return (1 - t) * (1 - t) * (1 - t);
155}
156function QB1(t) {
157 return t * t;
158}
159function QB2(t) {
160 return 2 * t * (1 - t);
161}
162function QB3(t) {
163 return (1 - t) * (1 - t);
164}
165
166class Property {
167 constructor(document, name, value) {
168 this.document = document;
169 this.name = name;
170 this.value = value;
171 this.isNormalizedColor = false;
172 }
173 static empty(document) {
174 return new Property(document, 'EMPTY', '');
175 }
176 split(separator = ' ') {
177 const { document, name } = this;
178 return compressSpaces(this.getString())
179 .trim()
180 .split(separator)
181 .map(value => new Property(document, name, value));
182 }
183 hasValue(zeroIsValue) {
184 const { value } = this;
185 return value !== null
186 && value !== ''
187 && (zeroIsValue || value !== 0)
188 && typeof value !== 'undefined';
189 }
190 isString(regexp) {
191 const { value } = this;
192 const result = typeof value === 'string';
193 if (!result || !regexp) {
194 return result;
195 }
196 return regexp.test(value);
197 }
198 isUrlDefinition() {
199 return this.isString(/^url\(/);
200 }
201 isPixels() {
202 if (!this.hasValue()) {
203 return false;
204 }
205 const asString = this.getString();
206 switch (true) {
207 case /px$/.test(asString):
208 case /^[0-9]+$/.test(asString):
209 return true;
210 default:
211 return false;
212 }
213 }
214 setValue(value) {
215 this.value = value;
216 return this;
217 }
218 getValue(def) {
219 if (typeof def === 'undefined' || this.hasValue()) {
220 return this.value;
221 }
222 return def;
223 }
224 getNumber(def) {
225 if (!this.hasValue()) {
226 if (typeof def === 'undefined') {
227 return 0;
228 }
229 return parseFloat(def);
230 }
231 const { value } = this;
232 let n = parseFloat(value);
233 if (this.isString(/%$/)) {
234 n = n / 100.0;
235 }
236 return n;
237 }
238 getString(def) {
239 if (typeof def === 'undefined' || this.hasValue()) {
240 return typeof this.value === 'undefined'
241 ? ''
242 : String(this.value);
243 }
244 return String(def);
245 }
246 getColor(def) {
247 let color = this.getString(def);
248 if (this.isNormalizedColor) {
249 return color;
250 }
251 this.isNormalizedColor = true;
252 color = normalizeColor(color);
253 this.value = color;
254 return color;
255 }
256 getDpi() {
257 return 96.0; // TODO: compute?
258 }
259 getRem() {
260 return this.document.rootEmSize;
261 }
262 getEm() {
263 return this.document.emSize;
264 }
265 getUnits() {
266 return this.getString().replace(/[0-9\.\-]/g, '');
267 }
268 getPixels(axisOrIsFontSize, processPercent = false) {
269 if (!this.hasValue()) {
270 return 0;
271 }
272 const [axis, isFontSize] = typeof axisOrIsFontSize === 'boolean'
273 ? [undefined, axisOrIsFontSize]
274 : [axisOrIsFontSize];
275 const { viewPort } = this.document.screen;
276 switch (true) {
277 case this.isString(/vmin$/):
278 return this.getNumber()
279 / 100.0
280 * Math.min(viewPort.computeSize('x'), viewPort.computeSize('y'));
281 case this.isString(/vmax$/):
282 return this.getNumber()
283 / 100.0
284 * Math.max(viewPort.computeSize('x'), viewPort.computeSize('y'));
285 case this.isString(/vw$/):
286 return this.getNumber()
287 / 100.0
288 * viewPort.computeSize('x');
289 case this.isString(/vh$/):
290 return this.getNumber()
291 / 100.0
292 * viewPort.computeSize('y');
293 case this.isString(/rem$/):
294 return this.getNumber() * this.getRem( /* viewPort */);
295 case this.isString(/em$/):
296 return this.getNumber() * this.getEm( /* viewPort */);
297 case this.isString(/ex$/):
298 return this.getNumber() * this.getEm( /* viewPort */) / 2.0;
299 case this.isString(/px$/):
300 return this.getNumber();
301 case this.isString(/pt$/):
302 return this.getNumber() * this.getDpi( /* viewPort */) * (1.0 / 72.0);
303 case this.isString(/pc$/):
304 return this.getNumber() * 15;
305 case this.isString(/cm$/):
306 return this.getNumber() * this.getDpi( /* viewPort */) / 2.54;
307 case this.isString(/mm$/):
308 return this.getNumber() * this.getDpi( /* viewPort */) / 25.4;
309 case this.isString(/in$/):
310 return this.getNumber() * this.getDpi( /* viewPort */);
311 case this.isString(/%$/) && isFontSize:
312 return this.getNumber() * this.getEm( /* viewPort */);
313 case this.isString(/%$/):
314 return this.getNumber() * viewPort.computeSize(axis);
315 default: {
316 const n = this.getNumber();
317 if (processPercent && n < 1.0) {
318 return n * viewPort.computeSize(axis);
319 }
320 return n;
321 }
322 }
323 }
324 getMilliseconds() {
325 if (!this.hasValue()) {
326 return 0;
327 }
328 if (this.isString(/ms$/)) {
329 return this.getNumber();
330 }
331 return this.getNumber() * 1000;
332 }
333 getRadians() {
334 if (!this.hasValue()) {
335 return 0;
336 }
337 switch (true) {
338 case this.isString(/deg$/):
339 return this.getNumber() * (Math.PI / 180.0);
340 case this.isString(/grad$/):
341 return this.getNumber() * (Math.PI / 200.0);
342 case this.isString(/rad$/):
343 return this.getNumber();
344 default:
345 return this.getNumber() * (Math.PI / 180.0);
346 }
347 }
348 getDefinition() {
349 const asString = this.getString();
350 let name = asString.match(/#([^\)'"]+)/);
351 if (name) {
352 name = name[1];
353 }
354 if (!name) {
355 name = asString;
356 }
357 return this.document.definitions[name];
358 }
359 getFillStyleDefinition(element, opacity) {
360 let def = this.getDefinition();
361 if (!def) {
362 return null;
363 }
364 // gradient
365 if (typeof def.createGradient === 'function') {
366 return def.createGradient(this.document.ctx, element, opacity);
367 }
368 // pattern
369 if (typeof def.createPattern === 'function') {
370 if (def.getHrefAttribute().hasValue()) {
371 const patternTransform = def.getAttribute('patternTransform');
372 def = def.getHrefAttribute().getDefinition();
373 if (patternTransform.hasValue()) {
374 def.getAttribute('patternTransform', true).setValue(patternTransform.value);
375 }
376 }
377 return def.createPattern(this.document.ctx, element, opacity);
378 }
379 return null;
380 }
381 getTextBaseline() {
382 if (!this.hasValue()) {
383 return null;
384 }
385 return Property.textBaselineMapping[this.getString()];
386 }
387 addOpacity(opacity) {
388 let value = this.getColor();
389 const len = value.length;
390 let commas = 0;
391 // Simulate old RGBColor version, which can't parse rgba.
392 for (let i = 0; i < len; i++) {
393 if (value[i] === ',') {
394 commas++;
395 }
396 if (commas === 3) {
397 break;
398 }
399 }
400 if (opacity.hasValue() && this.isString() && commas !== 3) {
401 const color = new RGBColor(value);
402 if (color.ok) {
403 color.alpha = opacity.getNumber();
404 value = color.toRGBA();
405 }
406 }
407 return new Property(this.document, this.name, value);
408 }
409}
410Property.textBaselineMapping = {
411 'baseline': 'alphabetic',
412 'before-edge': 'top',
413 'text-before-edge': 'top',
414 'middle': 'middle',
415 'central': 'middle',
416 'after-edge': 'bottom',
417 'text-after-edge': 'bottom',
418 'ideographic': 'ideographic',
419 'alphabetic': 'alphabetic',
420 'hanging': 'hanging',
421 'mathematical': 'alphabetic'
422};
423
424class ViewPort {
425 constructor() {
426 this.viewPorts = [];
427 }
428 clear() {
429 this.viewPorts = [];
430 }
431 setCurrent(width, height) {
432 this.viewPorts.push({
433 width,
434 height
435 });
436 }
437 removeCurrent() {
438 this.viewPorts.pop();
439 }
440 getCurrent() {
441 const { viewPorts } = this;
442 return viewPorts[viewPorts.length - 1];
443 }
444 get width() {
445 return this.getCurrent().width;
446 }
447 get height() {
448 return this.getCurrent().height;
449 }
450 computeSize(d) {
451 if (typeof d === 'number') {
452 return d;
453 }
454 if (d === 'x') {
455 return this.width;
456 }
457 if (d === 'y') {
458 return this.height;
459 }
460 return Math.sqrt(Math.pow(this.width, 2) + Math.pow(this.height, 2)) / Math.sqrt(2);
461 }
462}
463
464class Point {
465 constructor(x, y) {
466 this.x = x;
467 this.y = y;
468 }
469 static parse(point, defaultValue = 0) {
470 const [x = defaultValue, y = defaultValue] = toNumbers(point);
471 return new Point(x, y);
472 }
473 static parseScale(scale, defaultValue = 1) {
474 const [x = defaultValue, y = x] = toNumbers(scale);
475 return new Point(x, y);
476 }
477 static parsePath(path) {
478 const points = toNumbers(path);
479 const len = points.length;
480 const pathPoints = [];
481 for (let i = 0; i < len; i += 2) {
482 pathPoints.push(new Point(points[i], points[i + 1]));
483 }
484 return pathPoints;
485 }
486 angleTo(point) {
487 return Math.atan2(point.y - this.y, point.x - this.x);
488 }
489 applyTransform(transform) {
490 const { x, y } = this;
491 const xp = x * transform[0] + y * transform[2] + transform[4];
492 const yp = x * transform[1] + y * transform[3] + transform[5];
493 this.x = xp;
494 this.y = yp;
495 }
496}
497
498class Mouse {
499 constructor(screen) {
500 this.screen = screen;
501 this.working = false;
502 this.events = [];
503 this.eventElements = [];
504 this.onClick = this.onClick.bind(this);
505 this.onMouseMove = this.onMouseMove.bind(this);
506 }
507 isWorking() {
508 return this.working;
509 }
510 start() {
511 if (this.working) {
512 return;
513 }
514 const { screen, onClick, onMouseMove } = this;
515 const canvas = screen.ctx.canvas;
516 canvas.onclick = onClick;
517 canvas.onmousemove = onMouseMove;
518 this.working = true;
519 }
520 stop() {
521 if (!this.working) {
522 return;
523 }
524 const canvas = this.screen.ctx.canvas;
525 this.working = false;
526 canvas.onclick = null;
527 canvas.onmousemove = null;
528 }
529 hasEvents() {
530 return this.working && this.events.length > 0;
531 }
532 runEvents() {
533 if (!this.working) {
534 return;
535 }
536 const { screen: document, events, eventElements } = this;
537 const { style } = document.ctx.canvas;
538 if (style) {
539 style.cursor = '';
540 }
541 events.forEach(({ run }, i) => {
542 let element = eventElements[i];
543 while (element) {
544 run(element);
545 element = element.parent;
546 }
547 });
548 // done running, clear
549 this.events = [];
550 this.eventElements = [];
551 }
552 checkPath(element, ctx) {
553 if (!this.working || !ctx) {
554 return;
555 }
556 const { events, eventElements } = this;
557 events.forEach(({ x, y }, i) => {
558 if (!eventElements[i] && ctx.isPointInPath && ctx.isPointInPath(x, y)) {
559 eventElements[i] = element;
560 }
561 });
562 }
563 checkBoundingBox(element, boundingBox) {
564 if (!this.working || !boundingBox) {
565 return;
566 }
567 const { events, eventElements } = this;
568 events.forEach(({ x, y }, i) => {
569 if (!eventElements[i] && boundingBox.isPointInBox(x, y)) {
570 eventElements[i] = element;
571 }
572 });
573 }
574 mapXY(x, y) {
575 const { window, ctx } = this.screen;
576 const point = new Point(x, y);
577 let element = ctx.canvas;
578 while (element) {
579 point.x -= element.offsetLeft;
580 point.y -= element.offsetTop;
581 element = element.offsetParent;
582 }
583 if (window.scrollX) {
584 point.x += window.scrollX;
585 }
586 if (window.scrollY) {
587 point.y += window.scrollY;
588 }
589 return point;
590 }
591 onClick(evt) {
592 const { x, y } = this.mapXY((evt || event).clientX, (evt || event).clientY);
593 this.events.push({
594 type: 'onclick',
595 x,
596 y,
597 run(event) {
598 if (event.onClick) {
599 event.onClick();
600 }
601 }
602 });
603 }
604 onMouseMove(evt) {
605 const { x, y } = this.mapXY((evt || event).clientX, (evt || event).clientY);
606 this.events.push({
607 type: 'onmousemove',
608 x,
609 y,
610 run(event) {
611 if (event.onMouseMove) {
612 event.onMouseMove();
613 }
614 }
615 });
616 }
617}
618
619const defaultWindow = typeof window !== 'undefined'
620 ? window
621 : null;
622const defaultFetch = typeof fetch !== 'undefined'
623 ? fetch.bind(void 0) // `fetch` depends on context: `someObject.fetch(...)` will throw error.
624 : null;
625class Screen {
626 constructor(ctx, { fetch = defaultFetch, window = defaultWindow } = {}) {
627 this.ctx = ctx;
628 this.FRAMERATE = 30;
629 this.MAX_VIRTUAL_PIXELS = 30000;
630 this.CLIENT_WIDTH = 800;
631 this.CLIENT_HEIGHT = 600;
632 this.viewPort = new ViewPort();
633 this.mouse = new Mouse(this);
634 this.animations = [];
635 this.waits = [];
636 this.frameDuration = 0;
637 this.isReadyLock = false;
638 this.isFirstRender = true;
639 this.intervalId = null;
640 this.window = window;
641 this.fetch = fetch;
642 }
643 wait(checker) {
644 this.waits.push(checker);
645 }
646 ready() {
647 if (!this.readyPromise) {
648 return Promise.resolve();
649 }
650 return this.readyPromise;
651 }
652 isReady() {
653 if (this.isReadyLock) {
654 return true;
655 }
656 const isReadyLock = this.waits.every(_ => _());
657 if (isReadyLock) {
658 this.waits = [];
659 if (this.resolveReady) {
660 this.resolveReady();
661 }
662 }
663 this.isReadyLock = isReadyLock;
664 return isReadyLock;
665 }
666 setDefaults(ctx) {
667 // initial values and defaults
668 ctx.strokeStyle = 'rgba(0,0,0,0)';
669 ctx.lineCap = 'butt';
670 ctx.lineJoin = 'miter';
671 ctx.miterLimit = 4;
672 }
673 setViewBox({ document, ctx, aspectRatio, width, desiredWidth, height, desiredHeight, minX = 0, minY = 0, refX, refY, clip = false, clipX = 0, clipY = 0 }) {
674 // aspect ratio - http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute
675 const cleanAspectRatio = compressSpaces(aspectRatio).replace(/^defer\s/, ''); // ignore defer
676 const [aspectRatioAlign, aspectRatioMeetOrSlice] = cleanAspectRatio.split(' ');
677 const align = aspectRatioAlign || 'xMidYMid';
678 const meetOrSlice = aspectRatioMeetOrSlice || 'meet';
679 // calculate scale
680 const scaleX = width / desiredWidth;
681 const scaleY = height / desiredHeight;
682 const scaleMin = Math.min(scaleX, scaleY);
683 const scaleMax = Math.max(scaleX, scaleY);
684 let finalDesiredWidth = desiredWidth;
685 let finalDesiredHeight = desiredHeight;
686 if (meetOrSlice === 'meet') {
687 finalDesiredWidth *= scaleMin;
688 finalDesiredHeight *= scaleMin;
689 }
690 if (meetOrSlice === 'slice') {
691 finalDesiredWidth *= scaleMax;
692 finalDesiredHeight *= scaleMax;
693 }
694 const refXProp = new Property(document, 'refX', refX);
695 const refYProp = new Property(document, 'refY', refY);
696 const hasRefs = refXProp.hasValue() && refYProp.hasValue();
697 if (hasRefs) {
698 ctx.translate(-scaleMin * refXProp.getPixels('x'), -scaleMin * refYProp.getPixels('y'));
699 }
700 if (clip) {
701 const scaledClipX = scaleMin * clipX;
702 const scaledClipY = scaleMin * clipY;
703 ctx.beginPath();
704 ctx.moveTo(scaledClipX, scaledClipY);
705 ctx.lineTo(width, scaledClipY);
706 ctx.lineTo(width, height);
707 ctx.lineTo(scaledClipX, height);
708 ctx.closePath();
709 ctx.clip();
710 }
711 if (!hasRefs) {
712 const isMeetMinY = meetOrSlice === 'meet' && scaleMin === scaleY;
713 const isSliceMaxY = meetOrSlice === 'slice' && scaleMax === scaleY;
714 const isMeetMinX = meetOrSlice === 'meet' && scaleMin === scaleX;
715 const isSliceMaxX = meetOrSlice === 'slice' && scaleMax === scaleX;
716 if (/^xMid/.test(align) && (isMeetMinY || isSliceMaxY)) {
717 ctx.translate(width / 2.0 - finalDesiredWidth / 2.0, 0);
718 }
719 if (/YMid$/.test(align) && (isMeetMinX || isSliceMaxX)) {
720 ctx.translate(0, height / 2.0 - finalDesiredHeight / 2.0);
721 }
722 if (/^xMax/.test(align) && (isMeetMinY || isSliceMaxY)) {
723 ctx.translate(width - finalDesiredWidth, 0);
724 }
725 if (/YMax$/.test(align) && (isMeetMinX || isSliceMaxX)) {
726 ctx.translate(0, height - finalDesiredHeight);
727 }
728 }
729 // scale
730 switch (true) {
731 case align === 'none':
732 ctx.scale(scaleX, scaleY);
733 break;
734 case meetOrSlice === 'meet':
735 ctx.scale(scaleMin, scaleMin);
736 break;
737 case meetOrSlice === 'slice':
738 ctx.scale(scaleMax, scaleMax);
739 break;
740 }
741 // translate
742 ctx.translate(-minX, -minY);
743 }
744 start(element, { enableRedraw = false, ignoreMouse = false, ignoreAnimation = false, ignoreDimensions = false, ignoreClear = false, forceRedraw, scaleWidth, scaleHeight, offsetX, offsetY } = {}) {
745 const { FRAMERATE, mouse } = this;
746 const frameDuration = 1000 / FRAMERATE;
747 this.frameDuration = frameDuration;
748 this.readyPromise = new Promise((resolve) => {
749 this.resolveReady = resolve;
750 });
751 if (this.isReady()) {
752 this.render(element, ignoreDimensions, ignoreClear, scaleWidth, scaleHeight, offsetX, offsetY);
753 }
754 if (!enableRedraw) {
755 return;
756 }
757 let now = Date.now();
758 let then = now;
759 let delta = 0;
760 const tick = () => {
761 now = Date.now();
762 delta = now - then;
763 if (delta >= frameDuration) {
764 then = now - (delta % frameDuration);
765 if (this.shouldUpdate(ignoreAnimation, forceRedraw)) {
766 this.render(element, ignoreDimensions, ignoreClear, scaleWidth, scaleHeight, offsetX, offsetY);
767 mouse.runEvents();
768 }
769 }
770 this.intervalId = requestAnimationFrame(tick);
771 };
772 if (!ignoreMouse) {
773 mouse.start();
774 }
775 this.intervalId = requestAnimationFrame(tick);
776 }
777 stop() {
778 if (this.intervalId) {
779 requestAnimationFrame.cancel(this.intervalId);
780 this.intervalId = null;
781 }
782 this.mouse.stop();
783 }
784 shouldUpdate(ignoreAnimation, forceRedraw) {
785 // need update from animations?
786 if (!ignoreAnimation) {
787 const { frameDuration } = this;
788 const shouldUpdate = this.animations.reduce((shouldUpdate, animation) => animation.update(frameDuration) || shouldUpdate, false);
789 if (shouldUpdate) {
790 return true;
791 }
792 }
793 // need update from redraw?
794 if (typeof forceRedraw === 'function' && forceRedraw()) {
795 return true;
796 }
797 if (!this.isReadyLock && this.isReady()) {
798 return true;
799 }
800 // need update from mouse events?
801 if (this.mouse.hasEvents()) {
802 return true;
803 }
804 return false;
805 }
806 render(element, ignoreDimensions, ignoreClear, scaleWidth, scaleHeight, offsetX, offsetY) {
807 const { CLIENT_WIDTH, CLIENT_HEIGHT, viewPort, ctx, isFirstRender } = this;
808 const canvas = ctx.canvas;
809 viewPort.clear();
810 if (canvas.width && canvas.height) {
811 viewPort.setCurrent(canvas.width, canvas.height);
812 }
813 else {
814 viewPort.setCurrent(CLIENT_WIDTH, CLIENT_HEIGHT);
815 }
816 const widthStyle = element.getStyle('width');
817 const heightStyle = element.getStyle('height');
818 if (!ignoreDimensions && (isFirstRender
819 || typeof scaleWidth !== 'number' && typeof scaleHeight !== 'number')) {
820 // set canvas size
821 if (widthStyle.hasValue()) {
822 canvas.width = widthStyle.getPixels('x');
823 if (canvas.style) {
824 canvas.style.width = `${canvas.width}px`;
825 }
826 }
827 if (heightStyle.hasValue()) {
828 canvas.height = heightStyle.getPixels('y');
829 if (canvas.style) {
830 canvas.style.height = `${canvas.height}px`;
831 }
832 }
833 }
834 let cWidth = canvas.clientWidth || canvas.width;
835 let cHeight = canvas.clientHeight || canvas.height;
836 if (ignoreDimensions && widthStyle.hasValue() && heightStyle.hasValue()) {
837 cWidth = widthStyle.getPixels('x');
838 cHeight = heightStyle.getPixels('y');
839 }
840 viewPort.setCurrent(cWidth, cHeight);
841 if (typeof offsetX === 'number') {
842 element.getAttribute('x', true).setValue(offsetX);
843 }
844 if (typeof offsetY === 'number') {
845 element.getAttribute('y', true).setValue(offsetY);
846 }
847 if (typeof scaleWidth === 'number'
848 || typeof scaleHeight === 'number') {
849 const viewBox = toNumbers(element.getAttribute('viewBox').getString());
850 let xRatio = 0;
851 let yRatio = 0;
852 if (typeof scaleWidth === 'number') {
853 const widthStyle = element.getStyle('width');
854 if (widthStyle.hasValue()) {
855 xRatio = widthStyle.getPixels('x') / scaleWidth;
856 }
857 else if (!isNaN(viewBox[2])) {
858 xRatio = viewBox[2] / scaleWidth;
859 }
860 }
861 if (typeof scaleHeight === 'number') {
862 const heightStyle = element.getStyle('height');
863 if (heightStyle.hasValue()) {
864 yRatio = heightStyle.getPixels('y') / scaleHeight;
865 }
866 else if (!isNaN(viewBox[3])) {
867 yRatio = viewBox[3] / scaleHeight;
868 }
869 }
870 if (!xRatio) {
871 xRatio = yRatio;
872 }
873 if (!yRatio) {
874 yRatio = xRatio;
875 }
876 element.getAttribute('width', true).setValue(scaleWidth);
877 element.getAttribute('height', true).setValue(scaleHeight);
878 const transformStyle = element.getStyle('transform', true, true);
879 transformStyle.setValue(`${transformStyle.getString()} scale(${1.0 / xRatio}, ${1.0 / yRatio})`);
880 }
881 // clear and render
882 if (!ignoreClear) {
883 ctx.clearRect(0, 0, cWidth, cHeight);
884 }
885 element.render(ctx);
886 if (isFirstRender) {
887 this.isFirstRender = false;
888 }
889 }
890}
891Screen.defaultWindow = defaultWindow;
892Screen.defaultFetch = defaultFetch;
893
894const { defaultFetch: defaultFetch$1 } = Screen;
895const DefaultDOMParser = typeof DOMParser !== 'undefined'
896 ? DOMParser
897 : null;
898class Parser {
899 constructor({ fetch = defaultFetch$1, DOMParser = DefaultDOMParser } = {}) {
900 this.fetch = fetch;
901 this.DOMParser = DOMParser;
902 }
903 async parse(resource) {
904 if (/^</.test(resource)) {
905 return this.parseFromString(resource);
906 }
907 return this.load(resource);
908 }
909 parseFromString(xml) {
910 const parser = new this.DOMParser();
911 try {
912 return this.checkDocument(parser.parseFromString(xml, 'image/svg+xml'));
913 }
914 catch (err) {
915 return this.checkDocument(parser.parseFromString(xml, 'text/xml'));
916 }
917 }
918 checkDocument(document) {
919 const parserError = document.getElementsByTagName('parsererror')[0];
920 if (parserError) {
921 throw new Error(parserError.textContent);
922 }
923 return document;
924 }
925 async load(url) {
926 const response = await this.fetch(url);
927 const xml = await response.text();
928 return this.parseFromString(xml);
929 }
930}
931
932class Translate {
933 constructor(_, point) {
934 this.type = 'translate';
935 this.point = null;
936 this.point = Point.parse(point);
937 }
938 apply(ctx) {
939 const { x, y } = this.point;
940 ctx.translate(x || 0.0, y || 0.0);
941 }
942 unapply(ctx) {
943 const { x, y } = this.point;
944 ctx.translate(-1.0 * x || 0.0, -1.0 * y || 0.0);
945 }
946 applyToPoint(point) {
947 const { x, y } = this.point;
948 point.applyTransform([
949 1,
950 0,
951 0,
952 1,
953 x || 0.0,
954 y || 0.0
955 ]);
956 }
957}
958
959class Rotate {
960 constructor(document, rotate, transformOrigin) {
961 this.type = 'rotate';
962 this.angle = null;
963 this.originX = null;
964 this.originY = null;
965 this.cx = 0;
966 this.cy = 0;
967 const numbers = toNumbers(rotate);
968 this.angle = new Property(document, 'angle', numbers[0]);
969 this.originX = transformOrigin[0];
970 this.originY = transformOrigin[1];
971 this.cx = numbers[1] || 0;
972 this.cy = numbers[2] || 0;
973 }
974 apply(ctx) {
975 const { cx, cy, originX, originY, angle } = this;
976 const x = cx + originX.getPixels('x');
977 const y = cy + originY.getPixels('y');
978 ctx.translate(x, y);
979 ctx.rotate(angle.getRadians());
980 ctx.translate(-x, -y);
981 }
982 unapply(ctx) {
983 const { cx, cy, angle } = this;
984 ctx.translate(cx, cy);
985 ctx.rotate(-1.0 * angle.getRadians());
986 ctx.translate(-cx, -cy);
987 }
988 applyToPoint(point) {
989 const { cx, cy, angle } = this;
990 const rad = angle.getRadians();
991 point.applyTransform([
992 1,
993 0,
994 0,
995 1,
996 cx || 0.0,
997 cy || 0.0 // this.p.y
998 ]);
999 point.applyTransform([
1000 Math.cos(rad),
1001 Math.sin(rad),
1002 -Math.sin(rad),
1003 Math.cos(rad),
1004 0,
1005 0
1006 ]);
1007 point.applyTransform([
1008 1,
1009 0,
1010 0,
1011 1,
1012 -cx || 0.0,
1013 -cy || 0.0 // -this.p.y
1014 ]);
1015 }
1016}
1017
1018class Scale {
1019 constructor(_, scale) {
1020 this.type = 'scale';
1021 this.scale = null;
1022 const scaleSize = Point.parseScale(scale);
1023 // Workaround for node-canvas
1024 if (scaleSize.x === 0
1025 || scaleSize.y === 0) {
1026 scaleSize.x = PSEUDO_ZERO;
1027 scaleSize.y = PSEUDO_ZERO;
1028 }
1029 this.scale = scaleSize;
1030 }
1031 apply(ctx) {
1032 const { x, y } = this.scale;
1033 ctx.scale(x, y || x);
1034 }
1035 unapply(ctx) {
1036 const { x, y } = this.scale;
1037 ctx.scale(1.0 / x, 1.0 / y || x);
1038 }
1039 applyToPoint(point) {
1040 const { x, y } = this.scale;
1041 point.applyTransform([
1042 x || 0.0,
1043 0,
1044 0,
1045 y || 0.0,
1046 0,
1047 0
1048 ]);
1049 }
1050}
1051
1052class Matrix {
1053 constructor(_, matrix) {
1054 this.type = 'matrix';
1055 this.matrix = [];
1056 this.matrix = toNumbers(matrix);
1057 }
1058 apply(ctx) {
1059 const { matrix } = this;
1060 ctx.transform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
1061 }
1062 unapply(ctx) {
1063 const { matrix } = this;
1064 const a = matrix[0];
1065 const b = matrix[2];
1066 const c = matrix[4];
1067 const d = matrix[1];
1068 const e = matrix[3];
1069 const f = matrix[5];
1070 const g = 0.0;
1071 const h = 0.0;
1072 const i = 1.0;
1073 const det = 1 / (a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g));
1074 ctx.transform(det * (e * i - f * h), det * (f * g - d * i), det * (c * h - b * i), det * (a * i - c * g), det * (b * f - c * e), det * (c * d - a * f));
1075 }
1076 applyToPoint(point) {
1077 point.applyTransform(this.matrix);
1078 }
1079}
1080
1081class Skew extends Matrix {
1082 constructor(document, skew) {
1083 super(document, skew);
1084 this.type = 'skew';
1085 this.angle = null;
1086 this.angle = new Property(document, 'angle', skew);
1087 }
1088}
1089
1090class SkewX extends Skew {
1091 constructor(document, skew) {
1092 super(document, skew);
1093 this.type = 'skewX';
1094 this.matrix = [
1095 1,
1096 0,
1097 Math.tan(this.angle.getRadians()),
1098 1,
1099 0,
1100 0
1101 ];
1102 }
1103}
1104
1105class SkewY extends Skew {
1106 constructor(document, skew) {
1107 super(document, skew);
1108 this.type = 'skewY';
1109 this.matrix = [
1110 1,
1111 Math.tan(this.angle.getRadians()),
1112 0,
1113 1,
1114 0,
1115 0
1116 ];
1117 }
1118}
1119
1120function parseTransforms(transform) {
1121 return compressSpaces(transform)
1122 .trim()
1123 .replace(/\)([a-zA-Z])/g, ') $1')
1124 .replace(/\)(\s?,\s?)/g, ') ')
1125 .split(/\s(?=[a-z])/);
1126}
1127function parseTransform(transform) {
1128 const [type, value] = transform.split('(');
1129 return [
1130 type.trim(),
1131 value.trim().replace(')', '')
1132 ];
1133}
1134class Transform {
1135 constructor(document, transform, transformOrigin) {
1136 this.document = document;
1137 this.transforms = [];
1138 const data = parseTransforms(transform);
1139 data.forEach((transform) => {
1140 if (transform === 'none') {
1141 return;
1142 }
1143 const [type, value] = parseTransform(transform);
1144 const TransformType = Transform.transformTypes[type];
1145 if (typeof TransformType !== 'undefined') {
1146 this.transforms.push(new TransformType(this.document, value, transformOrigin));
1147 }
1148 });
1149 }
1150 static fromElement(document, element) {
1151 const transformStyle = element.getStyle('transform', false, true);
1152 const [transformOriginXProperty, transformOriginYProperty = transformOriginXProperty] = element.getStyle('transform-origin', false, true).split();
1153 const transformOrigin = [
1154 transformOriginXProperty,
1155 transformOriginYProperty
1156 ];
1157 if (transformStyle.hasValue()) {
1158 return new Transform(document, transformStyle.getString(), transformOrigin);
1159 }
1160 return null;
1161 }
1162 apply(ctx) {
1163 const { transforms } = this;
1164 const len = transforms.length;
1165 for (let i = 0; i < len; i++) {
1166 transforms[i].apply(ctx);
1167 }
1168 }
1169 unapply(ctx) {
1170 const { transforms } = this;
1171 const len = transforms.length;
1172 for (let i = len - 1; i >= 0; i--) {
1173 transforms[i].unapply(ctx);
1174 }
1175 }
1176 // TODO: applyToPoint unused ... remove?
1177 applyToPoint(point) {
1178 const { transforms } = this;
1179 const len = transforms.length;
1180 for (let i = 0; i < len; i++) {
1181 transforms[i].applyToPoint(point);
1182 }
1183 }
1184}
1185Transform.transformTypes = {
1186 translate: Translate,
1187 rotate: Rotate,
1188 scale: Scale,
1189 matrix: Matrix,
1190 skewX: SkewX,
1191 skewY: SkewY
1192};
1193
1194class Element {
1195 constructor(document, node, captureTextNodes = false) {
1196 this.document = document;
1197 this.node = node;
1198 this.captureTextNodes = captureTextNodes;
1199 this.attributes = {};
1200 this.styles = {};
1201 this.stylesSpecificity = {};
1202 this.animationFrozen = false;
1203 this.animationFrozenValue = '';
1204 this.parent = null;
1205 this.children = [];
1206 if (!node || node.nodeType !== 1) { // ELEMENT_NODE
1207 return;
1208 }
1209 // add attributes
1210 Array.from(node.attributes).forEach((attribute) => {
1211 const nodeName = normalizeAttributeName(attribute.nodeName);
1212 this.attributes[nodeName] = new Property(document, nodeName, attribute.value);
1213 });
1214 this.addStylesFromStyleDefinition();
1215 // add inline styles
1216 if (this.getAttribute('style').hasValue()) {
1217 const styles = this.getAttribute('style')
1218 .getString()
1219 .split(';')
1220 .map(_ => _.trim());
1221 styles.forEach((style) => {
1222 if (!style) {
1223 return;
1224 }
1225 const [name, value] = style.split(':').map(_ => _.trim());
1226 this.styles[name] = new Property(document, name, value);
1227 });
1228 }
1229 const { definitions } = document;
1230 const id = this.getAttribute('id');
1231 // add id
1232 if (id.hasValue()) {
1233 if (!definitions[id.getValue()]) {
1234 definitions[id.getValue()] = this;
1235 }
1236 }
1237 Array.from(node.childNodes).forEach((childNode) => {
1238 if (childNode.nodeType === 1) {
1239 this.addChild(childNode); // ELEMENT_NODE
1240 }
1241 else if (captureTextNodes && (childNode.nodeType === 3
1242 || childNode.nodeType === 4)) {
1243 const textNode = document.createTextNode(childNode);
1244 if (textNode.getText().length > 0) {
1245 this.addChild(textNode); // TEXT_NODE
1246 }
1247 }
1248 });
1249 }
1250 getAttribute(name, createIfNotExists = false) {
1251 const attr = this.attributes[name];
1252 if (!attr && createIfNotExists) {
1253 const attr = new Property(this.document, name, '');
1254 this.attributes[name] = attr;
1255 return attr;
1256 }
1257 return attr || Property.empty(this.document);
1258 }
1259 getHrefAttribute() {
1260 for (const key in this.attributes) {
1261 if (key === 'href' || /:href$/.test(key)) {
1262 return this.attributes[key];
1263 }
1264 }
1265 return Property.empty(this.document);
1266 }
1267 getStyle(name, createIfNotExists = false, skipAncestors = false) {
1268 const style = this.styles[name];
1269 if (style) {
1270 return style;
1271 }
1272 const attr = this.getAttribute(name);
1273 if (attr && attr.hasValue()) {
1274 this.styles[name] = attr; // move up to me to cache
1275 return attr;
1276 }
1277 if (!skipAncestors) {
1278 const { parent } = this;
1279 if (parent) {
1280 const parentStyle = parent.getStyle(name);
1281 if (parentStyle && parentStyle.hasValue()) {
1282 return parentStyle;
1283 }
1284 }
1285 }
1286 if (createIfNotExists) {
1287 const style = new Property(this.document, name, '');
1288 this.styles[name] = style;
1289 return style;
1290 }
1291 return style || Property.empty(this.document);
1292 }
1293 render(ctx) {
1294 // don't render display=none
1295 // don't render visibility=hidden
1296 if (this.getStyle('display').getString() === 'none'
1297 || this.getStyle('visibility').getString() === 'hidden') {
1298 return;
1299 }
1300 ctx.save();
1301 if (this.getStyle('mask').hasValue()) { // mask
1302 const mask = this.getStyle('mask').getDefinition();
1303 if (mask) {
1304 this.applyEffects(ctx);
1305 mask.apply(ctx, this);
1306 }
1307 }
1308 else if (this.getStyle('filter').getValue('none') !== 'none') { // filter
1309 const filter = this.getStyle('filter').getDefinition();
1310 if (filter) {
1311 this.applyEffects(ctx);
1312 filter.apply(ctx, this);
1313 }
1314 }
1315 else {
1316 this.setContext(ctx);
1317 this.renderChildren(ctx);
1318 this.clearContext(ctx);
1319 }
1320 ctx.restore();
1321 }
1322 setContext(_) { }
1323 applyEffects(ctx) {
1324 // transform
1325 const transform = Transform.fromElement(this.document, this);
1326 if (transform) {
1327 transform.apply(ctx);
1328 }
1329 // clip
1330 const clipPathStyleProp = this.getStyle('clip-path', false, true);
1331 if (clipPathStyleProp.hasValue()) {
1332 const clip = clipPathStyleProp.getDefinition();
1333 if (clip) {
1334 clip.apply(ctx);
1335 }
1336 }
1337 }
1338 clearContext(_) { }
1339 renderChildren(ctx) {
1340 this.children.forEach((child) => {
1341 child.render(ctx);
1342 });
1343 }
1344 addChild(childNode) {
1345 const child = childNode instanceof Element
1346 ? childNode
1347 : this.document.createElement(childNode);
1348 child.parent = this;
1349 if (!Element.ignoreChildTypes.includes(child.type)) {
1350 this.children.push(child);
1351 }
1352 }
1353 matchesSelector(selector) {
1354 const { node } = this;
1355 if (typeof node.matches === 'function') {
1356 return node.matches(selector);
1357 }
1358 const styleClasses = node.getAttribute('class');
1359 if (!styleClasses || styleClasses === '') {
1360 return false;
1361 }
1362 return styleClasses.split(' ').some((styleClass) => {
1363 if (`.${styleClass}` === selector) {
1364 return true;
1365 }
1366 });
1367 }
1368 addStylesFromStyleDefinition() {
1369 const { styles, stylesSpecificity } = this.document;
1370 for (const selector in styles) {
1371 if (selector[0] !== '@' && this.matchesSelector(selector)) {
1372 const style = styles[selector];
1373 const specificity = stylesSpecificity[selector];
1374 if (style) {
1375 for (const name in style) {
1376 let existingSpecificity = this.stylesSpecificity[name];
1377 if (typeof existingSpecificity === 'undefined') {
1378 existingSpecificity = '000';
1379 }
1380 if (specificity >= existingSpecificity) {
1381 this.styles[name] = style[name];
1382 this.stylesSpecificity[name] = specificity;
1383 }
1384 }
1385 }
1386 }
1387 }
1388 }
1389 removeStyles(element, ignoreStyles) {
1390 const toRestore = ignoreStyles.reduce((toRestore, name) => {
1391 const styleProp = element.getStyle(name);
1392 if (!styleProp.hasValue()) {
1393 return toRestore;
1394 }
1395 const value = styleProp.getString();
1396 styleProp.setValue('');
1397 return [
1398 ...toRestore,
1399 [name, value]
1400 ];
1401 }, []);
1402 return toRestore;
1403 }
1404 restoreStyles(element, styles) {
1405 styles.forEach(([name, value]) => {
1406 element.getStyle(name, true).setValue(value);
1407 });
1408 }
1409}
1410Element.ignoreChildTypes = [
1411 'title'
1412];
1413
1414class UnknownElement extends Element {
1415 constructor(document, node, captureTextNodes) {
1416 super(document, node, captureTextNodes);
1417 }
1418}
1419
1420function wrapFontFamily(fontFamily) {
1421 const trimmed = fontFamily.trim();
1422 return /^('|")/.test(trimmed)
1423 ? trimmed
1424 : `"${trimmed}"`;
1425}
1426function prepareFontFamily(fontFamily) {
1427 return typeof process === 'undefined'
1428 ? fontFamily
1429 : fontFamily
1430 .trim()
1431 .split(',')
1432 .map(wrapFontFamily)
1433 .join(',');
1434}
1435/**
1436 * https://developer.mozilla.org/en-US/docs/Web/CSS/font-style
1437 */
1438function prepareFontStyle(fontStyle) {
1439 if (!fontStyle) {
1440 return '';
1441 }
1442 const targetFontStyle = fontStyle.trim().toLowerCase();
1443 switch (targetFontStyle) {
1444 case 'normal':
1445 case 'italic':
1446 case 'oblique':
1447 case 'inherit':
1448 case 'initial':
1449 case 'unset':
1450 return targetFontStyle;
1451 default:
1452 if (/^oblique\s+(-|)\d+deg$/.test(targetFontStyle)) {
1453 return targetFontStyle;
1454 }
1455 return '';
1456 }
1457}
1458/**
1459 * https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight
1460 */
1461function prepareFontWeight(fontWeight) {
1462 if (!fontWeight) {
1463 return '';
1464 }
1465 const targetFontWeight = fontWeight.trim().toLowerCase();
1466 switch (targetFontWeight) {
1467 case 'normal':
1468 case 'bold':
1469 case 'lighter':
1470 case 'bolder':
1471 case 'inherit':
1472 case 'initial':
1473 case 'unset':
1474 return targetFontWeight;
1475 default:
1476 if (/^[\d.]+$/.test(targetFontWeight)) {
1477 return targetFontWeight;
1478 }
1479 return '';
1480 }
1481}
1482class Font {
1483 constructor(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) {
1484 const inheritFont = inherit
1485 ? typeof inherit === 'string'
1486 ? Font.parse(inherit)
1487 : inherit
1488 : {};
1489 this.fontFamily = fontFamily || inheritFont.fontFamily;
1490 this.fontSize = fontSize || inheritFont.fontSize;
1491 this.fontStyle = fontStyle || inheritFont.fontStyle;
1492 this.fontWeight = fontWeight || inheritFont.fontWeight;
1493 this.fontVariant = fontVariant || inheritFont.fontVariant;
1494 }
1495 static parse(font = '', inherit) {
1496 let fontStyle = '';
1497 let fontVariant = '';
1498 let fontWeight = '';
1499 let fontSize = '';
1500 let fontFamily = '';
1501 const parts = compressSpaces(font).trim().split(' ');
1502 const set = {
1503 fontSize: false,
1504 fontStyle: false,
1505 fontWeight: false,
1506 fontVariant: false
1507 };
1508 parts.forEach((part) => {
1509 switch (true) {
1510 case !set.fontStyle && Font.styles.includes(part):
1511 if (part !== 'inherit') {
1512 fontStyle = part;
1513 }
1514 set.fontStyle = true;
1515 break;
1516 case !set.fontVariant && Font.variants.includes(part):
1517 if (part !== 'inherit') {
1518 fontVariant = part;
1519 }
1520 set.fontStyle = true;
1521 set.fontVariant = true;
1522 break;
1523 case !set.fontWeight && Font.weights.includes(part):
1524 if (part !== 'inherit') {
1525 fontWeight = part;
1526 }
1527 set.fontStyle = true;
1528 set.fontVariant = true;
1529 set.fontWeight = true;
1530 break;
1531 case !set.fontSize:
1532 if (part !== 'inherit') {
1533 [fontSize] = part.split('/');
1534 }
1535 set.fontStyle = true;
1536 set.fontVariant = true;
1537 set.fontWeight = true;
1538 set.fontSize = true;
1539 break;
1540 default:
1541 if (part !== 'inherit') {
1542 fontFamily += part;
1543 }
1544 }
1545 });
1546 return new Font(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit);
1547 }
1548 toString() {
1549 return [
1550 prepareFontStyle(this.fontStyle),
1551 this.fontVariant,
1552 prepareFontWeight(this.fontWeight),
1553 this.fontSize,
1554 // Wrap fontFamily only on nodejs and only for canvas.ctx
1555 prepareFontFamily(this.fontFamily)
1556 ].join(' ').trim();
1557 }
1558}
1559Font.styles = 'normal|italic|oblique|inherit';
1560Font.variants = 'normal|small-caps|inherit';
1561Font.weights = 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit';
1562
1563class BoundingBox {
1564 constructor(x1 = Number.NaN, y1 = Number.NaN, x2 = Number.NaN, y2 = Number.NaN) {
1565 this.x1 = x1;
1566 this.y1 = y1;
1567 this.x2 = x2;
1568 this.y2 = y2;
1569 this.addPoint(x1, y1);
1570 this.addPoint(x2, y2);
1571 }
1572 get x() {
1573 return this.x1;
1574 }
1575 get y() {
1576 return this.y1;
1577 }
1578 get width() {
1579 return this.x2 - this.x1;
1580 }
1581 get height() {
1582 return this.y2 - this.y1;
1583 }
1584 addPoint(x, y) {
1585 if (typeof x !== 'undefined') {
1586 if (isNaN(this.x1) || isNaN(this.x2)) {
1587 this.x1 = x;
1588 this.x2 = x;
1589 }
1590 if (x < this.x1) {
1591 this.x1 = x;
1592 }
1593 if (x > this.x2) {
1594 this.x2 = x;
1595 }
1596 }
1597 if (typeof y !== 'undefined') {
1598 if (isNaN(this.y1) || isNaN(this.y2)) {
1599 this.y1 = y;
1600 this.y2 = y;
1601 }
1602 if (y < this.y1) {
1603 this.y1 = y;
1604 }
1605 if (y > this.y2) {
1606 this.y2 = y;
1607 }
1608 }
1609 }
1610 addX(x) {
1611 this.addPoint(x, null);
1612 }
1613 addY(y) {
1614 this.addPoint(null, y);
1615 }
1616 addBoundingBox(boundingBox) {
1617 if (!boundingBox) {
1618 return;
1619 }
1620 const { x1, y1, x2, y2 } = boundingBox;
1621 this.addPoint(x1, y1);
1622 this.addPoint(x2, y2);
1623 }
1624 sumCubic(t, p0, p1, p2, p3) {
1625 return (Math.pow(1 - t, 3) * p0
1626 + 3 * Math.pow(1 - t, 2) * t * p1
1627 + 3 * (1 - t) * Math.pow(t, 2) * p2
1628 + Math.pow(t, 3) * p3);
1629 }
1630 bezierCurveAdd(forX, p0, p1, p2, p3) {
1631 const b = 6 * p0 - 12 * p1 + 6 * p2;
1632 const a = -3 * p0 + 9 * p1 - 9 * p2 + 3 * p3;
1633 const c = 3 * p1 - 3 * p0;
1634 if (a === 0) {
1635 if (b === 0) {
1636 return;
1637 }
1638 const t = -c / b;
1639 if (0 < t && t < 1) {
1640 if (forX) {
1641 this.addX(this.sumCubic(t, p0, p1, p2, p3));
1642 }
1643 else {
1644 this.addY(this.sumCubic(t, p0, p1, p2, p3));
1645 }
1646 }
1647 return;
1648 }
1649 const b2ac = Math.pow(b, 2) - 4 * c * a;
1650 if (b2ac < 0) {
1651 return;
1652 }
1653 const t1 = (-b + Math.sqrt(b2ac)) / (2 * a);
1654 if (0 < t1 && t1 < 1) {
1655 if (forX) {
1656 this.addX(this.sumCubic(t1, p0, p1, p2, p3));
1657 }
1658 else {
1659 this.addY(this.sumCubic(t1, p0, p1, p2, p3));
1660 }
1661 }
1662 const t2 = (-b - Math.sqrt(b2ac)) / (2 * a);
1663 if (0 < t2 && t2 < 1) {
1664 if (forX) {
1665 this.addX(this.sumCubic(t2, p0, p1, p2, p3));
1666 }
1667 else {
1668 this.addY(this.sumCubic(t2, p0, p1, p2, p3));
1669 }
1670 }
1671 }
1672 // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
1673 addBezierCurve(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
1674 this.addPoint(p0x, p0y);
1675 this.addPoint(p3x, p3y);
1676 this.bezierCurveAdd(true, p0x, p1x, p2x, p3x);
1677 this.bezierCurveAdd(false, p0y, p1y, p2y, p3y);
1678 }
1679 addQuadraticCurve(p0x, p0y, p1x, p1y, p2x, p2y) {
1680 const cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0)
1681 const cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0)
1682 const cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0)
1683 const cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0)
1684 this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y);
1685 }
1686 isPointInBox(x, y) {
1687 const { x1, y1, x2, y2 } = this;
1688 return (x1 <= x
1689 && x <= x2
1690 && y1 <= y
1691 && y <= y2);
1692 }
1693}
1694
1695class RenderedElement extends Element {
1696 constructor() {
1697 super(...arguments);
1698 this.modifiedEmSizeStack = false;
1699 }
1700 calculateOpacity() {
1701 let opacity = 1.0;
1702 // tslint:disable-next-line: no-this-assignment
1703 let element = this;
1704 while (element) {
1705 const opacityStyle = element.getStyle('opacity', false, true); // no ancestors on style call
1706 if (opacityStyle.hasValue(true)) {
1707 opacity *= opacityStyle.getNumber();
1708 }
1709 element = element.parent;
1710 }
1711 return opacity;
1712 }
1713 setContext(ctx, fromMeasure = false) {
1714 if (!fromMeasure) { // causes stack overflow when measuring text with gradients
1715 // fill
1716 const fillStyleProp = this.getStyle('fill');
1717 const fillOpacityStyleProp = this.getStyle('fill-opacity');
1718 const strokeStyleProp = this.getStyle('stroke');
1719 const strokeOpacityProp = this.getStyle('stroke-opacity');
1720 if (fillStyleProp.isUrlDefinition()) {
1721 const fillStyle = fillStyleProp.getFillStyleDefinition(this, fillOpacityStyleProp);
1722 if (fillStyle) {
1723 ctx.fillStyle = fillStyle;
1724 }
1725 }
1726 else if (fillStyleProp.hasValue()) {
1727 if (fillStyleProp.getString() === 'currentColor') {
1728 fillStyleProp.setValue(this.getStyle('color').getColor());
1729 }
1730 const fillStyle = fillStyleProp.getColor();
1731 if (fillStyle !== 'inherit') {
1732 ctx.fillStyle = fillStyle === 'none'
1733 ? 'rgba(0,0,0,0)'
1734 : fillStyle;
1735 }
1736 }
1737 if (fillOpacityStyleProp.hasValue()) {
1738 const fillStyle = new Property(this.document, 'fill', ctx.fillStyle)
1739 .addOpacity(fillOpacityStyleProp)
1740 .getColor();
1741 ctx.fillStyle = fillStyle;
1742 }
1743 // stroke
1744 if (strokeStyleProp.isUrlDefinition()) {
1745 const strokeStyle = strokeStyleProp.getFillStyleDefinition(this, strokeOpacityProp);
1746 if (strokeStyle) {
1747 ctx.strokeStyle = strokeStyle;
1748 }
1749 }
1750 else if (strokeStyleProp.hasValue()) {
1751 if (strokeStyleProp.getString() === 'currentColor') {
1752 strokeStyleProp.setValue(this.getStyle('color').getColor());
1753 }
1754 const strokeStyle = strokeStyleProp.getString();
1755 if (strokeStyle !== 'inherit') {
1756 ctx.strokeStyle = strokeStyle === 'none'
1757 ? 'rgba(0,0,0,0)'
1758 : strokeStyle;
1759 }
1760 }
1761 if (strokeOpacityProp.hasValue()) {
1762 const strokeStyle = new Property(this.document, 'stroke', ctx.strokeStyle)
1763 .addOpacity(strokeOpacityProp)
1764 .getString();
1765 ctx.strokeStyle = strokeStyle;
1766 }
1767 const strokeWidthStyleProp = this.getStyle('stroke-width');
1768 if (strokeWidthStyleProp.hasValue()) {
1769 const newLineWidth = strokeWidthStyleProp.getPixels();
1770 ctx.lineWidth = !newLineWidth
1771 ? PSEUDO_ZERO // browsers don't respect 0 (or node-canvas? :-)
1772 : newLineWidth;
1773 }
1774 const strokeLinecapStyleProp = this.getStyle('stroke-linecap');
1775 const strokeLinejoinStyleProp = this.getStyle('stroke-linejoin');
1776 const strokeMiterlimitProp = this.getStyle('stroke-miterlimit');
1777 const pointOrderStyleProp = this.getStyle('paint-order');
1778 const strokeDasharrayStyleProp = this.getStyle('stroke-dasharray');
1779 const strokeDashoffsetProp = this.getStyle('stroke-dashoffset');
1780 if (strokeLinecapStyleProp.hasValue()) {
1781 ctx.lineCap = strokeLinecapStyleProp.getString();
1782 }
1783 if (strokeLinejoinStyleProp.hasValue()) {
1784 ctx.lineJoin = strokeLinejoinStyleProp.getString();
1785 }
1786 if (strokeMiterlimitProp.hasValue()) {
1787 ctx.miterLimit = strokeMiterlimitProp.getNumber();
1788 }
1789 if (pointOrderStyleProp.hasValue()) {
1790 // ?
1791 ctx.paintOrder = pointOrderStyleProp.getValue();
1792 }
1793 if (strokeDasharrayStyleProp.hasValue() && strokeDasharrayStyleProp.getString() !== 'none') {
1794 const gaps = toNumbers(strokeDasharrayStyleProp.getString());
1795 if (typeof ctx.setLineDash !== 'undefined') {
1796 ctx.setLineDash(gaps);
1797 }
1798 else if (typeof ctx.webkitLineDash !== 'undefined') {
1799 ctx.webkitLineDash = gaps;
1800 }
1801 else if (typeof ctx.mozDash !== 'undefined' && !(gaps.length === 1 && gaps[0] === 0)) {
1802 ctx.mozDash = gaps;
1803 }
1804 const offset = strokeDashoffsetProp.getPixels();
1805 if (typeof ctx.lineDashOffset !== 'undefined') {
1806 ctx.lineDashOffset = offset;
1807 }
1808 else if (typeof ctx.webkitLineDashOffset !== 'undefined') {
1809 ctx.webkitLineDashOffset = offset;
1810 }
1811 else if (typeof ctx.mozDashOffset !== 'undefined') {
1812 ctx.mozDashOffset = offset;
1813 }
1814 }
1815 }
1816 // font
1817 this.modifiedEmSizeStack = false;
1818 if (typeof ctx.font !== 'undefined') {
1819 const fontStyleProp = this.getStyle('font');
1820 const fontStyleStyleProp = this.getStyle('font-style');
1821 const fontVariantStyleProp = this.getStyle('font-variant');
1822 const fontWeightStyleProp = this.getStyle('font-weight');
1823 const fontSizeStyleProp = this.getStyle('font-size');
1824 const fontFamilyStyleProp = this.getStyle('font-family');
1825 const font = new Font(fontStyleStyleProp.getString(), fontVariantStyleProp.getString(), fontWeightStyleProp.getString(), fontSizeStyleProp.hasValue()
1826 ? `${fontSizeStyleProp.getPixels(true)}px`
1827 : '', fontFamilyStyleProp.getString(), Font.parse(fontStyleProp.getString(), ctx.font));
1828 fontStyleStyleProp.setValue(font.fontStyle);
1829 fontVariantStyleProp.setValue(font.fontVariant);
1830 fontWeightStyleProp.setValue(font.fontWeight);
1831 fontSizeStyleProp.setValue(font.fontSize);
1832 fontFamilyStyleProp.setValue(font.fontFamily);
1833 ctx.font = font.toString();
1834 if (fontSizeStyleProp.isPixels()) {
1835 this.document.emSize = fontSizeStyleProp.getPixels();
1836 this.modifiedEmSizeStack = true;
1837 }
1838 }
1839 if (!fromMeasure) {
1840 // effects
1841 this.applyEffects(ctx);
1842 // opacity
1843 ctx.globalAlpha = this.calculateOpacity();
1844 }
1845 }
1846 clearContext(ctx) {
1847 super.clearContext(ctx);
1848 if (this.modifiedEmSizeStack) {
1849 this.document.popEmSize();
1850 }
1851 }
1852}
1853
1854class TextElement extends RenderedElement {
1855 constructor(document, node, captureTextNodes) {
1856 super(document, node, new.target === TextElement
1857 ? true
1858 : captureTextNodes);
1859 this.type = 'text';
1860 this.x = 0;
1861 this.y = 0;
1862 this.measureCache = -1;
1863 }
1864 setContext(ctx, fromMeasure = false) {
1865 super.setContext(ctx, fromMeasure);
1866 const textBaseline = this.getStyle('dominant-baseline').getTextBaseline()
1867 || this.getStyle('alignment-baseline').getTextBaseline();
1868 if (textBaseline) {
1869 ctx.textBaseline = textBaseline;
1870 }
1871 }
1872 initializeCoordinates(ctx) {
1873 this.x = this.getAttribute('x').getPixels('x');
1874 this.y = this.getAttribute('y').getPixels('y');
1875 const dxAttr = this.getAttribute('dx');
1876 const dyAttr = this.getAttribute('dy');
1877 if (dxAttr.hasValue()) {
1878 this.x += dxAttr.getPixels('x');
1879 }
1880 if (dyAttr.hasValue()) {
1881 this.y += dyAttr.getPixels('y');
1882 }
1883 this.x += this.getAnchorDelta(ctx, this, 0);
1884 }
1885 getBoundingBox(ctx) {
1886 if (this.type !== 'text') {
1887 return this.getTElementBoundingBox(ctx);
1888 }
1889 this.initializeCoordinates(ctx);
1890 let boundingBox = null;
1891 this.children.forEach((_, i) => {
1892 const childBoundingBox = this.getChildBoundingBox(ctx, this, this, i);
1893 if (!boundingBox) {
1894 boundingBox = childBoundingBox;
1895 }
1896 else {
1897 boundingBox.addBoundingBox(childBoundingBox);
1898 }
1899 });
1900 return boundingBox;
1901 }
1902 getFontSize() {
1903 const { document, parent } = this;
1904 const inheritFontSize = Font.parse(document.ctx.font).fontSize;
1905 const fontSize = parent.getStyle('font-size').getNumber(inheritFontSize);
1906 return fontSize;
1907 }
1908 getTElementBoundingBox(ctx) {
1909 const fontSize = this.getFontSize();
1910 return new BoundingBox(this.x, this.y - fontSize, this.x + this.measureText(ctx), this.y);
1911 }
1912 getGlyph(font, text, i) {
1913 const char = text[i];
1914 let glyph = null;
1915 if (font.isArabic) {
1916 const len = text.length;
1917 const prevChar = text[i - 1];
1918 const nextChar = text[i + 1];
1919 let arabicForm = 'isolated';
1920 if ((i === 0 || prevChar === ' ') && i < len - 2 && nextChar !== ' ') {
1921 arabicForm = 'terminal';
1922 }
1923 if (i > 0 && prevChar !== ' ' && i < len - 2 && nextChar !== ' ') {
1924 arabicForm = 'medial';
1925 }
1926 if (i > 0 && prevChar !== ' ' && (i === len - 1 || nextChar === ' ')) {
1927 arabicForm = 'initial';
1928 }
1929 if (typeof font.glyphs[char] !== 'undefined') {
1930 glyph = font.glyphs[char][arabicForm];
1931 if (!glyph && font.glyphs[char].type === 'glyph') {
1932 glyph = font.glyphs[char];
1933 }
1934 }
1935 }
1936 else {
1937 glyph = font.glyphs[char];
1938 }
1939 if (!glyph) {
1940 glyph = font.missingGlyph;
1941 }
1942 return glyph;
1943 }
1944 getText() {
1945 return '';
1946 }
1947 getTextFromNode(node) {
1948 const textNode = node || this.node;
1949 const childNodes = Array.from(textNode.parentNode.childNodes);
1950 const index = childNodes.indexOf(textNode);
1951 const lastIndex = childNodes.length - 1;
1952 let text = compressSpaces(textNode.value
1953 || textNode.text
1954 || textNode.textContent
1955 || '');
1956 if (index === 0) {
1957 text = trimLeft(text);
1958 }
1959 if (index === lastIndex) {
1960 text = trimRight(text);
1961 }
1962 return text;
1963 }
1964 renderChildren(ctx) {
1965 if (this.type !== 'text') {
1966 this.renderTElementChildren(ctx);
1967 return;
1968 }
1969 this.initializeCoordinates(ctx);
1970 this.children.forEach((_, i) => {
1971 this.renderChild(ctx, this, this, i);
1972 });
1973 const { mouse } = this.document.screen;
1974 // Do not calc bounding box if mouse is not working.
1975 if (mouse.isWorking()) {
1976 mouse.checkBoundingBox(this, this.getBoundingBox(ctx));
1977 }
1978 }
1979 renderTElementChildren(ctx) {
1980 const { document, parent } = this;
1981 const renderText = this.getText();
1982 const customFont = parent.getStyle('font-family').getDefinition();
1983 if (customFont) {
1984 const { unitsPerEm } = customFont.fontFace;
1985 const ctxFont = Font.parse(document.ctx.font);
1986 const fontSize = parent.getStyle('font-size').getNumber(ctxFont.fontSize);
1987 const fontStyle = parent.getStyle('font-style').getString(ctxFont.fontStyle);
1988 const scale = fontSize / unitsPerEm;
1989 const text = customFont.isRTL
1990 ? renderText.split('').reverse().join('')
1991 : renderText;
1992 const dx = toNumbers(parent.getAttribute('dx').getString());
1993 const len = text.length;
1994 for (let i = 0; i < len; i++) {
1995 const glyph = this.getGlyph(customFont, text, i);
1996 ctx.translate(this.x, this.y);
1997 ctx.scale(scale, -scale);
1998 const lw = ctx.lineWidth;
1999 ctx.lineWidth = ctx.lineWidth * unitsPerEm / fontSize;
2000 if (fontStyle === 'italic') {
2001 ctx.transform(1, 0, .4, 1, 0, 0);
2002 }
2003 glyph.render(ctx);
2004 if (fontStyle === 'italic') {
2005 ctx.transform(1, 0, -.4, 1, 0, 0);
2006 }
2007 ctx.lineWidth = lw;
2008 ctx.scale(1 / scale, -1 / scale);
2009 ctx.translate(-this.x, -this.y);
2010 this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / unitsPerEm;
2011 if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) {
2012 this.x += dx[i];
2013 }
2014 }
2015 return;
2016 }
2017 const { x, y } = this;
2018 if (ctx.paintOrder === 'stroke') {
2019 if (ctx.strokeStyle) {
2020 ctx.strokeText(renderText, x, y);
2021 }
2022 if (ctx.fillStyle) {
2023 ctx.fillText(renderText, x, y);
2024 }
2025 }
2026 else {
2027 if (ctx.fillStyle) {
2028 ctx.fillText(renderText, x, y);
2029 }
2030 if (ctx.strokeStyle) {
2031 ctx.strokeText(renderText, x, y);
2032 }
2033 }
2034 }
2035 getAnchorDelta(ctx, parent, startI) {
2036 const textAnchor = this.getStyle('text-anchor').getString('start');
2037 if (textAnchor !== 'start') {
2038 const { children } = parent;
2039 const len = children.length;
2040 let child = null;
2041 let width = 0;
2042 for (let i = startI; i < len; i++) {
2043 child = children[i];
2044 if (i > startI && child.getAttribute('x').hasValue()
2045 || child.getAttribute('text-anchor').hasValue()) {
2046 break; // new group
2047 }
2048 width += child.measureTextRecursive(ctx);
2049 }
2050 return -1 * (textAnchor === 'end' ? width : width / 2.0);
2051 }
2052 return 0;
2053 }
2054 adjustChildCoordinates(ctx, textParent, parent, i) {
2055 const child = parent.children[i];
2056 if (typeof child.measureText !== 'function') {
2057 return child;
2058 }
2059 ctx.save();
2060 child.setContext(ctx, true);
2061 const xAttr = child.getAttribute('x');
2062 const yAttr = child.getAttribute('y');
2063 const dxAttr = child.getAttribute('dx');
2064 const dyAttr = child.getAttribute('dy');
2065 const textAnchor = child.getAttribute('text-anchor').getString('start');
2066 if (i === 0 && child.type !== 'textNode') {
2067 if (!xAttr.hasValue()) {
2068 xAttr.setValue(textParent.getAttribute('x').getValue('0'));
2069 }
2070 if (!yAttr.hasValue()) {
2071 yAttr.setValue(textParent.getAttribute('y').getValue('0'));
2072 }
2073 if (!dxAttr.hasValue()) {
2074 dxAttr.setValue(textParent.getAttribute('dx').getValue('0'));
2075 }
2076 if (!dyAttr.hasValue()) {
2077 dyAttr.setValue(textParent.getAttribute('dy').getValue('0'));
2078 }
2079 }
2080 if (xAttr.hasValue()) {
2081 child.x = xAttr.getPixels('x') + textParent.getAnchorDelta(ctx, parent, i);
2082 if (textAnchor !== 'start') {
2083 const width = child.measureTextRecursive(ctx);
2084 child.x += -1 * (textAnchor === 'end' ? width : width / 2.0);
2085 }
2086 if (dxAttr.hasValue()) {
2087 child.x += dxAttr.getPixels('x');
2088 }
2089 }
2090 else {
2091 if (textAnchor !== 'start') {
2092 const width = child.measureTextRecursive(ctx);
2093 textParent.x += -1 * (textAnchor === 'end' ? width : width / 2.0);
2094 }
2095 if (dxAttr.hasValue()) {
2096 textParent.x += dxAttr.getPixels('x');
2097 }
2098 child.x = textParent.x;
2099 }
2100 textParent.x = child.x + child.measureText(ctx);
2101 if (yAttr.hasValue()) {
2102 child.y = yAttr.getPixels('y');
2103 if (dyAttr.hasValue()) {
2104 child.y += dyAttr.getPixels('y');
2105 }
2106 }
2107 else {
2108 if (dyAttr.hasValue()) {
2109 textParent.y += dyAttr.getPixels('y');
2110 }
2111 child.y = textParent.y;
2112 }
2113 textParent.y = child.y;
2114 child.clearContext(ctx);
2115 ctx.restore();
2116 return child;
2117 }
2118 getChildBoundingBox(ctx, textParent, parent, i) {
2119 const child = this.adjustChildCoordinates(ctx, textParent, parent, i);
2120 // not a text node?
2121 if (typeof child.getBoundingBox !== 'function') {
2122 return null;
2123 }
2124 const boundingBox = child.getBoundingBox(ctx);
2125 if (!boundingBox) {
2126 return null;
2127 }
2128 child.children.forEach((_, i) => {
2129 const childBoundingBox = textParent.getChildBoundingBox(ctx, textParent, child, i);
2130 boundingBox.addBoundingBox(childBoundingBox);
2131 });
2132 return boundingBox;
2133 }
2134 renderChild(ctx, textParent, parent, i) {
2135 const child = this.adjustChildCoordinates(ctx, textParent, parent, i);
2136 child.render(ctx);
2137 child.children.forEach((_, i) => {
2138 textParent.renderChild(ctx, textParent, child, i);
2139 });
2140 }
2141 measureTextRecursive(ctx) {
2142 const width = this.children.reduce((width, child) => width + child.measureTextRecursive(ctx), this.measureText(ctx));
2143 return width;
2144 }
2145 measureText(ctx) {
2146 const { measureCache } = this;
2147 if (~measureCache) {
2148 return measureCache;
2149 }
2150 const renderText = this.getText();
2151 const measure = this.measureTargetText(ctx, renderText);
2152 this.measureCache = measure;
2153 return measure;
2154 }
2155 measureTargetText(ctx, targetText) {
2156 if (!targetText.length) {
2157 return 0;
2158 }
2159 const { parent } = this;
2160 const customFont = parent.getStyle('font-family').getDefinition();
2161 if (customFont) {
2162 const fontSize = this.getFontSize();
2163 const text = customFont.isRTL
2164 ? targetText.split('').reverse().join('')
2165 : targetText;
2166 const dx = toNumbers(parent.getAttribute('dx').getString());
2167 const len = text.length;
2168 let measure = 0;
2169 for (let i = 0; i < len; i++) {
2170 const glyph = this.getGlyph(customFont, text, i);
2171 measure += (glyph.horizAdvX || customFont.horizAdvX)
2172 * fontSize
2173 / customFont.fontFace.unitsPerEm;
2174 if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) {
2175 measure += dx[i];
2176 }
2177 }
2178 return measure;
2179 }
2180 if (!ctx.measureText) {
2181 return targetText.length * 10;
2182 }
2183 ctx.save();
2184 this.setContext(ctx, true);
2185 const { width: measure } = ctx.measureText(targetText);
2186 this.clearContext(ctx);
2187 ctx.restore();
2188 return measure;
2189 }
2190}
2191
2192class TSpanElement extends TextElement {
2193 constructor(document, node, captureTextNodes) {
2194 super(document, node, new.target === TSpanElement
2195 ? true
2196 : captureTextNodes);
2197 this.type = 'tspan';
2198 // if this node has children, then they own the text
2199 this.text = this.children.length > 0
2200 ? ''
2201 : this.getTextFromNode();
2202 }
2203 getText() {
2204 return this.text;
2205 }
2206}
2207
2208class TextNode extends TSpanElement {
2209 constructor() {
2210 super(...arguments);
2211 this.type = 'textNode';
2212 }
2213}
2214
2215class PathParser extends SVGPathData {
2216 constructor(path) {
2217 super(path
2218 // Fix spaces after signs.
2219 .replace(/[+-.]\s+/g, '-')
2220 // Remove invalid part.
2221 .replace(/[^MmZzLlHhVvCcSsQqTtAae\d\s.,+-].*/g, ''));
2222 this.control = null;
2223 this.start = null;
2224 this.current = null;
2225 this.command = null;
2226 this.commands = this.commands;
2227 this.i = -1;
2228 this.previousCommand = null;
2229 this.points = [];
2230 this.angles = [];
2231 }
2232 reset() {
2233 this.i = -1;
2234 this.command = null;
2235 this.previousCommand = null;
2236 this.start = new Point(0, 0);
2237 this.control = new Point(0, 0);
2238 this.current = new Point(0, 0);
2239 this.points = [];
2240 this.angles = [];
2241 }
2242 isEnd() {
2243 const { i, commands } = this;
2244 return i >= commands.length - 1;
2245 }
2246 next() {
2247 const command = this.commands[++this.i];
2248 this.previousCommand = this.command;
2249 this.command = command;
2250 return command;
2251 }
2252 getPoint(xProp = 'x', yProp = 'y') {
2253 const point = new Point(this.command[xProp], this.command[yProp]);
2254 return this.makeAbsolute(point);
2255 }
2256 getAsControlPoint(xProp, yProp) {
2257 const point = this.getPoint(xProp, yProp);
2258 this.control = point;
2259 return point;
2260 }
2261 getAsCurrentPoint(xProp, yProp) {
2262 const point = this.getPoint(xProp, yProp);
2263 this.current = point;
2264 return point;
2265 }
2266 getReflectedControlPoint() {
2267 const previousCommand = this.previousCommand.type;
2268 if (previousCommand !== SVGPathData.CURVE_TO
2269 && previousCommand !== SVGPathData.SMOOTH_CURVE_TO
2270 && previousCommand !== SVGPathData.QUAD_TO
2271 && previousCommand !== SVGPathData.SMOOTH_QUAD_TO) {
2272 return this.current;
2273 }
2274 // reflect point
2275 const { current: { x: cx, y: cy }, control: { x: ox, y: oy } } = this;
2276 const point = new Point(2 * cx - ox, 2 * cy - oy);
2277 return point;
2278 }
2279 makeAbsolute(point) {
2280 if (this.command.relative) {
2281 const { x, y } = this.current;
2282 point.x += x;
2283 point.y += y;
2284 }
2285 return point;
2286 }
2287 addMarker(point, from, priorTo) {
2288 const { points, angles } = this;
2289 // if the last angle isn't filled in because we didn't have this point yet ...
2290 if (priorTo && angles.length > 0 && !angles[angles.length - 1]) {
2291 angles[angles.length - 1] = points[points.length - 1].angleTo(priorTo);
2292 }
2293 this.addMarkerAngle(point, from ? from.angleTo(point) : null);
2294 }
2295 addMarkerAngle(point, angle) {
2296 this.points.push(point);
2297 this.angles.push(angle);
2298 }
2299 getMarkerPoints() {
2300 return this.points;
2301 }
2302 getMarkerAngles() {
2303 const { angles } = this;
2304 const len = angles.length;
2305 for (let i = 0; i < len; i++) {
2306 if (!angles[i]) {
2307 for (let j = i + 1; j < len; j++) {
2308 if (angles[j]) {
2309 angles[i] = angles[j];
2310 break;
2311 }
2312 }
2313 }
2314 }
2315 return angles;
2316 }
2317}
2318
2319class PathElement extends RenderedElement {
2320 constructor(document, node, captureTextNodes) {
2321 super(document, node, captureTextNodes);
2322 this.type = 'path';
2323 this.pathParser = null;
2324 this.pathParser = new PathParser(this.getAttribute('d').getString());
2325 }
2326 path(ctx) {
2327 const { pathParser } = this;
2328 const boundingBox = new BoundingBox();
2329 pathParser.reset();
2330 if (ctx) {
2331 ctx.beginPath();
2332 }
2333 while (!pathParser.isEnd()) {
2334 switch (pathParser.next().type) {
2335 case PathParser.MOVE_TO:
2336 this.pathM(ctx, boundingBox);
2337 break;
2338 case PathParser.LINE_TO:
2339 this.pathL(ctx, boundingBox);
2340 break;
2341 case PathParser.HORIZ_LINE_TO:
2342 this.pathH(ctx, boundingBox);
2343 break;
2344 case PathParser.VERT_LINE_TO:
2345 this.pathV(ctx, boundingBox);
2346 break;
2347 case PathParser.CURVE_TO:
2348 this.pathC(ctx, boundingBox);
2349 break;
2350 case PathParser.SMOOTH_CURVE_TO:
2351 this.pathS(ctx, boundingBox);
2352 break;
2353 case PathParser.QUAD_TO:
2354 this.pathQ(ctx, boundingBox);
2355 break;
2356 case PathParser.SMOOTH_QUAD_TO:
2357 this.pathT(ctx, boundingBox);
2358 break;
2359 case PathParser.ARC:
2360 this.pathA(ctx, boundingBox);
2361 break;
2362 case PathParser.CLOSE_PATH:
2363 this.pathZ(ctx, boundingBox);
2364 break;
2365 }
2366 }
2367 return boundingBox;
2368 }
2369 getBoundingBox(_) {
2370 return this.path();
2371 }
2372 getMarkers() {
2373 const { pathParser } = this;
2374 const points = pathParser.getMarkerPoints();
2375 const angles = pathParser.getMarkerAngles();
2376 const markers = points.map((point, i) => [
2377 point,
2378 angles[i]
2379 ]);
2380 return markers;
2381 }
2382 renderChildren(ctx) {
2383 this.path(ctx);
2384 this.document.screen.mouse.checkPath(this, ctx);
2385 const fillRuleStyleProp = this.getStyle('fill-rule');
2386 if (ctx.fillStyle !== '') {
2387 if (fillRuleStyleProp.getString('inherit') !== 'inherit') {
2388 ctx.fill(fillRuleStyleProp.getString());
2389 }
2390 else {
2391 ctx.fill();
2392 }
2393 }
2394 if (ctx.strokeStyle !== '') {
2395 ctx.stroke();
2396 }
2397 const markers = this.getMarkers();
2398 if (markers) {
2399 const markersLastIndex = markers.length - 1;
2400 const markerStartStyleProp = this.getStyle('marker-start');
2401 const markerMidStyleProp = this.getStyle('marker-mid');
2402 const markerEndStyleProp = this.getStyle('marker-end');
2403 if (markerStartStyleProp.isUrlDefinition()) {
2404 const marker = markerStartStyleProp.getDefinition();
2405 const [point, angle] = markers[0];
2406 marker.render(ctx, point, angle);
2407 }
2408 if (markerMidStyleProp.isUrlDefinition()) {
2409 const marker = markerMidStyleProp.getDefinition();
2410 for (let i = 1; i < markersLastIndex; i++) {
2411 const [point, angle] = markers[i];
2412 marker.render(ctx, point, angle);
2413 }
2414 }
2415 if (markerEndStyleProp.isUrlDefinition()) {
2416 const marker = markerEndStyleProp.getDefinition();
2417 const [point, angle] = markers[markersLastIndex];
2418 marker.render(ctx, point, angle);
2419 }
2420 }
2421 }
2422 static pathM(pathParser) {
2423 const point = pathParser.getAsCurrentPoint();
2424 pathParser.start = pathParser.current;
2425 return {
2426 point
2427 };
2428 }
2429 pathM(ctx, boundingBox) {
2430 const { pathParser } = this;
2431 const { point } = PathElement.pathM(pathParser);
2432 const { x, y } = point;
2433 pathParser.addMarker(point);
2434 boundingBox.addPoint(x, y);
2435 if (ctx) {
2436 ctx.moveTo(x, y);
2437 }
2438 }
2439 static pathL(pathParser) {
2440 const { current } = pathParser;
2441 const point = pathParser.getAsCurrentPoint();
2442 return {
2443 current,
2444 point
2445 };
2446 }
2447 pathL(ctx, boundingBox) {
2448 const { pathParser } = this;
2449 const { current, point } = PathElement.pathL(pathParser);
2450 const { x, y } = point;
2451 pathParser.addMarker(point, current);
2452 boundingBox.addPoint(x, y);
2453 if (ctx) {
2454 ctx.lineTo(x, y);
2455 }
2456 }
2457 static pathH(pathParser) {
2458 const { current, command } = pathParser;
2459 const point = new Point((command.relative ? current.x : 0) + command.x, current.y);
2460 pathParser.current = point;
2461 return {
2462 current,
2463 point
2464 };
2465 }
2466 pathH(ctx, boundingBox) {
2467 const { pathParser } = this;
2468 const { current, point } = PathElement.pathH(pathParser);
2469 const { x, y } = point;
2470 pathParser.addMarker(point, current);
2471 boundingBox.addPoint(x, y);
2472 if (ctx) {
2473 ctx.lineTo(x, y);
2474 }
2475 }
2476 static pathV(pathParser) {
2477 const { current, command } = pathParser;
2478 const point = new Point(current.x, (command.relative ? current.y : 0) + command.y);
2479 pathParser.current = point;
2480 return {
2481 current,
2482 point
2483 };
2484 }
2485 pathV(ctx, boundingBox) {
2486 const { pathParser } = this;
2487 const { current, point } = PathElement.pathV(pathParser);
2488 const { x, y } = point;
2489 pathParser.addMarker(point, current);
2490 boundingBox.addPoint(x, y);
2491 if (ctx) {
2492 ctx.lineTo(x, y);
2493 }
2494 }
2495 static pathC(pathParser) {
2496 const { current } = pathParser;
2497 const point = pathParser.getPoint('x1', 'y1');
2498 const controlPoint = pathParser.getAsControlPoint('x2', 'y2');
2499 const currentPoint = pathParser.getAsCurrentPoint();
2500 return {
2501 current,
2502 point,
2503 controlPoint,
2504 currentPoint
2505 };
2506 }
2507 pathC(ctx, boundingBox) {
2508 const { pathParser } = this;
2509 const { current, point, controlPoint, currentPoint } = PathElement.pathC(pathParser);
2510 pathParser.addMarker(currentPoint, controlPoint, point);
2511 boundingBox.addBezierCurve(current.x, current.y, point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2512 if (ctx) {
2513 ctx.bezierCurveTo(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2514 }
2515 }
2516 static pathS(pathParser) {
2517 const { current } = pathParser;
2518 const point = pathParser.getReflectedControlPoint();
2519 const controlPoint = pathParser.getAsControlPoint('x2', 'y2');
2520 const currentPoint = pathParser.getAsCurrentPoint();
2521 return {
2522 current,
2523 point,
2524 controlPoint,
2525 currentPoint
2526 };
2527 }
2528 pathS(ctx, boundingBox) {
2529 const { pathParser } = this;
2530 const { current, point, controlPoint, currentPoint } = PathElement.pathS(pathParser);
2531 pathParser.addMarker(currentPoint, controlPoint, point);
2532 boundingBox.addBezierCurve(current.x, current.y, point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2533 if (ctx) {
2534 ctx.bezierCurveTo(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2535 }
2536 }
2537 static pathQ(pathParser) {
2538 const { current } = pathParser;
2539 const controlPoint = pathParser.getAsControlPoint('x1', 'y1');
2540 const currentPoint = pathParser.getAsCurrentPoint();
2541 return {
2542 current,
2543 controlPoint,
2544 currentPoint
2545 };
2546 }
2547 pathQ(ctx, boundingBox) {
2548 const { pathParser } = this;
2549 const { current, controlPoint, currentPoint } = PathElement.pathQ(pathParser);
2550 pathParser.addMarker(currentPoint, controlPoint, controlPoint);
2551 boundingBox.addQuadraticCurve(current.x, current.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2552 if (ctx) {
2553 ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2554 }
2555 }
2556 static pathT(pathParser) {
2557 const { current } = pathParser;
2558 const controlPoint = pathParser.getReflectedControlPoint();
2559 pathParser.control = controlPoint;
2560 const currentPoint = pathParser.getAsCurrentPoint();
2561 return {
2562 current,
2563 controlPoint,
2564 currentPoint
2565 };
2566 }
2567 pathT(ctx, boundingBox) {
2568 const { pathParser } = this;
2569 const { current, controlPoint, currentPoint } = PathElement.pathT(pathParser);
2570 pathParser.addMarker(currentPoint, controlPoint, controlPoint);
2571 boundingBox.addQuadraticCurve(current.x, current.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2572 if (ctx) {
2573 ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2574 }
2575 }
2576 static pathA(pathParser) {
2577 const { current, command } = pathParser;
2578 let { rX, rY, xRot, lArcFlag, sweepFlag } = command;
2579 const xAxisRotation = xRot * (Math.PI / 180.0);
2580 const currentPoint = pathParser.getAsCurrentPoint();
2581 // Conversion from endpoint to center parameterization
2582 // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
2583 // x1', y1'
2584 const currp = new Point(Math.cos(xAxisRotation) * (current.x - currentPoint.x) / 2.0
2585 + Math.sin(xAxisRotation) * (current.y - currentPoint.y) / 2.0, -Math.sin(xAxisRotation) * (current.x - currentPoint.x) / 2.0
2586 + Math.cos(xAxisRotation) * (current.y - currentPoint.y) / 2.0);
2587 // adjust radii
2588 const l = (Math.pow(currp.x, 2) / Math.pow(rX, 2)
2589 + Math.pow(currp.y, 2) / Math.pow(rY, 2));
2590 if (l > 1) {
2591 rX *= Math.sqrt(l);
2592 rY *= Math.sqrt(l);
2593 }
2594 // cx', cy'
2595 let s = (lArcFlag === sweepFlag ? -1 : 1) * Math.sqrt(((Math.pow(rX, 2) * Math.pow(rY, 2))
2596 - (Math.pow(rX, 2) * Math.pow(currp.y, 2))
2597 - (Math.pow(rY, 2) * Math.pow(currp.x, 2))) / (Math.pow(rX, 2) * Math.pow(currp.y, 2)
2598 + Math.pow(rY, 2) * Math.pow(currp.x, 2)));
2599 if (isNaN(s)) {
2600 s = 0;
2601 }
2602 const cpp = new Point(s * rX * currp.y / rY, s * -rY * currp.x / rX);
2603 // cx, cy
2604 const centp = new Point((current.x + currentPoint.x) / 2.0
2605 + Math.cos(xAxisRotation) * cpp.x
2606 - Math.sin(xAxisRotation) * cpp.y, (current.y + currentPoint.y) / 2.0
2607 + Math.sin(xAxisRotation) * cpp.x
2608 + Math.cos(xAxisRotation) * cpp.y);
2609 // initial angle
2610 const a1 = vectorsAngle([1, 0], [(currp.x - cpp.x) / rX, (currp.y - cpp.y) / rY]); // θ1
2611 // angle delta
2612 const u = [(currp.x - cpp.x) / rX, (currp.y - cpp.y) / rY];
2613 const v = [(-currp.x - cpp.x) / rX, (-currp.y - cpp.y) / rY];
2614 let ad = vectorsAngle(u, v); // Δθ
2615 if (vectorsRatio(u, v) <= -1) {
2616 ad = Math.PI;
2617 }
2618 if (vectorsRatio(u, v) >= 1) {
2619 ad = 0;
2620 }
2621 return {
2622 currentPoint,
2623 rX,
2624 rY,
2625 sweepFlag,
2626 xAxisRotation,
2627 centp,
2628 a1,
2629 ad
2630 };
2631 }
2632 pathA(ctx, boundingBox) {
2633 const { pathParser } = this;
2634 const { currentPoint, rX, rY, sweepFlag, xAxisRotation, centp, a1, ad } = PathElement.pathA(pathParser);
2635 // for markers
2636 const dir = 1 - sweepFlag ? 1.0 : -1.0;
2637 const ah = a1 + dir * (ad / 2.0);
2638 const halfWay = new Point(centp.x + rX * Math.cos(ah), centp.y + rY * Math.sin(ah));
2639 pathParser.addMarkerAngle(halfWay, ah - dir * Math.PI / 2);
2640 pathParser.addMarkerAngle(currentPoint, ah - dir * Math.PI);
2641 boundingBox.addPoint(currentPoint.x, currentPoint.y); // TODO: this is too naive, make it better
2642 if (ctx && !isNaN(a1) && !isNaN(ad)) {
2643 const r = rX > rY ? rX : rY;
2644 const sx = rX > rY ? 1 : rX / rY;
2645 const sy = rX > rY ? rY / rX : 1;
2646 ctx.translate(centp.x, centp.y);
2647 ctx.rotate(xAxisRotation);
2648 ctx.scale(sx, sy);
2649 ctx.arc(0, 0, r, a1, a1 + ad, Boolean(1 - sweepFlag));
2650 ctx.scale(1 / sx, 1 / sy);
2651 ctx.rotate(-xAxisRotation);
2652 ctx.translate(-centp.x, -centp.y);
2653 }
2654 }
2655 static pathZ(pathParser) {
2656 pathParser.current = pathParser.start;
2657 }
2658 pathZ(ctx, boundingBox) {
2659 PathElement.pathZ(this.pathParser);
2660 if (ctx) {
2661 // only close path if it is not a straight line
2662 if (boundingBox.x1 !== boundingBox.x2
2663 && boundingBox.y1 !== boundingBox.y2) {
2664 ctx.closePath();
2665 }
2666 }
2667 }
2668}
2669
2670class SVGElement extends RenderedElement {
2671 constructor() {
2672 super(...arguments);
2673 this.type = 'svg';
2674 this.root = false;
2675 }
2676 setContext(ctx) {
2677 const { document } = this;
2678 const { screen, window } = document;
2679 const canvas = ctx.canvas;
2680 screen.setDefaults(ctx);
2681 if (canvas.style
2682 && typeof ctx.font !== 'undefined'
2683 && window
2684 && typeof window.getComputedStyle !== 'undefined') {
2685 ctx.font = window.getComputedStyle(canvas).getPropertyValue('font');
2686 const fontSizeProp = new Property(document, 'fontSize', Font.parse(ctx.font).fontSize);
2687 if (fontSizeProp.hasValue()) {
2688 document.rootEmSize = fontSizeProp.getPixels('y');
2689 document.emSize = document.rootEmSize;
2690 }
2691 }
2692 // create new view port
2693 if (!this.getAttribute('x').hasValue()) {
2694 this.getAttribute('x', true).setValue(0);
2695 }
2696 if (!this.getAttribute('y').hasValue()) {
2697 this.getAttribute('y', true).setValue(0);
2698 }
2699 let { width, height } = screen.viewPort;
2700 if (!this.getStyle('width').hasValue()) {
2701 this.getStyle('width', true).setValue('100%');
2702 }
2703 if (!this.getStyle('height').hasValue()) {
2704 this.getStyle('height', true).setValue('100%');
2705 }
2706 if (!this.getStyle('color').hasValue()) {
2707 this.getStyle('color', true).setValue('black');
2708 }
2709 const refXAttr = this.getAttribute('refX');
2710 const refYAttr = this.getAttribute('refY');
2711 const viewBoxAttr = this.getAttribute('viewBox');
2712 const viewBox = viewBoxAttr.hasValue()
2713 ? toNumbers(viewBoxAttr.getString())
2714 : null;
2715 const clip = !this.root
2716 && this.getStyle('overflow').getValue('hidden') !== 'visible';
2717 let minX = 0;
2718 let minY = 0;
2719 let clipX = 0;
2720 let clipY = 0;
2721 if (viewBox) {
2722 minX = viewBox[0];
2723 minY = viewBox[1];
2724 }
2725 if (!this.root) {
2726 width = this.getStyle('width').getPixels('x');
2727 height = this.getStyle('height').getPixels('y');
2728 if (this.type === 'marker') {
2729 clipX = minX;
2730 clipY = minY;
2731 minX = 0;
2732 minY = 0;
2733 }
2734 }
2735 screen.viewPort.setCurrent(width, height);
2736 if (this.node // is not temporary SVGElement
2737 && this.getStyle('transform', false, true).hasValue()
2738 && !this.getStyle('transform-origin', false, true).hasValue()) {
2739 this.getStyle('transform-origin', true, true).setValue('50% 50%');
2740 }
2741 super.setContext(ctx);
2742 ctx.translate(this.getAttribute('x').getPixels('x'), this.getAttribute('y').getPixels('y'));
2743 if (viewBox) {
2744 width = viewBox[2];
2745 height = viewBox[3];
2746 }
2747 document.setViewBox({
2748 ctx,
2749 aspectRatio: this.getAttribute('preserveAspectRatio').getString(),
2750 width: screen.viewPort.width,
2751 desiredWidth: width,
2752 height: screen.viewPort.height,
2753 desiredHeight: height,
2754 minX,
2755 minY,
2756 refX: refXAttr.getValue(),
2757 refY: refYAttr.getValue(),
2758 clip,
2759 clipX,
2760 clipY
2761 });
2762 if (viewBox) {
2763 screen.viewPort.removeCurrent();
2764 screen.viewPort.setCurrent(width, height);
2765 }
2766 }
2767 clearContext(ctx) {
2768 super.clearContext(ctx);
2769 this.document.screen.viewPort.removeCurrent();
2770 }
2771 /**
2772 * Resize SVG to fit in given size.
2773 * @param width
2774 * @param height
2775 * @param preserveAspectRatio
2776 */
2777 resize(width, height = width, preserveAspectRatio = false) {
2778 const widthAttr = this.getAttribute('width', true);
2779 const heightAttr = this.getAttribute('height', true);
2780 const viewBoxAttr = this.getAttribute('viewBox');
2781 const styleAttr = this.getAttribute('style');
2782 const originWidth = widthAttr.getNumber(0);
2783 const originHeight = heightAttr.getNumber(0);
2784 if (preserveAspectRatio) {
2785 if (typeof preserveAspectRatio === 'string') {
2786 this.getAttribute('preserveAspectRatio', true).setValue(preserveAspectRatio);
2787 }
2788 else {
2789 const preserveAspectRatioAttr = this.getAttribute('preserveAspectRatio');
2790 if (preserveAspectRatioAttr.hasValue()) {
2791 preserveAspectRatioAttr.setValue(preserveAspectRatioAttr.getString().replace(/^\s*(\S.*\S)\s*$/, '$1'));
2792 }
2793 }
2794 }
2795 widthAttr.setValue(width);
2796 heightAttr.setValue(height);
2797 if (!viewBoxAttr.hasValue()) {
2798 viewBoxAttr.setValue(`0 0 ${originWidth || width} ${originHeight || height}`);
2799 }
2800 if (styleAttr.hasValue()) {
2801 const widthStyle = this.getStyle('width');
2802 const heightStyle = this.getStyle('height');
2803 if (widthStyle.hasValue()) {
2804 widthStyle.setValue(`${width}px`);
2805 }
2806 if (heightStyle.hasValue()) {
2807 heightStyle.setValue(`${height}px`);
2808 }
2809 }
2810 }
2811}
2812
2813class RectElement extends PathElement {
2814 constructor() {
2815 super(...arguments);
2816 this.type = 'rect';
2817 }
2818 path(ctx) {
2819 const x = this.getAttribute('x').getPixels('x');
2820 const y = this.getAttribute('y').getPixels('y');
2821 const width = this.getStyle('width', false, true).getPixels('x');
2822 const height = this.getStyle('height', false, true).getPixels('y');
2823 const rxAttr = this.getAttribute('rx');
2824 const ryAttr = this.getAttribute('ry');
2825 let rx = rxAttr.getPixels('x');
2826 let ry = ryAttr.getPixels('y');
2827 if (rxAttr.hasValue() && !ryAttr.hasValue()) {
2828 ry = rx;
2829 }
2830 if (ryAttr.hasValue() && !rxAttr.hasValue()) {
2831 rx = ry;
2832 }
2833 rx = Math.min(rx, width / 2.0);
2834 ry = Math.min(ry, height / 2.0);
2835 if (ctx) {
2836 const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3);
2837 ctx.beginPath(); // always start the path so we don't fill prior paths
2838 if (height > 0 && width > 0) {
2839 ctx.moveTo(x + rx, y);
2840 ctx.lineTo(x + width - rx, y);
2841 ctx.bezierCurveTo(x + width - rx + (KAPPA * rx), y, x + width, y + ry - (KAPPA * ry), x + width, y + ry);
2842 ctx.lineTo(x + width, y + height - ry);
2843 ctx.bezierCurveTo(x + width, y + height - ry + (KAPPA * ry), x + width - rx + (KAPPA * rx), y + height, x + width - rx, y + height);
2844 ctx.lineTo(x + rx, y + height);
2845 ctx.bezierCurveTo(x + rx - (KAPPA * rx), y + height, x, y + height - ry + (KAPPA * ry), x, y + height - ry);
2846 ctx.lineTo(x, y + ry);
2847 ctx.bezierCurveTo(x, y + ry - (KAPPA * ry), x + rx - (KAPPA * rx), y, x + rx, y);
2848 ctx.closePath();
2849 }
2850 }
2851 return new BoundingBox(x, y, x + width, y + height);
2852 }
2853 getMarkers() {
2854 return null;
2855 }
2856}
2857
2858class CircleElement extends PathElement {
2859 constructor() {
2860 super(...arguments);
2861 this.type = 'circle';
2862 }
2863 path(ctx) {
2864 const cx = this.getAttribute('cx').getPixels('x');
2865 const cy = this.getAttribute('cy').getPixels('y');
2866 const r = this.getAttribute('r').getPixels();
2867 if (ctx && r > 0) {
2868 ctx.beginPath();
2869 ctx.arc(cx, cy, r, 0, Math.PI * 2, false);
2870 ctx.closePath();
2871 }
2872 return new BoundingBox(cx - r, cy - r, cx + r, cy + r);
2873 }
2874 getMarkers() {
2875 return null;
2876 }
2877}
2878
2879class EllipseElement extends PathElement {
2880 constructor() {
2881 super(...arguments);
2882 this.type = 'ellipse';
2883 }
2884 path(ctx) {
2885 const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3);
2886 const rx = this.getAttribute('rx').getPixels('x');
2887 const ry = this.getAttribute('ry').getPixels('y');
2888 const cx = this.getAttribute('cx').getPixels('x');
2889 const cy = this.getAttribute('cy').getPixels('y');
2890 if (ctx && rx > 0 && ry > 0) {
2891 ctx.beginPath();
2892 ctx.moveTo(cx + rx, cy);
2893 ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry);
2894 ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy);
2895 ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry);
2896 ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy);
2897 ctx.closePath();
2898 }
2899 return new BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry);
2900 }
2901 getMarkers() {
2902 return null;
2903 }
2904}
2905
2906class LineElement extends PathElement {
2907 constructor() {
2908 super(...arguments);
2909 this.type = 'line';
2910 }
2911 getPoints() {
2912 return [
2913 new Point(this.getAttribute('x1').getPixels('x'), this.getAttribute('y1').getPixels('y')),
2914 new Point(this.getAttribute('x2').getPixels('x'), this.getAttribute('y2').getPixels('y'))
2915 ];
2916 }
2917 path(ctx) {
2918 const [{ x: x0, y: y0 }, { x: x1, y: y1 }] = this.getPoints();
2919 if (ctx) {
2920 ctx.beginPath();
2921 ctx.moveTo(x0, y0);
2922 ctx.lineTo(x1, y1);
2923 }
2924 return new BoundingBox(x0, y0, x1, y1);
2925 }
2926 getMarkers() {
2927 const [p0, p1] = this.getPoints();
2928 const a = p0.angleTo(p1);
2929 return [
2930 [p0, a],
2931 [p1, a]
2932 ];
2933 }
2934}
2935
2936class PolylineElement extends PathElement {
2937 constructor(document, node, captureTextNodes) {
2938 super(document, node, captureTextNodes);
2939 this.type = 'polyline';
2940 this.points = [];
2941 this.points = Point.parsePath(this.getAttribute('points').getString());
2942 }
2943 path(ctx) {
2944 const { points } = this;
2945 const [{ x: x0, y: y0 }] = points;
2946 const boundingBox = new BoundingBox(x0, y0);
2947 if (ctx) {
2948 ctx.beginPath();
2949 ctx.moveTo(x0, y0);
2950 }
2951 points.forEach(({ x, y }) => {
2952 boundingBox.addPoint(x, y);
2953 if (ctx) {
2954 ctx.lineTo(x, y);
2955 }
2956 });
2957 return boundingBox;
2958 }
2959 getMarkers() {
2960 const { points } = this;
2961 const lastIndex = points.length - 1;
2962 const markers = [];
2963 points.forEach((point, i) => {
2964 if (i === lastIndex) {
2965 return;
2966 }
2967 markers.push([
2968 point,
2969 point.angleTo(points[i + 1])
2970 ]);
2971 });
2972 if (markers.length > 0) {
2973 markers.push([
2974 points[points.length - 1],
2975 markers[markers.length - 1][1]
2976 ]);
2977 }
2978 return markers;
2979 }
2980}
2981
2982class PolygonElement extends PolylineElement {
2983 constructor() {
2984 super(...arguments);
2985 this.type = 'polygon';
2986 }
2987 path(ctx) {
2988 const boundingBox = super.path(ctx);
2989 const [{ x, y }] = this.points;
2990 if (ctx) {
2991 ctx.lineTo(x, y);
2992 ctx.closePath();
2993 }
2994 return boundingBox;
2995 }
2996}
2997
2998class PatternElement extends Element {
2999 constructor() {
3000 super(...arguments);
3001 this.type = 'pattern';
3002 }
3003 createPattern(ctx, _, parentOpacityProp) {
3004 const width = this.getStyle('width').getPixels('x', true);
3005 const height = this.getStyle('height').getPixels('y', true);
3006 // render me using a temporary svg element
3007 const patternSvg = new SVGElement(this.document, null);
3008 patternSvg.attributes.viewBox = new Property(this.document, 'viewBox', this.getAttribute('viewBox').getValue());
3009 patternSvg.attributes.width = new Property(this.document, 'width', `${width}px`);
3010 patternSvg.attributes.height = new Property(this.document, 'height', `${height}px`);
3011 patternSvg.attributes.transform = new Property(this.document, 'transform', this.getAttribute('patternTransform').getValue());
3012 patternSvg.children = this.children;
3013 const patternCanvas = this.document.createCanvas(width, height);
3014 const patternCtx = patternCanvas.getContext('2d');
3015 const xAttr = this.getAttribute('x');
3016 const yAttr = this.getAttribute('y');
3017 if (xAttr.hasValue() && yAttr.hasValue()) {
3018 patternCtx.translate(xAttr.getPixels('x', true), yAttr.getPixels('y', true));
3019 }
3020 if (parentOpacityProp.hasValue()) {
3021 this.styles['fill-opacity'] = parentOpacityProp;
3022 }
3023 else {
3024 Reflect.deleteProperty(this.styles, 'fill-opacity');
3025 }
3026 // render 3x3 grid so when we transform there's no white space on edges
3027 for (let x = -1; x <= 1; x++) {
3028 for (let y = -1; y <= 1; y++) {
3029 patternCtx.save();
3030 patternSvg.attributes.x = new Property(this.document, 'x', x * patternCanvas.width);
3031 patternSvg.attributes.y = new Property(this.document, 'y', y * patternCanvas.height);
3032 patternSvg.render(patternCtx);
3033 patternCtx.restore();
3034 }
3035 }
3036 const pattern = ctx.createPattern(patternCanvas, 'repeat');
3037 return pattern;
3038 }
3039}
3040
3041class MarkerElement extends Element {
3042 constructor() {
3043 super(...arguments);
3044 this.type = 'marker';
3045 }
3046 render(ctx, point, angle) {
3047 if (!point) {
3048 return;
3049 }
3050 const { x, y } = point;
3051 const orient = this.getAttribute('orient').getValue('auto');
3052 const markerUnits = this.getAttribute('markerUnits').getValue('strokeWidth');
3053 ctx.translate(x, y);
3054 if (orient === 'auto') {
3055 ctx.rotate(angle);
3056 }
3057 if (markerUnits === 'strokeWidth') {
3058 ctx.scale(ctx.lineWidth, ctx.lineWidth);
3059 }
3060 ctx.save();
3061 // render me using a temporary svg element
3062 const markerSvg = new SVGElement(this.document, null);
3063 markerSvg.type = this.type;
3064 markerSvg.attributes.viewBox = new Property(this.document, 'viewBox', this.getAttribute('viewBox').getValue());
3065 markerSvg.attributes.refX = new Property(this.document, 'refX', this.getAttribute('refX').getValue());
3066 markerSvg.attributes.refY = new Property(this.document, 'refY', this.getAttribute('refY').getValue());
3067 markerSvg.attributes.width = new Property(this.document, 'width', this.getAttribute('markerWidth').getValue());
3068 markerSvg.attributes.height = new Property(this.document, 'height', this.getAttribute('markerHeight').getValue());
3069 markerSvg.attributes.overflow = new Property(this.document, 'overflow', this.getAttribute('overflow').getValue());
3070 markerSvg.attributes.fill = new Property(this.document, 'fill', this.getAttribute('fill').getColor('black'));
3071 markerSvg.attributes.stroke = new Property(this.document, 'stroke', this.getAttribute('stroke').getValue('none'));
3072 markerSvg.children = this.children;
3073 markerSvg.render(ctx);
3074 ctx.restore();
3075 if (markerUnits === 'strokeWidth') {
3076 ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth);
3077 }
3078 if (orient === 'auto') {
3079 ctx.rotate(-angle);
3080 }
3081 ctx.translate(-x, -y);
3082 }
3083}
3084
3085class DefsElement extends Element {
3086 constructor() {
3087 super(...arguments);
3088 this.type = 'defs';
3089 }
3090 render() {
3091 // NOOP
3092 }
3093}
3094
3095class GElement extends RenderedElement {
3096 constructor() {
3097 super(...arguments);
3098 this.type = 'g';
3099 }
3100 getBoundingBox(ctx) {
3101 const boundingBox = new BoundingBox();
3102 this.children.forEach((child) => {
3103 boundingBox.addBoundingBox(child.getBoundingBox(ctx));
3104 });
3105 return boundingBox;
3106 }
3107}
3108
3109class GradientElement extends Element {
3110 constructor(document, node, captureTextNodes) {
3111 super(document, node, captureTextNodes);
3112 this.attributesToInherit = [
3113 'gradientUnits'
3114 ];
3115 this.stops = [];
3116 const { stops, children } = this;
3117 children.forEach((child) => {
3118 if (child.type === 'stop') {
3119 stops.push(child);
3120 }
3121 });
3122 }
3123 getGradientUnits() {
3124 return this.getAttribute('gradientUnits').getString('objectBoundingBox');
3125 }
3126 createGradient(ctx, element, parentOpacityProp) {
3127 // tslint:disable-next-line: no-this-assignment
3128 let stopsContainer = this;
3129 if (this.getHrefAttribute().hasValue()) {
3130 stopsContainer = this.getHrefAttribute().getDefinition();
3131 this.inheritStopContainer(stopsContainer);
3132 }
3133 const { stops } = stopsContainer;
3134 const gradient = this.getGradient(ctx, element);
3135 if (!gradient) {
3136 return this.addParentOpacity(parentOpacityProp, stops[stops.length - 1].color);
3137 }
3138 stops.forEach((stop) => {
3139 gradient.addColorStop(stop.offset, this.addParentOpacity(parentOpacityProp, stop.color));
3140 });
3141 if (this.getAttribute('gradientTransform').hasValue()) {
3142 // render as transformed pattern on temporary canvas
3143 const { document } = this;
3144 const { MAX_VIRTUAL_PIXELS, viewPort } = document.screen;
3145 const [rootView] = viewPort.viewPorts;
3146 const rect = new RectElement(document, null);
3147 rect.attributes.x = new Property(document, 'x', -MAX_VIRTUAL_PIXELS / 3.0);
3148 rect.attributes.y = new Property(document, 'y', -MAX_VIRTUAL_PIXELS / 3.0);
3149 rect.attributes.width = new Property(document, 'width', MAX_VIRTUAL_PIXELS);
3150 rect.attributes.height = new Property(document, 'height', MAX_VIRTUAL_PIXELS);
3151 const group = new GElement(document, null);
3152 group.attributes.transform = new Property(document, 'transform', this.getAttribute('gradientTransform').getValue());
3153 group.children = [rect];
3154 const patternSvg = new SVGElement(document, null);
3155 patternSvg.attributes.x = new Property(document, 'x', 0);
3156 patternSvg.attributes.y = new Property(document, 'y', 0);
3157 patternSvg.attributes.width = new Property(document, 'width', rootView.width);
3158 patternSvg.attributes.height = new Property(document, 'height', rootView.height);
3159 patternSvg.children = [group];
3160 const patternCanvas = document.createCanvas(rootView.width, rootView.height);
3161 const patternCtx = patternCanvas.getContext('2d');
3162 patternCtx.fillStyle = gradient;
3163 patternSvg.render(patternCtx);
3164 return patternCtx.createPattern(patternCanvas, 'no-repeat');
3165 }
3166 return gradient;
3167 }
3168 inheritStopContainer(stopsContainer) {
3169 this.attributesToInherit.forEach((attributeToInherit) => {
3170 if (!this.getAttribute(attributeToInherit).hasValue()
3171 && stopsContainer.getAttribute(attributeToInherit).hasValue()) {
3172 this.getAttribute(attributeToInherit, true)
3173 .setValue(stopsContainer.getAttribute(attributeToInherit).getValue());
3174 }
3175 });
3176 }
3177 addParentOpacity(parentOpacityProp, color) {
3178 if (parentOpacityProp.hasValue()) {
3179 const colorProp = new Property(this.document, 'color', color);
3180 return colorProp.addOpacity(parentOpacityProp).getColor();
3181 }
3182 return color;
3183 }
3184}
3185
3186class LinearGradientElement extends GradientElement {
3187 constructor(document, node, captureTextNodes) {
3188 super(document, node, captureTextNodes);
3189 this.type = 'linearGradient';
3190 this.attributesToInherit.push('x1', 'y1', 'x2', 'y2');
3191 }
3192 getGradient(ctx, element) {
3193 const isBoundingBoxUnits = this.getGradientUnits() === 'objectBoundingBox';
3194 const boundingBox = isBoundingBoxUnits
3195 ? element.getBoundingBox(ctx)
3196 : null;
3197 if (isBoundingBoxUnits && !boundingBox) {
3198 return null;
3199 }
3200 if (!this.getAttribute('x1').hasValue()
3201 && !this.getAttribute('y1').hasValue()
3202 && !this.getAttribute('x2').hasValue()
3203 && !this.getAttribute('y2').hasValue()) {
3204 this.getAttribute('x1', true).setValue(0);
3205 this.getAttribute('y1', true).setValue(0);
3206 this.getAttribute('x2', true).setValue(1);
3207 this.getAttribute('y2', true).setValue(0);
3208 }
3209 const x1 = isBoundingBoxUnits
3210 ? boundingBox.x + boundingBox.width * this.getAttribute('x1').getNumber()
3211 : this.getAttribute('x1').getPixels('x');
3212 const y1 = isBoundingBoxUnits
3213 ? boundingBox.y + boundingBox.height * this.getAttribute('y1').getNumber()
3214 : this.getAttribute('y1').getPixels('y');
3215 const x2 = isBoundingBoxUnits
3216 ? boundingBox.x + boundingBox.width * this.getAttribute('x2').getNumber()
3217 : this.getAttribute('x2').getPixels('x');
3218 const y2 = isBoundingBoxUnits
3219 ? boundingBox.y + boundingBox.height * this.getAttribute('y2').getNumber()
3220 : this.getAttribute('y2').getPixels('y');
3221 if (x1 === x2 && y1 === y2) {
3222 return null;
3223 }
3224 return ctx.createLinearGradient(x1, y1, x2, y2);
3225 }
3226}
3227
3228class RadialGradientElement extends GradientElement {
3229 constructor(document, node, captureTextNodes) {
3230 super(document, node, captureTextNodes);
3231 this.type = 'radialGradient';
3232 this.attributesToInherit.push('cx', 'cy', 'r', 'fx', 'fy', 'fr');
3233 }
3234 getGradient(ctx, element) {
3235 const isBoundingBoxUnits = this.getGradientUnits() === 'objectBoundingBox';
3236 const boundingBox = element.getBoundingBox(ctx);
3237 if (isBoundingBoxUnits && !boundingBox) {
3238 return null;
3239 }
3240 if (!this.getAttribute('cx').hasValue()) {
3241 this.getAttribute('cx', true).setValue('50%');
3242 }
3243 if (!this.getAttribute('cy').hasValue()) {
3244 this.getAttribute('cy', true).setValue('50%');
3245 }
3246 if (!this.getAttribute('r').hasValue()) {
3247 this.getAttribute('r', true).setValue('50%');
3248 }
3249 const cx = isBoundingBoxUnits
3250 ? boundingBox.x + boundingBox.width * this.getAttribute('cx').getNumber()
3251 : this.getAttribute('cx').getPixels('x');
3252 const cy = isBoundingBoxUnits
3253 ? boundingBox.y + boundingBox.height * this.getAttribute('cy').getNumber()
3254 : this.getAttribute('cy').getPixels('y');
3255 let fx = cx;
3256 let fy = cy;
3257 if (this.getAttribute('fx').hasValue()) {
3258 fx = isBoundingBoxUnits
3259 ? boundingBox.x + boundingBox.width * this.getAttribute('fx').getNumber()
3260 : this.getAttribute('fx').getPixels('x');
3261 }
3262 if (this.getAttribute('fy').hasValue()) {
3263 fy = isBoundingBoxUnits
3264 ? boundingBox.y + boundingBox.height * this.getAttribute('fy').getNumber()
3265 : this.getAttribute('fy').getPixels('y');
3266 }
3267 const r = isBoundingBoxUnits
3268 ? (boundingBox.width + boundingBox.height) / 2.0 * this.getAttribute('r').getNumber()
3269 : this.getAttribute('r').getPixels();
3270 const fr = this.getAttribute('fr').getPixels();
3271 return ctx.createRadialGradient(fx, fy, fr, cx, cy, r);
3272 }
3273}
3274
3275class StopElement extends Element {
3276 constructor(document, node, captureTextNodes) {
3277 super(document, node, captureTextNodes);
3278 this.type = 'stop';
3279 const offset = Math.max(0, Math.min(1, this.getAttribute('offset').getNumber()));
3280 const stopOpacity = this.getStyle('stop-opacity');
3281 let stopColor = this.getStyle('stop-color', true);
3282 if (stopColor.getString() === '') {
3283 stopColor.setValue('#000');
3284 }
3285 if (stopOpacity.hasValue()) {
3286 stopColor = stopColor.addOpacity(stopOpacity);
3287 }
3288 this.offset = offset;
3289 this.color = stopColor.getColor();
3290 }
3291}
3292
3293class AnimateElement extends Element {
3294 constructor(document, node, captureTextNodes) {
3295 super(document, node, captureTextNodes);
3296 this.type = 'animate';
3297 this.duration = 0;
3298 this.initialValue = null;
3299 this.initialUnits = '';
3300 this.removed = false;
3301 this.frozen = false;
3302 document.screen.animations.push(this);
3303 this.begin = this.getAttribute('begin').getMilliseconds();
3304 this.maxDuration = this.begin + this.getAttribute('dur').getMilliseconds();
3305 this.from = this.getAttribute('from');
3306 this.to = this.getAttribute('to');
3307 this.values = this.getAttribute('values');
3308 if (this.values.hasValue()) {
3309 this.values.setValue(this.values.getString().split(';'));
3310 }
3311 }
3312 getProperty() {
3313 const attributeType = this.getAttribute('attributeType').getString();
3314 const attributeName = this.getAttribute('attributeName').getString();
3315 if (attributeType === 'CSS') {
3316 return this.parent.getStyle(attributeName, true);
3317 }
3318 return this.parent.getAttribute(attributeName, true);
3319 }
3320 calcValue() {
3321 const { initialUnits } = this;
3322 const { progress, from, to } = this.getProgress();
3323 // tween value linearly
3324 let newValue = from.getNumber() + (to.getNumber() - from.getNumber()) * progress;
3325 if (initialUnits === '%') {
3326 newValue *= 100.0; // numValue() returns 0-1 whereas properties are 0-100
3327 }
3328 return `${newValue}${initialUnits}`;
3329 }
3330 update(delta) {
3331 const { parent } = this;
3332 const prop = this.getProperty();
3333 // set initial value
3334 if (!this.initialValue) {
3335 this.initialValue = prop.getString();
3336 this.initialUnits = prop.getUnits();
3337 }
3338 // if we're past the end time
3339 if (this.duration > this.maxDuration) {
3340 const fill = this.getAttribute('fill').getString('remove');
3341 // loop for indefinitely repeating animations
3342 if (this.getAttribute('repeatCount').getString() === 'indefinite'
3343 || this.getAttribute('repeatDur').getString() === 'indefinite') {
3344 this.duration = 0;
3345 }
3346 else if (fill === 'freeze' && !this.frozen) {
3347 this.frozen = true;
3348 parent.animationFrozen = true;
3349 parent.animationFrozenValue = prop.getString();
3350 }
3351 else if (fill === 'remove' && !this.removed) {
3352 this.removed = true;
3353 prop.setValue(parent.animationFrozen
3354 ? parent.animationFrozenValue
3355 : this.initialValue);
3356 return true;
3357 }
3358 return false;
3359 }
3360 this.duration += delta;
3361 // if we're past the begin time
3362 let updated = false;
3363 if (this.begin < this.duration) {
3364 let newValue = this.calcValue(); // tween
3365 const typeAttr = this.getAttribute('type');
3366 if (typeAttr.hasValue()) {
3367 // for transform, etc.
3368 const type = typeAttr.getString();
3369 newValue = `${type}(${newValue})`;
3370 }
3371 prop.setValue(newValue);
3372 updated = true;
3373 }
3374 return updated;
3375 }
3376 getProgress() {
3377 const { document, values } = this;
3378 const result = {
3379 progress: (this.duration - this.begin) / (this.maxDuration - this.begin)
3380 };
3381 if (values.hasValue()) {
3382 const p = result.progress * (values.getValue().length - 1);
3383 const lb = Math.floor(p);
3384 const ub = Math.ceil(p);
3385 result.from = new Property(document, 'from', parseFloat(values.getValue()[lb]));
3386 result.to = new Property(document, 'to', parseFloat(values.getValue()[ub]));
3387 result.progress = (p - lb) / (ub - lb);
3388 }
3389 else {
3390 result.from = this.from;
3391 result.to = this.to;
3392 }
3393 return result;
3394 }
3395}
3396
3397class AnimateColorElement extends AnimateElement {
3398 constructor() {
3399 super(...arguments);
3400 this.type = 'animateColor';
3401 }
3402 calcValue() {
3403 const { progress, from, to } = this.getProgress();
3404 const colorFrom = new RGBColor(from.getColor());
3405 const colorTo = new RGBColor(to.getColor());
3406 if (colorFrom.ok && colorTo.ok) {
3407 // tween color linearly
3408 const r = colorFrom.r + (colorTo.r - colorFrom.r) * progress;
3409 const g = colorFrom.g + (colorTo.g - colorFrom.g) * progress;
3410 const b = colorFrom.b + (colorTo.b - colorFrom.b) * progress;
3411 // ? alpha
3412 return `rgb(${parseInt(r, 10)}, ${parseInt(g, 10)}, ${parseInt(b, 10)})`;
3413 }
3414 return this.getAttribute('from').getColor();
3415 }
3416}
3417
3418class AnimateTransformElement extends AnimateElement {
3419 constructor() {
3420 super(...arguments);
3421 this.type = 'animateTransform';
3422 }
3423 calcValue() {
3424 const { progress, from, to } = this.getProgress();
3425 // tween value linearly
3426 const transformFrom = toNumbers(from.getString());
3427 const transformTo = toNumbers(to.getString());
3428 const newValue = transformFrom.map((from, i) => {
3429 const to = transformTo[i];
3430 return from + (to - from) * progress;
3431 }).join(' ');
3432 return newValue;
3433 }
3434}
3435
3436class FontElement extends Element {
3437 constructor(document, node, captureTextNodes) {
3438 super(document, node, captureTextNodes);
3439 this.type = 'font';
3440 this.glyphs = {};
3441 this.horizAdvX = this.getAttribute('horiz-adv-x').getNumber();
3442 const { definitions } = document;
3443 const { children } = this;
3444 for (const child of children) {
3445 switch (child.type) {
3446 case 'font-face': {
3447 this.fontFace = child;
3448 const fontFamilyStyle = child.getStyle('font-family');
3449 if (fontFamilyStyle.hasValue()) {
3450 definitions[fontFamilyStyle.getString()] = this;
3451 }
3452 break;
3453 }
3454 case 'missing-glyph':
3455 this.missingGlyph = child;
3456 break;
3457 case 'glyph': {
3458 const glyph = child;
3459 if (glyph.arabicForm) {
3460 this.isRTL = true;
3461 this.isArabic = true;
3462 if (typeof this.glyphs[glyph.unicode] === 'undefined') {
3463 this.glyphs[glyph.unicode] = {};
3464 }
3465 this.glyphs[glyph.unicode][glyph.arabicForm] = glyph;
3466 }
3467 else {
3468 this.glyphs[glyph.unicode] = glyph;
3469 }
3470 break;
3471 }
3472 }
3473 }
3474 }
3475 render() {
3476 // NO RENDER
3477 }
3478}
3479
3480class FontFaceElement extends Element {
3481 constructor(document, node, captureTextNodes) {
3482 super(document, node, captureTextNodes);
3483 this.type = 'font-face';
3484 this.ascent = this.getAttribute('ascent').getNumber();
3485 this.descent = this.getAttribute('descent').getNumber();
3486 this.unitsPerEm = this.getAttribute('units-per-em').getNumber();
3487 }
3488}
3489
3490class MissingGlyphElement extends PathElement {
3491 constructor() {
3492 super(...arguments);
3493 this.type = 'missing-glyph';
3494 this.horizAdvX = 0;
3495 }
3496}
3497
3498class GlyphElement extends PathElement {
3499 constructor(document, node, captureTextNodes) {
3500 super(document, node, captureTextNodes);
3501 this.type = 'glyph';
3502 this.horizAdvX = this.getAttribute('horiz-adv-x').getNumber();
3503 this.unicode = this.getAttribute('unicode').getString();
3504 this.arabicForm = this.getAttribute('arabic-form').getString();
3505 }
3506}
3507
3508class TRefElement extends TextElement {
3509 constructor() {
3510 super(...arguments);
3511 this.type = 'tref';
3512 }
3513 getText() {
3514 const element = this.getHrefAttribute().getDefinition();
3515 if (element) {
3516 const firstChild = element.children[0];
3517 if (firstChild) {
3518 return firstChild.getText();
3519 }
3520 }
3521 return '';
3522 }
3523}
3524
3525class AElement extends TextElement {
3526 constructor(document, node, captureTextNodes) {
3527 super(document, node, captureTextNodes);
3528 this.type = 'a';
3529 const { childNodes } = node;
3530 const firstChild = childNodes[0];
3531 const hasText = childNodes.length > 0
3532 && Array.from(childNodes).every(node => node.nodeType === 3);
3533 this.hasText = hasText;
3534 this.text = hasText
3535 ? this.getTextFromNode(firstChild)
3536 : '';
3537 }
3538 getText() {
3539 return this.text;
3540 }
3541 renderChildren(ctx) {
3542 if (this.hasText) {
3543 // render as text element
3544 super.renderChildren(ctx);
3545 const { document, x, y } = this;
3546 const { mouse } = document.screen;
3547 const fontSize = new Property(document, 'fontSize', Font.parse(document.ctx.font).fontSize);
3548 // Do not calc bounding box if mouse is not working.
3549 if (mouse.isWorking()) {
3550 mouse.checkBoundingBox(this, new BoundingBox(x, y - fontSize.getPixels('y'), x + this.measureText(ctx), y));
3551 }
3552 }
3553 else if (this.children.length > 0) {
3554 // render as temporary group
3555 const g = new GElement(this.document, null);
3556 g.children = this.children;
3557 g.parent = this;
3558 g.render(ctx);
3559 }
3560 }
3561 onClick() {
3562 const { window } = this.document;
3563 if (window) {
3564 window.open(this.getHrefAttribute().getString());
3565 }
3566 }
3567 onMouseMove() {
3568 const ctx = this.document.ctx;
3569 ctx.canvas.style.cursor = 'pointer';
3570 }
3571}
3572
3573class TextPathElement extends TextElement {
3574 constructor(document, node, captureTextNodes) {
3575 super(document, node, captureTextNodes);
3576 this.type = 'textPath';
3577 this.textWidth = 0;
3578 this.textHeight = 0;
3579 this.pathLength = -1;
3580 this.glyphInfo = null;
3581 this.letterSpacingCache = [];
3582 this.measuresCache = new Map([['', 0]]);
3583 const pathElement = this.getHrefAttribute().getDefinition();
3584 this.text = this.getTextFromNode();
3585 this.dataArray = this.parsePathData(pathElement);
3586 }
3587 getText() {
3588 return this.text;
3589 }
3590 path(ctx) {
3591 const { dataArray } = this;
3592 if (ctx) {
3593 ctx.beginPath();
3594 }
3595 dataArray.forEach(({ type, points }) => {
3596 switch (type) {
3597 case PathParser.LINE_TO:
3598 if (ctx) {
3599 ctx.lineTo(points[0], points[1]);
3600 }
3601 break;
3602 case PathParser.MOVE_TO:
3603 if (ctx) {
3604 ctx.moveTo(points[0], points[1]);
3605 }
3606 break;
3607 case PathParser.CURVE_TO:
3608 if (ctx) {
3609 ctx.bezierCurveTo(points[0], points[1], points[2], points[3], points[4], points[5]);
3610 }
3611 break;
3612 case PathParser.QUAD_TO:
3613 if (ctx) {
3614 ctx.quadraticCurveTo(points[0], points[1], points[2], points[3]);
3615 }
3616 break;
3617 case PathParser.ARC: {
3618 const [cx, cy, rx, ry, theta, dTheta, psi, fs] = points;
3619 const r = (rx > ry) ? rx : ry;
3620 const scaleX = (rx > ry) ? 1 : rx / ry;
3621 const scaleY = (rx > ry) ? ry / rx : 1;
3622 if (ctx) {
3623 ctx.translate(cx, cy);
3624 ctx.rotate(psi);
3625 ctx.scale(scaleX, scaleY);
3626 ctx.arc(0, 0, r, theta, theta + dTheta, Boolean(1 - fs));
3627 ctx.scale(1 / scaleX, 1 / scaleY);
3628 ctx.rotate(-psi);
3629 ctx.translate(-cx, -cy);
3630 }
3631 break;
3632 }
3633 case PathParser.CLOSE_PATH:
3634 if (ctx) {
3635 ctx.closePath();
3636 }
3637 break;
3638 }
3639 });
3640 }
3641 renderChildren(ctx) {
3642 this.setTextData(ctx);
3643 ctx.save();
3644 const textDecoration = this.parent.getStyle('text-decoration').getString();
3645 const fontSize = this.getFontSize();
3646 const { glyphInfo } = this;
3647 const fill = ctx.fillStyle;
3648 if (textDecoration === 'underline') {
3649 ctx.beginPath();
3650 }
3651 glyphInfo.forEach((glyph, i) => {
3652 const { p0, p1, rotation, text: partialText } = glyph;
3653 ctx.save();
3654 ctx.translate(p0.x, p0.y);
3655 ctx.rotate(rotation);
3656 if (ctx.fillStyle) {
3657 ctx.fillText(partialText, 0, 0);
3658 }
3659 if (ctx.strokeStyle) {
3660 ctx.strokeText(partialText, 0, 0);
3661 }
3662 ctx.restore();
3663 if (textDecoration === 'underline') {
3664 if (i === 0) {
3665 ctx.moveTo(p0.x, p0.y + fontSize / 8);
3666 }
3667 ctx.lineTo(p1.x, p1.y + fontSize / 5);
3668 }
3669 //// To assist with debugging visually, uncomment following
3670 //
3671 // ctx.beginPath();
3672 // if (i % 2)
3673 // ctx.strokeStyle = 'red';
3674 // else
3675 // ctx.strokeStyle = 'green';
3676 // ctx.moveTo(p0.x, p0.y);
3677 // ctx.lineTo(p1.x, p1.y);
3678 // ctx.stroke();
3679 // ctx.closePath();
3680 });
3681 if (textDecoration === 'underline') {
3682 ctx.lineWidth = fontSize / 20;
3683 ctx.strokeStyle = fill;
3684 ctx.stroke();
3685 ctx.closePath();
3686 }
3687 ctx.restore();
3688 }
3689 getLetterSpacingAt(idx = 0) {
3690 return this.letterSpacingCache[idx] || 0;
3691 }
3692 findSegmentToFitChar(ctx, anchor, textFullWidth, fullPathWidth, spacesNumber, inputOffset, dy, c, charI) {
3693 let offset = inputOffset;
3694 let glyphWidth = this.measureText(ctx, c);
3695 if (c === ' '
3696 && anchor === 'justify'
3697 && textFullWidth < fullPathWidth) {
3698 glyphWidth += (fullPathWidth - textFullWidth) / spacesNumber;
3699 }
3700 if (charI > -1) {
3701 offset += this.getLetterSpacingAt(charI);
3702 }
3703 const splineStep = this.textHeight / 20;
3704 const p0 = this.getEquidistantPointOnPath(offset, splineStep, 0);
3705 const p1 = this.getEquidistantPointOnPath(offset + glyphWidth, splineStep, 0);
3706 const segment = {
3707 p0,
3708 p1
3709 };
3710 const rotation = p0 && p1
3711 ? Math.atan2((p1.y - p0.y), (p1.x - p0.x))
3712 : 0;
3713 if (dy) {
3714 const dyX = Math.cos(Math.PI / 2 + rotation) * dy;
3715 const dyY = Math.cos(-rotation) * dy;
3716 segment.p0 = {
3717 ...p0,
3718 x: p0.x + dyX,
3719 y: p0.y + dyY
3720 };
3721 segment.p1 = {
3722 ...p1,
3723 x: p1.x + dyX,
3724 y: p1.y + dyY
3725 };
3726 }
3727 offset += glyphWidth;
3728 return {
3729 offset,
3730 segment,
3731 rotation
3732 };
3733 }
3734 measureText(ctx, text) {
3735 const { measuresCache } = this;
3736 const targetText = text || this.getText();
3737 if (measuresCache.has(targetText)) {
3738 return measuresCache.get(targetText);
3739 }
3740 const measure = this.measureTargetText(ctx, targetText);
3741 measuresCache.set(targetText, measure);
3742 return measure;
3743 }
3744 // This method supposes what all custom fonts already loaded.
3745 // If some font will be loaded after this method call, <textPath> will not be rendered correctly.
3746 // You need to call this method manually to update glyphs cache.
3747 setTextData(ctx) {
3748 if (this.glyphInfo) {
3749 return;
3750 }
3751 const renderText = this.getText();
3752 const chars = renderText.split('');
3753 const spacesNumber = renderText.split(' ').length - 1;
3754 const dx = this.parent.getAttribute('dx').split().map(_ => _.getPixels('x'));
3755 const dy = this.parent.getAttribute('dy').getPixels('y');
3756 const anchor = this.parent.getStyle('text-anchor').getString('start');
3757 const thisSpacing = this.getStyle('letter-spacing');
3758 const parentSpacing = this.parent.getStyle('letter-spacing');
3759 let letterSpacing = 0;
3760 if (!thisSpacing.hasValue()
3761 || thisSpacing.getValue() === 'inherit') {
3762 letterSpacing = parentSpacing.getPixels();
3763 }
3764 else if (thisSpacing.hasValue()) {
3765 if (thisSpacing.getValue() !== 'initial'
3766 && thisSpacing.getValue() !== 'unset') {
3767 letterSpacing = thisSpacing.getPixels();
3768 }
3769 }
3770 // fill letter-spacing cache
3771 const letterSpacingCache = [];
3772 const textLen = renderText.length;
3773 this.letterSpacingCache = letterSpacingCache;
3774 for (let i = 0; i < textLen; i++) {
3775 letterSpacingCache.push(typeof dx[i] !== 'undefined'
3776 ? dx[i]
3777 : letterSpacing);
3778 }
3779 const dxSum = letterSpacingCache.reduce((acc, cur, i) => (i === 0
3780 ? 0
3781 : (acc + cur || 0)), 0);
3782 const textWidth = this.measureText(ctx);
3783 const textFullWidth = Math.max(textWidth + dxSum, 0);
3784 this.textWidth = textWidth;
3785 this.textHeight = this.getFontSize();
3786 this.glyphInfo = [];
3787 const fullPathWidth = this.getPathLength();
3788 const startOffset = this.getStyle('startOffset').getNumber(0) * fullPathWidth;
3789 let offset = 0;
3790 if (anchor === 'middle'
3791 || anchor === 'center') {
3792 offset = -textFullWidth / 2;
3793 }
3794 if (anchor === 'end'
3795 || anchor === 'right') {
3796 offset = -textFullWidth;
3797 }
3798 offset += startOffset;
3799 chars.forEach((char, i) => {
3800 // Find such segment what distance between p0 and p1 is approx. width of glyph
3801 const { offset: nextOffset, segment, rotation } = this.findSegmentToFitChar(ctx, anchor, textFullWidth, fullPathWidth, spacesNumber, offset, dy, char, i);
3802 offset = nextOffset;
3803 if (!segment.p0 || !segment.p1) {
3804 return;
3805 }
3806 // const width = this.getLineLength(
3807 // segment.p0.x,
3808 // segment.p0.y,
3809 // segment.p1.x,
3810 // segment.p1.y
3811 // );
3812 // Note: Since glyphs are rendered one at a time, any kerning pair data built into the font will not be used.
3813 // Can foresee having a rough pair table built in that the developer can override as needed.
3814 // Or use "dx" attribute of the <text> node as a naive replacement
3815 // const kern = 0;
3816 // placeholder for future implementation
3817 // const midpoint = this.getPointOnLine(
3818 // kern + width / 2.0,
3819 // segment.p0.x, segment.p0.y, segment.p1.x, segment.p1.y
3820 // );
3821 this.glyphInfo.push({
3822 // transposeX: midpoint.x,
3823 // transposeY: midpoint.y,
3824 text: chars[i],
3825 p0: segment.p0,
3826 p1: segment.p1,
3827 rotation
3828 });
3829 });
3830 }
3831 parsePathData(path) {
3832 this.pathLength = -1; // reset path length
3833 if (!path) {
3834 return [];
3835 }
3836 const pathCommands = [];
3837 const pathParser = path.pathParser;
3838 pathParser.reset();
3839 // convert l, H, h, V, and v to L
3840 while (!pathParser.isEnd()) {
3841 const { current } = pathParser;
3842 const startX = current ? current.x : 0;
3843 const startY = current ? current.y : 0;
3844 const command = pathParser.next();
3845 let nextCommandType = command.type;
3846 let points = [];
3847 switch (command.type) {
3848 case PathParser.MOVE_TO:
3849 this.pathM(pathParser, points);
3850 break;
3851 case PathParser.LINE_TO:
3852 nextCommandType = this.pathL(pathParser, points);
3853 break;
3854 case PathParser.HORIZ_LINE_TO:
3855 nextCommandType = this.pathH(pathParser, points);
3856 break;
3857 case PathParser.VERT_LINE_TO:
3858 nextCommandType = this.pathV(pathParser, points);
3859 break;
3860 case PathParser.CURVE_TO:
3861 this.pathC(pathParser, points);
3862 break;
3863 case PathParser.SMOOTH_CURVE_TO:
3864 nextCommandType = this.pathS(pathParser, points);
3865 break;
3866 case PathParser.QUAD_TO:
3867 this.pathQ(pathParser, points);
3868 break;
3869 case PathParser.SMOOTH_QUAD_TO:
3870 nextCommandType = this.pathT(pathParser, points);
3871 break;
3872 case PathParser.ARC:
3873 points = this.pathA(pathParser);
3874 break;
3875 case PathParser.CLOSE_PATH:
3876 PathElement.pathZ(pathParser);
3877 break;
3878 }
3879 if (command.type !== PathParser.CLOSE_PATH) {
3880 pathCommands.push({
3881 type: nextCommandType,
3882 points,
3883 start: {
3884 x: startX,
3885 y: startY
3886 },
3887 pathLength: this.calcLength(startX, startY, nextCommandType, points)
3888 });
3889 }
3890 else {
3891 pathCommands.push({
3892 type: PathParser.CLOSE_PATH,
3893 points: [],
3894 pathLength: 0
3895 });
3896 }
3897 }
3898 return pathCommands;
3899 }
3900 pathM(pathParser, points) {
3901 const { x, y } = PathElement.pathM(pathParser).point;
3902 points.push(x, y);
3903 }
3904 pathL(pathParser, points) {
3905 const { x, y } = PathElement.pathL(pathParser).point;
3906 points.push(x, y);
3907 return PathParser.LINE_TO;
3908 }
3909 pathH(pathParser, points) {
3910 const { x, y } = PathElement.pathH(pathParser).point;
3911 points.push(x, y);
3912 return PathParser.LINE_TO;
3913 }
3914 pathV(pathParser, points) {
3915 const { x, y } = PathElement.pathV(pathParser).point;
3916 points.push(x, y);
3917 return PathParser.LINE_TO;
3918 }
3919 pathC(pathParser, points) {
3920 const { point, controlPoint, currentPoint } = PathElement.pathC(pathParser);
3921 points.push(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
3922 }
3923 pathS(pathParser, points) {
3924 const { point, controlPoint, currentPoint } = PathElement.pathS(pathParser);
3925 points.push(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
3926 return PathParser.CURVE_TO;
3927 }
3928 pathQ(pathParser, points) {
3929 const { controlPoint, currentPoint } = PathElement.pathQ(pathParser);
3930 points.push(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
3931 }
3932 pathT(pathParser, points) {
3933 const { controlPoint, currentPoint } = PathElement.pathT(pathParser);
3934 points.push(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
3935 return PathParser.QUAD_TO;
3936 }
3937 pathA(pathParser) {
3938 let { rX, rY, sweepFlag, xAxisRotation, centp, a1, ad } = PathElement.pathA(pathParser);
3939 if (sweepFlag === 0 && ad > 0) {
3940 ad = ad - 2 * Math.PI;
3941 }
3942 if (sweepFlag === 1 && ad < 0) {
3943 ad = ad + 2 * Math.PI;
3944 }
3945 return [
3946 centp.x,
3947 centp.y,
3948 rX,
3949 rY,
3950 a1,
3951 ad,
3952 xAxisRotation,
3953 sweepFlag
3954 ];
3955 }
3956 calcLength(x, y, commandType, points) {
3957 let len = 0;
3958 let p1 = null;
3959 let p2 = null;
3960 let t = 0;
3961 switch (commandType) {
3962 case PathParser.LINE_TO:
3963 return this.getLineLength(x, y, points[0], points[1]);
3964 case PathParser.CURVE_TO:
3965 // Approximates by breaking curve into 100 line segments
3966 len = 0.0;
3967 p1 = this.getPointOnCubicBezier(0, x, y, points[0], points[1], points[2], points[3], points[4], points[5]);
3968 for (t = 0.01; t <= 1; t += 0.01) {
3969 p2 = this.getPointOnCubicBezier(t, x, y, points[0], points[1], points[2], points[3], points[4], points[5]);
3970 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
3971 p1 = p2;
3972 }
3973 return len;
3974 case PathParser.QUAD_TO:
3975 // Approximates by breaking curve into 100 line segments
3976 len = 0.0;
3977 p1 = this.getPointOnQuadraticBezier(0, x, y, points[0], points[1], points[2], points[3]);
3978 for (t = 0.01; t <= 1; t += 0.01) {
3979 p2 = this.getPointOnQuadraticBezier(t, x, y, points[0], points[1], points[2], points[3]);
3980 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
3981 p1 = p2;
3982 }
3983 return len;
3984 case PathParser.ARC:
3985 // Approximates by breaking curve into line segments
3986 len = 0.0;
3987 const start = points[4];
3988 // 4 = theta
3989 const dTheta = points[5];
3990 // 5 = dTheta
3991 const end = points[4] + dTheta;
3992 let inc = Math.PI / 180.0;
3993 // 1 degree resolution
3994 if (Math.abs(start - end) < inc) {
3995 inc = Math.abs(start - end);
3996 }
3997 // Note: for purpose of calculating arc length, not going to worry about rotating X-axis by angle psi
3998 p1 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], start, 0);
3999 if (dTheta < 0) { // clockwise
4000 for (t = start - inc; t > end; t -= inc) {
4001 p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0);
4002 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4003 p1 = p2;
4004 }
4005 }
4006 else { // counter-clockwise
4007 for (t = start + inc; t < end; t += inc) {
4008 p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0);
4009 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4010 p1 = p2;
4011 }
4012 }
4013 p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], end, 0);
4014 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4015 return len;
4016 }
4017 return 0;
4018 }
4019 getPointOnLine(dist, P1x, P1y, P2x, P2y, fromX = P1x, fromY = P1y) {
4020 const m = (P2y - P1y) / ((P2x - P1x) + PSEUDO_ZERO);
4021 let run = Math.sqrt(dist * dist / (1 + m * m));
4022 if (P2x < P1x) {
4023 run *= -1;
4024 }
4025 let rise = m * run;
4026 let pt = null;
4027 if (P2x === P1x) { // vertical line
4028 pt = {
4029 x: fromX,
4030 y: fromY + rise
4031 };
4032 }
4033 else if ((fromY - P1y) / ((fromX - P1x) + PSEUDO_ZERO) === m) {
4034 pt = {
4035 x: fromX + run,
4036 y: fromY + rise
4037 };
4038 }
4039 else {
4040 let ix = 0;
4041 let iy = 0;
4042 const len = this.getLineLength(P1x, P1y, P2x, P2y);
4043 if (len < PSEUDO_ZERO) {
4044 return null;
4045 }
4046 let u = (((fromX - P1x) * (P2x - P1x))
4047 + ((fromY - P1y) * (P2y - P1y)));
4048 u = u / (len * len);
4049 ix = P1x + u * (P2x - P1x);
4050 iy = P1y + u * (P2y - P1y);
4051 const pRise = this.getLineLength(fromX, fromY, ix, iy);
4052 const pRun = Math.sqrt(dist * dist - pRise * pRise);
4053 run = Math.sqrt(pRun * pRun / (1 + m * m));
4054 if (P2x < P1x) {
4055 run *= -1;
4056 }
4057 rise = m * run;
4058 pt = {
4059 x: ix + run,
4060 y: iy + rise
4061 };
4062 }
4063 return pt;
4064 }
4065 getPointOnPath(distance) {
4066 const fullLen = this.getPathLength();
4067 let cumulativePathLength = 0;
4068 let p = null;
4069 if (distance < -0.00005
4070 || distance - 0.00005 > fullLen) {
4071 return null;
4072 }
4073 const { dataArray } = this;
4074 for (const command of dataArray) {
4075 if (command
4076 && (command.pathLength < 0.00005
4077 || cumulativePathLength + command.pathLength + 0.00005 < distance)) {
4078 cumulativePathLength += command.pathLength;
4079 continue;
4080 }
4081 const delta = distance - cumulativePathLength;
4082 let currentT = 0;
4083 switch (command.type) {
4084 case PathParser.LINE_TO:
4085 p = this.getPointOnLine(delta, command.start.x, command.start.y, command.points[0], command.points[1], command.start.x, command.start.y);
4086 break;
4087 case PathParser.ARC:
4088 const start = command.points[4];
4089 // 4 = theta
4090 const dTheta = command.points[5];
4091 // 5 = dTheta
4092 const end = command.points[4] + dTheta;
4093 currentT = start + delta / command.pathLength * dTheta;
4094 if (dTheta < 0 && currentT < end
4095 || dTheta >= 0 && currentT > end) {
4096 break;
4097 }
4098 p = this.getPointOnEllipticalArc(command.points[0], command.points[1], command.points[2], command.points[3], currentT, command.points[6]);
4099 break;
4100 case PathParser.CURVE_TO:
4101 currentT = delta / command.pathLength;
4102 if (currentT > 1) {
4103 currentT = 1;
4104 }
4105 p = this.getPointOnCubicBezier(currentT, command.start.x, command.start.y, command.points[0], command.points[1], command.points[2], command.points[3], command.points[4], command.points[5]);
4106 break;
4107 case PathParser.QUAD_TO:
4108 currentT = delta / command.pathLength;
4109 if (currentT > 1) {
4110 currentT = 1;
4111 }
4112 p = this.getPointOnQuadraticBezier(currentT, command.start.x, command.start.y, command.points[0], command.points[1], command.points[2], command.points[3]);
4113 break;
4114 }
4115 if (p) {
4116 return p;
4117 }
4118 break;
4119 }
4120 return null;
4121 }
4122 getLineLength(x1, y1, x2, y2) {
4123 return Math.sqrt((x2 - x1) * (x2 - x1)
4124 + (y2 - y1) * (y2 - y1));
4125 }
4126 getPathLength() {
4127 if (this.pathLength === -1) {
4128 this.pathLength = this.dataArray.reduce((length, command) => (command.pathLength > 0
4129 ? length + command.pathLength
4130 : length), 0);
4131 }
4132 return this.pathLength;
4133 }
4134 getPointOnCubicBezier(pct, P1x, P1y, P2x, P2y, P3x, P3y, P4x, P4y) {
4135 const x = P4x * CB1(pct) + P3x * CB2(pct) + P2x * CB3(pct) + P1x * CB4(pct);
4136 const y = P4y * CB1(pct) + P3y * CB2(pct) + P2y * CB3(pct) + P1y * CB4(pct);
4137 return {
4138 x,
4139 y
4140 };
4141 }
4142 getPointOnQuadraticBezier(pct, P1x, P1y, P2x, P2y, P3x, P3y) {
4143 const x = P3x * QB1(pct) + P2x * QB2(pct) + P1x * QB3(pct);
4144 const y = P3y * QB1(pct) + P2y * QB2(pct) + P1y * QB3(pct);
4145 return {
4146 x,
4147 y
4148 };
4149 }
4150 getPointOnEllipticalArc(cx, cy, rx, ry, theta, psi) {
4151 const cosPsi = Math.cos(psi);
4152 const sinPsi = Math.sin(psi);
4153 const pt = {
4154 x: rx * Math.cos(theta),
4155 y: ry * Math.sin(theta)
4156 };
4157 return {
4158 x: cx + (pt.x * cosPsi - pt.y * sinPsi),
4159 y: cy + (pt.x * sinPsi + pt.y * cosPsi)
4160 };
4161 }
4162 // TODO need some optimisations. possibly build cache only for curved segments?
4163 buildEquidistantCache(inputStep, inputPrecision) {
4164 const fullLen = this.getPathLength();
4165 const precision = inputPrecision || 0.25; // accuracy vs performance
4166 const step = inputStep || fullLen / 100;
4167 if (!this.equidistantCache
4168 || this.equidistantCache.step !== step
4169 || this.equidistantCache.precision !== precision) {
4170 // Prepare cache
4171 this.equidistantCache = {
4172 step,
4173 precision,
4174 points: []
4175 };
4176 // Calculate points
4177 let s = 0;
4178 for (let l = 0; l <= fullLen; l += precision) {
4179 const p0 = this.getPointOnPath(l);
4180 const p1 = this.getPointOnPath(l + precision);
4181 if (!p0 || !p1) {
4182 continue;
4183 }
4184 s += this.getLineLength(p0.x, p0.y, p1.x, p1.y);
4185 if (s >= step) {
4186 this.equidistantCache.points.push({
4187 x: p0.x,
4188 y: p0.y,
4189 distance: l
4190 });
4191 s -= step;
4192 }
4193 }
4194 }
4195 }
4196 getEquidistantPointOnPath(targetDistance, step, precision) {
4197 this.buildEquidistantCache(step, precision);
4198 if (targetDistance < 0
4199 || targetDistance - this.getPathLength() > 0.00005) {
4200 return null;
4201 }
4202 const idx = Math.round(targetDistance
4203 / this.getPathLength()
4204 * (this.equidistantCache.points.length - 1));
4205 return this.equidistantCache.points[idx] || null;
4206 }
4207}
4208
4209class ImageElement extends RenderedElement {
4210 constructor(document, node, captureTextNodes) {
4211 super(document, node, captureTextNodes);
4212 this.type = 'image';
4213 this.loaded = false;
4214 const href = this.getHrefAttribute().getString();
4215 if (!href) {
4216 return;
4217 }
4218 const isSvg = /\.svg$/.test(href);
4219 document.images.push(this);
4220 if (!isSvg) {
4221 this.loadImage(href);
4222 }
4223 else {
4224 this.loadSvg(href);
4225 }
4226 this.isSvg = isSvg;
4227 }
4228 async loadImage(href) {
4229 try {
4230 const image = await this.document.createImage(href);
4231 this.image = image;
4232 }
4233 catch (err) {
4234 // tslint:disable-next-line: no-console
4235 console.error(`Error while loading image "${href}":`, err);
4236 }
4237 this.loaded = true;
4238 }
4239 async loadSvg(href) {
4240 try {
4241 const response = await this.document.fetch(href);
4242 const svg = await response.text();
4243 this.image = svg;
4244 }
4245 catch (err) {
4246 // tslint:disable-next-line: no-console
4247 console.error(`Error while loading image "${href}":`, err);
4248 }
4249 this.loaded = true;
4250 }
4251 renderChildren(ctx) {
4252 const { document, image, loaded } = this;
4253 const x = this.getAttribute('x').getPixels('x');
4254 const y = this.getAttribute('y').getPixels('y');
4255 const width = this.getStyle('width').getPixels('x');
4256 const height = this.getStyle('height').getPixels('y');
4257 if (!loaded || !image
4258 || !width || !height) {
4259 return;
4260 }
4261 ctx.save();
4262 if (this.isSvg) {
4263 document.canvg.forkString(ctx, this.image, {
4264 ignoreMouse: true,
4265 ignoreAnimation: true,
4266 ignoreDimensions: true,
4267 ignoreClear: true,
4268 offsetX: x,
4269 offsetY: y,
4270 scaleWidth: width,
4271 scaleHeight: height
4272 }).render();
4273 }
4274 else {
4275 const image = this.image;
4276 ctx.translate(x, y);
4277 document.setViewBox({
4278 ctx,
4279 aspectRatio: this.getAttribute('preserveAspectRatio').getString(),
4280 width,
4281 desiredWidth: image.width,
4282 height,
4283 desiredHeight: image.height
4284 });
4285 if (this.loaded) {
4286 if (typeof image.complete === 'undefined' || image.complete) {
4287 ctx.drawImage(image, 0, 0);
4288 }
4289 }
4290 }
4291 ctx.restore();
4292 }
4293 getBoundingBox() {
4294 const x = this.getAttribute('x').getPixels('x');
4295 const y = this.getAttribute('y').getPixels('y');
4296 const width = this.getStyle('width').getPixels('x');
4297 const height = this.getStyle('height').getPixels('y');
4298 return new BoundingBox(x, y, x + width, y + height);
4299 }
4300}
4301
4302class SymbolElement extends RenderedElement {
4303 constructor() {
4304 super(...arguments);
4305 this.type = 'symbol';
4306 }
4307 render(_) {
4308 // NO RENDER
4309 }
4310}
4311
4312class SVGFontLoader {
4313 constructor(document) {
4314 this.document = document;
4315 this.loaded = false;
4316 document.fonts.push(this);
4317 }
4318 async load(fontFamily, url) {
4319 try {
4320 const { document } = this;
4321 const svgDocument = await document.canvg.parser.load(url);
4322 const fonts = svgDocument.getElementsByTagName('font');
4323 Array.from(fonts).forEach((fontNode) => {
4324 const font = document.createElement(fontNode);
4325 document.definitions[fontFamily] = font;
4326 });
4327 }
4328 catch (err) {
4329 // tslint:disable-next-line: no-console
4330 console.error(`Error while loading font "${url}":`, err);
4331 }
4332 this.loaded = true;
4333 }
4334}
4335
4336class StyleElement extends Element {
4337 constructor(document, node, captureTextNodes) {
4338 super(document, node, captureTextNodes);
4339 this.type = 'style';
4340 const css = compressSpaces(Array.from(node.childNodes)
4341 .map((_) => _.data)
4342 .join('')
4343 .replace(/(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, '') // remove comments
4344 .replace(/@import.*;/g, '') // remove imports
4345 );
4346 const cssDefs = css.split('}');
4347 cssDefs.forEach((_) => {
4348 const def = _.trim();
4349 if (!def) {
4350 return;
4351 }
4352 const cssParts = def.split('{');
4353 const cssClasses = cssParts[0].split(',');
4354 const cssProps = cssParts[1].split(';');
4355 cssClasses.forEach((_) => {
4356 const cssClass = _.trim();
4357 if (!cssClass) {
4358 return;
4359 }
4360 const props = document.styles[cssClass] || {};
4361 cssProps.forEach((cssProp) => {
4362 const prop = cssProp.indexOf(':');
4363 const name = cssProp.substr(0, prop).trim();
4364 const value = cssProp.substr(prop + 1, cssProp.length - prop).trim();
4365 if (name && value) {
4366 props[name] = new Property(document, name, value);
4367 }
4368 });
4369 document.styles[cssClass] = props;
4370 document.stylesSpecificity[cssClass] = getSelectorSpecificity(cssClass);
4371 if (cssClass === '@font-face') { // && !nodeEnv
4372 const fontFamily = props['font-family'].getString().replace(/"|'/g, '');
4373 const srcs = props.src.getString().split(',');
4374 srcs.forEach((src) => {
4375 if (src.indexOf('format("svg")') > 0) {
4376 const url = parseExternalUrl(src);
4377 if (url) {
4378 new SVGFontLoader(document).load(fontFamily, url);
4379 }
4380 }
4381 });
4382 }
4383 });
4384 });
4385 }
4386}
4387StyleElement.parseExternalUrl = parseExternalUrl;
4388
4389class UseElement extends RenderedElement {
4390 constructor() {
4391 super(...arguments);
4392 this.type = 'use';
4393 }
4394 setContext(ctx) {
4395 super.setContext(ctx);
4396 const xAttr = this.getAttribute('x');
4397 const yAttr = this.getAttribute('y');
4398 if (xAttr.hasValue()) {
4399 ctx.translate(xAttr.getPixels('x'), 0);
4400 }
4401 if (yAttr.hasValue()) {
4402 ctx.translate(0, yAttr.getPixels('y'));
4403 }
4404 }
4405 path(ctx) {
4406 const { element } = this;
4407 if (element) {
4408 element.path(ctx);
4409 }
4410 }
4411 renderChildren(ctx) {
4412 const { document, element } = this;
4413 if (element) {
4414 let tempSvg = element;
4415 if (element.type === 'symbol') {
4416 // render me using a temporary svg element in symbol cases (http://www.w3.org/TR/SVG/struct.html#UseElement)
4417 tempSvg = new SVGElement(document, null);
4418 tempSvg.attributes.viewBox = new Property(document, 'viewBox', element.getAttribute('viewBox').getString());
4419 tempSvg.attributes.preserveAspectRatio = new Property(document, 'preserveAspectRatio', element.getAttribute('preserveAspectRatio').getString());
4420 tempSvg.attributes.overflow = new Property(document, 'overflow', element.getAttribute('overflow').getString());
4421 tempSvg.children = element.children;
4422 // element is still the parent of the children
4423 element.styles.opacity = new Property(document, 'opacity', this.calculateOpacity());
4424 }
4425 if (tempSvg.type === 'svg') {
4426 const widthStyle = this.getStyle('width', false, true);
4427 const heightStyle = this.getStyle('height', false, true);
4428 // if symbol or svg, inherit width/height from me
4429 if (widthStyle.hasValue()) {
4430 tempSvg.attributes.width = new Property(document, 'width', widthStyle.getString());
4431 }
4432 if (heightStyle.hasValue()) {
4433 tempSvg.attributes.height = new Property(document, 'height', heightStyle.getString());
4434 }
4435 }
4436 const oldParent = tempSvg.parent;
4437 tempSvg.parent = this;
4438 tempSvg.render(ctx);
4439 tempSvg.parent = oldParent;
4440 }
4441 }
4442 getBoundingBox(ctx) {
4443 const { element } = this;
4444 if (element) {
4445 return element.getBoundingBox(ctx);
4446 }
4447 return null;
4448 }
4449 elementTransform() {
4450 const { document, element } = this;
4451 return Transform.fromElement(document, element);
4452 }
4453 get element() {
4454 if (!this._element) {
4455 this._element = this.getHrefAttribute().getDefinition();
4456 }
4457 return this._element;
4458 }
4459}
4460
4461function imGet(img, x, y, width, _, rgba) {
4462 return img[y * width * 4 + x * 4 + rgba];
4463}
4464function imSet(img, x, y, width, _, rgba, val) {
4465 img[y * width * 4 + x * 4 + rgba] = val;
4466}
4467function m(matrix, i, v) {
4468 const mi = matrix[i];
4469 return mi * v;
4470}
4471function c(a, m1, m2, m3) {
4472 return m1 + Math.cos(a) * m2 + Math.sin(a) * m3;
4473}
4474class FeColorMatrixElement extends Element {
4475 constructor(document, node, captureTextNodes) {
4476 super(document, node, captureTextNodes);
4477 this.type = 'feColorMatrix';
4478 let matrix = toNumbers(this.getAttribute('values').getString());
4479 switch (this.getAttribute('type').getString('matrix')) { // http://www.w3.org/TR/SVG/filters.html#feColorMatrixElement
4480 case 'saturate': {
4481 const s = matrix[0];
4482 matrix = [
4483 0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0,
4484 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0,
4485 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0,
4486 0, 0, 0, 1, 0,
4487 0, 0, 0, 0, 1
4488 ];
4489 break;
4490 }
4491 case 'hueRotate': {
4492 const a = matrix[0] * Math.PI / 180.0;
4493 matrix = [
4494 c(a, 0.213, 0.787, -0.213), c(a, 0.715, -0.715, -0.715), c(a, 0.072, -0.072, 0.928), 0, 0,
4495 c(a, 0.213, -0.213, 0.143), c(a, 0.715, 0.285, 0.140), c(a, 0.072, -0.072, -0.283), 0, 0,
4496 c(a, 0.213, -0.213, -0.787), c(a, 0.715, -0.715, 0.715), c(a, 0.072, 0.928, 0.072), 0, 0,
4497 0, 0, 0, 1, 0,
4498 0, 0, 0, 0, 1
4499 ];
4500 break;
4501 }
4502 case 'luminanceToAlpha':
4503 matrix = [
4504 0, 0, 0, 0, 0,
4505 0, 0, 0, 0, 0,
4506 0, 0, 0, 0, 0,
4507 0.2125, 0.7154, 0.0721, 0, 0,
4508 0, 0, 0, 0, 1
4509 ];
4510 break;
4511 }
4512 this.matrix = matrix;
4513 this.includeOpacity = this.getAttribute('includeOpacity').hasValue();
4514 }
4515 apply(ctx, _, __, width, height) {
4516 // assuming x==0 && y==0 for now
4517 const { includeOpacity, matrix } = this;
4518 const srcData = ctx.getImageData(0, 0, width, height);
4519 for (let y = 0; y < height; y++) {
4520 for (let x = 0; x < width; x++) {
4521 const r = imGet(srcData.data, x, y, width, height, 0);
4522 const g = imGet(srcData.data, x, y, width, height, 1);
4523 const b = imGet(srcData.data, x, y, width, height, 2);
4524 const a = imGet(srcData.data, x, y, width, height, 3);
4525 let nr = m(matrix, 0, r) + m(matrix, 1, g) + m(matrix, 2, b) + m(matrix, 3, a) + m(matrix, 4, 1);
4526 let ng = m(matrix, 5, r) + m(matrix, 6, g) + m(matrix, 7, b) + m(matrix, 8, a) + m(matrix, 9, 1);
4527 let nb = m(matrix, 10, r) + m(matrix, 11, g) + m(matrix, 12, b) + m(matrix, 13, a) + m(matrix, 14, 1);
4528 let na = m(matrix, 15, r) + m(matrix, 16, g) + m(matrix, 17, b) + m(matrix, 18, a) + m(matrix, 19, 1);
4529 if (includeOpacity) {
4530 nr = ng = nb = 0;
4531 na *= a / 255;
4532 }
4533 imSet(srcData.data, x, y, width, height, 0, nr);
4534 imSet(srcData.data, x, y, width, height, 1, ng);
4535 imSet(srcData.data, x, y, width, height, 2, nb);
4536 imSet(srcData.data, x, y, width, height, 3, na);
4537 }
4538 }
4539 ctx.clearRect(0, 0, width, height);
4540 ctx.putImageData(srcData, 0, 0);
4541 }
4542}
4543
4544class MaskElement extends Element {
4545 constructor() {
4546 super(...arguments);
4547 this.type = 'mask';
4548 }
4549 apply(ctx, element) {
4550 const { document } = this;
4551 // render as temp svg
4552 let x = this.getAttribute('x').getPixels('x');
4553 let y = this.getAttribute('y').getPixels('y');
4554 let width = this.getStyle('width').getPixels('x');
4555 let height = this.getStyle('height').getPixels('y');
4556 if (!width && !height) {
4557 const boundingBox = new BoundingBox();
4558 this.children.forEach((child) => {
4559 boundingBox.addBoundingBox(child.getBoundingBox(ctx));
4560 });
4561 x = Math.floor(boundingBox.x1);
4562 y = Math.floor(boundingBox.y1);
4563 width = Math.floor(boundingBox.width);
4564 height = Math.floor(boundingBox.height);
4565 }
4566 const ignoredStyles = this.removeStyles(element, MaskElement.ignoreStyles);
4567 const maskCanvas = document.createCanvas(x + width, y + height);
4568 const maskCtx = maskCanvas.getContext('2d');
4569 document.screen.setDefaults(maskCtx);
4570 this.renderChildren(maskCtx);
4571 // convert mask to alpha with a fake node
4572 // TODO: refactor out apply from feColorMatrix
4573 new FeColorMatrixElement(document, ({
4574 nodeType: 1,
4575 childNodes: [],
4576 attributes: [
4577 { nodeName: 'type', value: 'luminanceToAlpha' },
4578 { nodeName: 'includeOpacity', value: 'true' }
4579 ]
4580 })).apply(maskCtx, 0, 0, x + width, y + height);
4581 const tmpCanvas = document.createCanvas(x + width, y + height);
4582 const tmpCtx = tmpCanvas.getContext('2d');
4583 document.screen.setDefaults(tmpCtx);
4584 element.render(tmpCtx);
4585 tmpCtx.globalCompositeOperation = 'destination-in';
4586 tmpCtx.fillStyle = maskCtx.createPattern(maskCanvas, 'no-repeat');
4587 tmpCtx.fillRect(0, 0, x + width, y + height);
4588 ctx.fillStyle = tmpCtx.createPattern(tmpCanvas, 'no-repeat');
4589 ctx.fillRect(0, 0, x + width, y + height);
4590 // reassign mask
4591 this.restoreStyles(element, ignoredStyles);
4592 }
4593 render(_) {
4594 // NO RENDER
4595 }
4596}
4597MaskElement.ignoreStyles = [
4598 'mask',
4599 'transform',
4600 'clip-path'
4601];
4602
4603const noop = () => { };
4604class ClipPathElement extends Element {
4605 constructor() {
4606 super(...arguments);
4607 this.type = 'clipPath';
4608 }
4609 apply(ctx) {
4610 const { document } = this;
4611 const contextProto = Reflect.getPrototypeOf(ctx);
4612 const { beginPath, closePath } = ctx;
4613 if (contextProto) {
4614 contextProto.beginPath = noop;
4615 contextProto.closePath = noop;
4616 }
4617 Reflect.apply(beginPath, ctx, []);
4618 this.children.forEach((child) => {
4619 if (typeof child.path === 'undefined') {
4620 return;
4621 }
4622 let transform = typeof child.elementTransform !== 'undefined'
4623 ? child.elementTransform()
4624 : null; // handle <use />
4625 if (!transform) {
4626 transform = Transform.fromElement(document, child);
4627 }
4628 if (transform) {
4629 transform.apply(ctx);
4630 }
4631 child.path(ctx);
4632 if (contextProto) {
4633 contextProto.closePath = closePath;
4634 }
4635 if (transform) {
4636 transform.unapply(ctx);
4637 }
4638 });
4639 Reflect.apply(closePath, ctx, []);
4640 ctx.clip();
4641 if (contextProto) {
4642 contextProto.beginPath = beginPath;
4643 contextProto.closePath = closePath;
4644 }
4645 }
4646 render(_) {
4647 // NO RENDER
4648 }
4649}
4650
4651class FilterElement extends Element {
4652 constructor() {
4653 super(...arguments);
4654 this.type = 'filter';
4655 }
4656 apply(ctx, element) {
4657 // render as temp svg
4658 const { document, children } = this;
4659 const boundingBox = element.getBoundingBox(ctx);
4660 if (!boundingBox) {
4661 return;
4662 }
4663 let px = 0;
4664 let py = 0;
4665 children.forEach((child) => {
4666 const efd = child.extraFilterDistance || 0;
4667 px = Math.max(px, efd);
4668 py = Math.max(py, efd);
4669 });
4670 const width = Math.floor(boundingBox.width);
4671 const height = Math.floor(boundingBox.height);
4672 const tmpCanvasWidth = width + 2 * px;
4673 const tmpCanvasHeight = height + 2 * py;
4674 if (tmpCanvasWidth < 1 || tmpCanvasHeight < 1) {
4675 return;
4676 }
4677 const x = Math.floor(boundingBox.x);
4678 const y = Math.floor(boundingBox.y);
4679 const ignoredStyles = this.removeStyles(element, FilterElement.ignoreStyles);
4680 const tmpCanvas = document.createCanvas(tmpCanvasWidth, tmpCanvasHeight);
4681 const tmpCtx = tmpCanvas.getContext('2d');
4682 document.screen.setDefaults(tmpCtx);
4683 tmpCtx.translate(-x + px, -y + py);
4684 element.render(tmpCtx);
4685 // apply filters
4686 children.forEach((child) => {
4687 if (typeof child.apply === 'function') {
4688 child.apply(tmpCtx, 0, 0, tmpCanvasWidth, tmpCanvasHeight);
4689 }
4690 });
4691 // render on me
4692 ctx.drawImage(tmpCanvas, 0, 0, tmpCanvasWidth, tmpCanvasHeight, x - px, y - py, tmpCanvasWidth, tmpCanvasHeight);
4693 this.restoreStyles(element, ignoredStyles);
4694 }
4695 render(_) {
4696 // NO RENDER
4697 }
4698}
4699FilterElement.ignoreStyles = [
4700 'filter',
4701 'transform',
4702 'clip-path'
4703];
4704
4705class FeDropShadowElement extends Element {
4706 constructor(document, node, captureTextNodes) {
4707 super(document, node, captureTextNodes);
4708 this.type = 'feDropShadow';
4709 this.addStylesFromStyleDefinition();
4710 }
4711 apply(_, __, ___, ____, _____) {
4712 // TODO: implement
4713 }
4714}
4715
4716class FeMorphologyElement extends Element {
4717 constructor() {
4718 super(...arguments);
4719 this.type = 'feMorphology';
4720 }
4721 apply(_, __, ___, ____, _____) {
4722 // TODO: implement
4723 }
4724}
4725
4726class FeCompositeElement extends Element {
4727 constructor() {
4728 super(...arguments);
4729 this.type = 'feComposite';
4730 }
4731 apply(_, __, ___, ____, _____) {
4732 // TODO: implement
4733 }
4734}
4735
4736class FeGaussianBlurElement extends Element {
4737 constructor(document, node, captureTextNodes) {
4738 super(document, node, captureTextNodes);
4739 this.type = 'feGaussianBlur';
4740 this.blurRadius = Math.floor(this.getAttribute('stdDeviation').getNumber());
4741 this.extraFilterDistance = this.blurRadius;
4742 }
4743 apply(ctx, x, y, width, height) {
4744 const { document, blurRadius } = this;
4745 const body = document.window
4746 ? document.window.document.body
4747 : null;
4748 const canvas = ctx.canvas;
4749 // StackBlur requires canvas be on document
4750 canvas.id = document.getUniqueId();
4751 if (body) {
4752 canvas.style.display = 'none';
4753 body.appendChild(canvas);
4754 }
4755 canvasRGBA(canvas, x, y, width, height, blurRadius);
4756 if (body) {
4757 body.removeChild(canvas);
4758 }
4759 }
4760}
4761
4762class TitleElement extends Element {
4763 constructor() {
4764 super(...arguments);
4765 this.type = 'title';
4766 }
4767}
4768
4769class DescElement extends Element {
4770 constructor() {
4771 super(...arguments);
4772 this.type = 'desc';
4773 }
4774}
4775
4776var elementTypes = {
4777 'svg': SVGElement,
4778 'rect': RectElement,
4779 'circle': CircleElement,
4780 'ellipse': EllipseElement,
4781 'line': LineElement,
4782 'polyline': PolylineElement,
4783 'polygon': PolygonElement,
4784 'path': PathElement,
4785 'pattern': PatternElement,
4786 'marker': MarkerElement,
4787 'defs': DefsElement,
4788 'linearGradient': LinearGradientElement,
4789 'radialGradient': RadialGradientElement,
4790 'stop': StopElement,
4791 'animate': AnimateElement,
4792 'animateColor': AnimateColorElement,
4793 'animateTransform': AnimateTransformElement,
4794 'font': FontElement,
4795 'font-face': FontFaceElement,
4796 'missing-glyph': MissingGlyphElement,
4797 'glyph': GlyphElement,
4798 'text': TextElement,
4799 'tspan': TSpanElement,
4800 'tref': TRefElement,
4801 'a': AElement,
4802 'textPath': TextPathElement,
4803 'image': ImageElement,
4804 'g': GElement,
4805 'symbol': SymbolElement,
4806 'style': StyleElement,
4807 'use': UseElement,
4808 'mask': MaskElement,
4809 'clipPath': ClipPathElement,
4810 'filter': FilterElement,
4811 'feDropShadow': FeDropShadowElement,
4812 'feMorphology': FeMorphologyElement,
4813 'feComposite': FeCompositeElement,
4814 'feColorMatrix': FeColorMatrixElement,
4815 'feGaussianBlur': FeGaussianBlurElement,
4816 'title': TitleElement,
4817 'desc': DescElement
4818};
4819
4820function createCanvas(width, height) {
4821 const canvas = document.createElement('canvas');
4822 canvas.width = width;
4823 canvas.height = height;
4824 return canvas;
4825}
4826async function createImage(src, anonymousCrossOrigin = false) {
4827 const image = document.createElement('img');
4828 if (anonymousCrossOrigin) {
4829 image.crossOrigin = 'Anonymous';
4830 }
4831 return new Promise((resolve, reject) => {
4832 image.onload = () => {
4833 resolve(image);
4834 };
4835 image.onerror = () => {
4836 reject();
4837 };
4838 image.src = src;
4839 });
4840}
4841class Document {
4842 constructor(canvg, { rootEmSize = 12, emSize = 12, createCanvas = Document.createCanvas, createImage = Document.createImage, anonymousCrossOrigin } = {}) {
4843 this.canvg = canvg;
4844 this.definitions = {};
4845 this.styles = {};
4846 this.stylesSpecificity = {};
4847 this.images = [];
4848 this.fonts = [];
4849 this.emSizeStack = [];
4850 this.uniqueId = 0;
4851 this.screen = canvg.screen;
4852 this.rootEmSize = rootEmSize;
4853 this.emSize = emSize;
4854 this.createCanvas = createCanvas;
4855 this.createImage = this.bindCreateImage(createImage, anonymousCrossOrigin);
4856 this.screen.wait(this.isImagesLoaded.bind(this));
4857 this.screen.wait(this.isFontsLoaded.bind(this));
4858 }
4859 bindCreateImage(createImage, anonymousCrossOrigin) {
4860 if (typeof anonymousCrossOrigin === 'boolean') {
4861 return (source, forceAnonymousCrossOrigin) => createImage(source, typeof forceAnonymousCrossOrigin === 'boolean'
4862 ? forceAnonymousCrossOrigin
4863 : anonymousCrossOrigin);
4864 }
4865 return createImage;
4866 }
4867 get window() {
4868 return this.screen.window;
4869 }
4870 get fetch() {
4871 return this.screen.fetch;
4872 }
4873 get ctx() {
4874 return this.screen.ctx;
4875 }
4876 get emSize() {
4877 const { emSizeStack } = this;
4878 return emSizeStack[emSizeStack.length - 1];
4879 }
4880 set emSize(value) {
4881 const { emSizeStack } = this;
4882 emSizeStack.push(value);
4883 }
4884 popEmSize() {
4885 const { emSizeStack } = this;
4886 emSizeStack.pop();
4887 }
4888 getUniqueId() {
4889 return `canvg${++this.uniqueId}`;
4890 }
4891 isImagesLoaded() {
4892 return this.images.every(_ => _.loaded);
4893 }
4894 isFontsLoaded() {
4895 return this.fonts.every(_ => _.loaded);
4896 }
4897 createDocumentElement(document) {
4898 const documentElement = this.createElement(document.documentElement);
4899 documentElement.root = true;
4900 documentElement.addStylesFromStyleDefinition();
4901 this.documentElement = documentElement;
4902 return documentElement;
4903 }
4904 createElement(node) {
4905 const elementType = node.nodeName.replace(/^[^:]+:/, '');
4906 const ElementType = Document.elementTypes[elementType];
4907 if (typeof ElementType !== 'undefined') {
4908 return new ElementType(this, node);
4909 }
4910 return new UnknownElement(this, node);
4911 }
4912 createTextNode(node) {
4913 return new TextNode(this, node);
4914 }
4915 setViewBox(config) {
4916 this.screen.setViewBox({
4917 document: this,
4918 ...config
4919 });
4920 }
4921}
4922Document.createCanvas = createCanvas;
4923Document.createImage = createImage;
4924Document.elementTypes = elementTypes;
4925
4926/**
4927 * SVG renderer on canvas.
4928 */
4929class Canvg {
4930 /**
4931 * Main constructor.
4932 * @param ctx - Rendering context.
4933 * @param svg - SVG Document.
4934 * @param options - Rendering options.
4935 */
4936 constructor(ctx, svg, options = {}) {
4937 this.parser = new Parser(options);
4938 this.screen = new Screen(ctx, options);
4939 this.options = options;
4940 const document = new Document(this, options);
4941 const documentElement = document.createDocumentElement(svg);
4942 this.document = document;
4943 this.documentElement = documentElement;
4944 }
4945 /**
4946 * Create Canvg instance from SVG source string or URL.
4947 * @param ctx - Rendering context.
4948 * @param svg - SVG source string or URL.
4949 * @param options - Rendering options.
4950 */
4951 static async from(ctx, svg, options = {}) {
4952 const parser = new Parser(options);
4953 const svgDocument = await parser.parse(svg);
4954 return new Canvg(ctx, svgDocument, options);
4955 }
4956 /**
4957 * Create Canvg instance from SVG source string.
4958 * @param ctx - Rendering context.
4959 * @param svg - SVG source string.
4960 * @param options - Rendering options.
4961 */
4962 static fromString(ctx, svg, options = {}) {
4963 const parser = new Parser(options);
4964 const svgDocument = parser.parseFromString(svg);
4965 return new Canvg(ctx, svgDocument, options);
4966 }
4967 /**
4968 * Create new Canvg instance with inherited options.
4969 * @param ctx - Rendering context.
4970 * @param svg - SVG source string or URL.
4971 * @param options - Rendering options.
4972 */
4973 fork(ctx, svg, options = {}) {
4974 return Canvg.from(ctx, svg, {
4975 ...this.options,
4976 ...options
4977 });
4978 }
4979 /**
4980 * Create new Canvg instance with inherited options.
4981 * @param ctx - Rendering context.
4982 * @param svg - SVG source string.
4983 * @param options - Rendering options.
4984 */
4985 forkString(ctx, svg, options = {}) {
4986 return Canvg.fromString(ctx, svg, {
4987 ...this.options,
4988 ...options
4989 });
4990 }
4991 /**
4992 * Document is ready promise.
4993 */
4994 ready() {
4995 return this.screen.ready();
4996 }
4997 /**
4998 * Document is ready value.
4999 */
5000 isReady() {
5001 return this.screen.isReady();
5002 }
5003 /**
5004 * Render only first frame, ignoring animations and mouse.
5005 * @param options - Rendering options.
5006 */
5007 async render(options = {}) {
5008 this.start({
5009 enableRedraw: true,
5010 ignoreAnimation: true,
5011 ignoreMouse: true,
5012 ...options
5013 });
5014 await this.ready();
5015 this.stop();
5016 }
5017 /**
5018 * Start rendering.
5019 * @param options - Render options.
5020 */
5021 start(options = {}) {
5022 const { documentElement, screen, options: baseOptions } = this;
5023 screen.start(documentElement, {
5024 enableRedraw: true,
5025 ...baseOptions,
5026 ...options
5027 });
5028 }
5029 /**
5030 * Stop rendering.
5031 */
5032 stop() {
5033 this.screen.stop();
5034 }
5035 /**
5036 * Resize SVG to fit in given size.
5037 * @param width
5038 * @param height
5039 * @param preserveAspectRatio
5040 */
5041 resize(width, height = width, preserveAspectRatio = false) {
5042 this.documentElement.resize(width, height, preserveAspectRatio);
5043 }
5044}
5045
5046/**
5047 * Options preset for `OffscreenCanvas`.
5048 * @param config - Preset requirements.
5049 * @param config.DOMParser - XML/HTML parser from string into DOM Document.
5050 */
5051function offscreen({ DOMParser: DOMParserFallback } = {}) {
5052 const preset = {
5053 window: null,
5054 ignoreAnimation: true,
5055 ignoreMouse: true,
5056 DOMParser: DOMParserFallback,
5057 createCanvas(width, height) {
5058 return new OffscreenCanvas(width, height);
5059 },
5060 async createImage(url) {
5061 const response = await fetch(url);
5062 const blob = await response.blob();
5063 const img = await createImageBitmap(blob);
5064 return img;
5065 }
5066 };
5067 if (typeof DOMParser !== 'undefined'
5068 || typeof DOMParserFallback === 'undefined') {
5069 Reflect.deleteProperty(preset, 'DOMParser');
5070 }
5071 return preset;
5072}
5073
5074/**
5075 * Options preset for `node-canvas`.
5076 * @param config - Preset requirements.
5077 * @param config.DOMParser - XML/HTML parser from string into DOM Document.
5078 * @param config.canvas - `node-canvas` exports.
5079 * @param config.fetch - WHATWG-compatible `fetch` function.
5080 */
5081function node({ DOMParser, canvas, fetch }) {
5082 return {
5083 window: null,
5084 ignoreAnimation: true,
5085 ignoreMouse: true,
5086 DOMParser,
5087 fetch,
5088 createCanvas: canvas.createCanvas,
5089 createImage: canvas.loadImage
5090 };
5091}
5092
5093var index = /*#__PURE__*/Object.freeze({
5094 __proto__: null,
5095 offscreen: offscreen,
5096 node: node
5097});
5098
5099export default Canvg;
5100export { AElement, AnimateColorElement, AnimateElement, AnimateTransformElement, BoundingBox, CB1, CB2, CB3, CB4, Canvg, CircleElement, ClipPathElement, DefsElement, DescElement, Document, Element, EllipseElement, FeColorMatrixElement, FeCompositeElement, FeDropShadowElement, FeGaussianBlurElement, FeMorphologyElement, FilterElement, Font, FontElement, FontFaceElement, GElement, GlyphElement, GradientElement, ImageElement, LineElement, LinearGradientElement, MarkerElement, MaskElement, Matrix, MissingGlyphElement, Mouse, PSEUDO_ZERO, Parser, PathElement, PathParser, PatternElement, Point, PolygonElement, PolylineElement, Property, QB1, QB2, QB3, RadialGradientElement, RectElement, RenderedElement, Rotate, SVGElement, SVGFontLoader, Scale, Screen, Skew, SkewX, SkewY, StopElement, StyleElement, SymbolElement, TRefElement, TSpanElement, TextElement, TextPathElement, TitleElement, Transform, Translate, UnknownElement, UseElement, ViewPort, compressSpaces, getSelectorSpecificity, normalizeAttributeName, normalizeColor, parseExternalUrl, index as presets, toNumbers, trimLeft, trimRight, vectorMagnitude, vectorsAngle, vectorsRatio };
5101//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguYmFiZWwuanMiLCJzb3VyY2VzIjpbXSwic291cmNlc0NvbnRlbnQiOltdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OyJ9