@wolsok/thanos 18.1.4 → 18.2.1

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.
package/README.md CHANGED
@@ -58,6 +58,7 @@ export class SharedModule {}
58
58
  | sound | boolean | true | play a windy, sandy sound while vaporizing |
59
59
  | soundVolume | number | 0.5 | volume of the sound from 0 to 1 |
60
60
  | crumble | string | shards | how the element breaks apart, see crumble variants |
61
+ | preload | boolean | true | warm up the effect while idle, so the first snap won't stutter |
61
62
 
62
63
  #### Sound
63
64
 
@@ -73,6 +74,21 @@ A limiter keeps many simultaneous snaps from getting too loud.
73
74
  Browsers only allow audio after the user interacted with the page, so snaps before the first click or tap stay silent.
74
75
  Turn it off with `provideWsThanosOptions({ sound: false })`.
75
76
 
77
+ #### Preloading
78
+
79
+ The first snap does work the browser hasn't done before: html2canvas runs for the first time,
80
+ and the browser compiles the WebGL shaders and the shaders for drawing the captured element.
81
+ To keep that from stalling the first snap, `wsThanos` snaps a tiny invisible element once while the browser is idle after the first render.
82
+ Browsers cache compiled shaders, so the first real snap does not have to compile them again.
83
+
84
+ Turn it off with `provideWsThanosOptions({ preload: false })`.
85
+ Using `WsThanosService` without the directive? Call `preload()` yourself, e.g. in a component's constructor:
86
+
87
+ ```typescript
88
+ const thanos = inject(WsThanosService);
89
+ afterNextRender(() => void thanos.preload());
90
+ ```
91
+
76
92
  #### GPU rendering
77
93
 
78
94
  The particles are simulated and drawn on the graphics card with WebGL2,
@@ -6,6 +6,7 @@ export function createWsThanosOptions(options) {
6
6
  sound: true,
7
7
  soundVolume: 0.5,
8
8
  crumble: 'shards',
9
+ preload: true,
9
10
  ...options,
10
11
  };
11
12
  }
