@ax-design/reveal-highlight 0.3.0-alpha.8 → 0.4.2

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 (86) hide show
  1. package/README.md +8 -167
  2. package/build/BaseConfig.d.ts +43 -0
  3. package/build/BaseConfig.d.ts.map +1 -0
  4. package/build/CanvasConfig.d.ts +7 -92
  5. package/build/CanvasConfig.d.ts.map +1 -1
  6. package/build/CustomElements.d.ts +20 -4
  7. package/build/CustomElements.d.ts.map +1 -1
  8. package/build/RevealBoundryStore.d.ts +50 -10
  9. package/build/RevealBoundryStore.d.ts.map +1 -1
  10. package/build/RevealStateManager.d.ts +3 -0
  11. package/build/RevealStateManager.d.ts.map +1 -1
  12. package/build/SvgConfig.d.ts +17 -0
  13. package/build/SvgConfig.d.ts.map +1 -0
  14. package/build/index.d.ts.map +1 -1
  15. package/build/main.js +169 -17
  16. package/build/utils/extractRGBValue.d.ts +2 -0
  17. package/build/utils/extractRGBValue.d.ts.map +1 -0
  18. package/build/utils/isValidateBorderDecorationType.d.ts +3 -0
  19. package/build/utils/isValidateBorderDecorationType.d.ts.map +1 -0
  20. package/build/utils/types.d.ts +103 -0
  21. package/build/utils/types.d.ts.map +1 -0
  22. package/es/BaseConfig.d.ts +43 -0
  23. package/es/BaseConfig.d.ts.map +1 -0
  24. package/es/BaseConfig.js +220 -0
  25. package/es/BaseConfig.js.map +1 -0
  26. package/es/CanvasConfig.d.ts +7 -92
  27. package/es/CanvasConfig.d.ts.map +1 -1
  28. package/es/CanvasConfig.js +35 -233
  29. package/es/CanvasConfig.js.map +1 -1
  30. package/es/CustomElements.d.ts +20 -4
  31. package/es/CustomElements.d.ts.map +1 -1
  32. package/es/CustomElements.js +115 -18
  33. package/es/CustomElements.js.map +1 -1
  34. package/es/RevealBoundryStore.d.ts +50 -10
  35. package/es/RevealBoundryStore.d.ts.map +1 -1
  36. package/es/RevealBoundryStore.js +129 -35
  37. package/es/RevealBoundryStore.js.map +1 -1
  38. package/es/RevealStateManager.d.ts +3 -0
  39. package/es/RevealStateManager.d.ts.map +1 -1
  40. package/es/RevealStateManager.js +43 -8
  41. package/es/RevealStateManager.js.map +1 -1
  42. package/es/SvgConfig.d.ts +17 -0
  43. package/es/SvgConfig.d.ts.map +1 -0
  44. package/es/SvgConfig.js +346 -0
  45. package/es/SvgConfig.js.map +1 -0
  46. package/es/index.d.ts.map +1 -1
  47. package/es/index.js +10 -16
  48. package/es/index.js.map +1 -1
  49. package/es/tsconfig.tsbuildinfo +1 -0
  50. package/es/utils/extractRGBValue.d.ts +2 -0
  51. package/es/utils/extractRGBValue.d.ts.map +1 -0
  52. package/es/utils/extractRGBValue.js +48 -0
  53. package/es/utils/extractRGBValue.js.map +1 -0
  54. package/es/utils/isValidateBorderDecorationType.d.ts +3 -0
  55. package/es/utils/isValidateBorderDecorationType.d.ts.map +1 -0
  56. package/es/utils/isValidateBorderDecorationType.js +10 -0
  57. package/es/utils/isValidateBorderDecorationType.js.map +1 -0
  58. package/es/utils/types.d.ts +103 -0
  59. package/es/utils/types.d.ts.map +1 -0
  60. package/es/utils/types.js +2 -0
  61. package/es/utils/types.js.map +1 -0
  62. package/package.json +35 -30
  63. package/src/BaseConfig.ts +311 -0
  64. package/src/CanvasConfig.ts +66 -380
  65. package/src/CustomElements.ts +137 -29
  66. package/src/RevealBoundryStore.ts +138 -39
  67. package/src/RevealStateManager.ts +48 -10
  68. package/src/SvgConfig.ts +422 -0
  69. package/src/index.ts +10 -16
  70. package/src/utils/extractRGBValue.ts +58 -0
  71. package/src/utils/isValidateBorderDecorationType.ts +8 -0
  72. package/src/utils/types.ts +106 -0
  73. package/.prettierrc +0 -6
  74. package/.travis.yml +0 -26
  75. package/.yarnrc.yml +0 -1
  76. package/docs/demo/debug.html +0 -73
  77. package/docs/demo/index.html +0 -1065
  78. package/docs/demo/performance-inspector.html +0 -60
  79. package/docs/demo/shapes-demo.html +0 -93
  80. package/docs/demo/single-unit.html +0 -39
  81. package/docs/logo.png +0 -0
  82. package/docs/logo.svg +0 -1
  83. package/docs/screen-record.gif +0 -0
  84. package/docs/src/logo.afdesign +0 -0
  85. package/rollup.config.js +0 -14
  86. package/tsconfig.json +0 -18
