@wolsok/thanos 18.3.3 → 18.4.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 +23 -0
- package/esm2022/lib/canvas-particle-renderer.js +31 -24
- package/esm2022/lib/canvas-particle-renderer.js.map +1 -1
- package/esm2022/lib/capture-scale.js +9 -6
- package/esm2022/lib/capture-scale.js.map +1 -1
- package/esm2022/lib/choose-particle-renderer.js +4 -3
- package/esm2022/lib/choose-particle-renderer.js.map +1 -1
- package/esm2022/lib/create-ws-thanos-options.js +2 -0
- package/esm2022/lib/create-ws-thanos-options.js.map +1 -1
- package/esm2022/lib/flow-field.js +58 -0
- package/esm2022/lib/flow-field.js.map +1 -0
- package/esm2022/lib/frame-rate.js +13 -0
- package/esm2022/lib/frame-rate.js.map +1 -0
- package/esm2022/lib/particle-renderer.js.map +1 -1
- package/esm2022/lib/particles.js +20 -10
- package/esm2022/lib/particles.js.map +1 -1
- package/esm2022/lib/simplex-noise.js +8 -4
- package/esm2022/lib/simplex-noise.js.map +1 -1
- package/esm2022/lib/vaporizing-front.js +32 -9
- package/esm2022/lib/vaporizing-front.js.map +1 -1
- package/esm2022/lib/webgl/webgl-particle-renderer.js +67 -9
- package/esm2022/lib/webgl/webgl-particle-renderer.js.map +1 -1
- package/esm2022/lib/webgl/webgl-shaders.js +50 -16
- package/esm2022/lib/webgl/webgl-shaders.js.map +1 -1
- package/esm2022/lib/wind.js +55 -0
- package/esm2022/lib/wind.js.map +1 -0
- package/esm2022/lib/ws-thanos.options.js.map +1 -1
- package/esm2022/lib/ws-thanos.service.js +47 -26
- package/esm2022/lib/ws-thanos.service.js.map +1 -1
- package/lib/canvas-particle-renderer.d.ts +9 -7
- package/lib/capture-scale.d.ts +6 -5
- package/lib/choose-particle-renderer.d.ts +3 -2
- package/lib/flow-field.d.ts +24 -0
- package/lib/frame-rate.d.ts +10 -0
- package/lib/particle-renderer.d.ts +6 -0
- package/lib/particles.d.ts +14 -6
- package/lib/vaporizing-front.d.ts +26 -4
- package/lib/webgl/webgl-particle-renderer.d.ts +3 -2
- package/lib/webgl/webgl-shaders.d.ts +7 -0
- package/lib/wind.d.ts +40 -0
- package/lib/ws-thanos.options.d.ts +6 -0
- package/lib/ws-thanos.service.d.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,6 +59,7 @@ export class SharedModule {}
|
|
|
59
59
|
| sound | boolean | true | play a windy, sandy sound while vaporizing |
|
|
60
60
|
| soundVolume | number | 0.5 | volume of the sound from 0 to 1 |
|
|
61
61
|
| crumble | string | shards | how the element breaks apart, see crumble variants |
|
|
62
|
+
| windDirection | number | 45 | degrees the dust blows to, clockwise from up, see wind direction |
|
|
62
63
|
| preload | boolean | true | warm up the effect while idle, so the first snap won't stutter |
|
|
63
64
|
|
|
64
65
|
#### Sound
|
|
@@ -102,6 +103,11 @@ so the particles get coarser. `maxParticleCount` instead leaves out random pixel
|
|
|
102
103
|
Both renderers follow it.
|
|
103
104
|
The effect canvas has a `data-ws-thanos-renderer` attribute (`webgl` or `canvas`) showing which renderer is in use.
|
|
104
105
|
|
|
106
|
+
#### Frame rate
|
|
107
|
+
|
|
108
|
+
The effect draws one frame for every frame the screen shows (`requestAnimationFrame`), 120 per second on a 120 Hz screen,
|
|
109
|
+
and looks the same at any frame rate. After a stalled frame or a hidden tab the particles carry on instead of jumping away.
|
|
110
|
+
|
|
105
111
|
#### Images
|
|
106
112
|
|
|
107
113
|
html2canvas captures the element, and it stretches every `<img>` over its box.
|
|
@@ -124,6 +130,23 @@ The variants need WebGL2, the canvas fallback always turns into `'dust'`.
|
|
|
124
130
|
The effect canvas shows the variant in use in its `data-ws-thanos-crumble` attribute.
|
|
125
131
|
`WS_THANOS_CRUMBLES` lists all variants, e.g. to pick a random one for every snap.
|
|
126
132
|
|
|
133
|
+
#### Wind direction
|
|
134
|
+
|
|
135
|
+
`windDirection` turns the whole effect: the dust blows that way and the vaporizing front sweeps against it,
|
|
136
|
+
so the element breaks apart on the side the dust blows to first.
|
|
137
|
+
It is an angle in degrees, clockwise from up like a css gradient angle:
|
|
138
|
+
`0` up, `90` right, `180` down, `270` left. The default `45` blows to the top right.
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<div wsThanos [wsThanosOptions]="{ windDirection: 270 }">
|
|
142
|
+
Blows away to the left
|
|
143
|
+
</div>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The effect canvas reaches past the element where the dust flies: up to one element width sideways and four element heights up or down,
|
|
147
|
+
less for the sides the dust only drifts towards.
|
|
148
|
+
For a wind that blows down it reaches below the element, so near the end of the page it can make the page scroll for a moment.
|
|
149
|
+
|
|
127
150
|
#### Options per element
|
|
128
151
|
|
|
129
152
|
Override any option for a single element with the `wsThanosOptions` input:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { MIN_PARTICLE_ALPHA, PARTICLE_STATE_LENGTH, ParticleStateIndex as P, } from './particles';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { FlowField } from './flow-field';
|
|
3
|
+
import { FRAME_RATE, frameFade } from './frame-rate';
|
|
4
|
+
import { isBehindFront, vaporizingFront } from './vaporizing-front';
|
|
4
5
|
/** the CPU can't keep up with more particles than this */
|
|
5
6
|
export const CANVAS_MAX_PARTICLE_COUNT = 400_000;
|
|
6
|
-
const FLOW_FIELD_RES = 0.05;
|
|
7
7
|
/** Fallback renderer that simulates on the CPU and draws with a 2d context. */
|
|
8
8
|
export class CanvasParticleRenderer {
|
|
9
9
|
constructor(canvas, particles, params) {
|
|
@@ -13,27 +13,24 @@ export class CanvasParticleRenderer {
|
|
|
13
13
|
this.kind = 'canvas';
|
|
14
14
|
/** the CPU can't afford to simulate cracks and shards */
|
|
15
15
|
this.crumble = 'dust';
|
|
16
|
-
this.noise = new SimplexNoise({
|
|
17
|
-
frequency: FLOW_FIELD.frequency,
|
|
18
|
-
min: 0,
|
|
19
|
-
});
|
|
20
16
|
canvas.width = params.width;
|
|
21
17
|
canvas.height = params.height;
|
|
22
|
-
const context = canvas.getContext('2d'
|
|
18
|
+
const context = canvas.getContext('2d');
|
|
23
19
|
if (context == null) {
|
|
24
20
|
throw new Error('Could not get 2d context for vaporize canvas');
|
|
25
21
|
}
|
|
26
22
|
this.context = context;
|
|
23
|
+
this.image = context.createImageData(params.width, params.height);
|
|
24
|
+
this.flowField = new FlowField(params.width, params.height, params.seed, params.wind);
|
|
27
25
|
}
|
|
28
26
|
render(animationState) {
|
|
29
27
|
updateParticlesOnCpu({
|
|
30
28
|
particles: this.particles,
|
|
31
29
|
animationState,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
seed: this.params.seed,
|
|
30
|
+
params: this.params,
|
|
31
|
+
flowField: this.flowField,
|
|
35
32
|
});
|
|
36
|
-
drawParticlesOnCpu(this.context, this.particles);
|
|
33
|
+
drawParticlesOnCpu(this.context, this.image, this.particles);
|
|
37
34
|
}
|
|
38
35
|
dispose() {
|
|
39
36
|
this.canvas.remove();
|
|
@@ -48,8 +45,14 @@ function isVisibleInside(state, base, width, height) {
|
|
|
48
45
|
y >= 0 &&
|
|
49
46
|
state[base + P.ALPHA] >= MIN_PARTICLE_ALPHA);
|
|
50
47
|
}
|
|
51
|
-
export function updateParticlesOnCpu({ particles: { state, count
|
|
52
|
-
const front = vaporizingFront(animationT,
|
|
48
|
+
export function updateParticlesOnCpu({ particles: { state, count }, animationState: { deltaTSec, animationT, maxWidth, maxHeight }, params: { particleAcceleration, seed, wind, front: frontGeometry }, flowField, }) {
|
|
49
|
+
const front = vaporizingFront(animationT, frontGeometry);
|
|
50
|
+
const fade = frameFade(front.fade, deltaTSec);
|
|
51
|
+
// the flow field pushes as much per second at any frame rate
|
|
52
|
+
const flowPerFrame = deltaTSec * FRAME_RATE;
|
|
53
|
+
const flow = new Float32Array(2);
|
|
54
|
+
// the release push is written for the default wind and turned into this one
|
|
55
|
+
const { cos, sin } = wind;
|
|
53
56
|
for (let i = 0; i < count; i++) {
|
|
54
57
|
const base = i * PARTICLE_STATE_LENGTH;
|
|
55
58
|
// only update particles that are inside view and visible
|
|
@@ -59,29 +62,33 @@ export function updateParticlesOnCpu({ particles: { state, count, maxParticleX,
|
|
|
59
62
|
const particleX = state[base + P.X];
|
|
60
63
|
const particleY = state[base + P.Y];
|
|
61
64
|
if (state[base + P.RELEASED_AT] === 0) {
|
|
62
|
-
if (isBehindFront(particleX, particleY,
|
|
65
|
+
if (isBehindFront(particleX, particleY, deltaTSec, seed, front)) {
|
|
63
66
|
state[base + P.RELEASED_AT] = Math.max(animationT, 0.0001);
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
const ax = Math.random();
|
|
68
|
+
const ay = -Math.random();
|
|
69
|
+
state[base + P.AX] = cos * ax - sin * ay;
|
|
70
|
+
state[base + P.AY] = sin * ax + cos * ay;
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
else {
|
|
69
74
|
// flow along the noise velocity field
|
|
70
|
-
|
|
71
|
-
state[base + P.
|
|
75
|
+
flowField.sample(particleX, particleY, flow);
|
|
76
|
+
state[base + P.AX] += flow[0] * flowPerFrame;
|
|
77
|
+
state[base + P.AY] += flow[1] * flowPerFrame;
|
|
72
78
|
}
|
|
73
79
|
state[base + P.VX] += state[base + P.AX] * particleAcceleration * deltaTSec;
|
|
74
80
|
state[base + P.VY] += state[base + P.AY] * particleAcceleration * deltaTSec;
|
|
75
81
|
state[base + P.X] += state[base + P.VX] * deltaTSec;
|
|
76
82
|
state[base + P.Y] += state[base + P.VY] * deltaTSec;
|
|
77
|
-
state[base + P.ALPHA] *=
|
|
83
|
+
state[base + P.ALPHA] *= fade;
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const image = context.getImageData(0, 0, width, height);
|
|
86
|
+
/** paints the particles into image and puts it on the canvas, image has the canvas size */
|
|
87
|
+
export function drawParticlesOnCpu(context, image, { state, colors, count }) {
|
|
88
|
+
const { width, height } = image;
|
|
84
89
|
const pixels = image.data;
|
|
90
|
+
// putImageData replaces every pixel, so clearing the image clears the canvas
|
|
91
|
+
pixels.fill(0);
|
|
85
92
|
for (let i = 0; i < count; i++) {
|
|
86
93
|
const base = i * PARTICLE_STATE_LENGTH;
|
|
87
94
|
if (!isVisibleInside(state, base, width, height)) {
|
|
@@ -1 +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"]}
|
|
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 { FlowField } from './flow-field';\nimport { FRAME_RATE, frameFade } from './frame-rate';\nimport { 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\nexport interface UpdateParticlesParams {\n particles: Particles;\n animationState: AnimationState;\n params: Pick<\n ParticleRendererParams,\n 'particleAcceleration' | 'seed' | 'wind' | 'front'\n >;\n flowField: FlowField;\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 flowField: FlowField;\n private readonly context: CanvasRenderingContext2D;\n /** drawn into and put on the canvas every frame */\n private readonly image: ImageData;\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');\n if (context == null) {\n throw new Error('Could not get 2d context for vaporize canvas');\n }\n this.context = context;\n this.image = context.createImageData(params.width, params.height);\n this.flowField = new FlowField(\n params.width,\n params.height,\n params.seed,\n params.wind\n );\n }\n\n public render(animationState: AnimationState): void {\n updateParticlesOnCpu({\n particles: this.particles,\n animationState,\n params: this.params,\n flowField: this.flowField,\n });\n drawParticlesOnCpu(this.context, this.image, 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 },\n animationState: { deltaTSec, animationT, maxWidth, maxHeight },\n params: { particleAcceleration, seed, wind, front: frontGeometry },\n flowField,\n}: UpdateParticlesParams): void {\n const front = vaporizingFront(animationT, frontGeometry);\n const fade = frameFade(front.fade, deltaTSec);\n // the flow field pushes as much per second at any frame rate\n const flowPerFrame = deltaTSec * FRAME_RATE;\n const flow = new Float32Array(2);\n // the release push is written for the default wind and turned into this one\n const { cos, sin } = wind;\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 (isBehindFront(particleX, particleY, deltaTSec, seed, front)) {\n state[base + P.RELEASED_AT] = Math.max(animationT, 0.0001);\n const ax = Math.random();\n const ay = -Math.random();\n state[base + P.AX] = cos * ax - sin * ay;\n state[base + P.AY] = sin * ax + cos * ay;\n }\n } else {\n // flow along the noise velocity field\n flowField.sample(particleX, particleY, flow);\n state[base + P.AX] += flow[0] * flowPerFrame;\n state[base + P.AY] += flow[1] * flowPerFrame;\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] *= fade;\n }\n}\n\n/** paints the particles into image and puts it on the canvas, image has the canvas size */\nexport function drawParticlesOnCpu(\n context: CanvasRenderingContext2D,\n image: ImageData,\n { state, colors, count }: Particles\n): void {\n const { width, height } = image;\n const pixels = image.data;\n // putImageData replaces every pixel, so clearing the image clears the canvas\n pixels.fill(0);\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"]}
|
|
@@ -1,7 +1,6 @@
|
|
|
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
1
|
const MAX_DEVICE_PIXEL_RATIO = 2;
|
|
2
|
+
/** the largest canvas area Safari allows, the smallest limit of the common browsers */
|
|
3
|
+
const MAX_CANVAS_PIXELS = 16_777_216;
|
|
5
4
|
/**
|
|
6
5
|
* Scale that captures an element of the given css size with at most maxCapturePixels pixels,
|
|
7
6
|
* Infinity without a limit: a missing, zero, negative or NaN one.
|
|
@@ -11,13 +10,17 @@ export function capturePixelsScale(width, height, maxCapturePixels) {
|
|
|
11
10
|
? Math.sqrt(maxCapturePixels / Math.max(1, width * height))
|
|
12
11
|
: Infinity;
|
|
13
12
|
}
|
|
13
|
+
/** scale that keeps the effect canvas around an element of the given css size a canvas browsers create */
|
|
14
|
+
export function effectCanvasScale(width, height, effectScale) {
|
|
15
|
+
return capturePixelsScale(width * effectScale.width, height * effectScale.height, MAX_CANVAS_PIXELS);
|
|
16
|
+
}
|
|
14
17
|
/**
|
|
15
18
|
* Scale used to capture the element: device pixels for crisp particles,
|
|
16
|
-
* 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 and area
|
|
17
20
|
* and the element stays within maxCapturePixels, below css pixels if needed.
|
|
18
21
|
*/
|
|
19
|
-
export function computeCaptureScale({ width, height, devicePixelRatio, maxCanvasSize, maxCapturePixels, }) {
|
|
22
|
+
export function computeCaptureScale({ width, height, effectScale, devicePixelRatio, maxCanvasSize, maxCapturePixels, }) {
|
|
20
23
|
const pixelRatio = devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
21
|
-
return Math.min(pixelRatio, MAX_DEVICE_PIXEL_RATIO, capturePixelsScale(width, height, maxCapturePixels), maxCanvasSize / Math.max(1, width *
|
|
24
|
+
return Math.min(pixelRatio, MAX_DEVICE_PIXEL_RATIO, capturePixelsScale(width, height, maxCapturePixels), effectCanvasScale(width, height, effectScale), maxCanvasSize / Math.max(1, width * effectScale.width), maxCanvasSize / Math.max(1, height * effectScale.height));
|
|
22
25
|
}
|
|
23
26
|
//# 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":"
|
|
1
|
+
{"version":3,"file":"capture-scale.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/capture-scale.ts"],"names":[],"mappings":"AAEA,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAEjC,uFAAuF;AACvF,MAAM,iBAAiB,GAAG,UAAU,CAAC;AAYrC;;;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,0GAA0G;AAC1G,MAAM,UAAU,iBAAiB,CAC/B,KAAa,EACb,MAAc,EACd,WAAwB;IAExB,OAAO,kBAAkB,CACvB,KAAK,GAAG,WAAW,CAAC,KAAK,EACzB,MAAM,GAAG,WAAW,CAAC,MAAM,EAC3B,iBAAiB,CAClB,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAClC,KAAK,EACL,MAAM,EACN,WAAW,EACX,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,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAC7C,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,EACtD,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CACzD,CAAC;AACJ,CAAC","sourcesContent":["import type { EffectScale } from './wind';\n\nconst MAX_DEVICE_PIXEL_RATIO = 2;\n\n/** the largest canvas area Safari allows, the smallest limit of the common browsers */\nconst MAX_CANVAS_PIXELS = 16_777_216;\n\nexport interface CaptureScaleParams {\n width: number;\n height: number;\n effectScale: EffectScale;\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/** scale that keeps the effect canvas around an element of the given css size a canvas browsers create */\nexport function effectCanvasScale(\n width: number,\n height: number,\n effectScale: EffectScale\n): number {\n return capturePixelsScale(\n width * effectScale.width,\n height * effectScale.height,\n MAX_CANVAS_PIXELS\n );\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 and area\n * and the element stays within maxCapturePixels, below css pixels if needed.\n */\nexport function computeCaptureScale({\n width,\n height,\n effectScale,\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 effectCanvasScale(width, height, effectScale),\n maxCanvasSize / Math.max(1, width * effectScale.width),\n maxCanvasSize / Math.max(1, height * effectScale.height)\n );\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { capturePixelsScale, computeCaptureScale } from './capture-scale';
|
|
1
|
+
import { capturePixelsScale, computeCaptureScale, effectCanvasScale, } 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';
|
|
@@ -34,7 +34,7 @@ export function chooseParticleRenderer({ maxParticleCount, devicePixelRatio, max
|
|
|
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
|
+
captureScale: (width, height, effectScale) => Math.min(1, capturePixelsScale(width, height, maxCapturePixels), effectCanvasScale(width, height, effectScale)),
|
|
38
38
|
create: createCanvasRenderer,
|
|
39
39
|
release: () => undefined,
|
|
40
40
|
};
|
|
@@ -62,9 +62,10 @@ export function chooseParticleRenderer({ maxParticleCount, devicePixelRatio, max
|
|
|
62
62
|
const maxCanvasSize = WebGlParticleRenderer.maxCanvasSize(gl);
|
|
63
63
|
return {
|
|
64
64
|
maxParticleCount,
|
|
65
|
-
captureScale: (width, height) => computeCaptureScale({
|
|
65
|
+
captureScale: (width, height, effectScale) => computeCaptureScale({
|
|
66
66
|
width,
|
|
67
67
|
height,
|
|
68
|
+
effectScale,
|
|
68
69
|
devicePixelRatio,
|
|
69
70
|
maxCanvasSize,
|
|
70
71
|
maxCapturePixels,
|
|
@@ -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,
|
|
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 {\n capturePixelsScale,\n computeCaptureScale,\n effectCanvasScale,\n} 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';\nimport type { EffectScale } from './wind';\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, see computeCaptureScale() */\n captureScale(width: number, height: number, effectScale: EffectScale): 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, effectScale) =>\n Math.min(\n 1,\n capturePixelsScale(width, height, maxCapturePixels),\n effectCanvasScale(width, height, effectScale)\n ),\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, effectScale) =>\n computeCaptureScale({\n width,\n height,\n effectScale,\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,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_WIND_DIRECTION } from './wind';
|
|
1
2
|
export function createWsThanosOptions(options) {
|
|
2
3
|
return {
|
|
3
4
|
animationLength: 5000,
|
|
@@ -6,6 +7,7 @@ export function createWsThanosOptions(options) {
|
|
|
6
7
|
sound: true,
|
|
7
8
|
soundVolume: 0.5,
|
|
8
9
|
crumble: 'shards',
|
|
10
|
+
windDirection: DEFAULT_WIND_DIRECTION,
|
|
9
11
|
preload: true,
|
|
10
12
|
...options,
|
|
11
13
|
};
|
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"create-ws-thanos-options.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/create-ws-thanos-options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAC;AAGhD,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,aAAa,EAAE,sBAAsB;QACrC,OAAO,EAAE,IAAI;QACb,GAAG,OAAO;KACX,CAAC;AACJ,CAAC","sourcesContent":["import { DEFAULT_WIND_DIRECTION } from './wind';\nimport { 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 windDirection: DEFAULT_WIND_DIRECTION,\n preload: true,\n ...options,\n };\n}\n"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { SimplexNoise } from './simplex-noise';
|
|
2
|
+
import { FLOW_FIELD } from './vaporizing-front';
|
|
3
|
+
import { windOf } from './wind';
|
|
4
|
+
/**
|
|
5
|
+
* Pixels between two samples of the flow field. The noise changes over about a hundred pixels,
|
|
6
|
+
* so interpolating between samples looks the same as computing it for every particle.
|
|
7
|
+
*/
|
|
8
|
+
export const FLOW_FIELD_CELL = 8;
|
|
9
|
+
/**
|
|
10
|
+
* The noise field released particles flow along, sampled once on a grid over the effect canvas:
|
|
11
|
+
* it only depends on the position, and looking it up is much cheaper than computing noise
|
|
12
|
+
* for every particle in every frame. Each grid point holds the push a particle gets
|
|
13
|
+
* in a frame at 60 Hz, already turned into the wind.
|
|
14
|
+
*/
|
|
15
|
+
export class FlowField {
|
|
16
|
+
/** @param noise only its raw3D() is used, frequency and range come from FLOW_FIELD */
|
|
17
|
+
constructor(width, height, seed, { cos, sin } = windOf(), cell = FLOW_FIELD_CELL, noise = new SimplexNoise()) {
|
|
18
|
+
this.columns = Math.ceil(width / cell) + 1;
|
|
19
|
+
this.rows = Math.ceil(height / cell) + 1;
|
|
20
|
+
this.cellsPerPixel = 1 / cell;
|
|
21
|
+
this.values = new Float32Array(this.columns * this.rows * 2);
|
|
22
|
+
const { frequency, xSeedOffset, ySeedDivisor } = FLOW_FIELD;
|
|
23
|
+
const xZ = (seed + xSeedOffset) * frequency;
|
|
24
|
+
const yY = (seed / ySeedDivisor) * frequency;
|
|
25
|
+
for (let row = 0; row < this.rows; row++) {
|
|
26
|
+
for (let column = 0; column < this.columns; column++) {
|
|
27
|
+
const x = column * cell * frequency;
|
|
28
|
+
const y = row * cell * frequency;
|
|
29
|
+
// the default wind pushes right and up, both noise mapped to [0, 1]
|
|
30
|
+
const pushX = noise.raw3D(x, y, xZ) * 0.5 + 0.5;
|
|
31
|
+
const pushY = -(noise.raw3D(x, yY, y) * 0.5 + 0.5);
|
|
32
|
+
const i = (row * this.columns + column) * 2;
|
|
33
|
+
this.values[i] = cos * pushX - sin * pushY;
|
|
34
|
+
this.values[i + 1] = sin * pushX + cos * pushY;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** writes the push at x, y into out, interpolated between the grid points */
|
|
39
|
+
sample(x, y, out) {
|
|
40
|
+
const { columns, rows, values, cellsPerPixel } = this;
|
|
41
|
+
const gridX = Math.min(Math.max(x * cellsPerPixel, 0), columns - 1.001);
|
|
42
|
+
const gridY = Math.min(Math.max(y * cellsPerPixel, 0), rows - 1.001);
|
|
43
|
+
const column = ~~gridX;
|
|
44
|
+
const row = ~~gridY;
|
|
45
|
+
const fx = gridX - column;
|
|
46
|
+
const fy = gridY - row;
|
|
47
|
+
const topLeft = (row * columns + column) * 2;
|
|
48
|
+
const bottomLeft = topLeft + columns * 2;
|
|
49
|
+
const topX = values[topLeft] + (values[topLeft + 2] - values[topLeft]) * fx;
|
|
50
|
+
const topY = values[topLeft + 1] + (values[topLeft + 3] - values[topLeft + 1]) * fx;
|
|
51
|
+
const bottomX = values[bottomLeft] + (values[bottomLeft + 2] - values[bottomLeft]) * fx;
|
|
52
|
+
const bottomY = values[bottomLeft + 1] +
|
|
53
|
+
(values[bottomLeft + 3] - values[bottomLeft + 1]) * fx;
|
|
54
|
+
out[0] = topX + (bottomX - topX) * fy;
|
|
55
|
+
out[1] = topY + (bottomY - topY) * fy;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=flow-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow-field.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/flow-field.ts"],"names":[],"mappings":"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","sourcesContent":["import { SimplexNoise } from './simplex-noise';\nimport { FLOW_FIELD } from './vaporizing-front';\nimport { Wind, windOf } from './wind';\n\n/**\n * Pixels between two samples of the flow field. The noise changes over about a hundred pixels,\n * so interpolating between samples looks the same as computing it for every particle.\n */\nexport const FLOW_FIELD_CELL = 8;\n\n/**\n * The noise field released particles flow along, sampled once on a grid over the effect canvas:\n * it only depends on the position, and looking it up is much cheaper than computing noise\n * for every particle in every frame. Each grid point holds the push a particle gets\n * in a frame at 60 Hz, already turned into the wind.\n */\nexport class FlowField {\n private readonly columns: number;\n private readonly rows: number;\n private readonly cellsPerPixel: number;\n /** x and y of the push per grid point */\n private readonly values: Float32Array;\n\n /** @param noise only its raw3D() is used, frequency and range come from FLOW_FIELD */\n public constructor(\n width: number,\n height: number,\n seed: number,\n { cos, sin }: Wind = windOf(),\n cell = FLOW_FIELD_CELL,\n noise = new SimplexNoise()\n ) {\n this.columns = Math.ceil(width / cell) + 1;\n this.rows = Math.ceil(height / cell) + 1;\n this.cellsPerPixel = 1 / cell;\n this.values = new Float32Array(this.columns * this.rows * 2);\n const { frequency, xSeedOffset, ySeedDivisor } = FLOW_FIELD;\n const xZ = (seed + xSeedOffset) * frequency;\n const yY = (seed / ySeedDivisor) * frequency;\n for (let row = 0; row < this.rows; row++) {\n for (let column = 0; column < this.columns; column++) {\n const x = column * cell * frequency;\n const y = row * cell * frequency;\n // the default wind pushes right and up, both noise mapped to [0, 1]\n const pushX = noise.raw3D(x, y, xZ) * 0.5 + 0.5;\n const pushY = -(noise.raw3D(x, yY, y) * 0.5 + 0.5);\n const i = (row * this.columns + column) * 2;\n this.values[i] = cos * pushX - sin * pushY;\n this.values[i + 1] = sin * pushX + cos * pushY;\n }\n }\n }\n\n /** writes the push at x, y into out, interpolated between the grid points */\n public sample(x: number, y: number, out: Float32Array): void {\n const { columns, rows, values, cellsPerPixel } = this;\n const gridX = Math.min(Math.max(x * cellsPerPixel, 0), columns - 1.001);\n const gridY = Math.min(Math.max(y * cellsPerPixel, 0), rows - 1.001);\n const column = ~~gridX;\n const row = ~~gridY;\n const fx = gridX - column;\n const fy = gridY - row;\n const topLeft = (row * columns + column) * 2;\n const bottomLeft = topLeft + columns * 2;\n const topX = values[topLeft] + (values[topLeft + 2] - values[topLeft]) * fx;\n const topY =\n values[topLeft + 1] + (values[topLeft + 3] - values[topLeft + 1]) * fx;\n const bottomX =\n values[bottomLeft] + (values[bottomLeft + 2] - values[bottomLeft]) * fx;\n const bottomY =\n values[bottomLeft + 1] +\n (values[bottomLeft + 3] - values[bottomLeft + 1]) * fx;\n out[0] = topX + (bottomX - topX) * fy;\n out[1] = topY + (bottomY - topY) * fy;\n }\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The simulation was tuned at this frame rate. Whatever is applied once per frame
|
|
3
|
+
* is scaled by the frame's length relative to it, so the effect looks the same on a 120 Hz screen.
|
|
4
|
+
*/
|
|
5
|
+
export const FRAME_RATE = 60;
|
|
6
|
+
/**
|
|
7
|
+
* the fade for one frame of the given length, fade is the fade for a frame at FRAME_RATE.
|
|
8
|
+
* A fade below 0, past the end of the animation, fades out completely.
|
|
9
|
+
*/
|
|
10
|
+
export function frameFade(fade, deltaTSec) {
|
|
11
|
+
return Math.pow(Math.max(0, fade), deltaTSec * FRAME_RATE);
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=frame-rate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-rate.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/frame-rate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,CAAC;AAE7B;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,SAAiB;IACvD,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC,CAAC;AAC7D,CAAC","sourcesContent":["/**\n * The simulation was tuned at this frame rate. Whatever is applied once per frame\n * is scaled by the frame's length relative to it, so the effect looks the same on a 120 Hz screen.\n */\nexport const FRAME_RATE = 60;\n\n/**\n * the fade for one frame of the given length, fade is the fade for a frame at FRAME_RATE.\n * A fade below 0, past the end of the animation, fades out completely.\n */\nexport function frameFade(fade: number, deltaTSec: number): number {\n return Math.pow(Math.max(0, fade), deltaTSec * FRAME_RATE);\n}\n"]}
|
|
@@ -1 +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"]}
|
|
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 { FrontGeometry } from './vaporizing-front';\nimport { Wind } from './wind';\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 /** where the dust blows to */\n wind: Wind;\n /** where the vaporizing front starts and ends */\n front: FrontGeometry;\n}\n"]}
|
package/esm2022/lib/particles.js
CHANGED
|
@@ -16,9 +16,9 @@ export var ParticleStateIndex;
|
|
|
16
16
|
export const PARTICLE_STATE_LENGTH = 8;
|
|
17
17
|
/**
|
|
18
18
|
* Creates one particle per visible pixel, randomly sampled down to maxParticleCount.
|
|
19
|
-
*
|
|
19
|
+
* The element's top left corner sits at offset in the effect canvas.
|
|
20
20
|
*/
|
|
21
|
-
export function createParticles(imageData, maxParticleCount,
|
|
21
|
+
export function createParticles(imageData, maxParticleCount, offset) {
|
|
22
22
|
const { width, height, data } = imageData;
|
|
23
23
|
const candidates = new Uint32Array(width * height);
|
|
24
24
|
let candidateCount = 0;
|
|
@@ -40,25 +40,35 @@ export function createParticles(imageData, maxParticleCount, effectHeight) {
|
|
|
40
40
|
}
|
|
41
41
|
const state = new Float32Array(count * PARTICLE_STATE_LENGTH);
|
|
42
42
|
const colors = new Uint8Array(count * 4);
|
|
43
|
-
|
|
44
|
-
let
|
|
45
|
-
let
|
|
43
|
+
let left = width;
|
|
44
|
+
let top = height;
|
|
45
|
+
let right = 0;
|
|
46
|
+
let bottom = 0;
|
|
46
47
|
for (let i = 0; i < count; i++) {
|
|
47
48
|
const pixel = candidates[i];
|
|
48
49
|
const x = pixel % width;
|
|
49
50
|
const y = ~~(pixel / width);
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
left = Math.min(left, x);
|
|
52
|
+
top = Math.min(top, y);
|
|
53
|
+
right = Math.max(right, x);
|
|
54
|
+
bottom = Math.max(bottom, y);
|
|
52
55
|
const base = i * PARTICLE_STATE_LENGTH;
|
|
53
|
-
state[base + ParticleStateIndex.X] = x;
|
|
54
|
-
state[base + ParticleStateIndex.Y] = y +
|
|
56
|
+
state[base + ParticleStateIndex.X] = x + offset.x;
|
|
57
|
+
state[base + ParticleStateIndex.Y] = y + offset.y;
|
|
55
58
|
state[base + ParticleStateIndex.ALPHA] = data[pixel * 4 + 3];
|
|
56
59
|
colors[i * 4] = data[pixel * 4];
|
|
57
60
|
colors[i * 4 + 1] = data[pixel * 4 + 1];
|
|
58
61
|
colors[i * 4 + 2] = data[pixel * 4 + 2];
|
|
59
62
|
colors[i * 4 + 3] = data[pixel * 4 + 3];
|
|
60
63
|
}
|
|
61
|
-
|
|
64
|
+
// without particles left > right and top > bottom, there is nothing to sweep
|
|
65
|
+
const bounds = {
|
|
66
|
+
left: left + offset.x,
|
|
67
|
+
top: top + offset.y,
|
|
68
|
+
right: right + offset.x,
|
|
69
|
+
bottom: bottom + offset.y,
|
|
70
|
+
};
|
|
71
|
+
return { count, state, colors, bounds, sampled };
|
|
62
72
|
}
|
|
63
73
|
/** a random sample of at most maxCount particles, e.g. when the CPU takes over from the GPU */
|
|
64
74
|
export function limitParticles(particles, maxCount) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"particles.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/particles.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;AAEjD,8DAA8D;AAC9D,MAAM,CAAN,IAAY,kBAUX;AAVD,WAAY,kBAAkB;IAC5B,qDAAK,CAAA;IACL,qDAAK,CAAA;IACL,uDAAM,CAAA;IACN,uDAAM,CAAA;IACN,uDAAM,CAAA;IACN,uDAAM,CAAA;IACN,6DAAS,CAAA;IACT,6FAA6F;IAC7F,yEAAe,CAAA;AACjB,CAAC,EAVW,kBAAkB,KAAlB,kBAAkB,QAU7B;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;
|
|
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\n/** box around the visible particles in effect canvas pixels, edges included */\nexport interface ParticleBounds {\n left: number;\n top: number;\n right: number;\n bottom: number;\n}\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 /** where the particles are at the start, the vaporizing front sweeps through this box */\n bounds: ParticleBounds;\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 * The element's top left corner sits at offset in the effect canvas.\n */\nexport function createParticles(\n imageData: ImageData,\n maxParticleCount: number,\n offset: { x: number; y: 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\n let left = width;\n let top = height;\n let right = 0;\n let bottom = 0;\n for (let i = 0; i < count; i++) {\n const pixel = candidates[i];\n const x = pixel % width;\n const y = ~~(pixel / width);\n left = Math.min(left, x);\n top = Math.min(top, y);\n right = Math.max(right, x);\n bottom = Math.max(bottom, y);\n\n const base = i * PARTICLE_STATE_LENGTH;\n state[base + ParticleStateIndex.X] = x + offset.x;\n state[base + ParticleStateIndex.Y] = y + offset.y;\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 // without particles left > right and top > bottom, there is nothing to sweep\n const bounds = {\n left: left + offset.x,\n top: top + offset.y,\n right: right + offset.x,\n bottom: bottom + offset.y,\n };\n return { count, state, colors, bounds, 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"]}
|
|
@@ -115,19 +115,19 @@ export class SimplexNoise {
|
|
|
115
115
|
const t0 = 0.5 - x0 * x0 - y0 * y0 - z0 * z0;
|
|
116
116
|
const n0 = t0 < 0
|
|
117
117
|
? 0.0
|
|
118
|
-
: Math.pow(t0, 4) *
|
|
118
|
+
: Math.pow(t0, 4) * dot3(SimplexNoise.GRAD3D[gi0], x0, y0, z0);
|
|
119
119
|
const t1 = 0.5 - x1 * x1 - y1 * y1 - z1 * z1;
|
|
120
120
|
const n1 = t1 < 0
|
|
121
121
|
? 0.0
|
|
122
|
-
: Math.pow(t1, 4) *
|
|
122
|
+
: Math.pow(t1, 4) * dot3(SimplexNoise.GRAD3D[gi1], x1, y1, z1);
|
|
123
123
|
const t2 = 0.5 - x2 * x2 - y2 * y2 - z2 * z2;
|
|
124
124
|
const n2 = t2 < 0
|
|
125
125
|
? 0.0
|
|
126
|
-
: Math.pow(t2, 4) *
|
|
126
|
+
: Math.pow(t2, 4) * dot3(SimplexNoise.GRAD3D[gi2], x2, y2, z2);
|
|
127
127
|
const t3 = 0.5 - x3 * x3 - y3 * y3 - z3 * z3;
|
|
128
128
|
const n3 = t3 < 0
|
|
129
129
|
? 0.0
|
|
130
|
-
: Math.pow(t3, 4) *
|
|
130
|
+
: Math.pow(t3, 4) * dot3(SimplexNoise.GRAD3D[gi3], x3, y3, z3);
|
|
131
131
|
// Add contributions from each corner to get the final noise value.
|
|
132
132
|
// The result is scaled to stay just inside [-1,1]
|
|
133
133
|
return 94.68493150681972 * (n0 + n1 + n2 + n3);
|
|
@@ -154,6 +154,10 @@ export class SimplexNoise {
|
|
|
154
154
|
return result;
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
/** like dot() without allocating, raw3D runs many times in a row */
|
|
158
|
+
function dot3(g, x, y, z) {
|
|
159
|
+
return g[0] * x + g[1] * y + g[2] * z;
|
|
160
|
+
}
|
|
157
161
|
function createOptionsWithDefaults(options) {
|
|
158
162
|
if (options != null) {
|
|
159
163
|
if (options['amplitude'] && typeof options.amplitude !== 'number') {
|