@wolsok/thanos 18.1.4 → 18.2.0
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 +16 -0
- package/esm2022/lib/create-ws-thanos-options.js +1 -0
- package/esm2022/lib/create-ws-thanos-options.js.map +1 -1
- package/esm2022/lib/ws-thanos.directive.js +9 -2
- package/esm2022/lib/ws-thanos.directive.js.map +1 -1
- package/esm2022/lib/ws-thanos.options.js.map +1 -1
- package/esm2022/lib/ws-thanos.service.js +114 -38
- package/esm2022/lib/ws-thanos.service.js.map +1 -1
- package/lib/ws-thanos.directive.d.ts +1 -0
- package/lib/ws-thanos.options.d.ts +2 -0
- package/lib/ws-thanos.service.d.ts +12 -0
- package/package.json +1 -1
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,
|
|
@@ -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,
|
|
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 =
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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"]}
|
|
@@ -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