@@ -2,19 +2,29 @@ import { RevealStateManager } from './RevealStateManager.js';
2
2
  import { RevealBoundaryStore } from './RevealBoundryStore.js';
3
3
  import { config } from './config.js';
4
4
 
5
+ function closest(dom: Element, selector: string): Element | null {
6
+ const r = dom.closest(selector);
7
+ if (r) return r;
8
+ const host = findParentShadowRoot(dom)?.host;
9
+ if (!host) return null;
10
+ return closest(host, selector)
11
+ }
12
+ function findParentShadowRoot(x: Node | undefined | null): ShadowRoot | null {
13
+ if (!x) return null;
14
+ if (x.parentNode && x.parentNode.nodeType === document.DOCUMENT_FRAGMENT_NODE) return x.parentNode as ShadowRoot;
15
+ return findParentShadowRoot(x.parentNode);
16
+ }
5
17
  class ServerSideHTMLElement {
6
18
  root = {
7
19
  innerHTML: '',
8
20
  querySelector: (_x: string) => null,
9
- }
10
-
21
+ };
22
+
11
23
  attachShadow = () => null;
12
24
  }
13
25
 
14
- const PatchedHTMLElement =
15
- typeof globalThis.Window === 'undefined'
16
- ? ServerSideHTMLElement as unknown as typeof HTMLElement
17
- : HTMLElement;
26
+ const PatchedHTMLElement =
27
+ typeof globalThis.Window === 'undefined' ? ((ServerSideHTMLElement as unknown) as typeof HTMLElement) : HTMLElement;
18
28
 
19
29
  export class AxRevealProvider extends PatchedHTMLElement {
20
30
  static readonly ElementName = 'ax-reveal-provider';
@@ -52,9 +62,11 @@ export class AxRevealBoundary extends PatchedHTMLElement {
52
62
  }
53
63
 
54
64
  if (oldStorage && newStorage) {
55
- this.dispatchEvent(new CustomEvent(AxRevealBoundary.replaceStorageEvent, {
56
- detail: { old: oldStorage, new: newStorage },
57
- }));
65
+ this.dispatchEvent(
66
+ new CustomEvent(AxRevealBoundary.replaceStorageEvent, {
67
+ detail: { old: oldStorage, new: newStorage },
68
+ })
69
+ );
58
70
  }
59
71
  }
60
72
 
@@ -71,27 +83,38 @@ export class AxRevealBoundary extends PatchedHTMLElement {
71
83
  return;
72
84
  }
73
85
 
74
- const parent = this.closest(AxRevealProvider.ElementName) as AxRevealProvider;
86
+ const parent = closest(this, AxRevealProvider.ElementName) as AxRevealProvider;
75
87
  const stateManager = parent ? parent.stateManager : AxRevealBoundary.stateManager;
76
88
 
77
89
  this.storage = stateManager.newBoundary(this);
78
90
  }
79
91
 
