@gemx-dev/clarity-visualize 0.8.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +1 -0
  2. package/build/clarity.visualize.js +2128 -0
  3. package/build/clarity.visualize.min.js +1 -0
  4. package/build/clarity.visualize.module.js +2125 -0
  5. package/package.json +45 -0
  6. package/rollup.config.ts +79 -0
  7. package/src/clarity.ts +3 -0
  8. package/src/data.ts +103 -0
  9. package/src/enrich.ts +80 -0
  10. package/src/global.ts +9 -0
  11. package/src/heatmap.ts +299 -0
  12. package/src/index.ts +2 -0
  13. package/src/interaction.ts +507 -0
  14. package/src/layout.ts +727 -0
  15. package/src/styles/blobUnavailable/chineseSimplified.svg +5 -0
  16. package/src/styles/blobUnavailable/chineseTraditional.svg +5 -0
  17. package/src/styles/blobUnavailable/dutch.svg +5 -0
  18. package/src/styles/blobUnavailable/english.svg +5 -0
  19. package/src/styles/blobUnavailable/french.svg +5 -0
  20. package/src/styles/blobUnavailable/german.svg +5 -0
  21. package/src/styles/blobUnavailable/iconOnly.svg +4 -0
  22. package/src/styles/blobUnavailable/italian.svg +5 -0
  23. package/src/styles/blobUnavailable/japanese.svg +5 -0
  24. package/src/styles/blobUnavailable/korean.svg +5 -0
  25. package/src/styles/blobUnavailable/portuguese.svg +5 -0
  26. package/src/styles/blobUnavailable/russian.svg +5 -0
  27. package/src/styles/blobUnavailable/spanish.svg +5 -0
  28. package/src/styles/blobUnavailable/turkish.svg +5 -0
  29. package/src/styles/iframeUnavailable/chineseSimplified.svg +5 -0
  30. package/src/styles/iframeUnavailable/chineseTraditional.svg +5 -0
  31. package/src/styles/iframeUnavailable/dutch.svg +5 -0
  32. package/src/styles/iframeUnavailable/english.svg +5 -0
  33. package/src/styles/iframeUnavailable/french.svg +5 -0
  34. package/src/styles/iframeUnavailable/german.svg +5 -0
  35. package/src/styles/iframeUnavailable/iconOnly.svg +4 -0
  36. package/src/styles/iframeUnavailable/italian.svg +5 -0
  37. package/src/styles/iframeUnavailable/japanese.svg +5 -0
  38. package/src/styles/iframeUnavailable/korean.svg +5 -0
  39. package/src/styles/iframeUnavailable/portuguese.svg +5 -0
  40. package/src/styles/iframeUnavailable/russian.svg +5 -0
  41. package/src/styles/iframeUnavailable/spanish.svg +5 -0
  42. package/src/styles/iframeUnavailable/turkish.svg +5 -0
  43. package/src/styles/imageMasked/chineseSimplified.svg +5 -0
  44. package/src/styles/imageMasked/chineseTraditional.svg +5 -0
  45. package/src/styles/imageMasked/dutch.svg +5 -0
  46. package/src/styles/imageMasked/english.svg +5 -0
  47. package/src/styles/imageMasked/french.svg +5 -0
  48. package/src/styles/imageMasked/german.svg +5 -0
  49. package/src/styles/imageMasked/iconOnly.svg +4 -0
  50. package/src/styles/imageMasked/italian.svg +5 -0
  51. package/src/styles/imageMasked/japanese.svg +5 -0
  52. package/src/styles/imageMasked/korean.svg +5 -0
  53. package/src/styles/imageMasked/portuguese.svg +5 -0
  54. package/src/styles/imageMasked/russian.svg +5 -0
  55. package/src/styles/imageMasked/spanish.svg +5 -0
  56. package/src/styles/imageMasked/turkish.svg +5 -0
  57. package/src/styles/pointer/click.css +31 -0
  58. package/src/styles/pointer/pointerIcon.svg +18 -0
  59. package/src/styles/shared.css +6 -0
  60. package/src/visualizer.ts +260 -0
  61. package/tsconfig.json +21 -0
  62. package/tslint.json +33 -0
  63. package/types/index.d.ts +10 -0
  64. package/types/string-import.d.ts +9 -0
  65. package/types/visualize.d.ts +236 -0
