@wolsok/thanos 17.0.38 → 18.1.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.
Files changed (48) hide show
  1. package/README.md +73 -12
  2. package/esm2022/lib/canvas-particle-renderer.js +98 -0
  3. package/esm2022/lib/canvas-particle-renderer.js.map +1 -0
  4. package/esm2022/lib/capture-scale.js +13 -0
  5. package/esm2022/lib/capture-scale.js.map +1 -0
  6. package/esm2022/lib/choose-particle-renderer.js +78 -0
  7. package/esm2022/lib/choose-particle-renderer.js.map +1 -0
  8. package/esm2022/lib/create-ws-thanos-options.js +4 -1
  9. package/esm2022/lib/create-ws-thanos-options.js.map +1 -1
  10. package/esm2022/lib/crumble-shape.js +24 -0
  11. package/esm2022/lib/crumble-shape.js.map +1 -0
  12. package/esm2022/lib/particle-renderer.js +1 -0
  13. package/esm2022/lib/particle-renderer.js.map +1 -0
  14. package/esm2022/lib/particles.js +84 -0
  15. package/esm2022/lib/particles.js.map +1 -0
  16. package/esm2022/lib/snap-sound-envelopes.js +38 -0
  17. package/esm2022/lib/snap-sound-envelopes.js.map +1 -0
  18. package/esm2022/lib/snap-sound.service.js +287 -0
  19. package/esm2022/lib/snap-sound.service.js.map +1 -0
  20. package/esm2022/lib/vaporizing-front.js +48 -0
  21. package/esm2022/lib/vaporizing-front.js.map +1 -0
  22. package/esm2022/lib/webgl/simplex-noise.glsl.js +69 -0
  23. package/esm2022/lib/webgl/simplex-noise.glsl.js.map +1 -0
  24. package/esm2022/lib/webgl/webgl-particle-renderer.js +308 -0
  25. package/esm2022/lib/webgl/webgl-particle-renderer.js.map +1 -0
  26. package/esm2022/lib/webgl/webgl-shaders.js +323 -0
  27. package/esm2022/lib/webgl/webgl-shaders.js.map +1 -0
  28. package/esm2022/lib/ws-thanos.directive.js +8 -6
  29. package/esm2022/lib/ws-thanos.directive.js.map +1 -1
  30. package/esm2022/lib/ws-thanos.options.js.map +1 -1
  31. package/esm2022/lib/ws-thanos.service.js +103 -220
  32. package/esm2022/lib/ws-thanos.service.js.map +1 -1
  33. package/lib/canvas-particle-renderer.d.ts +29 -0
  34. package/lib/capture-scale.d.ts +14 -0
  35. package/lib/choose-particle-renderer.d.ts +31 -0
  36. package/lib/crumble-shape.d.ts +11 -0
  37. package/lib/particle-renderer.d.ts +26 -0
  38. package/lib/particles.d.ts +35 -0
  39. package/lib/snap-sound-envelopes.d.ts +15 -0
  40. package/lib/snap-sound.service.d.ts +27 -0
  41. package/lib/vaporizing-front.d.ts +34 -0
  42. package/lib/webgl/simplex-noise.glsl.d.ts +5 -0
  43. package/lib/webgl/webgl-particle-renderer.d.ts +46 -0
  44. package/lib/webgl/webgl-shaders.d.ts +27 -0
  45. package/lib/ws-thanos.directive.d.ts +4 -2
  46. package/lib/ws-thanos.options.d.ts +14 -0
  47. package/lib/ws-thanos.service.d.ts +11 -8
  48. package/package.json +1 -1
package/README.md CHANGED
@@ -50,29 +50,83 @@ export class SharedModule {}
50
50
 
51
51
  #### `WsThanosOptions` to configure ws-thanos:
52
52
 
53
- | field | type | default | description |
54
- | -------------------- | :----: | ------: | ---------------------------------: |
55
- | animationLength | number | 5000 | the animation length in ms |
56
- | maxParticleCount | number | 400000 | max amount of particles |
57
- | particleAcceleration | number | 30 | speed of the particle acceleration |
53
+ | field | type | default | description |
54
+ | -------------------- | :-----: | ------: | -------------------------------------------------------------------: |
55
+ | animationLength | number | 5000 | the animation length in ms |
56
+ | maxParticleCount | number | 3000000 | max amount of particles (capped at 400000 without WebGL2, see below) |
57
+ | particleAcceleration | number | 30 | speed of the particle acceleration |
58
+ | sound | boolean | true | play a windy, sandy sound while vaporizing |
59
+ | soundVolume | number | 0.5 | volume of the sound from 0 to 1 |
60
+ | crumble | string | shards | how the element breaks apart, see crumble variants |
61
+
62
+ #### Sound
63
+
64
+ Every snap plays a sound, generated live with the Web Audio API (no audio files are downloaded),
65
+ timed to `animationLength` and slightly different every time:
66
+
67
+ 1. the element breaks with a crunch of many small cracks and a low rumble,
68
+ 2. a gusty wind slowly grows stronger and carries hissing, crackling sand,
69
+ 3. wind and sand get weaker in the last third and have faded away at 85% of the animation.
70
+
71
+ With `crumble: 'dust'` there is no crunch or rumble, the dust only crackles thinly.
72
+ A limiter keeps many simultaneous snaps from getting too loud.
73
+ Browsers only allow audio after the user interacted with the page, so snaps before the first click or tap stay silent.
74
+ Turn it off with `provideWsThanosOptions({ sound: false })`.
75
+
76
+ #### GPU rendering
77
+
78
+ The particles are simulated and drawn on the graphics card with WebGL2,
79
+ one particle per device pixel (up to a pixel ratio of 2) for crisp results on high resolution screens.
80
+ Without WebGL2 ws-thanos falls back to the CPU canvas renderer, which captures at css pixels and uses at most 400000 particles.
81
+ The effect canvas has a `data-ws-thanos-renderer` attribute (`webgl` or `canvas`) showing which renderer is in use.
82
+
83
+ #### Crumble variants
84
+
85
+ The `crumble` option decides how the element breaks apart while it vaporizes:
86
+
87
+ | crumble | effect |
88
+ | ---------- | ---------------------------------------------------------------------------- |
89
+ | `'dust'` | particles blow away along the vaporizing front |
90
+ | `'cracks'` | cracks spread through the element and get finer before it turns to dust |
91
+ | `'shards'` | (default) cracks split it into shards that shift and tilt, then turn to dust |
92
+ | `'chunks'` | whole chunks break off, jump away and crumble to dust |
93
+
94
+ The variants need WebGL2, the canvas fallback always turns into `'dust'`.
95
+ The effect canvas shows the variant in use in its `data-ws-thanos-crumble` attribute.
96
+
97
+ #### Options per element
98
+
99
+ Override any option for a single element with the `wsThanosOptions` input:
100
+
101
+ ```html
102
+ <div wsThanos [wsThanosOptions]="{ crumble: 'chunks', animationLength: 8000 }">
103
+ Breaks into chunks
104
+ </div>
105
+ ```
106
+
107
+ or pass them to `WsThanosService.vaporize(element, { crumble: 'cracks' })`.
58
108
 