80
- updatePointerPosition = (ev: MouseEvent) => {
81
- this.waitForStorage(storage => {
92
+ /**
93
+ * Update the position of your pointer.
94
+ * @param ev Pointer event from the listener.
95
+ */
96
+ updatePointerPosition = (ev: PointerEvent) => {
97
+ this.waitForStorage((storage) => {
82
98
  storage.clientX = ev.clientX;
83
99
  storage.clientY = ev.clientY;
84
100
  });
85
101
  };
86
102
 
87
- handlePointerEnter = () => this.waitForStorage(storage => storage.onPointerEnterBoundary());
88
- handlePointerLeave = () => this.waitForStorage(storage => storage.onPointerLeaveBoundary());
89
- handlePointerMove = (ev: MouseEvent) => this.updatePointerPosition(ev);
90
- handlePointerUp = () => this.waitForStorage(storage => storage.switchAnimation());
91
- handlePointerDown = (ev: MouseEvent) => this.waitForStorage(storage => {
92
- this.updatePointerPosition(ev);
93
- storage.initializeAnimation();
94
- });
103
+ handlePointerEnter = () => this.waitForStorage((storage) => storage.onPointerEnterBoundary());
104
+ handlePointerLeave = () => this.waitForStorage((storage) => storage.onPointerLeaveBoundary());
105
+ handlePointerMove = this.updatePointerPosition;
106
+ handlePointerUp = (ev: PointerEvent) => {
107
+ if (ev.pointerType === 'mouse') {
108
+ this.waitForStorage((storage) => storage.switchAnimation());
109
+ } else {
110
+ this.waitForStorage((storage) => storage.clearAll(false));
111
+ }
112
+ };
113
+ handlePointerDown = (ev: PointerEvent) =>
114
+ this.waitForStorage((storage) => {
115
+ this.updatePointerPosition(ev);
116
+ storage.initializeAnimation();
117
+ });
95
118
 
96
119
  connectedCallback() {
97
120
  this.appendStorage(true);
@@ -113,7 +136,7 @@ export class AxRevealBoundary extends PatchedHTMLElement {
113
136
  this.root.innerHTML = `
114
137
  <div>
115
138
  <slot></slot>
116
- </div>`
139
+ </div>`;
117
140
  }
118
141
  }
119
142
 
@@ -128,27 +151,36 @@ export class AxReveal extends PatchedHTMLElement {
128
151
  this.connectedCallback();
129
152
  }
130
153
 
154
+ handlePointerDown = (ev: PointerEvent) => {
155
+ this.setPointerCapture(ev.pointerId);
156
+ };
157
+
131
158
  disconnectedCallback() {
132
- this.boundary && this.boundary.waitForStorage(storage => storage.removeReveal(this.canvas));
159
+ this.boundary && this.boundary.waitForStorage((storage) => storage.removeReveal(this.canvas));
133
160
  }
134
161
 
135
162
  connectedCallback() {
136
- this.boundary = this.closest(AxRevealBoundary.ElementName) as AxRevealBoundary;
163
+ this.boundary = closest(this, AxRevealBoundary.ElementName) as AxRevealBoundary;
137
164
  if (!this.boundary)
138
- throw new SyntaxError('You must use ' + AxRevealBoundary.ElementName + ' as the boundary of acrylic!');
165
+ throw new SyntaxError(
166
+ 'You must use ' + AxRevealBoundary.ElementName + ' as the boundary of reveal highlight!'
167
+ );
139
168
 
140
- this.boundary.waitForStorage(storage => setTimeout(() => storage.addReveal(this.canvas, this), 0));
169
+ this.boundary.waitForStorage((storage) => setTimeout(() => storage.addReveal(this.canvas, this), 0));
170
+ this.addEventListener('pointerdown', this.handlePointerDown);
141
171
  }
142
172
 
143
173
  constructor() {
144
174
  super();
145
175
  this.root.innerHTML = `
146
- <div>
147
- <slot></slot>
176
+ <div class="ax-reveal">
148
177
  <canvas></canvas>
149
- </div>
178
+ <div class="content"><slot></slot></div>
179
+ </div>
150
180
  <style>
151
- div { display: content; }
181
+ .ax-reveal { display: content; }
182
+ .ax-reveal * { user-drag: none; -webkit-user-drag: none; touch-action: none; user-select: none; }
183
+ .content { position: relative; }
152
184
  canvas { top: 0; left: 0; pointer-events: none; width: 100%; height: 100%; position: absolute; }
153
185
  :host { display: inline-block; position: relative; }
154
186
  :host([block]) { display: block; }
@@ -157,8 +189,84 @@ export class AxReveal extends PatchedHTMLElement {
157
189
  :host([inline-flex]) { display: inline-flex; }
158
190
  :host([grid]) { display: grid; }
159
191
  :host([inline-grid]) { display: inline-grid; }
192
+ ::slotted(*) { user-drag: none; -webkit-user-drag: none; touch-action: none; user-select: none; }
160
193
  ::slotted(button) { outline:none; }
161
194
  </style>`;
162
195
  this.canvas = this.root.querySelector('canvas')!;
163
196
  }
164
197
  }
198
+
199
+ export class AxRevealNg extends PatchedHTMLElement {
200
+ static readonly ElementName = 'ax-reveal-ng';
201
+ private root = this.attachShadow({ mode: 'open' });
202
+ private svg: SVGElement;
203
+ private boundary!: AxRevealBoundary;
204
+
205
+ adoptedCallback() {
206
+ this.disconnectedCallback();
207
+ this.connectedCallback();
208
+ }
209
+
210
+ handlePointerDown = (ev: PointerEvent) => {
211
+ this.setPointerCapture(ev.pointerId);
212
+ };
213
+
214
+ disconnectedCallback() {
215
+ this.boundary && this.boundary.waitForStorage((storage) => storage.removeReveal(this.svg));
216
+ }
217
+
218
+ connectedCallback() {
219
+ this.boundary = closest(this, AxRevealBoundary.ElementName) as AxRevealBoundary;
220
+ if (!this.boundary)
221
+ throw new SyntaxError(
222
+ 'You must use ' + AxRevealBoundary.ElementName + ' as the boundary of reveal highlight!'
223
+ );
224
+
225
+ this.boundary.waitForStorage((storage) => setTimeout(() => storage.addReveal(this.svg, this), 0));
226
+ this.addEventListener('pointerdown', this.handlePointerDown);
227
+ }
228
+
229
+ constructor() {
230
+ super();
231
+ this.root.innerHTML = `
232
+ <div class="ax-reveal">
233
+ <svg>
234
+ <defs>
235
+ <radialGradient id="borderGrad" gradientUnits="userSpaceOnUse" r="10">
236
+ <stop id="borderCenter" offset="0%" />
237
+ <stop id="borderOut" offset="100%" />
238
+ </radialGradient>
239
+ <radialGradient id="fillGrad" gradientUnits="userSpaceOnUse" r="10">
240
+ <stop id="fillCenter" offset="0%" />
241
+ <stop id="fillOut" offset="100%" />
242
+ </radialGradient>
243
+ <radialGradient id="rippleGrad" gradientUnits="userSpaceOnUse" r="10">
244
+ <stop id="rippleCenter" offset="0%" />
245
+ <stop id="rippleMiddle" offset="0%" />
246
+ <stop id="rippleOut" offset="0%" />
247
+ </radialGradient>
248
+ </defs>
249
+ <path id="borderPath" fill="url(#borderGrad)" />
250
+ <path id="fillPath" fill="url(#fillGrad)" />
251
+ <path id="ripplePath" fill="url(#rippleGrad)" />
252
+ </svg>
253
+ <div class="content"><slot></slot></div>
254
+ </div>
255
+ <style>
256
+ .ax-reveal { display: contents; }
257
+ .ax-reveal * { user-drag: none; -webkit-user-drag: none; touch-action: none; user-select: none; }
258
+ .content { position: relative; }
259
+ svg { top: 0; left: 0; pointer-events: none; width: 100%; height: 100%; position: absolute; }
260
+ :host { display: inline-block; position: relative; }
261
+ :host([block]) { display: block; }
262
+ :host([inline-block]) { display: inline-block; }
263
+ :host([flex]) { display: flex; }
264
+ :host([inline-flex]) { display: inline-flex; }
265
+ :host([grid]) { display: grid; }
266
+ :host([inline-grid]) { display: inline-grid; }
267
+ ::slotted(*) { user-drag: none; -webkit-user-drag: none; touch-action: none; user-select: none; }
268
+ ::slotted(button) { outline:none; }
269
+ </style>`;
270
+ this.svg = this.root.querySelector('svg')!;
271
+ }
272
+ }
@@ -1,4 +1,7 @@
1
1
  import { CanvasConfig } from './CanvasConfig.js';
2
+ import { SvgConfig } from './SvgConfig.js';
3
+
4
+ type Config = CanvasConfig | SvgConfig;
2
5
 
3
6
  export class RevealBoundaryStore {
4
7
  id: number;
@@ -13,11 +16,10 @@ export class RevealBoundaryStore {
13
16
  paintedClientY = -1000;
14
17
 
15
18
  mouseInBoundary = false;
16
- canvasList: CanvasConfig[] = [];
19
+ canvasList: Config[] = [];
17
20
 
18
- animationQueue: CanvasConfig[] = [];
21
+ animationQueue: Config[] = [];
19
22
  animationFrame: number | null = null;
20
- dirty = false;
21
23
 
22
24
  mouseUpClientX = null;
23
25
  mouseUpClientY = null;
@@ -30,84 +32,131 @@ export class RevealBoundaryStore {
30
32
 
31
33
  maxRadius = -1;
32
34
 
35
+ requestedAnimationFrame = false;
36
+
33
37
  constructor(id: number, $el: HTMLElement) {
34
38
  this.id = id;
35
39
  this.container = $el;
36
40
  }
37
41
 
38
- addReveal = ($canvas: HTMLCanvasElement, $container: HTMLElement) => {
39
- const canvasConfig = new CanvasConfig(this, $canvas, $container);
42
+ addReveal = ($element: HTMLCanvasElement | SVGElement, $container: HTMLElement) => {
43
+ let config: Config;
44
+ const elementType = Object.prototype.toString.call($element);
45
+
46
+ if (elementType === '[object HTMLCanvasElement]') {
47
+ config = new CanvasConfig(this, $element as HTMLCanvasElement, $container);
48
+ } else if (elementType === '[object SVGSVGElement]') {
49
+ config = new SvgConfig(this, $element as SVGSVGElement, $container);
50
+ } else {
51
+ throw new TypeError(`Unknown element type, got: ${elementType}, Canvas or SVG element expected.`)
52
+ }
40
53
 
41
- canvasConfig.cacheBoundingRect();
42
- canvasConfig.cacheCanvasPaintingStyle();
43
- canvasConfig.updateCachedBitmap();
54
+ config.cacheBoundingRect();
55
+ config.cacheCanvasPaintingStyle();
56
+ config.updateCachedBitmap();
44
57
 
45
- this.canvasList.push(canvasConfig);
58
+ this.canvasList.push(config);
46
59
  };
47
60
 
48
- removeReveal = ($el: HTMLCanvasElement) => {
61
+ removeReveal = ($el: HTMLCanvasElement | SVGElement) => {
49
62
  this.canvasList = this.canvasList.filter((config) => {
50
- return config && config.canvas === $el;
63
+ return config && config.element !== $el;
51
64
  });
52
65
  };
53
66
 
54
67
  updateMaxRadius = (x: number) => {
55
68
  this.maxRadius = Math.max(x, this.maxRadius);
56
- }
69
+ };
57
70
 
58
- onPointerMoveOnScreen = (x: number, y: number) => {
59
- const boundingBox = this.container.getBoundingClientRect();
71
+ requestPaintAll = (cleanup?: boolean) => {
72
+ if (this.requestedAnimationFrame) return;
73
+ this.requestedAnimationFrame = true;
60
74
 
61
- if (x < boundingBox.left - this.maxRadius) return false;
62
- if (x > boundingBox.right + this.maxRadius) return false;
63
- if (y < boundingBox.top - this.maxRadius) return false;
64
- if (y > boundingBox.bottom + this.maxRadius) return false;
65
- this.mouseInBoundary = true;
75
+ this.animationFrame = window.requestAnimationFrame((frame) => {
76
+ this.requestedAnimationFrame = false;
77
+ this.paintAll(cleanup ? -1 : frame, true);
78
+ });
79
+ };
66
80
 
81
+ updatePointerPosition = (x: number, y: number) => {
67
82
  this.clientX = x;
68
83
  this.clientY = y;
69
84
 
70
- this.animationFrame = window.requestAnimationFrame(this.paintAll);
71
- return true
85
+ const boundingBox = this.container.getBoundingClientRect();
86
+
87
+ let mouseInBoundary = true;
88
+
89
+ if (x < boundingBox.left - this.maxRadius) mouseInBoundary = false;
90
+ if (x > boundingBox.right + this.maxRadius) mouseInBoundary = false;
91
+ if (y < boundingBox.top - this.maxRadius) mouseInBoundary = false;
92
+ if (y > boundingBox.bottom + this.maxRadius) mouseInBoundary = false;
93
+
94
+ this.mouseInBoundary = mouseInBoundary;
72
95
  }
73
96
 
97
+ onPointerMoveOnScreen = (x: number, y: number) => {
98
+ this.updatePointerPosition(x, y);
99
+ if (!this.mouseInBoundary) return;
100
+ this.requestPaintAll();
101
+ return true;
102
+ };
103
+
104
+ /**
105
+ * Repaint all canvas while user's pointer has entered the boundary.
106
+ */
74
107
  onPointerEnterBoundary = () => {
75
108
  this.mouseInBoundary = true;
76
109
 
77
110
  if (!this.animationFrame) {
78
- this.animationFrame = window.requestAnimationFrame(this.paintAll);
111
+ this.requestPaintAll();
79
112
  }
80
113
  };
81
114
 
115
+ /**
116
+ * Clear all canvas while user's pointer leave the bounary.
117
+ */
82
118
  onPointerLeaveBoundary = () => {
83
119
  this.mouseInBoundary = false;
84
- this.paintAll(0, true);
120
+ this.requestPaintAll(true);
85
121
  };
86
122
 
87
- paintAll = (frame: number, force?: boolean) => {
88
- if (!force && !this.mouseInBoundary && this.animationQueue.length === 0) {
123
+ /**
124
+ * The painting method that draw everything on your screen.
125
+ * @param frame current frame index, if this value is 0, it means all canvas
126
+ * should be cleaned.
127
+ * @param skipSamePointerPositionCheck Literally.
128
+ */
129
+ private paintAll = (frame: number, skipSamePointerPositionCheck?: boolean) => {
130
+ if (!skipSamePointerPositionCheck && !this.mouseInBoundary && this.animationQueue.length === 0) {
89
131
  return;
90
132
  }
91
133
 
134
+ // Animation duration checkup
92
135
  for (let i = 0; i < this.animationQueue.length; i++) {
93
136
  const config = this.animationQueue[i];
94
137
 
95
138
  if (!frame || config.currentFrameId === frame) {
96
- return;
139
+ continue;
97
140
  }
98
141
 
99
142
  config.currentFrameId = frame;
100
143
 
144
+ // Initialize the animation parameter
101
145
  if (config.mouseDownAnimateStartFrame === null) {
102
146
  config.mouseDownAnimateStartFrame = frame;
103
147
  }
104
148
 
149
+ // Calculate how long the animation has been playing
105
150
  const relativeFrame = frame - config.mouseDownAnimateStartFrame;
106
151
  config.mouseDownAnimateCurrentFrame = relativeFrame;
107
152
 
108
153
  const speed = config.cachedStyle.pressAnimationSpeed;
109
154
  const accelerateRate = config.cachedStyle.releaseAnimationAccelerateRate;
110
155
 
156
+ // We assume that Ripple is a one-way linear process from
157
+ // no diffusion to fully diffused,
158
+ // and Logic Frame represents the progress of the animation
159
+ // in this process.
111
160
  let unitLogicFrame = relativeFrame;
112
161
  if (config.mouseReleased && config.mouseDownAnimateReleasedFrame) {
113
162
  unitLogicFrame += (relativeFrame - config.mouseDownAnimateReleasedFrame) * accelerateRate;
@@ -115,34 +164,51 @@ export class RevealBoundaryStore {
115
164
  config.mouseDownAnimateLogicFrame = unitLogicFrame / speed;
116
165
 
117
166
  if (config.mouseDownAnimateLogicFrame > 1) {
118
- this.cleanUpAnimation(config);
167
+ // It will be cleaned later
168
+ config.cleanedUpForAnimation = true;
119
169
  }
120
170
  }
121
171
 
172
+ // Paint the canvas one by one
122
173
  for (let i = 0; i < this.canvasList.length; i++) {
123
174
  const config = this.canvasList[i];
124
175
  config.currentFrameId = frame;
125
- config.paint(force);
176
+
177
+ if (config.cleanedUpForAnimation) {
178
+ this.cleanUpAnimationUnit(config, false);
179
+ config.cleanedUpForAnimation = false;
180
+ } else {
181
+ const isPlayingAnimation = this.animationQueue.indexOf(config) === -1;
182
+
183
+ if (this.mouseInBoundary || isPlayingAnimation) {
184
+ config.paint(skipSamePointerPositionCheck);
185
+ } else {
186
+ config.clear(false);
187
+ }
188
+ }
126
189
  }
127
190
 
128
- this.dirty = true;
129
191
  this.paintedClientX = this.clientX;
130
192
  this.paintedClientY = this.clientY;
131
193
 
132
194
  if (this.mouseInBoundary || this.animationQueue.length !== 0) {
133
- this.animationFrame = window.requestAnimationFrame(this.paintAll);
195
+ this.requestPaintAll();
134
196
  } else {
135
197
  this.animationFrame = null;
136
198
  }
137
199
  };
138
200
 
139
- resetAll = () => {
140
- for (let i = 0; i < this.canvasList.length; i++) {
141
- const config = this.canvasList[i];
142
- config.paint();
143
- }
201
+ /**
202
+ * Clear all canvas.
203
+ */
204
+ clearAll = (forAnimation: boolean) => {
205
+ this.cleanupAnimation(forAnimation, true);
144
206
  };
145
207
 
208
+ /**
209
+ * This is the effect while user pressed on the reveal highlight,
210
+ * the ripple start to spread slowly.
211
+ */
146
212
  initializeAnimation = () => {
147
213
  const config = this.canvasList.find((x) => x.mouseInCanvas());
148
214
 
@@ -157,18 +223,43 @@ export class RevealBoundaryStore {
157
223
  config.mouseReleased = false;
158
224
  };
159
225
 
226
+ /**
227
+ * This is the effect while user released it's pointer, the spreading
228
+ * ripple animation will start to accelerate.
229
+ */
160
230
  switchAnimation = () => {
161
- this.animationQueue.forEach((config) => {
231
+ for (let i = 0; i < this.animationQueue.length; i++) {
232
+ const config = this.animationQueue[i];
233
+
162
234
  if (!config.mouseReleased) {
163
235
  config.mouseReleased = true;
164
236
  config.mouseDownAnimateReleasedFrame = config.mouseDownAnimateCurrentFrame;
165
237
  config.mouseUpClientX = this.clientX;
166
238
  config.mouseUpClientY = this.clientY;
167
239
  }
168
- });
240
+ }
169
241
  };
170
242
 
171
- cleanUpAnimation = (config: CanvasConfig) => {
243
+ /**
244
+ * Cleanup all animations.
245
+ * @param skipPaint Only cleanup data or also clean up the canvas
246
+ * @param forceClean Clean the canvas or not
247
+ */
248
+ cleanupAnimation = (skipPaint?: boolean, forceClean?: boolean) => {
249
+ for (let i = 0; i < this.animationQueue.length; i++) {
250
+ const config = this.animationQueue[i];
251
+
252
+ this.cleanUpAnimationUnit(config, skipPaint, forceClean);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Clean up the config of an animation, but the canvas won't cleaned up.
258
+ * @param config The config to be cleaned up.
259
+ * @param skipPaint Skip painting.
260
+ * @param forceClean Force clear the painting.
261
+ */
262
+ cleanUpAnimationUnit = (config: Config, skipPaint?: boolean, forceClean?: boolean) => {
172
263
  config.mouseUpClientX = null;
173
264
  config.mouseUpClientY = null;
174
265
  config.mouseDownAnimateStartFrame = null;
@@ -183,7 +274,15 @@ export class RevealBoundaryStore {
183
274
  this.animationQueue.splice(configIdx, 1);
184
275
  }
185
276
 
186
- config.paint(true);
277
+ if (!skipPaint) {
278
+ if (forceClean) {
279
+ config.clear(false);
280
+ } else if (this.mouseInBoundary) {
281
+ config.paint(true);
282
+ } else {
283
+ config.clear(true);
284
+ }
285
+ }
187
286
  };
188
287
 
189
288
  getRevealAnimationConfig = () => {
@@ -14,24 +14,62 @@ export class RevealStateManager {
14
14
  if (config.borderDetectionMode === 'experimentalAutoFit') {
15
15
  if (typeof Window === 'undefined') return;
16
16
 
17
- window.addEventListener('pointermove', (event) => {
18
- this.cachedMouseX = event.clientX;
19
- this.cachedMouseY = event.clientY;
20
-
21
- if (!this.requestedTraverseBoundaries) {
22
- window.requestAnimationFrame(this.traverseBoundaries);
23
- this.requestedTraverseBoundaries = true;
24
- }
17
+ window.addEventListener('pointermove', this.handlePointerMove);
18
+ // This is for touch screen
19
+ // Window.pointerleave won't be triggered if the finger
20
+ // leave the screen, so we use pointer up here.
21
+ window.addEventListener('pointerup', (ev) => {
22
+ if (ev.pointerType !== 'mouse') this.handlePointerLeave();
23
+ });
24
+ // This is for desktop
25
+ // If the mouse leave the window, we need to terminate
26
+ // all animations and clean up the canvas.
27
+ window.addEventListener('pointerleave', (ev) => {
28
+ if (ev.pointerType === 'mouse') this.handlePointerLeave();
29
+ });
30
+ // Handles boundary conditions such as the user scrolling
31
+ // the page, dragging the image, which can cause mouse events
32
+ // to fail, when Reveal should disappear to prevent the
33
+ // image from getting stuck.
34
+ window.addEventListener('pointercancel', (ev) => {
35
+ console.log('pointer cancel');
36
+ this.handlePointerLeave();
25
37
  });
26
38
  }
27
39
  }
28
40
 
41
+ pointerLeaveScheduled = false;
42
+
43
+ handlePointerLeave = () => {
44
+ if (this.pointerLeaveScheduled) return;
45
+
46
+ this.pointerLeaveScheduled = true;
47
+
48
+ window.requestAnimationFrame(() => {
49
+ for (let i = 0; i < this._storage.length; i++) {
50
+ this._storage[i].onPointerLeaveBoundary();
51
+ }
52
+
53
+ this.pointerLeaveScheduled = false;
54
+ });
55
+ }
56
+
57
+ handlePointerMove = (event: PointerEvent) => {
58
+ this.cachedMouseX = event.clientX;
59
+ this.cachedMouseY = event.clientY;
60
+
61
+ if (!this.requestedTraverseBoundaries) {
62
+ window.requestAnimationFrame(this.traverseBoundaries);
63
+ this.requestedTraverseBoundaries = true;
64
+ }
65
+ };
66
+
29
67
  traverseBoundaries = () => {
30
68
  this.requestedTraverseBoundaries = false;
31
69
  for (let i = 0; i < this._storage.length; i++) {
32
70
  this._storage[i].onPointerMoveOnScreen(this.cachedMouseX, this.cachedMouseY);
33
71
  }
34
- }
72
+ };
35
73
 
36
74
  newBoundary = ($el: HTMLElement) => {
37
75
  const hashId = this._currentHashId++;
@@ -43,6 +81,6 @@ export class RevealStateManager {
43
81
  };
44
82
 
45
83
  removeBoundary = (store: RevealBoundaryStore) => {
46
- this._storage = this._storage.filter(x => x !== store);
84
+ this._storage = this._storage.filter((x) => x !== store);
47
85
  };
48
86
  }