@gigo-ui/components 1.0.0-release → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +158 -132
  2. package/dist/components/chaos/BlackHoleSink.svelte +81 -0
  3. package/dist/components/chaos/BlackHoleSink.svelte.d.ts +16 -0
  4. package/dist/components/chaos/GigoCompactor.svelte +103 -0
  5. package/dist/components/chaos/GigoCompactor.svelte.d.ts +16 -0
  6. package/dist/components/chaos/PixelDissolve.svelte +81 -0
  7. package/dist/components/chaos/PixelDissolve.svelte.d.ts +16 -0
  8. package/dist/components/chaos/ShatterPane.svelte +84 -0
  9. package/dist/components/chaos/ShatterPane.svelte.d.ts +16 -0
  10. package/dist/components/chaos/internal/BlackHoleEngine.svelte +167 -0
  11. package/dist/components/chaos/internal/BlackHoleEngine.svelte.d.ts +11 -0
  12. package/dist/components/chaos/internal/CompactorEngine.svelte +195 -0
  13. package/dist/components/chaos/internal/CompactorEngine.svelte.d.ts +11 -0
  14. package/dist/components/chaos/internal/PixelDissolveEngine.svelte +168 -0
  15. package/dist/components/chaos/internal/PixelDissolveEngine.svelte.d.ts +11 -0
  16. package/dist/components/chaos/internal/ShatterEngine.svelte +208 -0
  17. package/dist/components/chaos/internal/ShatterEngine.svelte.d.ts +11 -0
  18. package/dist/docs/component-data.js +8 -0
  19. package/dist/docs/components/chaos/black-hole-sink.d.ts +2 -0
  20. package/dist/docs/components/chaos/black-hole-sink.js +71 -0
  21. package/dist/docs/components/chaos/gigo-compactor.d.ts +2 -0
  22. package/dist/docs/components/chaos/gigo-compactor.js +71 -0
  23. package/dist/docs/components/chaos/pixel-dissolve.d.ts +2 -0
  24. package/dist/docs/components/chaos/pixel-dissolve.js +71 -0
  25. package/dist/docs/components/chaos/shatter-pane.d.ts +2 -0
  26. package/dist/docs/components/chaos/shatter-pane.js +71 -0
  27. package/dist/index.d.ts +5 -1
  28. package/dist/index.js +4 -0
  29. package/dist/styles/globals.css +3 -0
  30. package/dist/types/index.d.ts +32 -0
  31. package/dist/utils/destruction-engine.d.ts +26 -0
  32. package/dist/utils/destruction-engine.js +75 -0
  33. package/package.json +73 -66