@@ -1 +1 @@
1
- {"version":3,"file":"create-ws-thanos-options.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/create-ws-thanos-options.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,qBAAqB,CACnC,OAAkC;IAElC,OAAO;QACL,eAAe,EAAE,IAAI;QACrB,gBAAgB,EAAE,SAAS;QAC3B,oBAAoB,EAAE,EAAE;QACxB,KAAK,EAAE,IAAI;QACX,WAAW,EAAE,GAAG;QAChB,OAAO,EAAE,QAAQ;QACjB,GAAG,OAAO;KACX,CAAC;AACJ,CAAC","sourcesContent":["import { WsThanosOptions } from './ws-thanos.options';\n\nexport function createWsThanosOptions(\n options?: Partial<WsThanosOptions>\n): WsThanosOptions {\n return {\n animationLength: 5000,\n maxParticleCount: 3_000_000,\n particleAcceleration: 30,\n sound: true,\n soundVolume: 0.5,\n crumble: 'shards',\n ...options,\n };\n}\n"]}
1
+ {"version":3,"file":"create-ws-thanos-options.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/create-ws-thanos-options.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,qBAAqB,CACnC,OAAkC;IAElC,OAAO;QACL,eAAe,EAAE,IAAI;QACrB,gBAAgB,EAAE,SAAS;QAC3B,oBAAoB,EAAE,EAAE;QACxB,KAAK,EAAE,IAAI;QACX,WAAW,EAAE,GAAG;QAChB,OAAO,EAAE,QAAQ;QACjB,OAAO,EAAE,IAAI;QACb,GAAG,OAAO;KACX,CAAC;AACJ,CAAC","sourcesContent":["import { WsThanosOptions } from './ws-thanos.options';\n\nexport function createWsThanosOptions(\n options?: Partial<WsThanosOptions>\n): WsThanosOptions {\n return {\n animationLength: 5000,\n maxParticleCount: 3_000_000,\n particleAcceleration: 30,\n sound: true,\n soundVolume: 0.5,\n crumble: 'shards',\n preload: true,\n ...options,\n };\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { Directive, ElementRef, Output, inject, input } from '@angular/core';
1
+ import { Directive, ElementRef, Output, afterNextRender, inject, input, } from '@angular/core';
2
2
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
3
3
  import { finalize, Subject, take, tap } from 'rxjs';
4
4
  import { WS_THANOS_OPTIONS_TOKEN } from './ws-thanos-options.token';
@@ -15,6 +15,13 @@ export class WsThanosDirective {
15
15
  this.vaporizeDomElem = inject(ElementRef);
16
16
  this.thanosService = inject(WsThanosService);
17
17
  this.thanosOptions = inject(WS_THANOS_OPTIONS_TOKEN);
18
+ // only in the browser, the service runs it once for all elements
19
+ afterNextRender(() => {
20
+ const { preload } = { ...this.thanosOptions, ...this.wsThanosOptions() };
21
+ if (preload) {
22
+ void this.thanosService.preload();
23
+ }
24
+ });
18
25
  }
19
26
  /**
20
27
  *
@@ -65,7 +72,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.3", ngImpor
65
72
  selector: '[wsThanos], ws-thanos',
66
73
  exportAs: 'thanos',
67
74
  }]
68
- }], propDecorators: { wsThanosOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "wsThanosOptions", required: false }] }], wsThanosComplete: [{
75
+ }], ctorParameters: () => [], propDecorators: { wsThanosOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "wsThanosOptions", required: false }] }], wsThanosComplete: [{
69
76
  type: Output
70
77
  }] } });
71
78
  //# sourceMappingURL=ws-thanos.directive.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ws-thanos.directive.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/ws-thanos.directive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAc,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAEhE,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;;AAQtD,MAAM,OAAO,iBAAiB;IAN9B;QAOU,4BAAuB,GAAG,IAAI,OAAO,EAAQ,CAAC;QAC9C,mBAAc,GAAG,kBAAkB,EAAE,CAAC;QAE9C,qDAAqD;QACrC,oBAAe,GAAG,KAAK;uGAA4B,CAAC;QAG7D,qBAAgB,GACrB,IAAI,CAAC,uBAAuB,CAAC,YAAY,EAAE,CAAC;QAE7B,oBAAe,GAC9B,MAAM,CAAC,UAAU,CAAC,CAAC;QACJ,kBAAa,GAAoB,MAAM,CAAC,eAAe,CAAC,CAAC;QACzD,kBAAa,GAAoB,MAAM,CACtD,uBAAuB,CACxB,CAAC;KA2CH;IAzCC;;;;;;;OAOG;IACI,SAAS,CAAC,UAAU,GAAG,IAAI;QAChC,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC;QAChD,OAAO,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC,IAAI,CACnE,GAAG,CAAC;YACF,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE;gBACf,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,EAAE;oBACvC,cAAc,EAAE,IAAI;oBACpB,UAAU;iBACX,CAAC,CAAC;YACL,CAAC;SACF,CAAC,EACF,QAAQ,CAAC,GAAG,EAAE;YACZ,IAAI,UAAU,EAAE,CAAC;gBACf,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,CAAC;iBAAM,CAAC;gBACN,qBAAqB;gBACrB,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,eAAe,CAAC;gBACxC,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC;YAC3B,CAAC;YACD,IAAI,CAAC,uBAAuB,CAAC,IAAI,EAAE,CAAC;QACtC,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACI,QAAQ,CAAC,UAAU,GAAG,IAAI;QAC/B,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,SAAS,EAAE,CAAC;QACjE,OAAO,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;8GA1DU,iBAAiB;kGAAjB,iBAAiB;;2FAAjB,iBAAiB;kBAN7B,SAAS;mBAAC;oBACT,4FAA4F;oBAC5F,8DAA8D;oBAC9D,QAAQ,EAAE,uBAAuB;oBACjC,QAAQ,EAAE,QAAQ;iBACnB;;sBAQE,MAAM","sourcesContent":["import { Directive, ElementRef, Output, inject, input } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { finalize, Observable, Subject, take, tap } from 'rxjs';\nimport { AnimationState } from './animation.state';\nimport { WS_THANOS_OPTIONS_TOKEN } from './ws-thanos-options.token';\nimport type { WsThanosOptions } from './ws-thanos.options';\nimport { WsThanosService } from './ws-thanos.service';\n\n@Directive({\n // Bare prefix is the published public API of @wolsok/thanos; renaming it would be breaking.\n // eslint-disable-next-line @angular-eslint/directive-selector\n selector: '[wsThanos], ws-thanos',\n exportAs: 'thanos',\n})\nexport class WsThanosDirective {\n private wsThanosCompleteSubject = new Subject<void>();\n private untilDestroyed = takeUntilDestroyed();\n\n /** override the provided options for this element */\n public readonly wsThanosOptions = input<Partial<WsThanosOptions>>();\n\n @Output()\n public wsThanosComplete: Observable<void> =\n this.wsThanosCompleteSubject.asObservable();\n\n private readonly vaporizeDomElem: ElementRef<HTMLElement> =\n inject(ElementRef);\n private readonly thanosService: WsThanosService = inject(WsThanosService);\n private readonly thanosOptions: WsThanosOptions = inject(\n WS_THANOS_OPTIONS_TOKEN\n );\n\n /**\n *\n * Vaporize the dom element of this directive\n * @param removeElem remove the vaporized dom element or blend back in after a while\n *\n * Subscribe to the returned observable to start vaporization\n *\n */\n public vaporize$(removeElem = true): Observable<AnimationState> {\n const elem = this.vaporizeDomElem.nativeElement;\n return this.thanosService.vaporize(elem, this.wsThanosOptions()).pipe(\n tap({\n error: (error) => {\n console.error('Error vaporizing', error, {\n elemTovaporize: elem,\n removeElem,\n });\n },\n }),\n finalize(() => {\n if (removeElem) {\n elem.remove();\n } else {\n // make visible again\n elem.style.transition = 'opacity 700ms';\n elem.style.opacity = '1';\n }\n this.wsThanosCompleteSubject.next();\n })\n );\n }\n\n /**\n * Vaporize the dom element of this directive\n * @param removeElem remove the vaporized dom element or blend back in after a while\n *\n */\n public vaporize(removeElem = true): Observable<void> {\n this.vaporize$(removeElem).pipe(this.untilDestroyed).subscribe();\n return this.wsThanosComplete.pipe(take(1));\n }\n}\n"]}
1
+ {"version":3,"file":"ws-thanos.directive.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/ws-thanos.directive.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,UAAU,EACV,MAAM,EACN,eAAe,EACf,MAAM,EACN,KAAK,GACN,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAc,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAEhE,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;;AAQtD,MAAM,OAAO,iBAAiB;IAkB5B;QAjBQ,4BAAuB,GAAG,IAAI,OAAO,EAAQ,CAAC;QAC9C,mBAAc,GAAG,kBAAkB,EAAE,CAAC;QAE9C,qDAAqD;QACrC,oBAAe,GAAG,KAAK;uGAA4B,CAAC;QAG7D,qBAAgB,GACrB,IAAI,CAAC,uBAAuB,CAAC,YAAY,EAAE,CAAC;QAE7B,oBAAe,GAC9B,MAAM,CAAC,UAAU,CAAC,CAAC;QACJ,kBAAa,GAAoB,MAAM,CAAC,eAAe,CAAC,CAAC;QACzD,kBAAa,GAAoB,MAAM,CACtD,uBAAuB,CACxB,CAAC;QAGA,iEAAiE;QACjE,eAAe,CAAC,GAAG,EAAE;YACnB,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,aAAa,EAAE,GAAG,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;YACzE,IAAI,OAAO,EAAE,CAAC;gBACZ,KAAK,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;YACpC,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;OAOG;IACI,SAAS,CAAC,UAAU,GAAG,IAAI;QAChC,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC;QAChD,OAAO,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC,IAAI,CACnE,GAAG,CAAC;YACF,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE;gBACf,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,EAAE;oBACvC,cAAc,EAAE,IAAI;oBACpB,UAAU;iBACX,CAAC,CAAC;YACL,CAAC;SACF,CAAC,EACF,QAAQ,CAAC,GAAG,EAAE;YACZ,IAAI,UAAU,EAAE,CAAC;gBACf,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,CAAC;iBAAM,CAAC;gBACN,qBAAqB;gBACrB,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,eAAe,CAAC;gBACxC,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC;YAC3B,CAAC;YACD,IAAI,CAAC,uBAAuB,CAAC,IAAI,EAAE,CAAC;QACtC,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACI,QAAQ,CAAC,UAAU,GAAG,IAAI;QAC/B,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,SAAS,EAAE,CAAC;QACjE,OAAO,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;8GApEU,iBAAiB;kGAAjB,iBAAiB;;2FAAjB,iBAAiB;kBAN7B,SAAS;mBAAC;oBACT,4FAA4F;oBAC5F,8DAA8D;oBAC9D,QAAQ,EAAE,uBAAuB;oBACjC,QAAQ,EAAE,QAAQ;iBACnB;;sBAQE,MAAM","sourcesContent":["import {\n Directive,\n ElementRef,\n Output,\n afterNextRender,\n inject,\n input,\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { finalize, Observable, Subject, take, tap } from 'rxjs';\nimport { AnimationState } from './animation.state';\nimport { WS_THANOS_OPTIONS_TOKEN } from './ws-thanos-options.token';\nimport type { WsThanosOptions } from './ws-thanos.options';\nimport { WsThanosService } from './ws-thanos.service';\n\n@Directive({\n // Bare prefix is the published public API of @wolsok/thanos; renaming it would be breaking.\n // eslint-disable-next-line @angular-eslint/directive-selector\n selector: '[wsThanos], ws-thanos',\n exportAs: 'thanos',\n})\nexport class WsThanosDirective {\n private wsThanosCompleteSubject = new Subject<void>();\n private untilDestroyed = takeUntilDestroyed();\n\n /** override the provided options for this element */\n public readonly wsThanosOptions = input<Partial<WsThanosOptions>>();\n\n @Output()\n public wsThanosComplete: Observable<void> =\n this.wsThanosCompleteSubject.asObservable();\n\n private readonly vaporizeDomElem: ElementRef<HTMLElement> =\n inject(ElementRef);\n private readonly thanosService: WsThanosService = inject(WsThanosService);\n private readonly thanosOptions: WsThanosOptions = inject(\n WS_THANOS_OPTIONS_TOKEN\n );\n\n public constructor() {\n // only in the browser, the service runs it once for all elements\n afterNextRender(() => {\n const { preload } = { ...this.thanosOptions, ...this.wsThanosOptions() };\n if (preload) {\n void this.thanosService.preload();\n }\n });\n }\n\n /**\n *\n * Vaporize the dom element of this directive\n * @param removeElem remove the vaporized dom element or blend back in after a while\n *\n * Subscribe to the returned observable to start vaporization\n *\n */\n public vaporize$(removeElem = true): Observable<AnimationState> {\n const elem = this.vaporizeDomElem.nativeElement;\n return this.thanosService.vaporize(elem, this.wsThanosOptions()).pipe(\n tap({\n error: (error) => {\n console.error('Error vaporizing', error, {\n elemTovaporize: elem,\n removeElem,\n });\n },\n }),\n finalize(() => {\n if (removeElem) {\n elem.remove();\n } else {\n // make visible again\n elem.style.transition = 'opacity 700ms';\n elem.style.opacity = '1';\n }\n this.wsThanosCompleteSubject.next();\n })\n );\n }\n\n /**\n * Vaporize the dom element of this directive\n * @param removeElem remove the vaporized dom element or blend back in after a while\n *\n */\n public vaporize(removeElem = true): Observable<void> {\n this.vaporize$(removeElem).pipe(this.untilDestroyed).subscribe();\n return this.wsThanosComplete.pipe(take(1));\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"ws-thanos.options.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/ws-thanos.options.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * How the element breaks apart (needs WebGL2, the canvas fallback always uses 'dust'):\n * - dust: particles blow away along the vaporizing front\n * - cracks: cracks spread through the element before it turns to dust\n * - shards: cracks split the element into shards that shift and tilt before they turn to dust\n * - chunks: whole chunks break off, jump away and then crumble to dust\n */\nexport type WsThanosCrumble = 'dust' | 'cracks' | 'shards' | 'chunks';\n\nexport interface WsThanosOptions {\n animationLength: number;\n maxParticleCount: number;\n particleAcceleration: number;\n /** play a windy, sandy sound while vaporizing */\n sound: boolean;\n /** volume of the snap sound between 0 and 1 */\n soundVolume: number;\n /** how the element breaks apart */\n crumble: WsThanosCrumble;\n}\n"]}
1
+ {"version":3,"file":"ws-thanos.options.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/ws-thanos.options.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * How the element breaks apart (needs WebGL2, the canvas fallback always uses 'dust'):\n * - dust: particles blow away along the vaporizing front\n * - cracks: cracks spread through the element before it turns to dust\n * - shards: cracks split the element into shards that shift and tilt before they turn to dust\n * - chunks: whole chunks break off, jump away and then crumble to dust\n */\nexport type WsThanosCrumble = 'dust' | 'cracks' | 'shards' | 'chunks';\n\nexport interface WsThanosOptions {\n animationLength: number;\n maxParticleCount: number;\n particleAcceleration: number;\n /** play a windy, sandy sound while vaporizing */\n sound: boolean;\n /** volume of the snap sound between 0 and 1 */\n soundVolume: number;\n /** how the element breaks apart */\n crumble: WsThanosCrumble;\n /** warm up the effect while the browser is idle, so the first snap does not stutter */\n preload: boolean;\n}\n"]}
@@ -9,6 +9,12 @@ import { WS_THANOS_OPTIONS_TOKEN } from './ws-thanos-options.token';
9
9
  import * as i0 from "@angular/core";
10
10
  /** a short cross fade hides small differences between capture and element */
11
11
  const FULL_COVERAGE_FADE_OUT_MS = 200;
12
+ /** resolves once the browser has nothing else to do */
13
+ function idle(window) {
14
+ return new Promise((resolve) => typeof window.requestIdleCallback === 'function'
15
+ ? window.requestIdleCallback(() => resolve())
16
+ : window.setTimeout(resolve, 200));
17
+ }
12
18
  export class WsThanosService {
13
19
  constructor() {
14
20
  this.thanosOptions = inject(WS_THANOS_OPTIONS_TOKEN);
@@ -25,48 +31,69 @@ export class WsThanosService {
25
31
  // nothing is prepared before someone subscribes
26
32
  return defer(() => this.vaporizeIntern(elem, { ...this.thanosOptions, ...options }));
27
33
  }
34
+ /**
35
+ * Snap a tiny invisible element once, so the first real snap starts without a stutter:
36
+ * html2canvas, reading back its canvas and compiling the WebGL shaders are slow the first time.
37
+ * Runs once while the browser is idle, the directive starts it unless `preload` is off.
38
+ */
39
+ preload() {
40
+ this.preloaded ??= this.preloadIntern().catch((error) => console.debug('ws-thanos: preload failed', error));
41
+ return this.preloaded;
42
+ }
43
+ async preloadIntern() {
44
+ const { body, defaultView: window } = this.document;
45
+ if (window == null || body == null) {
46
+ return;
47
+ }
48
+ await idle(window);
49
+ const elem = this.document.createElement('div');
50
+ elem.textContent = 'ws';
51
+ elem.setAttribute('aria-hidden', 'true');
52
+ // draws what cards usually draw: text, a gradient, rounded borders and a shadow.
53
+ // html2canvas ignores clip-path, so it captures the element while the page shows nothing
54
+ elem.style.cssText = [
55
+ 'position:fixed;left:0;top:0;width:32px;height:32px',
56
+ 'clip-path:inset(50%);pointer-events:none',
57
+ 'border:2px solid #888;border-radius:8px;box-shadow:0 2px 4px #0004',
58
+ 'background:linear-gradient(#fff,#888)',
59
+ ].join(';');
60
+ body.appendChild(elem);
61
+ const { maxParticleCount, particleAcceleration } = this.thanosOptions;
62
+ let rendererChoice;
63
+ let renderer;
64
+ try {
65
+ // the scale does not matter for a warm up
66
+ const capture = await this.capture(elem, () => 1);
67
+ await idle(window);
68
+ // take a WebGL context only now, not while waiting for idle time
69
+ rendererChoice = this.chooseRenderer(maxParticleCount);
70
+ // dust skips the crack shader, the other variants compile every shader
71
+ ({ renderer } = this.createRenderer(rendererChoice, capture, {
72
+ particleAcceleration,
73
+ crumble: 'shards',
74
+ }));
75
+ renderer.render({
76
+ deltaTSec: 0,
77
+ animationT: 0,
78
+ maxWidth: renderer.canvas.width,
79
+ maxHeight: renderer.canvas.height,
80
+ });
81
+ }
82
+ finally {
83
+ renderer?.dispose();
84
+ elem.remove();
85
+ rendererChoice?.release();
86
+ }
87
+ }
28
88
  vaporizeIntern(elem, options) {
29
89
  const { animationLength, maxParticleCount, particleAcceleration, crumble } = options;
30
- const window = this.document.defaultView;
31
90
  elem.style.opacity = elem.style.opacity || '1';
32
- const rendererChoice = chooseParticleRenderer({
33
- maxParticleCount,
34
- devicePixelRatio: window?.devicePixelRatio ?? 1,
35
- createCanvas: () => this.document.createElement('canvas'),
36
- gpuBudget: this.gpuBudget,
37
- });
38
- const { width, height } = elem.getBoundingClientRect();
39
- const scale = rendererChoice.captureScale(width, height);
40
- const seed = Math.random() * 1000;
91
+ const rendererChoice = this.chooseRenderer(maxParticleCount);
41
92
  let effect;
42
- const html2CanvasPromise = html2canvas(elem, {
43
- backgroundColor: null,
44
- scale,
45
- allowTaint: true,
46
- windowHeight: window?.innerHeight,
47
- windowWidth: window?.innerWidth,
48
- scrollY: -(window?.scrollY ?? 0),
49
- });
50
- return from(html2CanvasPromise).pipe(map((capturedElem) => {
51
- const effectWidth = capturedElem.width * EFFECT_WIDTH_SCALE;
52
- const effectHeight = capturedElem.height * EFFECT_HEIGHT_SCALE;
53
- const imageData = capturedElem
54
- .getContext('2d', { willReadFrequently: true })
55
- ?.getImageData(0, 0, capturedElem.width, capturedElem.height);
56
- if (imageData == null) {
57
- throw new Error('Could not get image data from canvas');
58
- }
59
- const particles = createParticles(imageData, rendererChoice.maxParticleCount, effectHeight);
60
- const renderer = rendererChoice.create(particles, {
61
- width: effectWidth,
62
- height: effectHeight,
63
- // particles move in device pixels, keep their speed in css pixels
64
- particleAcceleration: particleAcceleration * scale,
65
- pixelScale: scale,
66
- crumble,
67
- seed,
68
- });
69
- this.placeEffectCanvas(elem, renderer.canvas, width, height);
93
+ const capture = this.capture(elem, (width, height) => rendererChoice.captureScale(width, height));
94
+ return from(capture).pipe(map((capture) => {
95
+ const { renderer, particles } = this.createRenderer(rendererChoice, capture, { particleAcceleration, crumble });
96
+ this.placeEffectCanvas(elem, renderer.canvas, capture.width, capture.height);
70
97
  renderer.canvas.dataset['wsThanosRenderer'] = renderer.kind;
71
98
  renderer.canvas.dataset['wsThanosCrumble'] = renderer.crumble;
72
99
  // when every pixel became a particle, the particles replace the element right away
@@ -98,6 +125,55 @@ export class WsThanosService {
98
125
  rendererChoice.release();
99
126
  }));
100
127
  }
128
+ chooseRenderer(maxParticleCount) {
129
+ return chooseParticleRenderer({
130
+ maxParticleCount,
131
+ devicePixelRatio: this.document.defaultView?.devicePixelRatio ?? 1,
132
+ createCanvas: () => this.document.createElement('canvas'),
133
+ gpuBudget: this.gpuBudget,
134
+ });
135
+ }
136
+ /** draw the element into an image at the scale the renderer needs */
137
+ async capture(elem, captureScale) {
138
+ const window = this.document.defaultView;
139
+ const { width, height } = elem.getBoundingClientRect();
140
+ const scale = captureScale(width, height);
141
+ // a canvas that is read back is drawn on the CPU: a GPU canvas would compile
142
+ // the shaders for everything html2canvas draws on the first read back
143
+ const canvas = this.document.createElement('canvas');
144
+ canvas.width = Math.floor(Math.ceil(width) * scale);
145
+ canvas.height = Math.floor(Math.ceil(height) * scale);
146
+ const context = canvas.getContext('2d', { willReadFrequently: true });
147
+ if (context == null) {
148
+ throw new Error('Could not get a 2d context for the capture');
149
+ }
150
+ await html2canvas(elem, {
151
+ canvas,
152
+ backgroundColor: null,
153
+ scale,
154
+ allowTaint: true,
155
+ windowHeight: window?.innerHeight,
156
+ windowWidth: window?.innerWidth,
157
+ scrollY: -(window?.scrollY ?? 0),
158
+ });
159
+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
160
+ return { imageData, width, height, scale };
161
+ }
162
+ createRenderer(rendererChoice, { imageData, scale }, { particleAcceleration, crumble, }) {
163
+ const effectWidth = imageData.width * EFFECT_WIDTH_SCALE;
164
+ const effectHeight = imageData.height * EFFECT_HEIGHT_SCALE;
165
+ const particles = createParticles(imageData, rendererChoice.maxParticleCount, effectHeight);
166
+ const renderer = rendererChoice.create(particles, {
167
+ width: effectWidth,
168
+ height: effectHeight,
169
+ // particles move in device pixels, keep their speed in css pixels
170
+ particleAcceleration: particleAcceleration * scale,
171
+ pixelScale: scale,
172
+ crumble,
173
+ seed: Math.random() * 1000,
174
+ });
175
+ return { renderer, particles };
176
+ }
101
177
  /** the element sits at the bottom left of the larger effect canvas */
102
178
  placeEffectCanvas(elem, canvas, width, height) {
103
179
  const { left, top } = this.offsetInParent(elem);
@@ -1 +1 @@
1
- {"version":3,"file":"ws-thanos.service.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/ws-thanos.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { DOCUMENT, Injectable, inject } from '@angular/core';\n\nimport { default as html2canvas } from 'html2canvas';\nimport {\n animationFrameScheduler,\n defer,\n finalize,\n from,\n interval,\n map,\n Observable,\n switchMap,\n takeWhile,\n tap,\n timeInterval,\n} from 'rxjs';\nimport { AnimationState } from './animation.state';\nimport { EFFECT_HEIGHT_SCALE, EFFECT_WIDTH_SCALE } from './capture-scale';\nimport {\n chooseParticleRenderer,\n GpuContextBudget,\n} from './choose-particle-renderer';\nimport { ParticleRenderer } from './particle-renderer';\nimport { createParticles } from './particles';\nimport { SnapSound, SnapSoundService } from './snap-sound.service';\nimport { WS_THANOS_OPTIONS_TOKEN } from './ws-thanos-options.token';\nimport type { WsThanosOptions } from './ws-thanos.options';\n\n/** a short cross fade hides small differences between capture and element */\nconst FULL_COVERAGE_FADE_OUT_MS = 200;\n\ninterface RunningEffect {\n renderer: ParticleRenderer;\n sound?: SnapSound;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class WsThanosService {\n private readonly thanosOptions: WsThanosOptions = inject(\n WS_THANOS_OPTIONS_TOKEN\n );\n private readonly document = inject(DOCUMENT);\n private readonly snapSound = inject(SnapSoundService);\n /** shared by all effects, browsers only allow a few WebGL contexts */\n private readonly gpuBudget = new GpuContextBudget();\n\n /**\n * start the vaporize-effect.\n * @param options override the provided options for this element\n */\n public vaporize(\n elem: HTMLElement,\n options?: Partial<WsThanosOptions>\n ): Observable<AnimationState> {\n // nothing is prepared before someone subscribes\n return defer(() =>\n this.vaporizeIntern(elem, { ...this.thanosOptions, ...options })\n );\n }\n\n private vaporizeIntern(\n elem: HTMLElement,\n options: WsThanosOptions\n ): Observable<AnimationState> {\n const { animationLength, maxParticleCount, particleAcceleration, crumble } =\n options;\n const window = this.document.defaultView;\n\n elem.style.opacity = elem.style.opacity || '1';\n\n const rendererChoice = chooseParticleRenderer({\n maxParticleCount,\n devicePixelRatio: window?.devicePixelRatio ?? 1,\n createCanvas: () => this.document.createElement('canvas'),\n gpuBudget: this.gpuBudget,\n });\n const { width, height } = elem.getBoundingClientRect();\n const scale = rendererChoice.captureScale(width, height);\n const seed = Math.random() * 1000;\n let effect: RunningEffect | undefined;\n\n const html2CanvasPromise: Promise<HTMLCanvasElement> = html2canvas(elem, {\n backgroundColor: null,\n scale,\n allowTaint: true,\n windowHeight: window?.innerHeight,\n windowWidth: window?.innerWidth,\n scrollY: -(window?.scrollY ?? 0),\n });\n\n return from(html2CanvasPromise).pipe(\n map((capturedElem): RunningEffect => {\n const effectWidth = capturedElem.width * EFFECT_WIDTH_SCALE;\n const effectHeight = capturedElem.height * EFFECT_HEIGHT_SCALE;\n const imageData = capturedElem\n .getContext('2d', { willReadFrequently: true })\n ?.getImageData(0, 0, capturedElem.width, capturedElem.height);\n if (imageData == null) {\n throw new Error('Could not get image data from canvas');\n }\n\n const particles = createParticles(\n imageData,\n rendererChoice.maxParticleCount,\n effectHeight\n );\n const renderer = rendererChoice.create(particles, {\n width: effectWidth,\n height: effectHeight,\n // particles move in device pixels, keep their speed in css pixels\n particleAcceleration: particleAcceleration * scale,\n pixelScale: scale,\n crumble,\n seed,\n });\n\n this.placeEffectCanvas(elem, renderer.canvas, width, height);\n renderer.canvas.dataset['wsThanosRenderer'] = renderer.kind;\n renderer.canvas.dataset['wsThanosCrumble'] = renderer.crumble;\n // when every pixel became a particle, the particles replace the element right away\n const fadeOutMs = particles.sampled\n ? Math.floor(animationLength * 0.8)\n : FULL_COVERAGE_FADE_OUT_MS;\n elem.style.transition = `opacity ${fadeOutMs}ms ease-out`;\n elem.style.opacity = '0';\n\n const { sound, soundVolume } = options;\n effect = {\n renderer,\n sound: sound\n ? this.snapSound.play(\n animationLength,\n soundVolume,\n renderer.crumble\n )\n : undefined,\n };\n return effect;\n }),\n switchMap(({ renderer, sound }) => {\n let time = 0;\n return interval(1000 / 60, animationFrameScheduler).pipe(\n timeInterval(),\n tap((deltaT) => (time += deltaT.interval)),\n map((deltaT): AnimationState => ({\n deltaTSec: deltaT.interval / 1000,\n animationT: time / animationLength,\n maxWidth: renderer.canvas.width,\n maxHeight: renderer.canvas.height,\n })),\n tap((animationState) => renderer.render(animationState)),\n takeWhile((animationState) => animationState.animationT <= 1),\n // a completed effect lets the sound blow away, a cancelled one stops it\n tap({ unsubscribe: () => sound?.stop(), error: () => sound?.stop() })\n );\n }),\n finalize(() => {\n effect?.renderer.dispose();\n rendererChoice.release();\n })\n );\n }\n\n /** the element sits at the bottom left of the larger effect canvas */\n private placeEffectCanvas(\n elem: HTMLElement,\n canvas: HTMLCanvasElement,\n width: number,\n height: number\n ): void {\n const { left, top } = this.offsetInParent(elem);\n canvas.style.position = 'absolute';\n canvas.style.left = `${left}px`;\n canvas.style.top = `${top - height * (EFFECT_HEIGHT_SCALE - 1)}px`;\n canvas.style.width = `${width * EFFECT_WIDTH_SCALE}px`;\n canvas.style.height = `${height * EFFECT_HEIGHT_SCALE}px`;\n canvas.style.zIndex = '2000';\n canvas.style.pointerEvents = 'none';\n elem.insertAdjacentElement('beforebegin', canvas);\n }\n\n /** position of the element inside its parent, where absolute children are placed */\n private offsetInParent(elem: HTMLElement): { left: number; top: number } {\n const parent = elem.parentElement;\n if (parent == null) {\n return { left: 0, top: 0 };\n }\n parent.style.position = parent.style.position || 'relative';\n // absolute positions are relative to the parent's padding box\n const elemRect = elem.getBoundingClientRect();\n const parentRect = parent.getBoundingClientRect();\n return {\n left:\n elemRect.left - parentRect.left - parent.clientLeft + parent.scrollLeft,\n top: elemRect.top - parentRect.top - parent.clientTop + parent.scrollTop,\n };\n }\n}\n"]}
1
+ {"version":3,"file":"ws-thanos.service.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/ws-thanos.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { DOCUMENT, Injectable, inject } from '@angular/core';\n\nimport { default as html2canvas } from 'html2canvas';\nimport {\n animationFrameScheduler,\n defer,\n finalize,\n from,\n interval,\n map,\n Observable,\n switchMap,\n takeWhile,\n tap,\n timeInterval,\n} from 'rxjs';\nimport { AnimationState } from './animation.state';\nimport { EFFECT_HEIGHT_SCALE, EFFECT_WIDTH_SCALE } from './capture-scale';\nimport {\n chooseParticleRenderer,\n GpuContextBudget,\n ParticleRendererChoice,\n} from './choose-particle-renderer';\nimport { ParticleRenderer } from './particle-renderer';\nimport { createParticles, Particles } from './particles';\nimport { SnapSound, SnapSoundService } from './snap-sound.service';\nimport { WS_THANOS_OPTIONS_TOKEN } from './ws-thanos-options.token';\nimport type { WsThanosOptions } from './ws-thanos.options';\n\n/** a short cross fade hides small differences between capture and element */\nconst FULL_COVERAGE_FADE_OUT_MS = 200;\n\ninterface RunningEffect {\n renderer: ParticleRenderer;\n sound?: SnapSound;\n}\n\n/** the captured element, its css size and the device pixels per css pixel */\ninterface Capture {\n imageData: ImageData;\n width: number;\n height: number;\n scale: number;\n}\n\n/** resolves once the browser has nothing else to do */\nfunction idle(window: Window): Promise<void> {\n return new Promise((resolve) =>\n typeof window.requestIdleCallback === 'function'\n ? window.requestIdleCallback(() => resolve())\n : window.setTimeout(resolve, 200)\n );\n}\n\n@Injectable({ providedIn: 'root' })\nexport class WsThanosService {\n private readonly thanosOptions: WsThanosOptions = inject(\n WS_THANOS_OPTIONS_TOKEN\n );\n private readonly document = inject(DOCUMENT);\n private readonly snapSound = inject(SnapSoundService);\n /** shared by all effects, browsers only allow a few WebGL contexts */\n private readonly gpuBudget = new GpuContextBudget();\n private preloaded?: Promise<void>;\n\n /**\n * start the vaporize-effect.\n * @param options override the provided options for this element\n */\n public vaporize(\n elem: HTMLElement,\n options?: Partial<WsThanosOptions>\n ): Observable<AnimationState> {\n // nothing is prepared before someone subscribes\n return defer(() =>\n this.vaporizeIntern(elem, { ...this.thanosOptions, ...options })\n );\n }\n\n /**\n * Snap a tiny invisible element once, so the first real snap starts without a stutter:\n * html2canvas, reading back its canvas and compiling the WebGL shaders are slow the first time.\n * Runs once while the browser is idle, the directive starts it unless `preload` is off.\n */\n public preload(): Promise<void> {\n this.preloaded ??= this.preloadIntern().catch((error) =>\n console.debug('ws-thanos: preload failed', error)\n );\n return this.preloaded;\n }\n\n private async preloadIntern(): Promise<void> {\n const { body, defaultView: window } = this.document;\n if (window == null || body == null) {\n return;\n }\n await idle(window);\n\n const elem = this.document.createElement('div');\n elem.textContent = 'ws';\n elem.setAttribute('aria-hidden', 'true');\n // draws what cards usually draw: text, a gradient, rounded borders and a shadow.\n // html2canvas ignores clip-path, so it captures the element while the page shows nothing\n elem.style.cssText = [\n 'position:fixed;left:0;top:0;width:32px;height:32px',\n 'clip-path:inset(50%);pointer-events:none',\n 'border:2px solid #888;border-radius:8px;box-shadow:0 2px 4px #0004',\n 'background:linear-gradient(#fff,#888)',\n ].join(';');\n body.appendChild(elem);\n\n const { maxParticleCount, particleAcceleration } = this.thanosOptions;\n let rendererChoice: ParticleRendererChoice | undefined;\n let renderer: ParticleRenderer | undefined;\n try {\n // the scale does not matter for a warm up\n const capture = await this.capture(elem, () => 1);\n await idle(window);\n // take a WebGL context only now, not while waiting for idle time\n rendererChoice = this.chooseRenderer(maxParticleCount);\n // dust skips the crack shader, the other variants compile every shader\n ({ renderer } = this.createRenderer(rendererChoice, capture, {\n particleAcceleration,\n crumble: 'shards',\n }));\n renderer.render({\n deltaTSec: 0,\n animationT: 0,\n maxWidth: renderer.canvas.width,\n maxHeight: renderer.canvas.height,\n });\n } finally {\n renderer?.dispose();\n elem.remove();\n rendererChoice?.release();\n }\n }\n\n private vaporizeIntern(\n elem: HTMLElement,\n options: WsThanosOptions\n ): Observable<AnimationState> {\n const { animationLength, maxParticleCount, particleAcceleration, crumble } =\n options;\n\n elem.style.opacity = elem.style.opacity || '1';\n\n const rendererChoice = this.chooseRenderer(maxParticleCount);\n let effect: RunningEffect | undefined;\n\n const capture = this.capture(elem, (width, height) =>\n rendererChoice.captureScale(width, height)\n );\n return from(capture).pipe(\n map((capture): RunningEffect => {\n const { renderer, particles } = this.createRenderer(\n rendererChoice,\n capture,\n { particleAcceleration, crumble }\n );\n\n this.placeEffectCanvas(\n elem,\n renderer.canvas,\n capture.width,\n capture.height\n );\n renderer.canvas.dataset['wsThanosRenderer'] = renderer.kind;\n renderer.canvas.dataset['wsThanosCrumble'] = renderer.crumble;\n // when every pixel became a particle, the particles replace the element right away\n const fadeOutMs = particles.sampled\n ? Math.floor(animationLength * 0.8)\n : FULL_COVERAGE_FADE_OUT_MS;\n elem.style.transition = `opacity ${fadeOutMs}ms ease-out`;\n elem.style.opacity = '0';\n\n const { sound, soundVolume } = options;\n effect = {\n renderer,\n sound: sound\n ? this.snapSound.play(\n animationLength,\n soundVolume,\n renderer.crumble\n )\n : undefined,\n };\n return effect;\n }),\n switchMap(({ renderer, sound }) => {\n let time = 0;\n return interval(1000 / 60, animationFrameScheduler).pipe(\n timeInterval(),\n tap((deltaT) => (time += deltaT.interval)),\n map((deltaT): AnimationState => ({\n deltaTSec: deltaT.interval / 1000,\n animationT: time / animationLength,\n maxWidth: renderer.canvas.width,\n maxHeight: renderer.canvas.height,\n })),\n tap((animationState) => renderer.render(animationState)),\n takeWhile((animationState) => animationState.animationT <= 1),\n // a completed effect lets the sound blow away, a cancelled one stops it\n tap({ unsubscribe: () => sound?.stop(), error: () => sound?.stop() })\n );\n }),\n finalize(() => {\n effect?.renderer.dispose();\n rendererChoice.release();\n })\n );\n }\n\n private chooseRenderer(maxParticleCount: number): ParticleRendererChoice {\n return chooseParticleRenderer({\n maxParticleCount,\n devicePixelRatio: this.document.defaultView?.devicePixelRatio ?? 1,\n createCanvas: () => this.document.createElement('canvas'),\n gpuBudget: this.gpuBudget,\n });\n }\n\n /** draw the element into an image at the scale the renderer needs */\n private async capture(\n elem: HTMLElement,\n captureScale: ParticleRendererChoice['captureScale']\n ): Promise<Capture> {\n const window = this.document.defaultView;\n const { width, height } = elem.getBoundingClientRect();\n const scale = captureScale(width, height);\n // a canvas that is read back is drawn on the CPU: a GPU canvas would compile\n // the shaders for everything html2canvas draws on the first read back\n const canvas = this.document.createElement('canvas');\n canvas.width = Math.floor(Math.ceil(width) * scale);\n canvas.height = Math.floor(Math.ceil(height) * scale);\n const context = canvas.getContext('2d', { willReadFrequently: true });\n if (context == null) {\n throw new Error('Could not get a 2d context for the capture');\n }\n await html2canvas(elem, {\n canvas,\n backgroundColor: null,\n scale,\n allowTaint: true,\n windowHeight: window?.innerHeight,\n windowWidth: window?.innerWidth,\n scrollY: -(window?.scrollY ?? 0),\n });\n const imageData = context.getImageData(0, 0, canvas.width, canvas.height);\n return { imageData, width, height, scale };\n }\n\n private createRenderer(\n rendererChoice: ParticleRendererChoice,\n { imageData, scale }: Capture,\n {\n particleAcceleration,\n crumble,\n }: Pick<WsThanosOptions, 'particleAcceleration' | 'crumble'>\n ): { renderer: ParticleRenderer; particles: Particles } {\n const effectWidth = imageData.width * EFFECT_WIDTH_SCALE;\n const effectHeight = imageData.height * EFFECT_HEIGHT_SCALE;\n const particles = createParticles(\n imageData,\n rendererChoice.maxParticleCount,\n effectHeight\n );\n const renderer = rendererChoice.create(particles, {\n width: effectWidth,\n height: effectHeight,\n // particles move in device pixels, keep their speed in css pixels\n particleAcceleration: particleAcceleration * scale,\n pixelScale: scale,\n crumble,\n seed: Math.random() * 1000,\n });\n return { renderer, particles };\n }\n\n /** the element sits at the bottom left of the larger effect canvas */\n private placeEffectCanvas(\n elem: HTMLElement,\n canvas: HTMLCanvasElement,\n width: number,\n height: number\n ): void {\n const { left, top } = this.offsetInParent(elem);\n canvas.style.position = 'absolute';\n canvas.style.left = `${left}px`;\n canvas.style.top = `${top - height * (EFFECT_HEIGHT_SCALE - 1)}px`;\n canvas.style.width = `${width * EFFECT_WIDTH_SCALE}px`;\n canvas.style.height = `${height * EFFECT_HEIGHT_SCALE}px`;\n canvas.style.zIndex = '2000';\n canvas.style.pointerEvents = 'none';\n elem.insertAdjacentElement('beforebegin', canvas);\n }\n\n /** position of the element inside its parent, where absolute children are placed */\n private offsetInParent(elem: HTMLElement): { left: number; top: number } {\n const parent = elem.parentElement;\n if (parent == null) {\n return { left: 0, top: 0 };\n }\n parent.style.position = parent.style.position || 'relative';\n // absolute positions are relative to the parent's padding box\n const elemRect = elem.getBoundingClientRect();\n const parentRect = parent.getBoundingClientRect();\n return {\n left:\n elemRect.left - parentRect.left - parent.clientLeft + parent.scrollLeft,\n top: elemRect.top - parentRect.top - parent.clientTop + parent.scrollTop,\n };\n }\n}\n"]}
@@ -11,6 +11,7 @@ export declare class WsThanosDirective {
11
11
  private readonly vaporizeDomElem;
12
12
  private readonly thanosService;
13
13
  private readonly thanosOptions;
14
+ constructor();
14
15
  /**
15
16
  *
16
17
  * Vaporize the dom element of this directive
@@ -16,4 +16,6 @@ export interface WsThanosOptions {
16
16
  soundVolume: number;
17
17
  /** how the element breaks apart */
18
18
  crumble: WsThanosCrumble;
19
+ /** warm up the effect while the browser is idle, so the first snap does not stutter */
20
+ preload: boolean;
19
21
  }
@@ -8,12 +8,24 @@ export declare class WsThanosService {
8
8
  private readonly snapSound;
9
9
  /** shared by all effects, browsers only allow a few WebGL contexts */
10
10
  private readonly gpuBudget;
11
+ private preloaded?;
11
12
  /**
12
13
  * start the vaporize-effect.
13
14
  * @param options override the provided options for this element
14
15
  */
15
16
  vaporize(elem: HTMLElement, options?: Partial<WsThanosOptions>): Observable<AnimationState>;
17
+ /**
18
+ * Snap a tiny invisible element once, so the first real snap starts without a stutter:
19
+ * html2canvas, reading back its canvas and compiling the WebGL shaders are slow the first time.
20
+ * Runs once while the browser is idle, the directive starts it unless `preload` is off.
21
+ */
22
+ preload(): Promise<void>;
23
+ private preloadIntern;
16
24
  private vaporizeIntern;
25
+ private chooseRenderer;
26
+ /** draw the element into an image at the scale the renderer needs */
27
+ private capture;
28
+ private createRenderer;
17
29
  /** the element sits at the bottom left of the larger effect canvas */
18
30
  private placeEffectCanvas;
19
31
  /** position of the element inside its parent, where absolute children are placed */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wolsok/thanos",
3
- "version": "18.1.4",
3
+ "version": "18.2.1",
4
4
  "description": "Wolf Soko's implementation of a cool Thanos snapping vaporizing effect for destroying dom elements",
5
5
  "bugs": {
6
6
  "url": "https://github.com/WolfSoko/wol-sok-mono/issues"
@@ -24,9 +24,9 @@
24
24
  "npm"
25
25
  ],
26
26
  "peerDependencies": {
27
- "@angular/core": "22.2.3",
27
+ "@angular/core": "^20.0.0 || ^21.0.0 || ^22.0.0",
28
28
  "html2canvas": "^1.4.1",
29
- "rxjs": "7.8.2"
29
+ "rxjs": "^7.4.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "tslib": "^2.3.0"