59
109
  ### `WsThanosDirective` usage
60
110
 
61
111
  Use the directive `wsThanos` on your element and reference it using `@ViewChild(WsThanosDirective)` in your component or
62
112
  directly in html via template ref:
63
113
 
64
- ```
65
- <div wsThanos
66
- #thanos="thanos"
67
- (wsThanosComplete)=onComplete()>
114
+ ```html
115
+ <div wsThanos #thanos="thanos" (wsThanosComplete)="onComplete()">
68
116
  This div will be vaporized on click
69
- </div>
70
- <button (click)="thanos.vaporizeAndScrollIntoView(removeElement)">
117
+ </div>
118
+ <button (click)="thanos.vaporize(true)">Vaporize</button>
71
119
  ```
72
120
 
121
+ - `vaporize(removeElem = true)` starts the effect and returns an observable that emits once it is complete.
122
+ With `removeElem = false` the element fades back in afterwards instead of being removed.
123
+ - `vaporize$(removeElem = true)` returns an observable of the `AnimationState` of every frame. Subscribe to start the effect.
124
+
73
125
  ### `WsThanosService` usage
74
126
 
75
- Inject the 'WsThanosService' into your class. Call 'vaporizeAndScrollIntoView(removeElement)' and subscribe to it.
127
+ Inject the `WsThanosService` into your class and subscribe to `vaporize(element, options?)` to vaporize any element.
128
+ It emits the `AnimationState` of every frame and completes when the effect is done, unsubscribing cancels it.
129
+ The element is not removed, that is up to you.
76
130
 
77
131
  ## Collaboration
78
132
 
@@ -134,6 +188,13 @@ See the [Nx Release documentation](https://nx.dev/features/manage-releases) for
134
188
 
135
189
  ## Migration
136
190
 
191
+ To the release with sound and GPU rendering
192
+
193
+ - The snap sound is on by default. Turn it off with `provideWsThanosOptions({ sound: false })`.
194
+ - Elements crumble into shards by default. Use `provideWsThanosOptions({ crumble: 'dust' })` for the previous look.
195
+ - `maxParticleCount` defaults to 3000000, one particle per device pixel on the GPU.
196
+ Without WebGL2 at most 400000 particles are used, even if you configured more.
197
+
137
198
  From `1.0.1` to `2.0.0`
138
199
 
139
200
  - Replace `WsThanosModule` in imports and exports with `WsThanosDirective`
@@ -0,0 +1,98 @@
1
+ import { MIN_PARTICLE_ALPHA, PARTICLE_STATE_LENGTH, ParticleStateIndex as P, } from './particles';
2
+ import { SimplexNoise } from './simplex-noise';
3
+ import { FLOW_FIELD, isBehindFront, vaporizingFront } from './vaporizing-front';
4
+ /** the CPU can't keep up with more particles than this */
5
+ export const CANVAS_MAX_PARTICLE_COUNT = 400_000;
6
+ const FLOW_FIELD_RES = 0.05;
7
+ /** Fallback renderer that simulates on the CPU and draws with a 2d context. */
8
+ export class CanvasParticleRenderer {
9
+ constructor(canvas, particles, params) {
10
+ this.canvas = canvas;
11
+ this.particles = particles;
12
+ this.params = params;
13
+ this.kind = 'canvas';
14
+ /** the CPU can't afford to simulate cracks and shards */
15
+ this.crumble = 'dust';
16
+ this.noise = new SimplexNoise({
17
+ frequency: FLOW_FIELD.frequency,
18
+ min: 0,
19
+ });
20
+ canvas.width = params.width;
21
+ canvas.height = params.height;
22
+ const context = canvas.getContext('2d', { willReadFrequently: true });
23
+ if (context == null) {
24
+ throw new Error('Could not get 2d context for vaporize canvas');
25
+ }
26
+ this.context = context;
27
+ }
28
+ render(animationState) {
29
+ updateParticlesOnCpu({
30
+ particles: this.particles,
31
+ animationState,
32
+ particleAcceleration: this.params.particleAcceleration,
33
+ noise: this.noise,
34
+ seed: this.params.seed,
35
+ });
36
+ drawParticlesOnCpu(this.context, this.particles);
37
+ }
38
+ dispose() {
39
+ this.canvas.remove();
40
+ }
41
+ }
42
+ function isVisibleInside(state, base, width, height) {
43
+ const x = state[base + P.X];
44
+ const y = state[base + P.Y];
45
+ return (x <= width &&
46
+ x >= 0 &&
47
+ y <= height &&
48
+ y >= 0 &&
49
+ state[base + P.ALPHA] >= MIN_PARTICLE_ALPHA);
50
+ }
51
+ export function updateParticlesOnCpu({ particles: { state, count, maxParticleX, minParticleY }, animationState: { deltaTSec, animationT, maxWidth, maxHeight }, particleAcceleration, noise, seed, }) {
52
+ const front = vaporizingFront(animationT, maxParticleX, minParticleY, maxHeight);
53
+ for (let i = 0; i < count; i++) {
54
+ const base = i * PARTICLE_STATE_LENGTH;
55
+ // only update particles that are inside view and visible
56
+ if (!isVisibleInside(state, base, maxWidth, maxHeight)) {
57
+ continue;
58
+ }
59
+ const particleX = state[base + P.X];
60
+ const particleY = state[base + P.Y];
61
+ if (state[base + P.RELEASED_AT] === 0) {
62
+ if (isBehindFront(particleX, particleY, maxHeight, deltaTSec, seed, front)) {
63
+ state[base + P.RELEASED_AT] = Math.max(animationT, 0.0001);
64
+ state[base + P.AX] = Math.random();
65
+ state[base + P.AY] = Math.random() * -1;
66
+ }
67
+ }
68
+ else {
69
+ // flow along the noise velocity field
70
+ state[base + P.AX] += noise.scaled3D(particleX, particleY, seed + FLOW_FIELD.xSeedOffset, FLOW_FIELD_RES);
71
+ state[base + P.AY] -= noise.scaled3D(particleX, seed / FLOW_FIELD.ySeedDivisor, particleY, FLOW_FIELD_RES);
72
+ }
73
+ state[base + P.VX] += state[base + P.AX] * particleAcceleration * deltaTSec;
74
+ state[base + P.VY] += state[base + P.AY] * particleAcceleration * deltaTSec;
75
+ state[base + P.X] += state[base + P.VX] * deltaTSec;
76
+ state[base + P.Y] += state[base + P.VY] * deltaTSec;
77
+ state[base + P.ALPHA] *= front.fade;
78
+ }
79
+ }
80
+ export function drawParticlesOnCpu(context, { state, colors, count }) {
81
+ const { width, height } = context.canvas;
82
+ context.clearRect(0, 0, width, height);
83
+ const image = context.getImageData(0, 0, width, height);
84
+ const pixels = image.data;
85
+ for (let i = 0; i < count; i++) {
86
+ const base = i * PARTICLE_STATE_LENGTH;
87
+ if (!isVisibleInside(state, base, width, height)) {
88
+ continue;
89
+ }
90
+ const pixel = (~~state[base + P.Y] * width + ~~state[base + P.X]) * 4;
91
+ pixels[pixel] = colors[i * 4];
92
+ pixels[pixel + 1] = colors[i * 4 + 1];
93
+ pixels[pixel + 2] = colors[i * 4 + 2];
94
+ pixels[pixel + 3] = ~~state[base + P.ALPHA];
95
+ }
96
+ context.putImageData(image, 0, 0);
97
+ }
98
+ //# sourceMappingURL=canvas-particle-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-particle-renderer.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/canvas-particle-renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import { AnimationState } from './animation.state';\nimport { ParticleRenderer, ParticleRendererParams } from './particle-renderer';\nimport {\n MIN_PARTICLE_ALPHA,\n PARTICLE_STATE_LENGTH,\n ParticleStateIndex as P,\n Particles,\n} from './particles';\nimport { SimplexNoise } from './simplex-noise';\nimport { FLOW_FIELD, isBehindFront, vaporizingFront } from './vaporizing-front';\n\n/** the CPU can't keep up with more particles than this */\nexport const CANVAS_MAX_PARTICLE_COUNT = 400_000;\n\nconst FLOW_FIELD_RES = 0.05;\n\nexport interface UpdateParticlesParams {\n particles: Particles;\n animationState: AnimationState;\n particleAcceleration: number;\n noise: SimplexNoise;\n seed: number;\n}\n\n/** Fallback renderer that simulates on the CPU and draws with a 2d context. */\nexport class CanvasParticleRenderer implements ParticleRenderer {\n public readonly kind = 'canvas';\n /** the CPU can't afford to simulate cracks and shards */\n public readonly crumble = 'dust';\n private readonly noise = new SimplexNoise({\n frequency: FLOW_FIELD.frequency,\n min: 0,\n });\n private readonly context: CanvasRenderingContext2D;\n\n public constructor(\n public readonly canvas: HTMLCanvasElement,\n private readonly particles: Particles,\n private readonly params: ParticleRendererParams\n ) {\n canvas.width = params.width;\n canvas.height = params.height;\n const context = canvas.getContext('2d', { willReadFrequently: true });\n if (context == null) {\n throw new Error('Could not get 2d context for vaporize canvas');\n }\n this.context = context;\n }\n\n public render(animationState: AnimationState): void {\n updateParticlesOnCpu({\n particles: this.particles,\n animationState,\n particleAcceleration: this.params.particleAcceleration,\n noise: this.noise,\n seed: this.params.seed,\n });\n drawParticlesOnCpu(this.context, this.particles);\n }\n\n public dispose(): void {\n this.canvas.remove();\n }\n}\n\nfunction isVisibleInside(\n state: Float32Array,\n base: number,\n width: number,\n height: number\n): boolean {\n const x = state[base + P.X];\n const y = state[base + P.Y];\n return (\n x <= width &&\n x >= 0 &&\n y <= height &&\n y >= 0 &&\n state[base + P.ALPHA] >= MIN_PARTICLE_ALPHA\n );\n}\n\nexport function updateParticlesOnCpu({\n particles: { state, count, maxParticleX, minParticleY },\n animationState: { deltaTSec, animationT, maxWidth, maxHeight },\n particleAcceleration,\n noise,\n seed,\n}: UpdateParticlesParams): void {\n const front = vaporizingFront(\n animationT,\n maxParticleX,\n minParticleY,\n maxHeight\n );\n\n for (let i = 0; i < count; i++) {\n const base = i * PARTICLE_STATE_LENGTH;\n // only update particles that are inside view and visible\n if (!isVisibleInside(state, base, maxWidth, maxHeight)) {\n continue;\n }\n const particleX = state[base + P.X];\n const particleY = state[base + P.Y];\n\n if (state[base + P.RELEASED_AT] === 0) {\n if (\n isBehindFront(particleX, particleY, maxHeight, deltaTSec, seed, front)\n ) {\n state[base + P.RELEASED_AT] = Math.max(animationT, 0.0001);\n state[base + P.AX] = Math.random();\n state[base + P.AY] = Math.random() * -1;\n }\n } else {\n // flow along the noise velocity field\n state[base + P.AX] += noise.scaled3D(\n particleX,\n particleY,\n seed + FLOW_FIELD.xSeedOffset,\n FLOW_FIELD_RES\n );\n state[base + P.AY] -= noise.scaled3D(\n particleX,\n seed / FLOW_FIELD.ySeedDivisor,\n particleY,\n FLOW_FIELD_RES\n );\n }\n\n state[base + P.VX] += state[base + P.AX] * particleAcceleration * deltaTSec;\n state[base + P.VY] += state[base + P.AY] * particleAcceleration * deltaTSec;\n state[base + P.X] += state[base + P.VX] * deltaTSec;\n state[base + P.Y] += state[base + P.VY] * deltaTSec;\n state[base + P.ALPHA] *= front.fade;\n }\n}\n\nexport function drawParticlesOnCpu(\n context: CanvasRenderingContext2D,\n { state, colors, count }: Particles\n): void {\n const { width, height } = context.canvas;\n context.clearRect(0, 0, width, height);\n const image = context.getImageData(0, 0, width, height);\n const pixels = image.data;\n for (let i = 0; i < count; i++) {\n const base = i * PARTICLE_STATE_LENGTH;\n if (!isVisibleInside(state, base, width, height)) {\n continue;\n }\n const pixel = (~~state[base + P.Y] * width + ~~state[base + P.X]) * 4;\n pixels[pixel] = colors[i * 4];\n pixels[pixel + 1] = colors[i * 4 + 1];\n pixels[pixel + 2] = colors[i * 4 + 2];\n pixels[pixel + 3] = ~~state[base + P.ALPHA];\n }\n context.putImageData(image, 0, 0);\n}\n"]}
@@ -0,0 +1,13 @@
1
+ /** the effect canvas is wider and taller than the element so particles have room to fly */
2
+ export const EFFECT_WIDTH_SCALE = 2;
3
+ export const EFFECT_HEIGHT_SCALE = 5;
4
+ const MAX_DEVICE_PIXEL_RATIO = 2;
5
+ /**
6
+ * 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.
8
+ */
9
+ export function computeCaptureScale({ width, height, devicePixelRatio, maxCanvasSize, }) {
10
+ 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));
12
+ }
13
+ //# sourceMappingURL=capture-scale.js.map
@@ -0,0 +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;AASjC;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAClC,KAAK,EACL,MAAM,EACN,gBAAgB,EAChB,aAAa,GACM;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,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}\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 */\nexport function computeCaptureScale({\n width,\n height,\n devicePixelRatio,\n maxCanvasSize,\n}: CaptureScaleParams): number {\n const pixelRatio = devicePixelRatio > 0 ? devicePixelRatio : 1;\n return Math.min(\n pixelRatio,\n MAX_DEVICE_PIXEL_RATIO,\n maxCanvasSize / Math.max(1, width * EFFECT_WIDTH_SCALE),\n maxCanvasSize / Math.max(1, height * EFFECT_HEIGHT_SCALE)\n );\n}\n"]}
@@ -0,0 +1,78 @@
1
+ import { computeCaptureScale } from './capture-scale';
2
+ import { CANVAS_MAX_PARTICLE_COUNT, CanvasParticleRenderer, } from './canvas-particle-renderer';
3
+ import { limitParticles } from './particles';
4
+ import { WebGlParticleRenderer } from './webgl/webgl-particle-renderer';
5
+ /**
6
+ * Browsers drop the oldest WebGL context once about 16 are alive,
7
+ * so only this many effects run on the GPU at the same time.
8
+ */
9
+ const MAX_GPU_EFFECTS = 12;
10
+ /** counts the effects that hold a WebGL context */
11
+ export class GpuContextBudget {
12
+ constructor(max = MAX_GPU_EFFECTS) {
13
+ this.max = max;
14
+ this.live = 0;
15
+ }
16
+ tryAcquire() {
17
+ if (this.live >= this.max) {
18
+ return false;
19
+ }
20
+ this.live++;
21
+ return true;
22
+ }
23
+ release() {
24
+ this.live = Math.max(0, this.live - 1);
25
+ }
26
+ }
27
+ /**
28
+ * Picks the GPU renderer when WebGL2 is available and the GPU budget allows it,
29
+ * the CPU canvas renderer otherwise. If the GPU renderer fails to set up,
30
+ * it falls back to the canvas renderer.
31
+ */
32
+ export function chooseParticleRenderer({ maxParticleCount, devicePixelRatio, createCanvas, gpuBudget = new GpuContextBudget(), }) {
33
+ const createCanvasRenderer = (particles, params) => new CanvasParticleRenderer(createCanvas(), limitParticles(particles, CANVAS_MAX_PARTICLE_COUNT), params);
34
+ const canvasChoice = {
35
+ maxParticleCount: Math.min(maxParticleCount, CANVAS_MAX_PARTICLE_COUNT),
36
+ captureScale: () => 1,
37
+ create: createCanvasRenderer,
38
+ release: () => undefined,
39
+ };
40
+ if (!gpuBudget.tryAcquire()) {
41
+ return canvasChoice;
42
+ }
43
+ const canvas = createCanvas();
44
+ const gl = WebGlParticleRenderer.createContext(canvas);
45
+ if (gl == null) {
46
+ gpuBudget.release();
47
+ return canvasChoice;
48
+ }
49
+ let released = false;
50
+ const release = () => {
51
+ if (released) {
52
+ return;
53
+ }
54
+ released = true;
55
+ // the renderer may have freed it already
56
+ if (!gl.isContextLost()) {
57
+ gl.getExtension('WEBGL_lose_context')?.loseContext();
58
+ }
59
+ gpuBudget.release();
60
+ };
61
+ const maxCanvasSize = WebGlParticleRenderer.maxCanvasSize(gl);
62
+ return {
63
+ maxParticleCount,
64
+ captureScale: (width, height) => computeCaptureScale({ width, height, devicePixelRatio, maxCanvasSize }),
65
+ create: (particles, params) => {
66
+ try {
67
+ return new WebGlParticleRenderer(canvas, gl, particles, params);
68
+ }
69
+ catch (error) {
70
+ console.warn('ws-thanos: falling back to canvas rendering', error);
71
+ release();
72
+ return createCanvasRenderer(particles, params);
73
+ }
74
+ },
75
+ release,
76
+ };
77
+ }
78
+ //# sourceMappingURL=choose-particle-renderer.js.map
@@ -0,0 +1 @@
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 { 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 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 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 captureScale: () => 1,\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({ width, height, devicePixelRatio, maxCanvasSize }),\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,8 +1,11 @@
1
1
  export function createWsThanosOptions(options) {
2
2
  return {
3
3
  animationLength: 5000,
4
- maxParticleCount: 400000,
4
+ maxParticleCount: 3_000_000,
5
5
  particleAcceleration: 30,
6
+ sound: true,
7
+ soundVolume: 0.5,
8
+ crumble: 'shards',
6
9
  ...options,
7
10
  };
8
11
  }
@@ -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,MAAM;QACxB,oBAAoB,EAAE,EAAE;QACxB,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: 400000,\n particleAcceleration: 30,\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,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"]}
@@ -0,0 +1,24 @@
1
+ const NONE = { coarseCrack: 0, fineCrack: 0, shardGrow: 0 };
2
+ function smoothstep(edge0, edge1, x) {
3
+ const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
4
+ return t * t * (3 - 2 * t);
5
+ }
6
+ /** cracks get wider and finer cracks appear while the element decays */
7
+ export function crumbleShape(crumble, animationT) {
8
+ if (crumble === 'dust') {
9
+ return NONE;
10
+ }
11
+ if (crumble === 'chunks') {
12
+ return {
13
+ coarseCrack: (0.8 + 1.2 * smoothstep(0, 0.4, animationT)) / 2,
14
+ fineCrack: 0,
15
+ shardGrow: 0,
16
+ };
17
+ }
18
+ return {
19
+ coarseCrack: (0.8 + 4 * smoothstep(0, 0.55, animationT)) / 2,
20
+ fineCrack: (2.5 * smoothstep(0.12, 0.6, animationT)) / 2,
21
+ shardGrow: crumble === 'shards' ? smoothstep(0.03, 0.6, animationT) : 0,
22
+ };
23
+ }
24
+ //# sourceMappingURL=crumble-shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crumble-shape.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/crumble-shape.ts"],"names":[],"mappings":"AAWA,MAAM,IAAI,GAAiB,EAAE,WAAW,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;AAE1E,SAAS,UAAU,CAAC,KAAa,EAAE,KAAa,EAAE,CAAS;IACzD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAClE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,YAAY,CAC1B,OAAwB,EACxB,UAAkB;IAElB,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO;YACL,WAAW,EAAE,CAAC,GAAG,GAAG,GAAG,GAAG,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC,GAAG,CAAC;YAC7D,SAAS,EAAE,CAAC;YACZ,SAAS,EAAE,CAAC;SACb,CAAC;IACJ,CAAC;IACD,OAAO;QACL,WAAW,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,GAAG,CAAC;QAC5D,SAAS,EAAE,CAAC,GAAG,GAAG,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC,GAAG,CAAC;QACxD,SAAS,EAAE,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;KACxE,CAAC;AACJ,CAAC","sourcesContent":["import type { WsThanosCrumble } from './ws-thanos.options';\n\nexport interface CrumbleShape {\n /** half width of the coarse cracks in css pixels, particles closer to a crack break off */\n coarseCrack: number;\n /** half width of the fine cracks in css pixels */\n fineCrack: number;\n /** 0 to 1, how far the shards have shifted and tilted */\n shardGrow: number;\n}\n\nconst NONE: CrumbleShape = { coarseCrack: 0, fineCrack: 0, shardGrow: 0 };\n\nfunction smoothstep(edge0: number, edge1: number, x: number): number {\n const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));\n return t * t * (3 - 2 * t);\n}\n\n/** cracks get wider and finer cracks appear while the element decays */\nexport function crumbleShape(\n crumble: WsThanosCrumble,\n animationT: number\n): CrumbleShape {\n if (crumble === 'dust') {\n return NONE;\n }\n if (crumble === 'chunks') {\n return {\n coarseCrack: (0.8 + 1.2 * smoothstep(0, 0.4, animationT)) / 2,\n fineCrack: 0,\n shardGrow: 0,\n };\n }\n return {\n coarseCrack: (0.8 + 4 * smoothstep(0, 0.55, animationT)) / 2,\n fineCrack: (2.5 * smoothstep(0.12, 0.6, animationT)) / 2,\n shardGrow: crumble === 'shards' ? smoothstep(0.03, 0.6, animationT) : 0,\n };\n}\n"]}
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=particle-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"particle-renderer.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/particle-renderer.ts"],"names":[],"mappings":"","sourcesContent":["import { AnimationState } from './animation.state';\nimport { WsThanosCrumble } from './ws-thanos.options';\n\n/** moves and draws the particles of one vaporizing element */\nexport interface ParticleRenderer {\n /** where the simulation runs */\n readonly kind: 'webgl' | 'canvas';\n /** the effect canvas, positioned by the caller */\n readonly canvas: HTMLCanvasElement;\n /** how this renderer breaks the element apart */\n readonly crumble: WsThanosCrumble;\n /** advance the simulation by one frame and draw it */\n render(animationState: AnimationState): void;\n /** free all resources, the canvas is not usable afterwards */\n dispose(): void;\n}\n\nexport interface ParticleRendererParams {\n /** effect canvas size in device pixels */\n width: number;\n height: number;\n particleAcceleration: number;\n /** device pixels per css pixel */\n pixelScale: number;\n crumble: WsThanosCrumble;\n /** random seed that makes every snap look different */\n seed: number;\n}\n"]}
@@ -0,0 +1,84 @@
1
+ /** alpha (0-255) below which a particle counts as invisible */
2
+ export const MIN_PARTICLE_ALPHA = ~~(255 * 0.01);
3
+ /** float layout of one particle in {@link Particles.state} */
4
+ export var ParticleStateIndex;
5
+ (function (ParticleStateIndex) {
6
+ ParticleStateIndex[ParticleStateIndex["X"] = 0] = "X";
7
+ ParticleStateIndex[ParticleStateIndex["Y"] = 1] = "Y";
8
+ ParticleStateIndex[ParticleStateIndex["VX"] = 2] = "VX";
9
+ ParticleStateIndex[ParticleStateIndex["VY"] = 3] = "VY";
10
+ ParticleStateIndex[ParticleStateIndex["AX"] = 4] = "AX";
11
+ ParticleStateIndex[ParticleStateIndex["AY"] = 5] = "AY";
12
+ ParticleStateIndex[ParticleStateIndex["ALPHA"] = 6] = "ALPHA";
13
+ /** animationT when the particle broke off, 0 while attached, negative while a chunk falls */
14
+ ParticleStateIndex[ParticleStateIndex["RELEASED_AT"] = 7] = "RELEASED_AT";
15
+ })(ParticleStateIndex || (ParticleStateIndex = {}));
16
+ export const PARTICLE_STATE_LENGTH = 8;
17
+ /**
18
+ * Creates one particle per visible pixel, randomly sampled down to maxParticleCount.
19
+ * Particles are placed at the bottom of an effect canvas of the given height.
20
+ */
21
+ export function createParticles(imageData, maxParticleCount, effectHeight) {
22
+ const { width, height, data } = imageData;
23
+ const candidates = new Uint32Array(width * height);
24
+ let candidateCount = 0;
25
+ for (let pixel = 0; pixel < width * height; pixel++) {
26
+ if (data[pixel * 4 + 3] >= MIN_PARTICLE_ALPHA) {
27
+ candidates[candidateCount++] = pixel;
28
+ }
29
+ }
30
+ const count = Math.min(candidateCount, maxParticleCount);
31
+ const sampled = count < candidateCount;
32
+ if (sampled) {
33
+ // partial Fisher-Yates: the first count candidates become a random sample
34
+ for (let i = 0; i < count; i++) {
35
+ const j = i + ~~(Math.random() * (candidateCount - i));
36
+ const swap = candidates[i];
37
+ candidates[i] = candidates[j];
38
+ candidates[j] = swap;
39
+ }
40
+ }
41
+ const state = new Float32Array(count * PARTICLE_STATE_LENGTH);
42
+ const colors = new Uint8Array(count * 4);
43
+ const offsetY = effectHeight - height;
44
+ let maxParticleX = 0;
45
+ let minParticleY = effectHeight;
46
+ for (let i = 0; i < count; i++) {
47
+ const pixel = candidates[i];
48
+ const x = pixel % width;
49
+ const y = ~~(pixel / width);
50
+ maxParticleX = Math.max(maxParticleX, x);
51
+ minParticleY = Math.min(minParticleY, y);
52
+ const base = i * PARTICLE_STATE_LENGTH;
53
+ state[base + ParticleStateIndex.X] = x;
54
+ state[base + ParticleStateIndex.Y] = y + offsetY;
55
+ state[base + ParticleStateIndex.ALPHA] = data[pixel * 4 + 3];
56
+ colors[i * 4] = data[pixel * 4];
57
+ colors[i * 4 + 1] = data[pixel * 4 + 1];
58
+ colors[i * 4 + 2] = data[pixel * 4 + 2];
59
+ colors[i * 4 + 3] = data[pixel * 4 + 3];
60
+ }
61
+ return { count, state, colors, maxParticleX, minParticleY, sampled };
62
+ }
63
+ /** a random sample of at most maxCount particles, e.g. when the CPU takes over from the GPU */
64
+ export function limitParticles(particles, maxCount) {
65
+ if (particles.count <= maxCount) {
66
+ return particles;
67
+ }
68
+ const indices = new Uint32Array(particles.count);
69
+ for (let i = 0; i < particles.count; i++) {
70
+ indices[i] = i;
71
+ }
72
+ const state = new Float32Array(maxCount * PARTICLE_STATE_LENGTH);
73
+ const colors = new Uint8Array(maxCount * 4);
74
+ for (let i = 0; i < maxCount; i++) {
75
+ // partial Fisher-Yates over the particle indices
76
+ const j = i + ~~(Math.random() * (particles.count - i));
77
+ const picked = indices[j];
78
+ indices[j] = indices[i];
79
+ state.set(particles.state.subarray(picked * PARTICLE_STATE_LENGTH, (picked + 1) * PARTICLE_STATE_LENGTH), i * PARTICLE_STATE_LENGTH);
80
+ colors.set(particles.colors.subarray(picked * 4, picked * 4 + 4), i * 4);
81
+ }
82
+ return { ...particles, count: maxCount, state, colors, sampled: true };
83
+ }
84
+ //# sourceMappingURL=particles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"particles.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/particles.ts"],"names":[],"mappings":"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","sourcesContent":["/** alpha (0-255) below which a particle counts as invisible */\nexport const MIN_PARTICLE_ALPHA = ~~(255 * 0.01);\n\n/** float layout of one particle in {@link Particles.state} */\nexport enum ParticleStateIndex {\n X = 0,\n Y = 1,\n VX = 2,\n VY = 3,\n AX = 4,\n AY = 5,\n ALPHA = 6,\n /** animationT when the particle broke off, 0 while attached, negative while a chunk falls */\n RELEASED_AT = 7,\n}\n\nexport const PARTICLE_STATE_LENGTH = 8;\n\nexport interface Particles {\n count: number;\n /** position, velocity, acceleration, alpha (0-255) and release time per particle */\n state: Float32Array;\n /** rgba (0-255) per particle */\n colors: Uint8Array;\n /** right most particle x of the captured image, the vaporizing front starts here */\n maxParticleX: number;\n /** top most particle y of the captured image */\n minParticleY: number;\n /** true when only a random sample of the visible pixels became particles */\n sampled: boolean;\n}\n\n/**\n * Creates one particle per visible pixel, randomly sampled down to maxParticleCount.\n * Particles are placed at the bottom of an effect canvas of the given height.\n */\nexport function createParticles(\n imageData: ImageData,\n maxParticleCount: number,\n effectHeight: number\n): Particles {\n const { width, height, data } = imageData;\n\n const candidates = new Uint32Array(width * height);\n let candidateCount = 0;\n for (let pixel = 0; pixel < width * height; pixel++) {\n if (data[pixel * 4 + 3] >= MIN_PARTICLE_ALPHA) {\n candidates[candidateCount++] = pixel;\n }\n }\n\n const count = Math.min(candidateCount, maxParticleCount);\n const sampled = count < candidateCount;\n if (sampled) {\n // partial Fisher-Yates: the first count candidates become a random sample\n for (let i = 0; i < count; i++) {\n const j = i + ~~(Math.random() * (candidateCount - i));\n const swap = candidates[i];\n candidates[i] = candidates[j];\n candidates[j] = swap;\n }\n }\n\n const state = new Float32Array(count * PARTICLE_STATE_LENGTH);\n const colors = new Uint8Array(count * 4);\n const offsetY = effectHeight - height;\n\n let maxParticleX = 0;\n let minParticleY = effectHeight;\n for (let i = 0; i < count; i++) {\n const pixel = candidates[i];\n const x = pixel % width;\n const y = ~~(pixel / width);\n maxParticleX = Math.max(maxParticleX, x);\n minParticleY = Math.min(minParticleY, y);\n\n const base = i * PARTICLE_STATE_LENGTH;\n state[base + ParticleStateIndex.X] = x;\n state[base + ParticleStateIndex.Y] = y + offsetY;\n state[base + ParticleStateIndex.ALPHA] = data[pixel * 4 + 3];\n colors[i * 4] = data[pixel * 4];\n colors[i * 4 + 1] = data[pixel * 4 + 1];\n colors[i * 4 + 2] = data[pixel * 4 + 2];\n colors[i * 4 + 3] = data[pixel * 4 + 3];\n }\n return { count, state, colors, maxParticleX, minParticleY, sampled };\n}\n\n/** a random sample of at most maxCount particles, e.g. when the CPU takes over from the GPU */\nexport function limitParticles(\n particles: Particles,\n maxCount: number\n): Particles {\n if (particles.count <= maxCount) {\n return particles;\n }\n const indices = new Uint32Array(particles.count);\n for (let i = 0; i < particles.count; i++) {\n indices[i] = i;\n }\n const state = new Float32Array(maxCount * PARTICLE_STATE_LENGTH);\n const colors = new Uint8Array(maxCount * 4);\n for (let i = 0; i < maxCount; i++) {\n // partial Fisher-Yates over the particle indices\n const j = i + ~~(Math.random() * (particles.count - i));\n const picked = indices[j];\n indices[j] = indices[i];\n state.set(\n particles.state.subarray(\n picked * PARTICLE_STATE_LENGTH,\n (picked + 1) * PARTICLE_STATE_LENGTH\n ),\n i * PARTICLE_STATE_LENGTH\n );\n colors.set(particles.colors.subarray(picked * 4, picked * 4 + 4), i * 4);\n }\n return { ...particles, count: maxCount, state, colors, sampled: true };\n}\n"]}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Loudness of the snap sound layers over the animation progress (0 to 1).
3
+ * The element breaks with a rumble, then the wind slowly grows stronger and
4
+ * carries crackling sand, both get weaker in the last third and fade away.
5
+ */
6
+ /** part of the animation after which the sound has blown away */
7
+ export const SILENT_AT = 0.85;
8
+ /** part of the animation after which the rumble of the breaking element is over */
9
+ export const RUMBLE_END = 0.33;
10
+ const WIND_PEAK = 0.6;
11
+ function smoothstep(edge0, edge1, x) {
12
+ const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
13
+ return t * t * (3 - 2 * t);
14
+ }
15
+ /** everything fades from the peak of the wind until it is silent */
16
+ function fadeAway(progress, from) {
17
+ return 1 - smoothstep(from, SILENT_AT, progress);
18
+ }
19
+ /** the element breaks: a loud rumble right at the start that dies away within the first third */
20
+ export function rumbleEnvelope(progress) {
21
+ if (progress >= RUMBLE_END) {
22
+ return 0;
23
+ }
24
+ const attack = smoothstep(0, 0.004, progress);
25
+ const decay = Math.exp(-Math.max(0, progress - 0.004) / 0.06);
26
+ return attack * decay * fadeAway(progress, 0.25);
27
+ }
28
+ /** the wind starts quietly and slowly grows stronger, then fades in the last third */
29
+ export function windEnvelope(progress) {
30
+ const fadeIn = smoothstep(0, 0.04, progress);
31
+ const grow = 0.15 + 0.85 * smoothstep(0.02, WIND_PEAK, progress);
32
+ return fadeIn * grow * fadeAway(progress, WIND_PEAK);
33
+ }
34
+ /** sand crackles as the wind picks it up, gets weaker in the last third and fades */
35
+ export function sandEnvelope(progress) {
36
+ return smoothstep(0.06, 0.55, progress) * fadeAway(progress, 0.58);
37
+ }
38
+ //# sourceMappingURL=snap-sound-envelopes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snap-sound-envelopes.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/snap-sound-envelopes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,iEAAiE;AACjE,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC;AAE9B,mFAAmF;AACnF,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC;AAE/B,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB,SAAS,UAAU,CAAC,KAAa,EAAE,KAAa,EAAE,CAAS;IACzD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAClE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED,oEAAoE;AACpE,SAAS,QAAQ,CAAC,QAAgB,EAAE,IAAY;IAC9C,OAAO,CAAC,GAAG,UAAU,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;AACnD,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,IAAI,QAAQ,IAAI,UAAU,EAAE,CAAC;QAC3B,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IAC9D,OAAO,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AACnD,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;IACjE,OAAO,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AACvD,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,OAAO,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AACrE,CAAC","sourcesContent":["/**\n * Loudness of the snap sound layers over the animation progress (0 to 1).\n * The element breaks with a rumble, then the wind slowly grows stronger and\n * carries crackling sand, both get weaker in the last third and fade away.\n */\n\n/** part of the animation after which the sound has blown away */\nexport const SILENT_AT = 0.85;\n\n/** part of the animation after which the rumble of the breaking element is over */\nexport const RUMBLE_END = 0.33;\n\nconst WIND_PEAK = 0.6;\n\nfunction smoothstep(edge0: number, edge1: number, x: number): number {\n const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));\n return t * t * (3 - 2 * t);\n}\n\n/** everything fades from the peak of the wind until it is silent */\nfunction fadeAway(progress: number, from: number): number {\n return 1 - smoothstep(from, SILENT_AT, progress);\n}\n\n/** the element breaks: a loud rumble right at the start that dies away within the first third */\nexport function rumbleEnvelope(progress: number): number {\n if (progress >= RUMBLE_END) {\n return 0;\n }\n const attack = smoothstep(0, 0.004, progress);\n const decay = Math.exp(-Math.max(0, progress - 0.004) / 0.06);\n return attack * decay * fadeAway(progress, 0.25);\n}\n\n/** the wind starts quietly and slowly grows stronger, then fades in the last third */\nexport function windEnvelope(progress: number): number {\n const fadeIn = smoothstep(0, 0.04, progress);\n const grow = 0.15 + 0.85 * smoothstep(0.02, WIND_PEAK, progress);\n return fadeIn * grow * fadeAway(progress, WIND_PEAK);\n}\n\n/** sand crackles as the wind picks it up, gets weaker in the last third and fades */\nexport function sandEnvelope(progress: number): number {\n return smoothstep(0.06, 0.55, progress) * fadeAway(progress, 0.58);\n}\n"]}