@wolsok/thanos 18.2.1 → 18.2.3
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 +6 -0
- package/esm2022/lib/capture-scale.js +13 -3
- package/esm2022/lib/capture-scale.js.map +1 -1
- package/esm2022/lib/choose-particle-renderer.js +11 -4
- package/esm2022/lib/choose-particle-renderer.js.map +1 -1
- package/esm2022/lib/ws-thanos.options.js.map +1 -1
- package/esm2022/lib/ws-thanos.service.js +6 -5
- package/esm2022/lib/ws-thanos.service.js.map +1 -1
- package/lib/capture-scale.d.ts +10 -2
- package/lib/choose-particle-renderer.d.ts +3 -1
- package/lib/ws-thanos.options.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,7 @@ export class SharedModule {}
|
|
|
54
54
|
| -------------------- | :-----: | ------: | -------------------------------------------------------------------: |
|
|
55
55
|
| animationLength | number | 5000 | the animation length in ms |
|
|
56
56
|
| maxParticleCount | number | 3000000 | max amount of particles (capped at 400000 without WebGL2, see below) |
|
|
57
|
+
| maxCapturePixels | number | unset | most pixels to capture the element with, see GPU rendering |
|
|
57
58
|
| particleAcceleration | number | 30 | speed of the particle acceleration |
|
|
58
59
|
| sound | boolean | true | play a windy, sandy sound while vaporizing |
|
|
59
60
|
| soundVolume | number | 0.5 | volume of the sound from 0 to 1 |
|
|
@@ -94,6 +95,11 @@ afterNextRender(() => void thanos.preload());
|
|
|
94
95
|
The particles are simulated and drawn on the graphics card with WebGL2,
|
|
95
96
|
one particle per device pixel (up to a pixel ratio of 2) for crisp results on high resolution screens.
|
|
96
97
|
Without WebGL2 ws-thanos falls back to the CPU canvas renderer, which captures at css pixels and uses at most 400000 particles.
|
|
98
|
+
|
|
99
|
+
A big element on a big screen makes millions of pixels, and the effect canvas around it is five times as tall.
|
|
100
|
+
Set `maxCapturePixels` to capture it with fewer: ws-thanos lowers the pixel ratio, below css pixels if needed,
|
|
101
|
+
so the particles get coarser. `maxParticleCount` instead leaves out random pixels at full resolution.
|
|
102
|
+
Both renderers follow it.
|
|
97
103
|
The effect canvas has a `data-ws-thanos-renderer` attribute (`webgl` or `canvas`) showing which renderer is in use.
|
|
98
104
|
|
|
99
105
|
#### Crumble variants
|
|
@@ -2,12 +2,22 @@
|
|
|
2
2
|
export const EFFECT_WIDTH_SCALE = 2;
|
|
3
3
|
export const EFFECT_HEIGHT_SCALE = 5;
|
|
4
4
|
const MAX_DEVICE_PIXEL_RATIO = 2;
|
|
5
|
+
/**
|
|
6
|
+
* Scale that captures an element of the given css size with at most maxCapturePixels pixels,
|
|
7
|
+
* Infinity without a limit: a missing, zero, negative or NaN one.
|
|
8
|
+
*/
|
|
9
|
+
export function capturePixelsScale(width, height, maxCapturePixels) {
|
|
10
|
+
return maxCapturePixels != null && maxCapturePixels > 0
|
|
11
|
+
? Math.sqrt(maxCapturePixels / Math.max(1, width * height))
|
|
12
|
+
: Infinity;
|
|
13
|
+
}
|
|
5
14
|
/**
|
|
6
15
|
* Scale used to capture the element: device pixels for crisp particles,
|
|
7
|
-
* but small enough that the effect canvas fits the device's max canvas size
|
|
16
|
+
* but small enough that the effect canvas fits the device's max canvas size
|
|
17
|
+
* and the element stays within maxCapturePixels, below css pixels if needed.
|
|
8
18
|
*/
|
|
9
|
-
export function computeCaptureScale({ width, height, devicePixelRatio, maxCanvasSize, }) {
|
|
19
|
+
export function computeCaptureScale({ width, height, devicePixelRatio, maxCanvasSize, maxCapturePixels, }) {
|
|
10
20
|
const pixelRatio = devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
11
|
-
return Math.min(pixelRatio, MAX_DEVICE_PIXEL_RATIO, maxCanvasSize / Math.max(1, width * EFFECT_WIDTH_SCALE), maxCanvasSize / Math.max(1, height * EFFECT_HEIGHT_SCALE));
|
|
21
|
+
return Math.min(pixelRatio, MAX_DEVICE_PIXEL_RATIO, capturePixelsScale(width, height, maxCapturePixels), maxCanvasSize / Math.max(1, width * EFFECT_WIDTH_SCALE), maxCanvasSize / Math.max(1, height * EFFECT_HEIGHT_SCALE));
|
|
12
22
|
}
|
|
13
23
|
//# sourceMappingURL=capture-scale.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture-scale.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/capture-scale.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AACpC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AACrC,MAAM,sBAAsB,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"capture-scale.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/capture-scale.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AACpC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AACrC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAWjC;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAa,EACb,MAAc,EACd,gBAAyB;IAEzB,OAAO,gBAAgB,IAAI,IAAI,IAAI,gBAAgB,GAAG,CAAC;QACrD,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC;QAC3D,CAAC,CAAC,QAAQ,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAClC,KAAK,EACL,MAAM,EACN,gBAAgB,EAChB,aAAa,EACb,gBAAgB,GACG;IACnB,MAAM,UAAU,GAAG,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/D,OAAO,IAAI,CAAC,GAAG,CACb,UAAU,EACV,sBAAsB,EACtB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,gBAAgB,CAAC,EACnD,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,kBAAkB,CAAC,EACvD,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAAC,CAC1D,CAAC;AACJ,CAAC","sourcesContent":["/** the effect canvas is wider and taller than the element so particles have room to fly */\nexport const EFFECT_WIDTH_SCALE = 2;\nexport const EFFECT_HEIGHT_SCALE = 5;\nconst MAX_DEVICE_PIXEL_RATIO = 2;\n\nexport interface CaptureScaleParams {\n width: number;\n height: number;\n devicePixelRatio: number;\n maxCanvasSize: number;\n /** the most pixels to capture the element with, no limit when unset */\n maxCapturePixels?: number;\n}\n\n/**\n * Scale that captures an element of the given css size with at most maxCapturePixels pixels,\n * Infinity without a limit: a missing, zero, negative or NaN one.\n */\nexport function capturePixelsScale(\n width: number,\n height: number,\n maxCapturePixels?: number\n): number {\n return maxCapturePixels != null && maxCapturePixels > 0\n ? Math.sqrt(maxCapturePixels / Math.max(1, width * height))\n : Infinity;\n}\n\n/**\n * Scale used to capture the element: device pixels for crisp particles,\n * but small enough that the effect canvas fits the device's max canvas size\n * and the element stays within maxCapturePixels, below css pixels if needed.\n */\nexport function computeCaptureScale({\n width,\n height,\n devicePixelRatio,\n maxCanvasSize,\n maxCapturePixels,\n}: CaptureScaleParams): number {\n const pixelRatio = devicePixelRatio > 0 ? devicePixelRatio : 1;\n return Math.min(\n pixelRatio,\n MAX_DEVICE_PIXEL_RATIO,\n capturePixelsScale(width, height, maxCapturePixels),\n maxCanvasSize / Math.max(1, width * EFFECT_WIDTH_SCALE),\n maxCanvasSize / Math.max(1, height * EFFECT_HEIGHT_SCALE)\n );\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computeCaptureScale } from './capture-scale';
|
|
1
|
+
import { capturePixelsScale, computeCaptureScale } from './capture-scale';
|
|
2
2
|
import { CANVAS_MAX_PARTICLE_COUNT, CanvasParticleRenderer, } from './canvas-particle-renderer';
|
|
3
3
|
import { limitParticles } from './particles';
|
|
4
4
|
import { WebGlParticleRenderer } from './webgl/webgl-particle-renderer';
|
|
@@ -29,11 +29,12 @@ export class GpuContextBudget {
|
|
|
29
29
|
* the CPU canvas renderer otherwise. If the GPU renderer fails to set up,
|
|
30
30
|
* it falls back to the canvas renderer.
|
|
31
31
|
*/
|
|
32
|
-
export function chooseParticleRenderer({ maxParticleCount, devicePixelRatio, createCanvas, gpuBudget = new GpuContextBudget(), }) {
|
|
32
|
+
export function chooseParticleRenderer({ maxParticleCount, devicePixelRatio, maxCapturePixels, createCanvas, gpuBudget = new GpuContextBudget(), }) {
|
|
33
33
|
const createCanvasRenderer = (particles, params) => new CanvasParticleRenderer(createCanvas(), limitParticles(particles, CANVAS_MAX_PARTICLE_COUNT), params);
|
|
34
34
|
const canvasChoice = {
|
|
35
35
|
maxParticleCount: Math.min(maxParticleCount, CANVAS_MAX_PARTICLE_COUNT),
|
|
36
|
-
|
|
36
|
+
// css pixels keep the CPU work low, fewer for a big element
|
|
37
|
+
captureScale: (width, height) => Math.min(1, capturePixelsScale(width, height, maxCapturePixels)),
|
|
37
38
|
create: createCanvasRenderer,
|
|
38
39
|
release: () => undefined,
|
|
39
40
|
};
|
|
@@ -61,7 +62,13 @@ export function chooseParticleRenderer({ maxParticleCount, devicePixelRatio, cre
|
|
|
61
62
|
const maxCanvasSize = WebGlParticleRenderer.maxCanvasSize(gl);
|
|
62
63
|
return {
|
|
63
64
|
maxParticleCount,
|
|
64
|
-
captureScale: (width, height) => computeCaptureScale({
|
|
65
|
+
captureScale: (width, height) => computeCaptureScale({
|
|
66
|
+
width,
|
|
67
|
+
height,
|
|
68
|
+
devicePixelRatio,
|
|
69
|
+
maxCanvasSize,
|
|
70
|
+
maxCapturePixels,
|
|
71
|
+
}),
|
|
65
72
|
create: (particles, params) => {
|
|
66
73
|
try {
|
|
67
74
|
return new WebGlParticleRenderer(canvas, gl, particles, params);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"choose-particle-renderer.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/choose-particle-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"choose-particle-renderer.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/choose-particle-renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import { capturePixelsScale, computeCaptureScale } from './capture-scale';\nimport {\n CANVAS_MAX_PARTICLE_COUNT,\n CanvasParticleRenderer,\n} from './canvas-particle-renderer';\nimport { ParticleRenderer, ParticleRendererParams } from './particle-renderer';\nimport { limitParticles, Particles } from './particles';\nimport { WebGlParticleRenderer } from './webgl/webgl-particle-renderer';\n\n/**\n * Browsers drop the oldest WebGL context once about 16 are alive,\n * so only this many effects run on the GPU at the same time.\n */\nconst MAX_GPU_EFFECTS = 12;\n\n/** counts the effects that hold a WebGL context */\nexport class GpuContextBudget {\n private live = 0;\n\n public constructor(private readonly max = MAX_GPU_EFFECTS) {}\n\n public tryAcquire(): boolean {\n if (this.live >= this.max) {\n return false;\n }\n this.live++;\n return true;\n }\n\n public release(): void {\n this.live = Math.max(0, this.live - 1);\n }\n}\n\nexport interface ChooseParticleRendererParams {\n maxParticleCount: number;\n devicePixelRatio: number;\n /** the most pixels an element is captured with, no limit when unset */\n maxCapturePixels?: number;\n createCanvas: () => HTMLCanvasElement;\n gpuBudget?: GpuContextBudget;\n}\n\nexport interface ParticleRendererChoice {\n /** how many particles this renderer can handle */\n readonly maxParticleCount: number;\n /** scale to capture an element of the given css size */\n captureScale(width: number, height: number): number;\n create(\n particles: Particles,\n params: ParticleRendererParams\n ): ParticleRenderer;\n /** free the reserved GPU context, call it once the effect is over */\n release(): void;\n}\n\n/**\n * Picks the GPU renderer when WebGL2 is available and the GPU budget allows it,\n * the CPU canvas renderer otherwise. If the GPU renderer fails to set up,\n * it falls back to the canvas renderer.\n */\nexport function chooseParticleRenderer({\n maxParticleCount,\n devicePixelRatio,\n maxCapturePixels,\n createCanvas,\n gpuBudget = new GpuContextBudget(),\n}: ChooseParticleRendererParams): ParticleRendererChoice {\n const createCanvasRenderer = (\n particles: Particles,\n params: ParticleRendererParams\n ): ParticleRenderer =>\n new CanvasParticleRenderer(\n createCanvas(),\n limitParticles(particles, CANVAS_MAX_PARTICLE_COUNT),\n params\n );\n const canvasChoice: ParticleRendererChoice = {\n maxParticleCount: Math.min(maxParticleCount, CANVAS_MAX_PARTICLE_COUNT),\n // css pixels keep the CPU work low, fewer for a big element\n captureScale: (width, height) =>\n Math.min(1, capturePixelsScale(width, height, maxCapturePixels)),\n create: createCanvasRenderer,\n release: () => undefined,\n };\n\n if (!gpuBudget.tryAcquire()) {\n return canvasChoice;\n }\n const canvas = createCanvas();\n const gl = WebGlParticleRenderer.createContext(canvas);\n if (gl == null) {\n gpuBudget.release();\n return canvasChoice;\n }\n\n let released = false;\n const release = () => {\n if (released) {\n return;\n }\n released = true;\n // the renderer may have freed it already\n if (!gl.isContextLost()) {\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n }\n gpuBudget.release();\n };\n const maxCanvasSize = WebGlParticleRenderer.maxCanvasSize(gl);\n return {\n maxParticleCount,\n captureScale: (width, height) =>\n computeCaptureScale({\n width,\n height,\n devicePixelRatio,\n maxCanvasSize,\n maxCapturePixels,\n }),\n create: (particles, params) => {\n try {\n return new WebGlParticleRenderer(canvas, gl, particles, params);\n } catch (error) {\n console.warn('ws-thanos: falling back to canvas rendering', error);\n release();\n return createCanvasRenderer(particles, params);\n }\n },\n release,\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 /** warm up the effect while the browser is idle, so the first snap does not stutter */\n preload: boolean;\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 /**\n * the most pixels the element is captured with, every one a particle:\n * a big element is captured at a lower pixel ratio, below css pixels if needed.\n * Unlike maxParticleCount, which leaves out random pixels, the particles get coarser.\n * No limit when unset.\n */\n maxCapturePixels?: 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"]}
|
|
@@ -58,7 +58,7 @@ export class WsThanosService {
|
|
|
58
58
|
'background:linear-gradient(#fff,#888)',
|
|
59
59
|
].join(';');
|
|
60
60
|
body.appendChild(elem);
|
|
61
|
-
const { maxParticleCount, particleAcceleration } = this.thanosOptions;
|
|
61
|
+
const { maxParticleCount, maxCapturePixels, particleAcceleration } = this.thanosOptions;
|
|
62
62
|
let rendererChoice;
|
|
63
63
|
let renderer;
|
|
64
64
|
try {
|
|
@@ -66,7 +66,7 @@ export class WsThanosService {
|
|
|
66
66
|
const capture = await this.capture(elem, () => 1);
|
|
67
67
|
await idle(window);
|
|
68
68
|
// take a WebGL context only now, not while waiting for idle time
|
|
69
|
-
rendererChoice = this.chooseRenderer(maxParticleCount);
|
|
69
|
+
rendererChoice = this.chooseRenderer(maxParticleCount, maxCapturePixels);
|
|
70
70
|
// dust skips the crack shader, the other variants compile every shader
|
|
71
71
|
({ renderer } = this.createRenderer(rendererChoice, capture, {
|
|
72
72
|
particleAcceleration,
|
|
@@ -86,9 +86,9 @@ export class WsThanosService {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
vaporizeIntern(elem, options) {
|
|
89
|
-
const { animationLength, maxParticleCount, particleAcceleration, crumble } = options;
|
|
89
|
+
const { animationLength, maxParticleCount, maxCapturePixels, particleAcceleration, crumble, } = options;
|
|
90
90
|
elem.style.opacity = elem.style.opacity || '1';
|
|
91
|
-
const rendererChoice = this.chooseRenderer(maxParticleCount);
|
|
91
|
+
const rendererChoice = this.chooseRenderer(maxParticleCount, maxCapturePixels);
|
|
92
92
|
let effect;
|
|
93
93
|
const capture = this.capture(elem, (width, height) => rendererChoice.captureScale(width, height));
|
|
94
94
|
return from(capture).pipe(map((capture) => {
|
|
@@ -125,9 +125,10 @@ export class WsThanosService {
|
|
|
125
125
|
rendererChoice.release();
|
|
126
126
|
}));
|
|
127
127
|
}
|
|
128
|
-
chooseRenderer(maxParticleCount) {
|
|
128
|
+
chooseRenderer(maxParticleCount, maxCapturePixels) {
|
|
129
129
|
return chooseParticleRenderer({
|
|
130
130
|
maxParticleCount,
|
|
131
|
+
maxCapturePixels,
|
|
131
132
|
devicePixelRatio: this.document.defaultView?.devicePixelRatio ?? 1,
|
|
132
133
|
createCanvas: () => this.document.createElement('canvas'),
|
|
133
134
|
gpuBudget: this.gpuBudget,
|
|
@@ -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 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"]}
|
|
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, maxCapturePixels, particleAcceleration } =\n 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, maxCapturePixels);\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 {\n animationLength,\n maxParticleCount,\n maxCapturePixels,\n particleAcceleration,\n crumble,\n } = options;\n\n elem.style.opacity = elem.style.opacity || '1';\n\n const rendererChoice = this.chooseRenderer(\n maxParticleCount,\n maxCapturePixels\n );\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(\n maxParticleCount: number,\n maxCapturePixels: number | undefined\n ): ParticleRendererChoice {\n return chooseParticleRenderer({\n maxParticleCount,\n maxCapturePixels,\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"]}
|
package/lib/capture-scale.d.ts
CHANGED
|
@@ -6,9 +6,17 @@ export interface CaptureScaleParams {
|
|
|
6
6
|
height: number;
|
|
7
7
|
devicePixelRatio: number;
|
|
8
8
|
maxCanvasSize: number;
|
|
9
|
+
/** the most pixels to capture the element with, no limit when unset */
|
|
10
|
+
maxCapturePixels?: number;
|
|
9
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Scale that captures an element of the given css size with at most maxCapturePixels pixels,
|
|
14
|
+
* Infinity without a limit: a missing, zero, negative or NaN one.
|
|
15
|
+
*/
|
|
16
|
+
export declare function capturePixelsScale(width: number, height: number, maxCapturePixels?: number): number;
|
|
10
17
|
/**
|
|
11
18
|
* Scale used to capture the element: device pixels for crisp particles,
|
|
12
|
-
* but small enough that the effect canvas fits the device's max canvas size
|
|
19
|
+
* but small enough that the effect canvas fits the device's max canvas size
|
|
20
|
+
* and the element stays within maxCapturePixels, below css pixels if needed.
|
|
13
21
|
*/
|
|
14
|
-
export declare function computeCaptureScale({ width, height, devicePixelRatio, maxCanvasSize, }: CaptureScaleParams): number;
|
|
22
|
+
export declare function computeCaptureScale({ width, height, devicePixelRatio, maxCanvasSize, maxCapturePixels, }: CaptureScaleParams): number;
|
|
@@ -11,6 +11,8 @@ export declare class GpuContextBudget {
|
|
|
11
11
|
export interface ChooseParticleRendererParams {
|
|
12
12
|
maxParticleCount: number;
|
|
13
13
|
devicePixelRatio: number;
|
|
14
|
+
/** the most pixels an element is captured with, no limit when unset */
|
|
15
|
+
maxCapturePixels?: number;
|
|
14
16
|
createCanvas: () => HTMLCanvasElement;
|
|
15
17
|
gpuBudget?: GpuContextBudget;
|
|
16
18
|
}
|
|
@@ -28,4 +30,4 @@ export interface ParticleRendererChoice {
|
|
|
28
30
|
* the CPU canvas renderer otherwise. If the GPU renderer fails to set up,
|
|
29
31
|
* it falls back to the canvas renderer.
|
|
30
32
|
*/
|
|
31
|
-
export declare function chooseParticleRenderer({ maxParticleCount, devicePixelRatio, createCanvas, gpuBudget, }: ChooseParticleRendererParams): ParticleRendererChoice;
|
|
33
|
+
export declare function chooseParticleRenderer({ maxParticleCount, devicePixelRatio, maxCapturePixels, createCanvas, gpuBudget, }: ChooseParticleRendererParams): ParticleRendererChoice;
|
|
@@ -9,6 +9,13 @@ export type WsThanosCrumble = 'dust' | 'cracks' | 'shards' | 'chunks';
|
|
|
9
9
|
export interface WsThanosOptions {
|
|
10
10
|
animationLength: number;
|
|
11
11
|
maxParticleCount: number;
|
|
12
|
+
/**
|
|
13
|
+
* the most pixels the element is captured with, every one a particle:
|
|
14
|
+
* a big element is captured at a lower pixel ratio, below css pixels if needed.
|
|
15
|
+
* Unlike maxParticleCount, which leaves out random pixels, the particles get coarser.
|
|
16
|
+
* No limit when unset.
|
|
17
|
+
*/
|
|
18
|
+
maxCapturePixels?: number;
|
|
12
19
|
particleAcceleration: number;
|
|
13
20
|
/** play a windy, sandy sound while vaporizing */
|
|
14
21
|
sound: boolean;
|
package/package.json
CHANGED