@@ -0,0 +1,84 @@
1
+ <!-- Glass shatter wrapper — Voronoi fracture engine -->
2
+ <script lang="ts">
3
+ import { onDestroy } from 'svelte';
4
+ import type { Snippet } from 'svelte';
5
+ import { captureSnapshot } from '../../utils/destruction-engine.js';
6
+
7
+ export interface ShatterPaneProps {
8
+ isShattered?: boolean;
9
+ shardCount?: number;
10
+ intensity?: number;
11
+ tint?: string;
12
+ debugMode?: boolean;
13
+ class?: string;
14
+ children: Snippet;
15
+ }
16
+
17
+ let {
18
+ isShattered = $bindable(false),
19
+ shardCount = 48,
20
+ intensity = 7,
21
+ tint = 'rgba(180,220,255,0.08)',
22
+ debugMode = false,
23
+ class: className = '',
24
+ children,
25
+ }: ShatterPaneProps = $props();
26
+
27
+ let wrapper: HTMLDivElement | undefined = $state();
28
+ let snapshotData: string | null = $state(null);
29
+ let snapshotRect: DOMRect | null = $state(null);
30
+ let isCapturing = $state(false);
31
+ let EngineComponent: any = $state(null);
32
+ let hidden = $state(false);
33
+
34
+ $effect(() => {
35
+ if (isShattered && !snapshotData && !isCapturing && wrapper) {
36
+ doCapture();
37
+ }
38
+ });
39
+
40
+ async function doCapture() {
41
+ if (!wrapper || isCapturing) return;
42
+ isCapturing = true;
43
+ try {
44
+ const result = await captureSnapshot(wrapper);
45
+ snapshotRect = result.rect;
46
+ snapshotData = result.dataURL;
47
+ hidden = true;
48
+ const mod = await import('./internal/ShatterEngine.svelte');
49
+ EngineComponent = mod.default;
50
+ } catch (err) {
51
+ if (debugMode) console.error('[ShatterPane] capture failed', err);
52
+ hidden = false;
53
+ } finally {
54
+ isCapturing = false;
55
+ }
56
+ }
57
+
58
+ export function shatter() { if (wrapper) { isShattered = true; doCapture(); } }
59
+ export function restore() {
60
+ isShattered = false;
61
+ hidden = false; snapshotData = null; snapshotRect = null; EngineComponent = null;
62
+ }
63
+ </script>
64
+
65
+ <div
66
+ bind:this={wrapper}
67
+ class={className}
68
+ style:visibility={hidden ? 'hidden' : 'visible'}
69
+ data-gigo-shatter="true"
70
+ >
71
+ {@render children()}
72
+ </div>
73
+
74
+ {#if EngineComponent && snapshotData && snapshotRect}
75
+ {@const Engine = EngineComponent}
76
+ <Engine
77
+ snapshot={snapshotData}
78
+ rect={snapshotRect}
79
+ {shardCount}
80
+ {intensity}
81
+ {tint}
82
+ {debugMode}
83
+ />
84
+ {/if}
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from 'svelte';
2
+ export interface ShatterPaneProps {
3
+ isShattered?: boolean;
4
+ shardCount?: number;
5
+ intensity?: number;
6
+ tint?: string;
7
+ debugMode?: boolean;
8
+ class?: string;
9
+ children: Snippet;
10
+ }
11
+ declare const ShatterPane: import("svelte").Component<ShatterPaneProps, {
12
+ shatter: () => void;
13
+ restore: () => void;
14
+ }, "isShattered">;
15
+ type ShatterPane = ReturnType<typeof ShatterPane>;
16
+ export default ShatterPane;
@@ -0,0 +1,167 @@
1
+ <!-- Gravitational singularity — Canvas2D particle warp -->
2
+ <script lang="ts">
3
+ import { onMount, onDestroy } from 'svelte';
4
+ import { mountPortalCanvas, loadImage, rand } from '../../../utils/destruction-engine.js';
5
+
6
+ interface Props {
7
+ snapshot: string;
8
+ rect: DOMRect;
9
+ resolution?: number;
10
+ intensity?: number; // 0–10 suck speed
11
+ glowColor?: string; // CSS colour for the accretion ring
12
+ debugMode?: boolean;
13
+ }
14
+
15
+ let {
16
+ snapshot, rect,
17
+ resolution = 4,
18
+ intensity = 7,
19
+ glowColor = 'rgba(255,120,0,0.7)',
20
+ debugMode = false,
21
+ }: Props = $props();
22
+
23
+ let destroyed = false;
24
+ let rafId = 0;
25
+ let disposeCallbacks: Array<() => void> = [];
26
+
27
+ onMount(async () => {
28
+ const vw = window.innerWidth;
29
+ const vh = window.innerHeight;
30
+ const dpr = window.devicePixelRatio ?? 1;
31
+
32
+ const canvas = mountPortalCanvas(vw * dpr, vh * dpr);
33
+ canvas.style.width = '100vw';
34
+ canvas.style.height = '100vh';
35
+ disposeCallbacks.push(() => canvas.remove());
36
+
37
+ const ctx = canvas.getContext('2d')!;
38
+ ctx.scale(dpr, dpr);
39
+
40
+ const img = await loadImage(snapshot);
41
+
42
+ // Sample at resolution
43
+ const cols = Math.ceil(rect.width / resolution);
44
+ const rows = Math.ceil(rect.height / resolution);
45
+
46
+ const offscreen = new OffscreenCanvas(cols, rows);
47
+ const offCtx = offscreen.getContext('2d')!;
48
+ offCtx.drawImage(img, 0, 0, cols, rows);
49
+ const imgData = offCtx.getImageData(0, 0, cols, rows);
50
+ const pixels = imgData.data;
51
+
52
+ // Singularity centre in canvas coords
53
+ const bx = rect.left + rect.width / 2;
54
+ const by = rect.top + rect.height / 2;
55
+
56
+ interface Particle {
57
+ x: number; y: number;
58
+ ox: number; oy: number; // origin
59
+ r: number; g: number; b: number; a: number;
60
+ alive: boolean;
61
+ }
62
+
63
+ const particles: Particle[] = [];
64
+
65
+ for (let row = 0; row < rows; row++) {
66
+ for (let col = 0; col < cols; col++) {
67
+ const pIdx = (row * cols + col) * 4;
68
+ if (pixels[pIdx + 3] < 10) continue;
69
+ const px = rect.left + col * resolution;
70
+ const py = rect.top + row * resolution;
71
+ particles.push({
72
+ x: px, y: py, ox: px, oy: py,
73
+ r: pixels[pIdx], g: pixels[pIdx + 1], b: pixels[pIdx + 2],
74
+ a: pixels[pIdx + 3] / 255,
75
+ alive: true,
76
+ });
77
+ }
78
+ }
79
+
80
+ const pw = resolution;
81
+ let t = 0; // normalised time 0→1
82
+
83
+ // Accretion glow ring radii
84
+ let ringR = Math.max(rect.width, rect.height) * 0.55;
85
+
86
+ function tick() {
87
+ if (destroyed) return;
88
+ ctx.clearRect(0, 0, vw, vh);
89
+
90
+ t += 0.012 * (1 + intensity * 0.08);
91
+
92
+ // Strength grows exponentially — slow suck at first, violent at end
93
+ const s = Math.pow(t, 2.2) * intensity * 0.18;
94
+
95
+ // ── Accretion disk glow ──────────────────────────────────────────────
96
+ ringR = Math.max(rect.width, rect.height) * 0.55 * (1 - t * 0.9);
97
+ if (ringR > 1) {
98
+ const grad = ctx.createRadialGradient(bx, by, ringR * 0.3, bx, by, ringR);
99
+ grad.addColorStop(0, 'transparent');
100
+ grad.addColorStop(0.6, glowColor);
101
+ grad.addColorStop(1, 'transparent');
102
+ ctx.globalAlpha = Math.min(t * 2, 1) * 0.8;
103
+ ctx.fillStyle = grad;
104
+ ctx.beginPath();
105
+ ctx.arc(bx, by, ringR, 0, Math.PI * 2);
106
+ ctx.fill();
107
+ ctx.globalAlpha = 1;
108
+ }
109
+
110
+ // ── Dark core that grows ─────────────────────────────────────────────
111
+ const coreR = ringR * 0.25 * t;
112
+ if (coreR > 0.5) {
113
+ ctx.fillStyle = 'black';
114
+ ctx.beginPath();
115
+ ctx.arc(bx, by, coreR, 0, Math.PI * 2);
116
+ ctx.fill();
117
+ }
118
+
119
+ let anyAlive = false;
120
+
121
+ particles.forEach((p) => {
122
+ if (!p.alive) return;
123
+
124
+ const dx = bx - p.x;
125
+ const dy = by - p.y;
126
+ const dist = Math.sqrt(dx * dx + dy * dy) + 0.001;
127
+
128
+ // Gravitational acceleration (stronger near centre)
129
+ const accel = s * (1 / (dist * 0.05 + 1));
130
+ p.x += (dx / dist) * accel;
131
+ p.y += (dy / dist) * accel;
132
+
133
+ // Swirl tangential component (accretion disk spin)
134
+ const swirl = s * 0.5;
135
+ p.x += (-dy / dist) * swirl;
136
+ p.y += (dx / dist) * swirl;
137
+
138
+ // Fade as they approach centre
139
+ const newDist = Math.sqrt((bx - p.x) ** 2 + (by - p.y) ** 2);
140
+ p.a = Math.max(0, p.a - (newDist < 8 ? 0.1 : 0.002));
141
+
142
+ if (newDist < 3 || p.a <= 0) { p.alive = false; return; }
143
+
144
+ anyAlive = true;
145
+
146
+ // Particle redness increases near singularity (gravitational redshift effect)
147
+ const heat = Math.min((1 - newDist / Math.max(rect.width, rect.height)) * 2, 1);
148
+ const rs = Math.min(255, p.r + heat * 80);
149
+ const gs = Math.max(0, p.g - heat * 60);
150
+ const bs = Math.max(0, p.b - heat * 120);
151
+
152
+ ctx.fillStyle = `rgba(${rs|0},${gs|0},${bs|0},${p.a})`;
153
+ ctx.fillRect(p.x, p.y, pw, pw);
154
+ });
155
+
156
+ if (anyAlive || t < 1) rafId = requestAnimationFrame(tick);
157
+ }
158
+
159
+ rafId = requestAnimationFrame(tick);
160
+ });
161
+
162
+ onDestroy(() => {
163
+ destroyed = true;
164
+ cancelAnimationFrame(rafId);
165
+ disposeCallbacks.forEach((fn) => fn());
166
+ });
167
+ </script>
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ snapshot: string;
3
+ rect: DOMRect;
4
+ resolution?: number;
5
+ intensity?: number;
6
+ glowColor?: string;
7
+ debugMode?: boolean;
8
+ }
9
+ declare const BlackHoleEngine: import("svelte").Component<Props, {}, "">;
10
+ type BlackHoleEngine = ReturnType<typeof BlackHoleEngine>;
11
+ export default BlackHoleEngine;
@@ -0,0 +1,195 @@
1
+ <!-- Voxel crush engine — Three.js InstancedMesh + Rapier3D rigid bodies -->
2
+ <script lang="ts">
3
+ import { onMount, onDestroy } from 'svelte';
4
+ import {
5
+ mountPortalCanvas,
6
+ loadImage,
7
+ cellUV,
8
+ rand,
9
+ randSign,
10
+ } from '../../../utils/destruction-engine.js';
11
+
12
+ interface Props {
13
+ snapshot: string;
14
+ rect: DOMRect;
15
+ cols?: number;
16
+ rows?: number;
17
+ intensity?: number;
18
+ debugMode?: boolean;
19
+ }
20
+
21
+ let {
22
+ snapshot, rect,
23
+ cols = 20, rows = 20,
24
+ intensity = 7,
25
+ debugMode = false,
26
+ }: Props = $props();
27
+
28
+ let canvas: HTMLCanvasElement | null = null;
29
+ let rafId = 0;
30
+ let destroyed = false;
31
+ let cleanup: Array<() => void> = [];
32
+
33
+ onMount(async () => {
34
+ const [THREE, RAPIER] = await Promise.all([
35
+ import('three'),
36
+ import('@dimforge/rapier3d-compat'),
37
+ ]);
38
+ await RAPIER.init();
39
+ if (destroyed) return;
40
+
41
+ const vw = window.innerWidth;
42
+ const vh = window.innerHeight;
43
+ const dpr = window.devicePixelRatio ?? 1;
44
+
45
+ // portal canvas — fixed overlay
46
+ canvas = mountPortalCanvas(vw * dpr, vh * dpr);
47
+ canvas.style.width = '100vw';
48
+ canvas.style.height = '100vh';
49
+
50
+ const renderer = new THREE.WebGLRenderer({
51
+ canvas, alpha: true, antialias: true, powerPreference: 'high-performance',
52
+ });
53
+ renderer.setPixelRatio(dpr);
54
+ renderer.setSize(vw, vh, false);
55
+ cleanup.push(() => renderer.dispose());
56
+
57
+ const scene = new THREE.Scene();
58
+ const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 1000);
59
+ camera.position.z = 10;
60
+
61
+ // texture from snapshot
62
+ const img = await loadImage(snapshot);
63
+ const texture = new THREE.Texture(img);
64
+ texture.needsUpdate = true;
65
+ texture.minFilter = THREE.LinearFilter;
66
+ texture.magFilter = THREE.LinearFilter;
67
+ cleanup.push(() => texture.dispose());
68
+
69
+ // cell dimensions in NDC space
70
+ const cellW = (rect.width / vw) * 2 / cols;
71
+ const cellH = (rect.height / vh) * 2 / rows;
72
+ const depth = Math.min(cellW, cellH) * 0.5;
73
+ const total = cols * rows;
74
+
75
+ const geo = new THREE.BoxGeometry(cellW, cellH, depth);
76
+ cleanup.push(() => geo.dispose());
77
+
78
+ // per-instance UV data so each shard shows its own slice
79
+ const uvOff = new Float32Array(total * 2);
80
+ const uvRep = new Float32Array(total * 2);
81
+ for (let r = 0; r < rows; r++) {
82
+ for (let c = 0; c < cols; c++) {
83
+ const i = r * cols + c;
84
+ const uv = cellUV(c, r, cols, rows);
85
+ uvOff[i * 2] = uv.offsetX;
86
+ uvOff[i * 2 + 1] = uv.offsetY;
87
+ uvRep[i * 2] = uv.repeatX;
88
+ uvRep[i * 2 + 1] = uv.repeatY;
89
+ }
90
+ }
91
+
92
+ const material = new THREE.ShaderMaterial({
93
+ uniforms: { uTexture: { value: texture } },
94
+ vertexShader: `
95
+ attribute vec2 instanceUVOffset;
96
+ attribute vec2 instanceUVRepeat;
97
+ varying vec2 vUV;
98
+ void main() {
99
+ vUV = uv * instanceUVRepeat + instanceUVOffset;
100
+ gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0);
101
+ }`,
102
+ fragmentShader: `
103
+ uniform sampler2D uTexture;
104
+ varying vec2 vUV;
105
+ void main() {
106
+ gl_FragColor = texture2D(uTexture, vUV);
107
+ if (gl_FragColor.a < 0.01) discard;
108
+ }`,
109
+ transparent: true,
110
+ });
111
+ cleanup.push(() => material.dispose());
112
+
113
+ const mesh = new THREE.InstancedMesh(geo, material, total);
114
+ mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
115
+ geo.setAttribute('instanceUVOffset', new THREE.InstancedBufferAttribute(uvOff, 2));
116
+ geo.setAttribute('instanceUVRepeat', new THREE.InstancedBufferAttribute(uvRep, 2));
117
+ scene.add(mesh);
118
+
119
+ // physics
120
+ const world = new RAPIER.World({ x: 0, y: -9.81 * (1 + intensity * 0.3), z: 0 });
121
+ cleanup.push(() => world.free());
122
+
123
+ // floor
124
+ world.createCollider(RAPIER.ColliderDesc.cuboid(10, 0.01, 10).setTranslation(0, -1.05, 0));
125
+
126
+ // rect origin in NDC
127
+ const ndcL = (rect.left / vw) * 2 - 1;
128
+ const ndcT = -((rect.top / vh) * 2 - 1);
129
+ const cx = ndcL + (rect.width / vw);
130
+ const cy = ndcT - (rect.height / vh);
131
+
132
+ const dummy = new THREE.Object3D();
133
+ const bodies: ReturnType<typeof world.createRigidBody>[] = [];
134
+
135
+ for (let r = 0; r < rows; r++) {
136
+ for (let c = 0; c < cols; c++) {
137
+ const x = ndcL + (c + 0.5) * cellW;
138
+ const y = ndcT - (r + 0.5) * cellH;
139
+
140
+ const body = world.createRigidBody(
141
+ RAPIER.RigidBodyDesc.dynamic()
142
+ .setTranslation(x, y, 0)
143
+ .setLinearDamping(0.2)
144
+ .setAngularDamping(0.3)
145
+ );
146
+
147
+ world.createCollider(
148
+ RAPIER.ColliderDesc.cuboid(cellW * 0.45, cellH * 0.45, depth * 0.45)
149
+ .setRestitution(0.25).setFriction(0.6),
150
+ body
151
+ );
152
+
153
+ // implosion toward centre
154
+ const dx = cx - x, dy = cy - y;
155
+ const dist = Math.sqrt(dx * dx + dy * dy) + 0.001;
156
+ const str = intensity * 0.8;
157
+ body.applyImpulse({ x: (dx / dist) * str, y: (dy / dist) * str, z: rand(-0.1, 0.1) }, true);
158
+ body.applyTorqueImpulse({ x: randSign() * rand(0, 0.05), y: randSign() * rand(0, 0.05), z: randSign() * rand(0, 0.3) }, true);
159
+
160
+ bodies.push(body);
161
+ }
162
+ }
163
+
164
+ // gravity kick after implosion phase
165
+ setTimeout(() => {
166
+ if (destroyed) return;
167
+ bodies.forEach((b) => b.applyImpulse({ x: 0, y: -intensity * 0.4, z: 0 }, true));
168
+ }, 500);
169
+
170
+ function tick() {
171
+ if (destroyed) return;
172
+ world.step();
173
+ for (let i = 0; i < total; i++) {
174
+ const t = bodies[i].translation();
175
+ const q = bodies[i].rotation();
176
+ dummy.position.set(t.x, t.y, t.z);
177
+ dummy.quaternion.set(q.x, q.y, q.z, q.w);
178
+ dummy.updateMatrix();
179
+ mesh.setMatrixAt(i, dummy.matrix);
180
+ }
181
+ mesh.instanceMatrix.needsUpdate = true;
182
+ renderer.render(scene, camera);
183
+ rafId = requestAnimationFrame(tick);
184
+ }
185
+
186
+ rafId = requestAnimationFrame(tick);
187
+ });
188
+
189
+ onDestroy(() => {
190
+ destroyed = true;
191
+ cancelAnimationFrame(rafId);
192
+ cleanup.forEach((fn) => fn());
193
+ canvas?.remove();
194
+ });
195
+ </script>
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ snapshot: string;
3
+ rect: DOMRect;
4
+ cols?: number;
5
+ rows?: number;
6
+ intensity?: number;
7
+ debugMode?: boolean;
8
+ }
9
+ declare const CompactorEngine: import("svelte").Component<Props, {}, "">;
10
+ type CompactorEngine = ReturnType<typeof CompactorEngine>;
11
+ export default CompactorEngine;
@@ -0,0 +1,168 @@
1
+ <!-- Per-pixel dissolution with wave contagion — Canvas2D -->
2
+ <script lang="ts">
3
+ import { onMount, onDestroy } from 'svelte';
4
+ import { mountPortalCanvas, loadImage, rand, randSign } from '../../../utils/destruction-engine.js';
5
+
6
+ interface Props {
7
+ snapshot: string;
8
+ rect: DOMRect;
9
+ resolution?: number; // pixel sample step (1 = full, 2 = half, 4 = quarter)
10
+ waveSeed?: number; // number of initial wave-infection seeds
11
+ intensity?: number;
12
+ debugMode?: boolean;
13
+ }
14
+
15
+ let {
16
+ snapshot, rect,
17
+ resolution = 3,
18
+ waveSeed = 8,
19
+ intensity = 7,
20
+ debugMode = false,
21
+ }: Props = $props();
22
+
23
+ let destroyed = false;
24
+ let rafId = 0;
25
+ let disposeCallbacks: Array<() => void> = [];
26
+
27
+ onMount(async () => {
28
+ const vw = window.innerWidth;
29
+ const vh = window.innerHeight;
30
+ const dpr = window.devicePixelRatio ?? 1;
31
+
32
+ const canvas = mountPortalCanvas(vw * dpr, vh * dpr);
33
+ canvas.style.width = '100vw';
34
+ canvas.style.height = '100vh';
35
+ disposeCallbacks.push(() => canvas.remove());
36
+
37
+ const ctx = canvas.getContext('2d')!;
38
+ ctx.scale(dpr, dpr);
39
+
40
+ // Sample the snapshot at reduced resolution
41
+ const offscreen = new OffscreenCanvas(Math.ceil(rect.width / resolution), Math.ceil(rect.height / resolution));
42
+ const offCtx = offscreen.getContext('2d')!;
43
+ const img = await loadImage(snapshot);
44
+ offCtx.drawImage(img, 0, 0, offscreen.width, offscreen.height);
45
+ const imgData = offCtx.getImageData(0, 0, offscreen.width, offscreen.height);
46
+ const pixels = imgData.data;
47
+
48
+ const cols = offscreen.width;
49
+ const rows = offscreen.height;
50
+
51
+ interface Particle {
52
+ x: number; y: number; // canvas coords
53
+ vx: number; vy: number;
54
+ r: number; g: number; b: number; a: number;
55
+ alive: boolean;
56
+ dissolving: boolean;
57
+ dissolveDelay: number; // frames before it starts moving
58
+ }
59
+
60
+ // Build particle grid
61
+ const grid: (Particle | null)[] = new Array(cols * rows).fill(null);
62
+
63
+ for (let row = 0; row < rows; row++) {
64
+ for (let col = 0; col < cols; col++) {
65
+ const pIdx = (row * cols + col) * 4;
66
+ const a = pixels[pIdx + 3];
67
+ if (a < 10) continue;
68
+ grid[row * cols + col] = {
69
+ x: rect.left + col * resolution,
70
+ y: rect.top + row * resolution,
71
+ vx: 0, vy: 0,
72
+ r: pixels[pIdx],
73
+ g: pixels[pIdx + 1],
74
+ b: pixels[pIdx + 2],
75
+ a: a / 255,
76
+ alive: true,
77
+ dissolving: false,
78
+ dissolveDelay: 0,
79
+ };
80
+ }
81
+ }
82
+
83
+ // Infect initial seeds
84
+ function infect(idx: number, delay = 0) {
85
+ const p = grid[idx];
86
+ if (!p || p.dissolving) return;
87
+ p.dissolving = true;
88
+ p.dissolveDelay = delay;
89
+ const angle = rand(0, Math.PI * 2);
90
+ const speed = rand(0.3, 2.5) * (1 + intensity * 0.15);
91
+ p.vx = Math.cos(angle) * speed * 0.3 + randSign() * rand(0, 0.2);
92
+ p.vy = rand(-speed * 0.5, 0); // slight upward burst then gravity
93
+ }
94
+
95
+ // Wave propagation — spread infection to neighbours each frame
96
+ let infectionFrontiers: number[] = [];
97
+
98
+ for (let i = 0; i < waveSeed; i++) {
99
+ const idx = Math.floor(rand(0, cols * rows));
100
+ infect(idx);
101
+ infectionFrontiers.push(idx);
102
+ }
103
+
104
+ let frame = 0;
105
+ const gravAccel = 0.12 * (1 + intensity * 0.1);
106
+ const spread = Math.max(1, Math.round(intensity * 0.6));
107
+
108
+ function tick() {
109
+ if (destroyed) return;
110
+ frame++;
111
+ ctx.clearRect(0, 0, vw, vh);
112
+
113
+ // Spread infection
114
+ const nextFrontier: number[] = [];
115
+ infectionFrontiers.forEach((idx) => {
116
+ const row = Math.floor(idx / cols);
117
+ const col = idx % cols;
118
+ const delayBase = rand(2, 8);
119
+ [[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
120
+ if (Math.random() > 0.6) return; // stochastic — looks organic
121
+ const nr = row + dr, nc = col + dc;
122
+ if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) return;
123
+ const nIdx = nr * cols + nc;
124
+ const n = grid[nIdx];
125
+ if (n && !n.dissolving) {
126
+ infect(nIdx, delayBase);
127
+ nextFrontier.push(nIdx);
128
+ }
129
+ });
130
+ });
131
+ infectionFrontiers = nextFrontier;
132
+
133
+ let anyAlive = false;
134
+ const pw = resolution;
135
+
136
+ for (let i = 0; i < grid.length; i++) {
137
+ const p = grid[i];
138
+ if (!p || !p.alive) continue;
139
+
140
+ if (p.dissolving) {
141
+ if (p.dissolveDelay > 0) { p.dissolveDelay--; }
142
+ else {
143
+ p.vy += gravAccel;
144
+ p.vx *= 0.98;
145
+ p.x += p.vx;
146
+ p.y += p.vy;
147
+ p.a -= 0.004;
148
+ if (p.a <= 0 || p.y > vh + 20) { p.alive = false; continue; }
149
+ }
150
+ }
151
+
152
+ anyAlive = true;
153
+ ctx.fillStyle = `rgba(${p.r},${p.g},${p.b},${p.a})`;
154
+ ctx.fillRect(p.x, p.y, pw, pw);
155
+ }
156
+
157
+ if (anyAlive) rafId = requestAnimationFrame(tick);
158
+ }
159
+
160
+ rafId = requestAnimationFrame(tick);
161
+ });
162
+
163
+ onDestroy(() => {
164
+ destroyed = true;
165
+ cancelAnimationFrame(rafId);
166
+ disposeCallbacks.forEach((fn) => fn());
167
+ });
168
+ </script>
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ snapshot: string;
3
+ rect: DOMRect;
4
+ resolution?: number;
5
+ waveSeed?: number;
6
+ intensity?: number;
7
+ debugMode?: boolean;
8
+ }
9
+ declare const PixelDissolveEngine: import("svelte").Component<Props, {}, "">;
10
+ type PixelDissolveEngine = ReturnType<typeof PixelDissolveEngine>;
11
+ export default PixelDissolveEngine;