@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.
- package/README.md +158 -132
- package/dist/components/chaos/BlackHoleSink.svelte +81 -0
- package/dist/components/chaos/BlackHoleSink.svelte.d.ts +16 -0
- package/dist/components/chaos/GigoCompactor.svelte +103 -0
- package/dist/components/chaos/GigoCompactor.svelte.d.ts +16 -0
- package/dist/components/chaos/PixelDissolve.svelte +81 -0
- package/dist/components/chaos/PixelDissolve.svelte.d.ts +16 -0
- package/dist/components/chaos/ShatterPane.svelte +84 -0
- package/dist/components/chaos/ShatterPane.svelte.d.ts +16 -0
- package/dist/components/chaos/internal/BlackHoleEngine.svelte +167 -0
- package/dist/components/chaos/internal/BlackHoleEngine.svelte.d.ts +11 -0
- package/dist/components/chaos/internal/CompactorEngine.svelte +195 -0
- package/dist/components/chaos/internal/CompactorEngine.svelte.d.ts +11 -0
- package/dist/components/chaos/internal/PixelDissolveEngine.svelte +168 -0
- package/dist/components/chaos/internal/PixelDissolveEngine.svelte.d.ts +11 -0
- package/dist/components/chaos/internal/ShatterEngine.svelte +208 -0
- package/dist/components/chaos/internal/ShatterEngine.svelte.d.ts +11 -0
- package/dist/docs/component-data.js +8 -0
- package/dist/docs/components/chaos/black-hole-sink.d.ts +2 -0
- package/dist/docs/components/chaos/black-hole-sink.js +71 -0
- package/dist/docs/components/chaos/gigo-compactor.d.ts +2 -0
- package/dist/docs/components/chaos/gigo-compactor.js +71 -0
- package/dist/docs/components/chaos/pixel-dissolve.d.ts +2 -0
- package/dist/docs/components/chaos/pixel-dissolve.js +71 -0
- package/dist/docs/components/chaos/shatter-pane.d.ts +2 -0
- package/dist/docs/components/chaos/shatter-pane.js +71 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/dist/styles/globals.css +3 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/utils/destruction-engine.d.ts +26 -0
- package/dist/utils/destruction-engine.js +75 -0
- 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;
|