@@ -0,0 +1,507 @@
1
+ import { Asset, Constant, PlaybackState, Point, Setting } from "@clarity-types/visualize";
2
+ import { Data, Layout } from "clarity-js";
3
+ import type { Interaction } from "clarity-decode"
4
+ import { LayoutHelper } from "./layout";
5
+ import pointerSvg from "./styles/pointer/pointerIcon.svg";
6
+ import clickStyle from "./styles/pointer/click.css";
7
+ import { BooleanFlag } from "clarity-decode/types/data";
8
+ import { ClickVizualizationData } from "clarity-decode/types/interaction";
9
+
10
+ export class InteractionHelper {
11
+ static TRAIL_START_COLOR = [242, 97, 12]; // rgb(242,97,12)
12
+ static TRAIL_END_COLOR = [249, 220, 209]; // rgb(249,220,209)
13
+
14
+ hoverId: number = null;
15
+ targetId: number = null;
16
+ points: Point[] = [];
17
+ scrollPointIndex = 0;
18
+ clickAudio = null;
19
+ layout: LayoutHelper;
20
+ state: PlaybackState;
21
+ vnext: boolean;
22
+ visualizedClicks: ClickVizualizationData[] = [];
23
+
24
+ constructor(state: PlaybackState, layout: LayoutHelper, vnext: boolean) {
25
+ this.state = state;
26
+ this.layout = layout;
27
+ this.vnext = vnext;
28
+ }
29
+
30
+ public reset = (): void => {
31
+ this.points = [];
32
+ this.scrollPointIndex = 0;
33
+ this.clickAudio = null;
34
+ this.hoverId = null;
35
+ this.targetId = null;
36
+ this.layout.reset();
37
+ };
38
+
39
+ public scroll = (event: Interaction.ScrollEvent): void => {
40
+ let data = event.data;
41
+ let doc = this.state.window.document;
42
+ let de = doc.documentElement;
43
+ let scrollTarget = this.layout.element(data.target as number) as HTMLElement || doc.body;
44
+ let scrollable = scrollTarget.scrollHeight > scrollTarget.clientHeight || scrollTarget.scrollWidth > scrollTarget.clientWidth;
45
+ if (scrollTarget && scrollable) {
46
+ scrollTarget.scrollTo(data.x, data.y);
47
+ // In an edge case, scrolling API doesn't work when css on HTML element has height:100% and overflow:auto
48
+ // In those cases, we fall back to scrolling the body element.
49
+ if (scrollTarget === de && scrollTarget.offsetTop !== data.y) {
50
+ scrollTarget = doc.body;
51
+ scrollTarget.scrollTo(data.x, data.y);
52
+ }
53
+ }
54
+
55
+ // Position canvas relative to scroll events on the parent page
56
+ if (scrollTarget === de || scrollTarget === doc.body) {
57
+ if (!scrollable) {
58
+ this.state.window.scrollTo(data.x, data.y);
59
+ }
60
+ let canvas = this.overlay();
61
+ if (canvas) {
62
+ canvas.style.left = data.x + Constant.Pixel;
63
+ canvas.style.top = data.y + Constant.Pixel;
64
+ canvas.width = de.clientWidth;
65
+ canvas.height = de.clientHeight;
66
+ }
67
+ this.scrollPointIndex = this.points.length;
68
+ }
69
+ };
70
+
71
+ public resize = (event: Interaction.ResizeEvent): void => {
72
+ let data = event.data;
73
+ let width = data.width;
74
+ let height = data.height;
75
+ if (this.state.options.onresize) {
76
+ this.state.options.onresize(width, height);
77
+ }
78
+ };
79
+
80
+ public visibility = (event: Interaction.VisibilityEvent): void => {
81
+ let doc = this.state.window.document;
82
+ if (doc && doc.documentElement && event.data.visible === BooleanFlag.False) {
83
+ // if the website has styles on the <html> node then we need to save the reference to them before we change them
84
+ // to indicate the window was hidden. This is to ensure that we can restore the original styles when the window is visible again.
85
+ const bg = doc.documentElement.style.backgroundColor;
86
+ if (bg) {
87
+ doc.documentElement.setAttribute(Constant.OriginalBackgroundColor, bg);
88
+ }
89
+ const o = doc.documentElement.style.opacity;
90
+ if (o) {
91
+ doc.documentElement.setAttribute(Constant.OriginalOpacity, o);
92
+ }
93
+ doc.documentElement.style.backgroundColor = Constant.Black;
94
+ doc.documentElement.style.opacity = Constant.HiddenOpacity;
95
+ } else {
96
+ if (doc.documentElement.getAttribute(Constant.OriginalBackgroundColor)) {
97
+ doc.documentElement.style.backgroundColor = doc.documentElement.getAttribute(Constant.OriginalBackgroundColor);
98
+ } else {
99
+ doc.documentElement.style.backgroundColor = '';
100
+ }
101
+ if (doc.documentElement.getAttribute(Constant.OriginalOpacity)) {
102
+ doc.documentElement.style.opacity = doc.documentElement.getAttribute(Constant.OriginalOpacity);
103
+ } else {
104
+ doc.documentElement.style.opacity = '';
105
+ }
106
+ }
107
+ };
108
+
109
+ public input = (event: Interaction.InputEvent): void => {
110
+ let data = event.data;
111
+ let el = this.layout.element(data.target as number) as HTMLInputElement;
112
+ if (el) {
113
+ switch (el.type) {
114
+ case "checkbox":
115
+ case "radio":
116
+ el.checked = data.value === "true";
117
+ break;
118
+ default:
119
+ el.value = data.value;
120
+ break;
121
+ }
122
+ }
123
+ };
124
+
125
+ public selection = (event: Interaction.SelectionEvent): void => {
126
+ let data = event.data;
127
+ let doc = this.state.window.document;
128
+ let s = doc.getSelection();
129
+ // Wrapping selection code inside a try / catch to avoid throwing errors when dealing with elements inside the shadow DOM.
130
+ try { s.setBaseAndExtent(this.layout.element(data.start as number), data.startOffset, this.layout.element(data.end as number), data.endOffset); } catch (ex) {
131
+ console.warn("Exception encountered while trying to set selection: " + ex);
132
+ }
133
+ };
134
+
135
+ public pointer = (event: Interaction.PointerEvent): void => {
136
+ if (!this.state.options.pointer) {
137
+ return;
138
+ }
139
+ let data = event.data;
140
+ let type = event.event;
141
+ let doc = this.state.window.document;
142
+ let de = doc.documentElement;
143
+ let p = doc.getElementById(Constant.PointerLayer);
144
+ let pointerWidth = Setting.PointerWidth;
145
+ let pointerHeight = Setting.PointerHeight;
146
+
147
+ if (p === null) {
148
+ p = doc.createElement("DIV");
149
+ p.id = Constant.PointerLayer;
150
+ de.appendChild(p);
151
+
152
+ // Add custom styles
153
+ let style = doc.createElement("STYLE");
154
+ style.textContent =
155
+ "@keyframes pulsate-one { 0% { transform: scale(1, 1); opacity: 1; } 100% { transform: scale(3, 3); opacity: 0; } }" +
156
+ "@keyframes pulsate-two { 0% { transform: scale(1, 1); opacity: 1; } 100% { transform: scale(5, 5); opacity: 0; } }" +
157
+ "@keyframes pulsate-touch { 0% { transform: scale(1, 1); opacity: 1; } 100% { transform: scale(2, 2); opacity: 0; } }" +
158
+ "@keyframes disappear { 90% { transform: scale(1, 1); opacity: 1; } 100% { transform: scale(1.3, 1.3); opacity: 0; } }" +
159
+ `#${Constant.InteractionCanvas} { position: absolute; left: 0; top: 0; z-index: ${Setting.ZIndex}; background: none; }` +
160
+ `#${Constant.PointerLayer} { position: absolute; z-index: ${Setting.ZIndex}; url(${Asset.Pointer}) no-repeat left center; width: ${pointerWidth}px; height: ${pointerHeight}px; }` +
161
+ this.getClickLayerStyle() +
162
+ `.${Constant.TouchLayer} { background: radial-gradient(rgba(242,97,12,1), transparent); }` +
163
+ `.${Constant.TouchRing} { background: transparent; border: 1px solid rgba(242,97,12,0.8); }` +
164
+ `.${Constant.PointerClickLayer} { background-image: url(${Asset.Click}); }` +
165
+ `.${Constant.PointerNone} { background: none; }` +
166
+ this.getPointerStyle();
167
+
168
+ p.appendChild(style);
169
+ }
170
+
171
+ p.style.left = (data.x - Setting.PointerOffset) + Constant.Pixel;
172
+ p.style.top = (data.y - Setting.PointerOffset) + Constant.Pixel;
173
+ let title = "Pointer"
174
+ switch (type) {
175
+ case Data.Event.Click:
176
+ title = "Click";
177
+ this.visualizedClicks.push({
178
+ doc: de,
179
+ click: this.drawClick(doc, data.x, data.y, title),
180
+ time: event.time
181
+ });
182
+ if (this.state.options.onclickMismatch) {
183
+ const originalTarget = this.layout.element(data.target as number);
184
+ let correctTargetHit = false;
185
+ const elementsUnderClick = doc.elementsFromPoint(data.x, data.y);
186
+ for (const elementUnderClick of elementsUnderClick) {
187
+ if (originalTarget === elementUnderClick) {
188
+ correctTargetHit = true;
189
+ }
190
+ }
191
+ if (!correctTargetHit) {
192
+ this.state.options.onclickMismatch({
193
+ time: event.time,
194
+ x: data.x,
195
+ y: data.y,
196
+ nodeId: data.target as number});
197
+ }
198
+ }
199
+
200
+ p.className = Constant.PointerNone;
201
+ break;
202
+ case Data.Event.DoubleClick:
203
+ title = "Click";
204
+ this.visualizedClicks.push({
205
+ doc: de,
206
+ click: this.drawClick(doc, data.x, data.y, title),
207
+ time: event.time
208
+ });
209
+ p.className = Constant.PointerNone;
210
+ break;
211
+ case Data.Event.TouchStart:
212
+ case Data.Event.TouchEnd:
213
+ case Data.Event.TouchCancel:
214
+ title = "Touch";
215
+ this.visualizedClicks.push({
216
+ doc: de,
217
+ click: this.drawTouch(doc, data.x, data.y, title),
218
+ time: event.time
219
+ });
220
+ p.className = Constant.PointerNone;
221
+ break;
222
+ case Data.Event.TouchMove:
223
+ title = "Touch Move";
224
+ p.className = Constant.PointerNone;
225
+ break;
226
+ case Data.Event.MouseMove:
227
+ title = "Mouse Move";
228
+ p.className = Constant.PointerMove;
229
+ this.addPoint({ time: event.time, x: data.x, y: data.y });
230
+ this.targetId = data.target as number;
231
+ break;
232
+ default:
233
+ p.className = Constant.PointerMove;
234
+ break;
235
+ }
236
+ p.setAttribute(Constant.Title, `${title} (${data.x}${Constant.Pixel}, ${data.y}${Constant.Pixel})`);
237
+ };
238
+
239
+ public clearOldClickVisualizations = (currentTimestamp: number): void => {
240
+ if (this.vnext) {
241
+ while(this.visualizedClicks.length > Setting.MaxClicksDisplayed) {
242
+ const visualizedClick = this.visualizedClicks.shift();
243
+ this.fadeOutElement(visualizedClick.click, visualizedClick.doc);
244
+ }
245
+
246
+ var tooOldClicks = this.visualizedClicks.filter(click => currentTimestamp - click.time > Setting.MaxClickDisplayDuration);
247
+ tooOldClicks.forEach(click => {
248
+ this.fadeOutElement(click.click, click.doc);
249
+ this.visualizedClicks.splice(this.visualizedClicks.indexOf(click), 1);
250
+ });
251
+ }
252
+ }
253
+
254
+ private fadeOutElement = (element: HTMLElement, document: HTMLElement): void => {
255
+ element.classList.add("clarity-click-hidden");
256
+ setTimeout(() => { document.removeChild(element); }, 10000);
257
+ }
258
+
259
+ private hover = (): void => {
260
+ if (this.targetId && this.targetId !== this.hoverId) {
261
+ let depth = 0;
262
+ // First, remove any previous hover class assignments
263
+ let hoverNode = this.hoverId ? this.layout.element(this.hoverId) as HTMLElement : null;
264
+ while (hoverNode && depth < Setting.HoverDepth) {
265
+ if ("removeAttribute" in hoverNode) { hoverNode.removeAttribute(Constant.HoverAttribute); }
266
+ hoverNode = hoverNode.parentElement;
267
+ depth++;
268
+ }
269
+ // Then, add hover class on elements that are below the pointer
270
+ depth = 0;
271
+ let targetNode = this.targetId ? this.layout.element(this.targetId) as HTMLElement : null;
272
+ while (targetNode && depth < Setting.HoverDepth) {
273
+ if ("setAttribute" in targetNode) { targetNode.setAttribute(Constant.HoverAttribute, Layout.Constant.Empty); }
274
+ targetNode = targetNode.parentElement;
275
+ depth++;
276
+ }
277
+ // Finally, update hoverId to reflect the new node
278
+ this.hoverId = this.targetId;
279
+ }
280
+ };
281
+
282
+ private addPoint = (point: Point): void => {
283
+ let last = this.points.length > 0 ? this.points[this.points.length - 1] : null;
284
+ if (last && point.x === last.x && point.y === last.y) {
285
+ last.time = point.time;
286
+ } else { this.points.push(point); }
287
+ }
288
+
289
+ private drawTouch = (doc: Document, x: number, y: number, title: string): HTMLElement => {
290
+ let de = doc.documentElement;
291
+ let touch = doc.createElement("DIV");
292
+ touch.className = Constant.TouchLayer;
293
+ touch.setAttribute(Constant.Title, `${title} (${x}${Constant.Pixel}, ${y}${Constant.Pixel})`);
294
+ touch.style.left = (x - Setting.ClickRadius / 2) + Constant.Pixel;
295
+ touch.style.top = (y - Setting.ClickRadius / 2) + Constant.Pixel
296
+ touch.style.animation = "disappear 1 1s";
297
+ touch.style.animationFillMode = "forwards";
298
+ de.appendChild(touch);
299
+
300
+ // First pulsating ring
301
+ let ringOne = touch.cloneNode() as HTMLElement;
302
+ ringOne.className = Constant.TouchRing;
303
+ ringOne.style.left = "-0.5" + Constant.Pixel;
304
+ ringOne.style.top = "-0.5" + Constant.Pixel;
305
+ ringOne.style.animation = "pulsate-touch 1 1s";
306
+ ringOne.style.animationFillMode = "forwards";
307
+ touch.appendChild(ringOne);
308
+
309
+ return touch;
310
+ };
311
+
312
+ private drawClick = (doc: Document, x: number, y: number, title: string): HTMLElement => {
313
+ let de = doc.documentElement;
314
+ let click = doc.createElement("DIV");
315
+ click.className = Constant.ClickLayer;
316
+
317
+ click.setAttribute(Constant.Title, `${title} (${x}${Constant.Pixel}, ${y}${Constant.Pixel})`);
318
+ click.style.left = (x - Setting.ClickRadius / 2) + Constant.Pixel;
319
+ click.style.top = (y - Setting.ClickRadius / 2) + Constant.Pixel
320
+
321
+ // First pulsating ring
322
+ let ringOne = click.cloneNode() as HTMLElement;
323
+ ringOne.className = Constant.ClickRing;
324
+ ringOne.style.left = "-0.5" + Constant.Pixel;
325
+ ringOne.style.top = "-0.5" + Constant.Pixel;
326
+ ringOne.style.animation = "pulsate-one 1 1s";
327
+ ringOne.style.animationFillMode = "forwards";
328
+ click.appendChild(ringOne);
329
+
330
+ if (this.vnext) {
331
+ let center = doc.createElement("DIV");
332
+ center.className = `${Constant.ClickLayer}-center`;
333
+ click.appendChild(center);
334
+ } else {
335
+ // Second pulsating ring
336
+ let ringTwo = ringOne.cloneNode() as HTMLElement;
337
+ ringTwo.style.animation = "pulsate-two 1 1s";
338
+ click.appendChild(ringTwo);
339
+ }
340
+ de.appendChild(click);
341
+
342
+ // Play sound
343
+ if (typeof Audio !== Constant.Undefined) {
344
+ if (this.clickAudio === null) {
345
+ this.clickAudio = new Audio(Asset.Sound);
346
+ click.appendChild(this.clickAudio);
347
+ }
348
+ this.clickAudio.play();
349
+ }
350
+ return click;
351
+ };
352
+
353
+ private overlay = (): HTMLCanvasElement => {
354
+ // Create canvas for visualizing interactions
355
+ let doc = this.state.window.document;
356
+ let de = doc.documentElement;
357
+ let canvas = doc.getElementById(Constant.InteractionCanvas) as HTMLCanvasElement;
358
+ if (canvas === null) {
359
+ canvas = doc.createElement("canvas");
360
+ canvas.id = Constant.InteractionCanvas;
361
+ canvas.width = 0;
362
+ canvas.height = 0;
363
+ de.appendChild(canvas);
364
+ }
365
+
366
+ if (canvas.width !== de.clientWidth || canvas.height !== de.clientHeight) {
367
+ canvas.width = de.clientWidth;
368
+ canvas.height = de.clientHeight;
369
+ }
370
+
371
+ return canvas;
372
+ };
373
+
374
+ private match = (time: number): Point[] => {
375
+ let p = [];
376
+ for (let i = this.points.length - 1; i > 0; i--) {
377
+ // Each pixel in the trail has a pixel life of 3s. The only exception to this is if the user scrolled.
378
+ // We reset the trail after every scroll event to avoid drawing weird looking trail.
379
+ if (i >= this.scrollPointIndex && time - this.points[i].time < Setting.PixelLife) {
380
+ p.push(this.points[i]);
381
+ } else { break; }
382
+ }
383
+ return p.slice(0, Setting.MaxTrailPoints);
384
+ };
385
+
386
+ public trail = (now: number): void => {
387
+ const canvas = this.overlay();
388
+ if (this.state.options.canvas && canvas) {
389
+ const ctx = canvas.getContext('2d');
390
+ const path = this.state.options.keyframes ? this.curve(this.points.reverse()) : this.curve(this.match(now));
391
+ // Update hovered elements
392
+ this.hover();
393
+ // We need at least two points to create a line
394
+ if (path.length > 1) {
395
+ let last = path[0];
396
+ // Start off by clearing whatever was on the canvas before
397
+ // The current implementation is inefficient. We have to redraw canvas all over again for every point.
398
+ // In future we should batch pointer events and minimize the number of times we have to redraw canvas.
399
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
400
+ let count = path.length;
401
+ let offsetX = canvas.offsetLeft;
402
+ let offsetY = canvas.offsetTop;
403
+ for (let i = 1; i < count; i++) {
404
+ let current = path[i];
405
+
406
+ // Compute percentage position of these points compared to all points in the path
407
+ let lastFactor = 1 - ((i - 1) / count);
408
+ let currentFactor = 1 - (i / count);
409
+
410
+ // Generate a color gradient that goes from red -> yellow -> green -> light blue -> blue
411
+ let gradient = ctx.createLinearGradient(last.x, last.y, current.x, current.y);
412
+ gradient.addColorStop(1, this.color(currentFactor))
413
+ gradient.addColorStop(0, this.color(lastFactor))
414
+
415
+ // Line width of the trail shrinks as the position of the point goes farther away.
416
+ ctx.lineWidth = Setting.TrailWidth * currentFactor;
417
+ ctx.lineCap = Constant.Round;
418
+ ctx.lineJoin = Constant.Round;
419
+ ctx.strokeStyle = gradient;
420
+ ctx.beginPath();
421
+
422
+ // The coordinates need to be relative to where canvas is rendered.
423
+ // In case of scrolling on the page, canvas may be relative to viewport
424
+ // while trail points are relative to screen origin (0, 0). We make the adjustment so trail looks right.
425
+ ctx.moveTo(last.x - offsetX, last.y - offsetY);
426
+ ctx.lineTo(current.x - offsetX, current.y - offsetY);
427
+ ctx.stroke();
428
+ ctx.closePath();
429
+ last = current;
430
+ }
431
+ }
432
+ // If we are only rendering key frames, clear points array after each key frame
433
+ if (this.state.options.keyframes) { this.points = []; }
434
+ }
435
+ };
436
+
437
+ private color = (factor: number): string => {
438
+ let s = InteractionHelper.TRAIL_START_COLOR;
439
+ let e = InteractionHelper.TRAIL_END_COLOR;
440
+ let c = [];
441
+ for (let i = 0; i < 3; i++) { c[i] = Math.round(e[i] + factor * (s[i] - e[i])); }
442
+ return `rgba(${c[0]}, ${c[1]}, ${c[2]}, ${factor})`;
443
+ };
444
+
445
+ // Reference: https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Cardinal_spline
446
+ private curve = (path: Point[]): Point[] => {
447
+ const tension = 0.5;
448
+ let p = [];
449
+ let output = [];
450
+
451
+ // Make a copy of the input points so we don't make any side effects
452
+ p = path.slice(0);
453
+ // The algorithm require a valid previous and next point for each point in the original input
454
+ // Duplicate first and last point in the path to the beginning and the end of the array respectively
455
+ // E.g. [{x:37,y:45}, {x:54,y:34}] => [{x:37,y:45}, {x:37,y:45}, {x:54,y:34}, {x:54,y:34}]
456
+ p.unshift(path[0]);
457
+ p.push(path[path.length - 1]);
458
+ // Loop through the points, and generate intermediate points to make a smooth trail
459
+ for (let i = 1; i < p.length - 2; i++) {
460
+ const time = p[i].time;
461
+ const segments = Math.max(Math.min(Math.round(this.distance(p[i], p[i - 1])), 10), 1);
462
+ for (let t = 0; t <= segments; t++) {
463
+
464
+ // Compute tension vectors
465
+ let t1: Point = { time, x: (p[i + 1].x - p[i - 1].x) * tension, y: (p[i + 1].y - p[i - 1].y) * tension };
466
+ let t2: Point = { time, x: (p[i + 2].x - p[i].x) * tension, y: (p[i + 2].y - p[i].y) * tension };
467
+ let step = t / segments;
468
+
469
+ // Compute cardinals
470
+ let c1 = 2 * Math.pow(step, 3) - 3 * Math.pow(step, 2) + 1;
471
+ let c2 = -(2 * Math.pow(step, 3)) + 3 * Math.pow(step, 2);
472
+ let c3 = Math.pow(step, 3) - 2 * Math.pow(step, 2) + step;
473
+ let c4 = Math.pow(step, 3) - Math.pow(step, 2);
474
+
475
+ // Compute new point with common control vectors
476
+ let x = c1 * p[i].x + c2 * p[i + 1].x + c3 * t1.x + c4 * t2.x;
477
+ let y = c1 * p[i].y + c2 * p[i + 1].y + c3 * t1.y + c4 * t2.y;
478
+
479
+ output.push({ time, x, y });
480
+ }
481
+ }
482
+ return output;
483
+ };
484
+
485
+ private distance = (a: Point, b: Point): number => {
486
+ const dx = a.x - b.x;
487
+ const dy = a.y - b.y;
488
+ return Math.sqrt(dx * dx + dy * dy);
489
+ };
490
+
491
+ private getPointerStyle = (): string => {
492
+ if (this.vnext) {
493
+ return `.${Constant.PointerMove} { ${pointerSvg} }`;
494
+ } else {
495
+ return `.${Constant.PointerMove} { background-image: url(${Asset.Pointer}); }`;
496
+ }
497
+ }
498
+
499
+ private getClickLayerStyle = (): string => {
500
+ if (this.vnext) {
501
+ return clickStyle;
502
+ } else {
503
+ return `.${Constant.ClickLayer}, .${Constant.ClickRing}, .${Constant.TouchLayer}, .${Constant.TouchRing} { position: absolute; z-index: ${Setting.ZIndex}; border-radius: 50%; background: radial-gradient(rgba(0,90,158,0.8), transparent); width: ${Setting.ClickRadius}px; height: ${Setting.ClickRadius}px;}` +
504
+ `.${Constant.ClickRing} { background: transparent; border: 1px solid rgba(0,90,158,0.8); }`;
505
+ }
506
+ }
507
+ }