motion-library 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +16 -0
- package/README.md +37 -2
- package/anims/basics/anticipation.js +56 -0
- package/anims/basics/card-flip.js +39 -0
- package/anims/basics/easing-race.js +57 -0
- package/anims/basics/fade-scale.js +44 -0
- package/anims/basics/stagger-grid.js +43 -0
- package/anims/chrome/black-hole.js +81 -0
- package/anims/chrome/cloth-reveal.js +156 -0
- package/anims/chrome/dna-helix.js +52 -0
- package/anims/chrome/dust-bust.js +109 -0
- package/anims/chrome/extruded-type.js +64 -0
- package/anims/chrome/fractal-tree.js +75 -0
- package/anims/chrome/gem.js +111 -0
- package/anims/chrome/geode.js +102 -0
- package/anims/chrome/glass-lens.js +73 -0
- package/anims/chrome/glass-prism.js +95 -0
- package/anims/chrome/gyroid.js +69 -0
- package/anims/chrome/holo-card.js +52 -0
- package/anims/chrome/infinite-tunnel.js +81 -0
- package/anims/chrome/inflated-type.js +141 -0
- package/anims/chrome/isometric-city.js +97 -0
- package/anims/chrome/jelly-cube.js +329 -0
- package/anims/chrome/klein-bottle.js +80 -0
- package/anims/chrome/liquid-metal.js +94 -0
- package/anims/chrome/low-poly-planet.js +163 -0
- package/anims/chrome/mandelbulb.js +79 -0
- package/anims/chrome/melting-logo.js +126 -0
- package/anims/chrome/menger-sponge.js +79 -0
- package/anims/chrome/neon-geodesic.js +149 -0
- package/anims/chrome/paper-fold.js +135 -0
- package/anims/chrome/path-spheres.js +112 -0
- package/anims/chrome/point-globe.js +81 -0
- package/anims/chrome/portal.js +105 -0
- package/anims/chrome/rubiks-cube.js +74 -0
- package/anims/chrome/sdf-morph.js +129 -0
- package/anims/chrome/shatter.js +120 -0
- package/anims/chrome/soap-bubble.js +86 -0
- package/anims/chrome/terrain-flyover.js +78 -0
- package/anims/chrome/tesseract.js +148 -0
- package/anims/chrome/torus-knot.js +89 -0
- package/anims/chrome/volumetric-clouds.js +70 -0
- package/anims/flow/bio-shore.js +140 -0
- package/anims/flow/calligraphy.js +116 -0
- package/anims/flow/chladni.js +152 -0
- package/anims/flow/cloud-chamber.js +101 -0
- package/anims/flow/confetti.js +46 -0
- package/anims/flow/curl-flow.js +52 -0
- package/anims/flow/dandelion.js +80 -0
- package/anims/flow/dla-crystal.js +57 -0
- package/anims/flow/drone-show.js +357 -0
- package/anims/flow/embers.js +63 -0
- package/anims/flow/firefly-sync.js +107 -0
- package/anims/flow/galaxy.js +69 -0
- package/anims/flow/galton-board.js +155 -0
- package/anims/flow/gpu-particles.js +73 -0
- package/anims/flow/harmonograph.js +61 -0
- package/anims/flow/holi.js +153 -0
- package/anims/flow/iron-filings.js +112 -0
- package/anims/flow/lightning.js +72 -0
- package/anims/flow/murmuration.js +105 -0
- package/anims/flow/orbitals.js +195 -0
- package/anims/flow/particle-life.js +87 -0
- package/anims/flow/physarum.js +171 -0
- package/anims/flow/pin-art.js +181 -0
- package/anims/flow/rainbow.js +127 -0
- package/anims/flow/roche-limit.js +161 -0
- package/anims/flow/sand-mandala.js +121 -0
- package/anims/flow/scramble-crossing.js +152 -0
- package/anims/flow/snowfall.js +84 -0
- package/anims/flow/sparks.js +71 -0
- package/anims/flow/spider-web.js +123 -0
- package/anims/flow/starry-night.js +121 -0
- package/anims/flow/states-of-matter.js +113 -0
- package/anims/flow/strange-attractor.js +54 -0
- package/anims/flow/wheat-field.js +186 -0
- package/anims/flow/wind-tunnel.js +227 -0
- package/anims/fluid/bubbles.js +152 -0
- package/anims/fluid/caustics.js +45 -0
- package/anims/fluid/dripping-text.js +53 -0
- package/anims/fluid/ferrofluid.js +56 -0
- package/anims/fluid/fire-smoke.js +83 -0
- package/anims/fluid/honey-drip.js +167 -0
- package/anims/fluid/ink-drop.js +50 -0
- package/anims/fluid/ink-fluid.js +125 -0
- package/anims/fluid/lava-lamp.js +56 -0
- package/anims/fluid/liquid-distort.js +64 -0
- package/anims/fluid/liquid-glass-fill.js +53 -0
- package/anims/fluid/marbling.js +53 -0
- package/anims/fluid/mercury.js +177 -0
- package/anims/fluid/ocean-waves.js +50 -0
- package/anims/fluid/paint-swirl.js +153 -0
- package/anims/fluid/raindrops-glass.js +60 -0
- package/anims/fluid/ripples.js +57 -0
- package/anims/fluid/sph-water.js +75 -0
- package/anims/fluid/underwater.js +55 -0
- package/anims/fluid/water-surface-3d.js +98 -0
- package/anims/hover/cursor-trail.js +37 -0
- package/anims/hover/magnetic.js +41 -0
- package/anims/hover/ripple.js +31 -0
- package/anims/hover/spotlight.js +31 -0
- package/anims/hover/tilt.js +36 -0
- package/anims/micro/arc-spinner.js +22 -0
- package/anims/micro/like-burst.js +42 -0
- package/anims/micro/skeleton.js +36 -0
- package/anims/micro/submit-success.js +45 -0
- package/anims/micro/toggle.js +35 -0
- package/anims/morph/gooey.js +36 -0
- package/anims/morph/icon-morph.js +45 -0
- package/anims/morph/line-draw.js +34 -0
- package/anims/morph/liquid-button.js +53 -0
- package/anims/morph/shape-morph.js +61 -0
- package/anims/registry-more.js +327 -0
- package/anims/registry.js +294 -0
- package/anims/scroll/parallax.js +50 -0
- package/anims/scroll/pinned-track.js +40 -0
- package/anims/scroll/scrub-reveal.js +34 -0
- package/anims/scroll/velocity-skew.js +38 -0
- package/anims/scroll/zoom-through.js +41 -0
- package/anims/shaders/aurora.js +47 -0
- package/anims/shaders/dither.js +69 -0
- package/anims/shaders/domain-warp.js +52 -0
- package/anims/shaders/guilloche.js +42 -0
- package/anims/shaders/kaleidoscope.js +64 -0
- package/anims/shaders/reaction-diffusion.js +85 -0
- package/anims/shaders/rgb-glitch.js +56 -0
- package/anims/shaders/truchet.js +57 -0
- package/anims/shaders/voronoi.js +50 -0
- package/anims/springs/ball-collisions.js +47 -0
- package/anims/springs/pendulum-wave.js +35 -0
- package/anims/springs/rubber-band.js +50 -0
- package/anims/springs/squash-bounce.js +50 -0
- package/anims/springs/verlet-rope.js +46 -0
- package/anims/text/letter-rise.js +40 -0
- package/anims/text/line-mask.js +26 -0
- package/anims/text/scramble.js +32 -0
- package/anims/text/typewriter.js +35 -0
- package/anims/text/vf-wave.js +26 -0
- package/anims/transitions/circle-reveal.js +42 -0
- package/anims/transitions/push-slide.js +40 -0
- package/anims/transitions/shared-expand.js +43 -0
- package/anims/transitions/staircase-wipe.js +41 -0
- package/anims/transitions/view-transition.js +43 -0
- package/bin.mjs +155 -0
- package/js/code.js +122 -0
- package/package.json +10 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Dust to the wind: a marble bust made of a hundred and fifty thousand points, each sitting on the surface with its own normal so
|
|
2
|
+
// it lights like stone. A dissolving front sweeps across it from one shoulder; every point the front passes flares
|
|
3
|
+
// like an ember, breaks loose and is carried off by a swirling wind, cooling to grey dust. Then the dust is called
|
|
4
|
+
// back along curving paths and the bust stands whole again. The surface is sampled from simple shapes (an egg of a
|
|
5
|
+
// head, a nose, a brow, ears, a bun of hair, a neck, shoulders), keeping only the points no other shape covers.
|
|
6
|
+
const PARAMS = { points: 150000, wind: 1, speed: 1 };
|
|
7
|
+
|
|
8
|
+
export default function (stage) {
|
|
9
|
+
const canvas = document.createElement('canvas');
|
|
10
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
11
|
+
stage.append(canvas);
|
|
12
|
+
const gl = canvas.getContext('webgl2', { antialias: true });
|
|
13
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
14
|
+
const N = Math.max(10000, Math.round(PARAMS.points));
|
|
15
|
+
// the bust's parts: ellipsoids [cx, cy, cz, rx, ry, rz], each sampled on its surface
|
|
16
|
+
const PARTS = [[0, 1.0, 0, 0.46, 0.66, 0.52, 4], [0, 0.97, 0.5, 0.07, 0.17, 0.11, 0.25], [0, 1.2, 0.33, 0.34, 0.05, 0.13, 0.15], [0.45, 1.02, -0.02, 0.07, 0.16, 0.11, 0.22], [-0.45, 1.02, -0.02, 0.07, 0.16, 0.11, 0.22],
|
|
17
|
+
[0, 1.52, -0.36, 0.3, 0.27, 0.27, 0.8], [0, 0.3, -0.02, 0.25, 0.45, 0.24, 1.1], [0, -0.42, -0.05, 1.15, 0.5, 0.48, 3.2]];
|
|
18
|
+
const inside = (x, y, z, skip) => PARTS.some((p, i) => i !== skip && ((x - p[0]) / p[3]) ** 2 + ((y - p[1]) / p[4]) ** 2 + ((z - p[2]) / p[5]) ** 2 < 0.985);
|
|
19
|
+
const home = new Float32Array(N * 3), nrm = new Float32Array(N * 3);
|
|
20
|
+
const total = PARTS.reduce((a, p) => a + p[6], 0);
|
|
21
|
+
for (let k = 0; k < N;) {
|
|
22
|
+
let r = Math.random() * total, pi = 0; while (r > PARTS[pi][6]) { r -= PARTS[pi][6]; pi++; }
|
|
23
|
+
const p = PARTS[pi], u = Math.random() * 2 - 1, a = Math.random() * 6.283, s = Math.sqrt(1 - u * u);
|
|
24
|
+
const ux = Math.cos(a) * s, uy = u, uz = Math.sin(a) * s, x = p[0] + ux * p[3], y = p[1] + uy * p[4], z = p[2] + uz * p[5];
|
|
25
|
+
if (inside(x, y, z, pi) || (pi === 7 && y < -0.62)) continue; // hidden inside another part, or below the cut of the chest
|
|
26
|
+
let nx = ux / p[3], ny = uy / p[4], nz = uz / p[5]; const l = Math.hypot(nx, ny, nz);
|
|
27
|
+
home.set([x, y, z], k * 3); nrm.set([nx / l, ny / l, nz / l], k * 3); k++;
|
|
28
|
+
}
|
|
29
|
+
const pos = new Float32Array(home), vel = new Float32Array(N * 3), free = new Float32Array(N), seed = Float32Array.from({ length: N }, () => Math.random());
|
|
30
|
+
|
|
31
|
+
const sh = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s)); return s; };
|
|
32
|
+
const prog = (vs, fs) => {
|
|
33
|
+
const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs)); gl.linkProgram(p);
|
|
34
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
|
|
35
|
+
const u = {}; for (let i = 0, n = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS); i < n; i++) { const nm = gl.getActiveUniform(p, i).name; u[nm] = gl.getUniformLocation(p, nm); }
|
|
36
|
+
return { p, u };
|
|
37
|
+
};
|
|
38
|
+
const ptP = prog(`#version 300 es
|
|
39
|
+
layout(location = 0) in vec3 P; layout(location = 1) in vec3 Nn; layout(location = 2) in float Fr;
|
|
40
|
+
uniform mat4 M; uniform float PS; out vec3 col;
|
|
41
|
+
void main() {
|
|
42
|
+
gl_Position = M * vec4(P, 1);
|
|
43
|
+
vec3 L = normalize(vec3(-.55, .6, .6)), rim = normalize(vec3(.8, .3, -.6));
|
|
44
|
+
float dif = max(dot(Nn, L), 0.), back = pow(max(dot(Nn, rim), 0.), 3.);
|
|
45
|
+
vec3 marble = vec3(.93, .9, .85) * (.16 + .84 * dif) + vec3(.5, .62, .8) * back * .5;
|
|
46
|
+
vec3 ember = vec3(1., .55, .2) * 1.6, dust = vec3(.42, .4, .38);
|
|
47
|
+
col = Fr <= 0. ? marble : mix(ember, dust, smoothstep(0., .6, Fr)) * (1. - smoothstep(1.6, 3., Fr) * .8);
|
|
48
|
+
gl_PointSize = PS * (Fr > 0. ? 1. - .5 * smoothstep(0., 2., Fr) : 1.);
|
|
49
|
+
}`, `#version 300 es
|
|
50
|
+
precision mediump float; in vec3 col; out vec4 o; void main() { vec2 q = gl_PointCoord * 2. - 1.; if (dot(q, q) > 1.) discard; o = vec4(pow(col, vec3(1. / 2.2)), 1); }`);
|
|
51
|
+
const bgP = prog(`#version 300 es
|
|
52
|
+
layout(location = 0) in vec2 p; out vec2 v; void main() { v = p; gl_Position = vec4(p, .9999, 1); }`, `#version 300 es
|
|
53
|
+
precision highp float; in vec2 v; out vec4 o; void main() { vec3 c = mix(vec3(.11, .1, .1), vec3(.025, .024, .026), length(v * vec2(.65, 1.)) * .9); o = vec4(c, 1); }`);
|
|
54
|
+
const vao = gl.createVertexArray(); gl.bindVertexArray(vao);
|
|
55
|
+
const pb = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, pb); gl.bufferData(gl.ARRAY_BUFFER, pos.byteLength, gl.DYNAMIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
|
|
56
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, nrm, gl.STATIC_DRAW); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
|
|
57
|
+
const fb = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, fb); gl.bufferData(gl.ARRAY_BUFFER, free.byteLength, gl.DYNAMIC_DRAW); gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 0, 0);
|
|
58
|
+
const qv = gl.createVertexArray(); gl.bindVertexArray(qv); gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
59
|
+
gl.bindVertexArray(null);
|
|
60
|
+
|
|
61
|
+
let W = 1, H = 1;
|
|
62
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = canvas.width = Math.max(1, (stage.clientWidth * d) | 0); H = canvas.height = Math.max(1, (stage.clientHeight * d) | 0); };
|
|
63
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
64
|
+
const matrix = t => {
|
|
65
|
+
const az = 0.45 + 0.25 * Math.sin(t * 0.15), el = 0.12, dist = 5.2, E = [Math.sin(az) * Math.cos(el) * dist, 0.75 + Math.sin(el) * dist, Math.cos(az) * Math.cos(el) * dist], T = [0, 0.62, 0];
|
|
66
|
+
const f = T.map((v, i) => v - E[i]), fl = Math.hypot(...f); for (let i = 0; i < 3; i++) f[i] /= fl;
|
|
67
|
+
const rl = Math.hypot(f[2], f[0]), r = [-f[2] / rl, 0, f[0] / rl], u = [r[1] * f[2] - r[2] * f[1], r[2] * f[0] - r[0] * f[2], r[0] * f[1] - r[1] * f[0]];
|
|
68
|
+
const V = [r[0], u[0], -f[0], 0, r[1], u[1], -f[1], 0, r[2], u[2], -f[2], 0, -(r[0] * E[0] + r[1] * E[1] + r[2] * E[2]), -(u[0] * E[0] + u[1] * E[1] + u[2] * E[2]), f[0] * E[0] + f[1] * E[1] + f[2] * E[2], 1];
|
|
69
|
+
const F = 1 / Math.tan(0.36), A = W / H, n = 0.1, fa = 60, nf = 1 / (n - fa), P = [F / A, 0, 0, 0, 0, F, 0, 0, 0, 0, (fa + n) * nf, -1, 0, 0, 2 * fa * n * nf, 0], out = new Float32Array(16);
|
|
70
|
+
for (let c = 0; c < 4; c++) for (let rr = 0; rr < 4; rr++) { let s = 0; for (let k = 0; k < 4; k++) s += P[k * 4 + rr] * V[c * 4 + k]; out[c * 4 + rr] = s; }
|
|
71
|
+
return out;
|
|
72
|
+
};
|
|
73
|
+
let raf, last = performance.now(), t = 0, phase = 0;
|
|
74
|
+
function frame(now) {
|
|
75
|
+
const dt = Math.min(0.04, (now - last) / 1000) * PARAMS.speed; last = now; t += dt;
|
|
76
|
+
// a cycle: whole (2.5 s), the front sweeps across (4.5 s), dust drifts (2 s), called back (3 s)
|
|
77
|
+
const c = t % 12;
|
|
78
|
+
const front = c < 2.5 ? -3 : c < 7 ? -1.6 + ((c - 2.5) / 4.5) * 3.6 : 3; // position of the dissolving plane along (1, .5, .3)
|
|
79
|
+
const back = c > 9 ? Math.min(1, (c - 9) / 2.6) : 0;
|
|
80
|
+
for (let i = 0; i < N; i++) {
|
|
81
|
+
const i3 = i * 3, hx = home[i3], hy = home[i3 + 1], hz = home[i3 + 2];
|
|
82
|
+
if (back > 0) { // called home: ease back along a curve
|
|
83
|
+
const e = back * back * (3 - 2 * back), sw = Math.sin(e * Math.PI) * 0.6 * (seed[i] - 0.5);
|
|
84
|
+
pos[i3] += (hx + sw - pos[i3]) * Math.min(1, e * 0.25 + 0.02); pos[i3 + 1] += (hy - pos[i3 + 1]) * Math.min(1, e * 0.25 + 0.02); pos[i3 + 2] += (hz - pos[i3 + 2]) * Math.min(1, e * 0.25 + 0.02);
|
|
85
|
+
free[i] = Math.max(0, free[i] - dt * 2); if (back >= 1) { pos[i3] = hx; pos[i3 + 1] = hy; pos[i3 + 2] = hz; free[i] = 0; vel[i3] = vel[i3 + 1] = vel[i3 + 2] = 0; }
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (free[i] <= 0) {
|
|
89
|
+
const s = hx * 0.82 + hy * 0.45 + hz * 0.3 + (seed[i] - 0.5) * 0.25 + Math.sin(hy * 9 + hx * 7) * 0.06;
|
|
90
|
+
if (s < front) { free[i] = 0.001; vel[i3] = 0.2; vel[i3 + 1] = 0.25; vel[i3 + 2] = 0; }
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// the wind: a swirling field that carries the dust off to the left and up
|
|
94
|
+
const x = pos[i3], y = pos[i3 + 1], z = pos[i3 + 2], wnd = PARAMS.wind;
|
|
95
|
+
const ax = (-1.2 + Math.sin(y * 2.3 + t * 0.9) * 0.9 + Math.cos(z * 1.7 - t) * 0.5) * wnd, ay = (0.45 + Math.sin(x * 1.9 + t * 0.7) * 0.6) * wnd, az = Math.sin(x * 1.3 + y * 1.1 + t) * 0.8 * wnd;
|
|
96
|
+
vel[i3] += (ax - vel[i3]) * dt * 1.6; vel[i3 + 1] += (ay - vel[i3 + 1]) * dt * 1.6; vel[i3 + 2] += (az - vel[i3 + 2]) * dt * 1.6;
|
|
97
|
+
pos[i3] += vel[i3] * dt; pos[i3 + 1] += vel[i3 + 1] * dt; pos[i3 + 2] += vel[i3 + 2] * dt;
|
|
98
|
+
free[i] += dt;
|
|
99
|
+
}
|
|
100
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, W, H); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
101
|
+
gl.enable(gl.DEPTH_TEST); gl.useProgram(bgP.p); gl.bindVertexArray(qv); gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
102
|
+
gl.useProgram(ptP.p); gl.uniformMatrix4fv(ptP.u.M, false, matrix(t)); gl.uniform1f(ptP.u.PS, Math.max(1.6, H / 300) * Math.sqrt(150000 / N));
|
|
103
|
+
gl.bindVertexArray(vao); gl.bindBuffer(gl.ARRAY_BUFFER, pb); gl.bufferSubData(gl.ARRAY_BUFFER, 0, pos); gl.bindBuffer(gl.ARRAY_BUFFER, fb); gl.bufferSubData(gl.ARRAY_BUFFER, 0, free);
|
|
104
|
+
gl.drawArrays(gl.POINTS, 0, N); gl.bindVertexArray(null); gl.disable(gl.DEPTH_TEST);
|
|
105
|
+
raf = requestAnimationFrame(frame);
|
|
106
|
+
}
|
|
107
|
+
raf = requestAnimationFrame(frame);
|
|
108
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
109
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Extruded type: a word drawn once into a mask, then stacked back to front as dozens of layers. Each layer is
|
|
2
|
+
// placed by the same 3D rotation (an exact orthographic projection is just a 2D affine transform), so the
|
|
3
|
+
// stack reads as solid letters with depth. The face gets a moving chrome sweep; the sides darken with depth.
|
|
4
|
+
const PARAMS = { depth: 1, layers: 36, tilt: 1 };
|
|
5
|
+
|
|
6
|
+
export default function (stage) {
|
|
7
|
+
const canvas = document.createElement('canvas');
|
|
8
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
9
|
+
stage.append(canvas);
|
|
10
|
+
const ctx = canvas.getContext('2d');
|
|
11
|
+
let W = 0, H = 0, mask, side, sideLit, face, mw = 0, mh = 0;
|
|
12
|
+
const WORD = 'DEPTH';
|
|
13
|
+
function build() {
|
|
14
|
+
const fs = Math.min(W * 0.19, H * 0.36);
|
|
15
|
+
mask = document.createElement('canvas'); const m = mask.getContext('2d');
|
|
16
|
+
m.font = `900 ${fs}px "Mona Sans", system-ui, sans-serif`;
|
|
17
|
+
mw = Math.ceil(m.measureText(WORD).width + fs * 0.2); mh = Math.ceil(fs * 1.1);
|
|
18
|
+
mask.width = mw; mask.height = mh;
|
|
19
|
+
m.font = `900 ${fs}px "Mona Sans", system-ui, sans-serif`; m.fontStretch = 'expanded';
|
|
20
|
+
m.textAlign = 'center'; m.textBaseline = 'middle'; m.fillStyle = '#fff'; m.fillText(WORD, mw / 2, mh * 0.53);
|
|
21
|
+
const tint = c => { const k = document.createElement('canvas'); k.width = mw; k.height = mh; const g = k.getContext('2d'); g.drawImage(mask, 0, 0); g.globalCompositeOperation = 'source-in'; g.fillStyle = c; g.fillRect(0, 0, mw, mh); return k; };
|
|
22
|
+
side = tint('#2a1458'); sideLit = tint('#8b5cf6');
|
|
23
|
+
face = document.createElement('canvas'); face.width = mw; face.height = mh;
|
|
24
|
+
}
|
|
25
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth; H = stage.clientHeight; canvas.width = W * d; canvas.height = H * d; ctx.setTransform(d, 0, 0, d, 0, 0); build(); };
|
|
26
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
27
|
+
let px = 0, py = 0, lastMove = -1e9;
|
|
28
|
+
const onMove = e => { const b = canvas.getBoundingClientRect(); px = (e.clientX - b.left) / b.width * 2 - 1; py = (e.clientY - b.top) / b.height * 2 - 1; lastMove = performance.now(); };
|
|
29
|
+
canvas.addEventListener('pointermove', onMove);
|
|
30
|
+
let ry = 0, rx = 0, raf; const t0 = performance.now();
|
|
31
|
+
function frame(now) {
|
|
32
|
+
const t = (now - t0) / 1000, d = Math.min(devicePixelRatio, 2);
|
|
33
|
+
const idle = now - lastMove > 1500;
|
|
34
|
+
const ty = (idle ? Math.sin(t * 0.7) * 0.55 : px * 0.7) * PARAMS.tilt, tx = (idle ? Math.sin(t * 0.45 + 1) * 0.28 : py * 0.4) * PARAMS.tilt;
|
|
35
|
+
ry += (ty - ry) * 0.08; rx += (tx - rx) * 0.08;
|
|
36
|
+
const bg = ctx.createLinearGradient(0, 0, 0, H); bg.addColorStop(0, '#f3efe8'); bg.addColorStop(1, '#ddd5c8');
|
|
37
|
+
ctx.setTransform(d, 0, 0, d, 0, 0); ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
|
|
38
|
+
// rotation (Y then X); orthographic projection of the text plane = this 2x2 matrix plus a per-layer offset
|
|
39
|
+
const cy = Math.cos(ry), sy = Math.sin(ry), cx = Math.cos(rx), sx = Math.sin(rx);
|
|
40
|
+
const a = cy, b = sx * sy, c = 0, dd = cx, ox = sy, oy = -cy * sx; // where the x and y axes land on screen, and how far one unit of depth shifts
|
|
41
|
+
const depth = Math.min(W, H) * 0.16 * PARAMS.depth, L = Math.max(4, Math.round(PARAMS.layers));
|
|
42
|
+
// soft shadow on the "floor"
|
|
43
|
+
ctx.save(); ctx.translate(W / 2 + ry * 20, H * 0.8); ctx.scale(1, 0.18); const sg = ctx.createRadialGradient(0, 0, 0, 0, 0, mw * 0.5);
|
|
44
|
+
sg.addColorStop(0, 'rgba(60,40,90,.35)'); sg.addColorStop(1, 'rgba(60,40,90,0)'); ctx.fillStyle = sg; ctx.fillRect(-mw, -mw, mw * 2, mw * 2); ctx.restore();
|
|
45
|
+
const light = 0.5 + 0.5 * Math.sin(ry + 0.6); // sides facing the light read brighter
|
|
46
|
+
for (let i = L; i >= 1; i--) {
|
|
47
|
+
const z = depth * i / L, persp = 1 - z * 0.0009 * (1 + sx);
|
|
48
|
+
ctx.setTransform(d * a * persp, d * b * persp, d * c, d * dd * persp, d * (W / 2 + ox * z), d * (H / 2 - 8 + oy * z));
|
|
49
|
+
ctx.globalAlpha = 1; ctx.drawImage(side, -mw / 2, -mh / 2);
|
|
50
|
+
ctx.globalAlpha = (1 - i / L) * 0.55 + light * 0.35; ctx.drawImage(sideLit, -mw / 2, -mh / 2);
|
|
51
|
+
}
|
|
52
|
+
// the face: chrome bands sweeping across, clipped to the letters
|
|
53
|
+
const f = face.getContext('2d'); f.globalCompositeOperation = 'source-over'; f.clearRect(0, 0, mw, mh); f.drawImage(mask, 0, 0); f.globalCompositeOperation = 'source-in';
|
|
54
|
+
const sweep = (t * 0.25 + ry * 0.4) % 1, g = f.createLinearGradient(0, 0, mw, mh * 0.6);
|
|
55
|
+
[[0, '#d9d2ff'], [0.25, '#6d4fd6'], [0.45, '#ffffff'], [0.55, '#b9a8ff'], [0.8, '#4b2fa8'], [1, '#e8e2ff']].forEach(([o, col]) => g.addColorStop((o + sweep) % 1, col));
|
|
56
|
+
f.fillStyle = g; f.fillRect(0, 0, mw, mh);
|
|
57
|
+
ctx.globalAlpha = 1; ctx.setTransform(d * a, d * b, d * c, d * dd, d * W / 2, d * (H / 2 - 8));
|
|
58
|
+
ctx.drawImage(face, -mw / 2, -mh / 2);
|
|
59
|
+
ctx.globalAlpha = 1;
|
|
60
|
+
raf = requestAnimationFrame(frame);
|
|
61
|
+
}
|
|
62
|
+
raf = requestAnimationFrame(frame);
|
|
63
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', onMove); canvas.remove(); };
|
|
64
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// 3D fractal tree: a trunk that splits into three branches, each turned about the parent's axis by the golden angle
|
|
2
|
+
// and tilted outward, each shorter and thinner, eight levels deep. The whole tree grows in: a branch only starts once
|
|
3
|
+
// its parent is full length, with an ease-out so each one shoots then settles. Wind sways every branch by an amount
|
|
4
|
+
// that grows toward the tips, blossoms open at the ends, petals drift down, and the camera circles the tree.
|
|
5
|
+
const PARAMS = { depth: 7, wind: 1 };
|
|
6
|
+
|
|
7
|
+
export default function (stage) {
|
|
8
|
+
const canvas = document.createElement('canvas');
|
|
9
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
10
|
+
stage.append(canvas);
|
|
11
|
+
const ctx = canvas.getContext('2d');
|
|
12
|
+
let W = 0, H = 0;
|
|
13
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth || 1; H = stage.clientHeight || 1; canvas.width = W * d; canvas.height = H * d; ctx.setTransform(d, 0, 0, d, 0, 0); };
|
|
14
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
15
|
+
// build the branch list once: each with a level, a parent, a direction offset and a length
|
|
16
|
+
const GA = Math.PI * (3 - Math.sqrt(5)), D = Math.round(PARAMS.depth), branches = [];
|
|
17
|
+
let seed = 7; const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
|
|
18
|
+
(function grow(parent, level, yaw, pitch, len) {
|
|
19
|
+
const b = { parent, level, yaw, pitch, len, i: branches.length }; branches.push(b);
|
|
20
|
+
if (level >= D) return;
|
|
21
|
+
for (let k = 0; k < 3; k++) grow(b, level + 1, yaw + k * 2.1 + GA * level + (rnd() - 0.5) * 0.6, 0.45 + rnd() * 0.25, len * (0.68 + rnd() * 0.1));
|
|
22
|
+
})(null, 0, 0, 0, 1);
|
|
23
|
+
const petals = Array.from({ length: 60 }, () => ({ x: (rnd() - 0.5) * 3, y: rnd() * 3, z: (rnd() - 0.5) * 3, ph: rnd() * 6.28 }));
|
|
24
|
+
const ease = x => 1 - (1 - Math.min(1, Math.max(0, x))) ** 3;
|
|
25
|
+
let raf; const t0 = performance.now();
|
|
26
|
+
function frame(now) {
|
|
27
|
+
const t = (now - t0) / 1000, S = Math.min(W, H), cycle = 16, g = (t % cycle) * 1.6; // grow, hold, regrow
|
|
28
|
+
const bg = ctx.createLinearGradient(0, 0, 0, H); bg.addColorStop(0, '#1b1530'); bg.addColorStop(1, '#3a2340');
|
|
29
|
+
ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
|
|
30
|
+
ctx.fillStyle = 'rgba(0,0,0,.25)'; ctx.beginPath(); ctx.ellipse(W / 2, H * 0.93, S * 0.24, S * 0.03, 0, 0, 7); ctx.fill();
|
|
31
|
+
const cam = t * 0.15, cc = Math.cos(cam), cs = Math.sin(cam);
|
|
32
|
+
const proj = p => { const x = p[0] * cc - p[2] * cs, z = p[0] * cs + p[2] * cc, k = 4 / (4 + z); return [W / 2 + x * S * 0.17 * k, H * 0.93 - p[1] * S * 0.17 * k, z, k]; };
|
|
33
|
+
// positions: walk the list (parents come first), building each branch's frame from its parent's
|
|
34
|
+
const P = new Array(branches.length), segs = [];
|
|
35
|
+
for (const b of branches) {
|
|
36
|
+
const grown = ease(g - b.level * 0.55); // each level waits for the one before
|
|
37
|
+
if (grown <= 0) continue;
|
|
38
|
+
const sway = Math.sin(t * 1.3 + b.level * 0.7 + b.yaw) * 0.04 * b.level * PARAMS.wind;
|
|
39
|
+
const base = b.parent ? P[b.parent.i].end : [0, 0, 0];
|
|
40
|
+
const pd = b.parent ? P[b.parent.i].dir : [0, 1, 0];
|
|
41
|
+
if (!base) continue;
|
|
42
|
+
// tilt away from the parent's direction by pitch, around it by yaw
|
|
43
|
+
const up = pd, side = Math.abs(up[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0];
|
|
44
|
+
let a = [up[1] * side[2] - up[2] * side[1], up[2] * side[0] - up[0] * side[2], up[0] * side[1] - up[1] * side[0]]; const al = Math.hypot(...a); a = a.map(v => v / al);
|
|
45
|
+
const c2 = [up[1] * a[2] - up[2] * a[1], up[2] * a[0] - up[0] * a[2], up[0] * a[1] - up[1] * a[0]];
|
|
46
|
+
const yw = b.yaw + sway, pt = b.level ? b.pitch + sway * 0.5 : sway * 0.3;
|
|
47
|
+
const dir = up.map((u, k) => u * Math.cos(pt) + (a[k] * Math.cos(yw) + c2[k] * Math.sin(yw)) * Math.sin(pt));
|
|
48
|
+
const L = b.len * grown, end = base.map((v, k) => v + dir[k] * L);
|
|
49
|
+
P[b.i] = { end: grown >= 1 ? end : end, dir };
|
|
50
|
+
segs.push({ a: base, b: end, w: (D + 1 - b.level) ** 1.5 * 0.9, level: b.level, tip: b.level === D, grown });
|
|
51
|
+
}
|
|
52
|
+
const pr = segs.map(s => ({ s, A: proj(s.a), B: proj(s.b) }));
|
|
53
|
+
pr.sort((x, y) => (y.A[2] + y.B[2]) - (x.A[2] + x.B[2])); // far first
|
|
54
|
+
ctx.lineCap = 'round';
|
|
55
|
+
for (const { s, A, B } of pr) {
|
|
56
|
+
const shade = 60 + s.level * 9 - (A[2] + B[2]) * 12;
|
|
57
|
+
ctx.strokeStyle = `rgb(${shade + 30},${shade},${shade - 10})`; ctx.lineWidth = Math.max(0.6, s.w * A[3] * S / 500);
|
|
58
|
+
ctx.beginPath(); ctx.moveTo(A[0], A[1]); ctx.lineTo(B[0], B[1]); ctx.stroke();
|
|
59
|
+
if (s.tip && s.grown >= 1) { // blossoms open at the tips
|
|
60
|
+
const open = Math.min(1, (g - (D + 1) * 0.55) * 0.8);
|
|
61
|
+
if (open > 0) { ctx.fillStyle = `rgba(255,${170 + (s.level * 13) % 40},${200},${0.75 * open})`; ctx.beginPath(); ctx.arc(B[0], B[1], S * 0.0055 * B[3] * (0.6 + open), 0, 7); ctx.fill(); }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// petals falling
|
|
65
|
+
ctx.fillStyle = 'rgba(255,190,215,.8)';
|
|
66
|
+
for (const p of petals) {
|
|
67
|
+
p.y -= 0.004; p.x += Math.sin(t + p.ph) * 0.004 * PARAMS.wind; if (p.y < 0) p.y = 3;
|
|
68
|
+
if (g < (D + 2) * 0.55) continue;
|
|
69
|
+
const [x, y, , k] = proj([p.x, p.y, p.z]); ctx.beginPath(); ctx.ellipse(x, y, 2.2 * k, 1.3 * k, t + p.ph, 0, 7); ctx.fill();
|
|
70
|
+
}
|
|
71
|
+
raf = requestAnimationFrame(frame);
|
|
72
|
+
}
|
|
73
|
+
raf = requestAnimationFrame(frame);
|
|
74
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
75
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Diamond: a round brilliant cut built from 49 flat facets (table, crown, girdle, pavilion), traced exactly as the
|
|
2
|
+
// intersection of 49 half-spaces. Light enters, then bounces inside: at each facet it either escapes or, past the
|
|
3
|
+
// critical angle, is totally reflected back in, which is what makes a diamond bright. Red, green and blue are traced
|
|
4
|
+
// separately with slightly different refractive indices (dispersion), so the escaping light splits into fire. Strip
|
|
5
|
+
// lights in a dark studio give it something to sparkle with as it turns.
|
|
6
|
+
const PARAMS = { ior: 2.42, fire: 1 };
|
|
7
|
+
|
|
8
|
+
export default function (stage) {
|
|
9
|
+
const canvas = document.createElement('canvas');
|
|
10
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
11
|
+
stage.append(canvas);
|
|
12
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
13
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
14
|
+
// the cut: each facet a plane n·p = d with n pointing out
|
|
15
|
+
const planes = [], deg = Math.PI / 180;
|
|
16
|
+
const add = (n, d) => { const l = Math.hypot(...n); planes.push(n[0] / l, n[1] / l, n[2] / l, d / l); };
|
|
17
|
+
add([0, 1, 0], 0.4); // table
|
|
18
|
+
const through = (n, p) => n[0] * p[0] + n[1] * p[1] + n[2] * p[2];
|
|
19
|
+
for (let k = 0; k < 8; k++) { // crown mains and upper girdle facets
|
|
20
|
+
const f = k * Math.PI / 4, g = f + Math.PI / 8;
|
|
21
|
+
let n = [Math.sin(34.5 * deg) * Math.cos(f), Math.cos(34.5 * deg), Math.sin(34.5 * deg) * Math.sin(f)]; add(n, through(n, [Math.cos(f), 0.04, Math.sin(f)]));
|
|
22
|
+
n = [Math.sin(46 * deg) * Math.cos(g), Math.cos(46 * deg), Math.sin(46 * deg) * Math.sin(g)]; add(n, through(n, [Math.cos(g), 0.03, Math.sin(g)]));
|
|
23
|
+
}
|
|
24
|
+
for (let k = 0; k < 16; k++) { const f = k * Math.PI / 8; add([Math.cos(f), 0, Math.sin(f)], 1); } // girdle
|
|
25
|
+
for (let k = 0; k < 8; k++) { // pavilion mains to the culet, lower girdle facets
|
|
26
|
+
const f = k * Math.PI / 4 + Math.PI / 8, g = k * Math.PI / 4;
|
|
27
|
+
let n = [Math.sin(40.8 * deg) * Math.cos(f), -Math.cos(40.8 * deg), Math.sin(40.8 * deg) * Math.sin(f)]; add(n, through(n, [0, -0.86, 0]));
|
|
28
|
+
n = [Math.sin(43 * deg) * Math.cos(g), -Math.cos(43 * deg), Math.sin(43 * deg) * Math.sin(g)]; add(n, through(n, [Math.cos(g), -0.03, Math.sin(g)]));
|
|
29
|
+
}
|
|
30
|
+
const NP = planes.length / 4;
|
|
31
|
+
const vs = `#version 300 es
|
|
32
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
33
|
+
const fs = `#version 300 es
|
|
34
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
35
|
+
uniform vec2 R; uniform float T, IOR, FIRE; uniform mat3 ROT; uniform vec4 PL[${NP}];
|
|
36
|
+
vec3 env(vec3 d) { // a dark studio with strip lights and a ring light
|
|
37
|
+
vec3 c = vec3(.012, .014, .02) + vec3(.05, .05, .06) * smoothstep(-.2, 1., d.y);
|
|
38
|
+
float az = atan(d.z, d.x);
|
|
39
|
+
for (int i = 0; i < 9; i++) { float a = float(i) * .698 + .3; c += vec3(4.5) * smoothstep(.06, .0, abs(sin((az - a) * .5))) * smoothstep(.0, .2, d.y) * smoothstep(.75, .45, d.y); }
|
|
40
|
+
c += vec3(4., 3.6, 3.2) * smoothstep(.04, .0, abs(d.y - .9)); // ring light overhead
|
|
41
|
+
c += vec3(.6, .7, 1.) * smoothstep(.15, .0, abs(d.y + .2)) * .4;
|
|
42
|
+
return c;
|
|
43
|
+
}
|
|
44
|
+
// entering the convex cut from outside: x = t, and the facet hit in yzw
|
|
45
|
+
vec4 enter(vec3 ro, vec3 rd) {
|
|
46
|
+
float tn = -1e9, tf = 1e9; vec3 nn = vec3(0);
|
|
47
|
+
for (int i = 0; i < ${NP}; i++) {
|
|
48
|
+
vec3 n = PL[i].xyz; float den = dot(n, rd), dist = PL[i].w - dot(n, ro);
|
|
49
|
+
if (abs(den) < 1e-6) { if (dist < 0.) return vec4(-1); continue; }
|
|
50
|
+
float t = dist / den;
|
|
51
|
+
if (den < 0.) { if (t > tn) { tn = t; nn = n; } } else tf = min(tf, t);
|
|
52
|
+
}
|
|
53
|
+
return tn < tf && tn > 0. ? vec4(tn, nn) : vec4(-1);
|
|
54
|
+
}
|
|
55
|
+
// leaving from inside: the first facet ahead
|
|
56
|
+
vec4 leave(vec3 p, vec3 rd) {
|
|
57
|
+
float tf = 1e9; vec3 nn = vec3(0, 1, 0);
|
|
58
|
+
for (int i = 0; i < ${NP}; i++) { vec3 n = PL[i].xyz; float den = dot(n, rd); if (den > 1e-6) { float t = (PL[i].w - dot(n, p)) / den; if (t < tf) { tf = t; nn = n; } } }
|
|
59
|
+
return vec4(tf, nn);
|
|
60
|
+
}
|
|
61
|
+
// one colour channel through the stone
|
|
62
|
+
float trace(vec3 ro, vec3 rd, float ior, int ch) {
|
|
63
|
+
vec4 h = enter(ro, rd); if (h.x < 0.) return -1.;
|
|
64
|
+
vec3 n = h.yzw, p = ro + rd * h.x;
|
|
65
|
+
float c = dot(-rd, n), F0 = pow((ior - 1.) / (ior + 1.), 2.), F = F0 + (1. - F0) * pow(1. - c, 5.);
|
|
66
|
+
float acc = F * env(reflect(rd, n))[ch], thr = 1. - F;
|
|
67
|
+
vec3 d = refract(rd, n, 1. / ior);
|
|
68
|
+
for (int b = 0; b < 7; b++) {
|
|
69
|
+
vec4 l = leave(p + d * 1e-4, d); p = p + d * (l.x + 1e-4); vec3 m = l.yzw;
|
|
70
|
+
vec3 out_ = refract(d, -m, ior);
|
|
71
|
+
if (dot(out_, out_) < .01) { d = reflect(d, -m); continue; } // total internal reflection: stays inside
|
|
72
|
+
float ci = dot(d, m), Fi = F0 + (1. - F0) * pow(1. - ci, 5.);
|
|
73
|
+
acc += thr * (1. - Fi) * env(out_)[ch]; thr *= Fi; d = reflect(d, -m);
|
|
74
|
+
}
|
|
75
|
+
return acc;
|
|
76
|
+
}
|
|
77
|
+
vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
|
|
78
|
+
void main() {
|
|
79
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
|
|
80
|
+
vec3 eye = vec3(0, 1.6, -3.4), f = normalize(vec3(0, -.05, 0) - eye), r = normalize(cross(vec3(0, 1, 0), f)), u = cross(f, r);
|
|
81
|
+
vec3 rd0 = normalize(f * 2.1 + r * q.x + u * q.y);
|
|
82
|
+
vec3 ro = ROT * eye, rd = ROT * rd0; // turn the world, not the stone
|
|
83
|
+
vec3 bg = mix(vec3(.02, .022, .03), vec3(.07, .07, .09), uv.y);
|
|
84
|
+
float sp = .025 * FIRE;
|
|
85
|
+
float rr = trace(ro, rd, IOR - sp, 0);
|
|
86
|
+
vec3 col;
|
|
87
|
+
if (rr < 0.) col = bg + vec3(.04) * smoothstep(.9, .0, length(q - vec2(0, -.55)) * vec2(1, 3).x);
|
|
88
|
+
else col = vec3(rr, trace(ro, rd, IOR, 1), trace(ro, rd, IOR + sp, 2));
|
|
89
|
+
col = aces(col * 1.5);
|
|
90
|
+
o = vec4(pow(col, vec3(1. / 2.2)), 1);
|
|
91
|
+
}`;
|
|
92
|
+
const sh = (t, s) => { const x = gl.createShader(t); gl.shaderSource(x, s); gl.compileShader(x); if (!gl.getShaderParameter(x, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(x)); return x; };
|
|
93
|
+
const prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, fs)); gl.bindAttribLocation(prog, 0, 'p'); gl.linkProgram(prog); gl.useProgram(prog);
|
|
94
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
95
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
96
|
+
const U = {}; ['R', 'T', 'IOR', 'FIRE', 'ROT', 'PL'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
97
|
+
gl.uniform4fv(U.PL, new Float32Array(planes));
|
|
98
|
+
const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, Math.max(1, Math.sqrt(380000 / (w * h)))); canvas.width = w * d | 0; canvas.height = h * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
99
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
100
|
+
let raf; const t0 = performance.now();
|
|
101
|
+
function frame(now) {
|
|
102
|
+
const t = (now - t0) / 1000, a = t * 0.35, b = Math.sin(t * 0.4) * 0.18; // turn, with a gentle rock
|
|
103
|
+
const ca = Math.cos(a), sa = Math.sin(a), cb = Math.cos(b), sb = Math.sin(b);
|
|
104
|
+
gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, t); gl.uniform1f(U.IOR, PARAMS.ior); gl.uniform1f(U.FIRE, PARAMS.fire);
|
|
105
|
+
gl.uniformMatrix3fv(U.ROT, false, new Float32Array([ca, sb * sa, -cb * sa, 0, cb, sb, sa, -sb * ca, cb * ca]));
|
|
106
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
107
|
+
raf = requestAnimationFrame(frame);
|
|
108
|
+
}
|
|
109
|
+
raf = requestAnimationFrame(frame);
|
|
110
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
111
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Geode: a rough grey stone that cracks in half and opens like a book, and inside it is lined with amethyst. All of
|
|
2
|
+
// it is one distance function, raymarched. The stone is a sphere roughened with noise; each half keeps only one side
|
|
3
|
+
// of the cut and has a hollow carved out of it. The hollow's wall is pushed inward into points by a Voronoi pattern
|
|
4
|
+
// on the sphere of directions: every cell becomes a faceted crystal pointing at the centre, paler at its tip. On the
|
|
5
|
+
// cut face you see the layers in section: the rind, the stone, bands of agate, then the crystals.
|
|
6
|
+
const PARAMS = { crystals: 1, sparkle: 1, speed: 1 };
|
|
7
|
+
|
|
8
|
+
export default function (stage) {
|
|
9
|
+
const canvas = document.createElement('canvas');
|
|
10
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
11
|
+
stage.append(canvas);
|
|
12
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
13
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
14
|
+
const FS = `#version 300 es
|
|
15
|
+
precision highp float; in vec2 uv; out vec4 o; uniform vec2 R; uniform float T, OPEN, CR, SP;
|
|
16
|
+
mat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }
|
|
17
|
+
vec3 h3(vec3 p) { p = fract(p * vec3(.1031, .103, .0973)); p += dot(p, p.yxz + 33.33); return fract((p.xxy + p.yxx) * p.zyx); }
|
|
18
|
+
float n3(vec3 p) { vec3 i = floor(p), f = fract(p); f = f * f * (3. - 2. * f);
|
|
19
|
+
return mix(mix(mix(h3(i).x, h3(i + vec3(1, 0, 0)).x, f.x), mix(h3(i + vec3(0, 1, 0)).x, h3(i + vec3(1, 1, 0)).x, f.x), f.y), mix(mix(h3(i + vec3(0, 0, 1)).x, h3(i + vec3(1, 0, 1)).x, f.x), mix(h3(i + vec3(0, 1, 1)).x, h3(i + vec3(1, 1, 1)).x, f.x), f.y), f.z); }
|
|
20
|
+
float fbm(vec3 p) { return n3(p) * .55 + n3(p * 2.1) * .28 + n3(p * 4.3) * .17; }
|
|
21
|
+
// crystals: distance to the nearest Voronoi point among directions; near a point the wall stands tallest
|
|
22
|
+
vec2 vor(vec3 n) {
|
|
23
|
+
vec3 p = n * 5.5 * CR, i = floor(p), f = fract(p); float d1 = 9., d2 = 9.;
|
|
24
|
+
for (int z = -1; z <= 1; z++) for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++) {
|
|
25
|
+
vec3 g = vec3(x, y, z), q = g + h3(i + g) * .8 + .1 - f; float d = max(max(abs(q.x), abs(q.y)), abs(q.z)) * .7 + length(q) * .3; // faceted
|
|
26
|
+
if (d < d1) { d2 = d1; d1 = d; } else if (d < d2) d2 = d;
|
|
27
|
+
}
|
|
28
|
+
return vec2(d1, d2 - d1);
|
|
29
|
+
}
|
|
30
|
+
float rock(vec3 p) { return length(p) - 1. - .07 * fbm(p * 2.6) - .025 * fbm(p * 9.); }
|
|
31
|
+
float cavity(vec3 p) { vec2 v = vor(normalize(p)); return .7 - .26 * smoothstep(.75, .0, v.x) - length(p); } // > 0 inside the stone around the hollow
|
|
32
|
+
// one half: the stone, minus the hollow, minus everything on the far side of the cut
|
|
33
|
+
float half_(vec3 p, out float mat) {
|
|
34
|
+
float r = rock(p), c = cavity(p), cut = p.x;
|
|
35
|
+
float d = max(max(r, c), cut);
|
|
36
|
+
mat = cut > max(r, c) - .002 ? 2. : (c > r ? 1. : 0.); // 0 outer stone, 1 crystal, 2 the cut face
|
|
37
|
+
return d;
|
|
38
|
+
}
|
|
39
|
+
vec3 local(vec3 p, float s) { // each half swings open about a hinge at the back
|
|
40
|
+
p.x *= s; p -= vec3(-.06, 0, -1.) * vec3(1., 1., 1.); p.xz *= rot(-OPEN * 1.25); p += vec3(-.06, 0, -1.);
|
|
41
|
+
return p;
|
|
42
|
+
}
|
|
43
|
+
float map(vec3 p, out float mat, out float side) {
|
|
44
|
+
float ma, mb; vec3 pa = local(p, 1.), pb = local(p, -1.);
|
|
45
|
+
float da = half_(pa, ma), db = half_(pb, mb);
|
|
46
|
+
side = da < db ? 1. : -1.; mat = da < db ? ma : mb; return min(da, db) * .7;
|
|
47
|
+
}
|
|
48
|
+
float map(vec3 p) { float m, s; return map(p, m, s); }
|
|
49
|
+
vec3 nrm(vec3 p) { vec2 e = vec2(.0015, 0); return normalize(vec3(map(p + e.xyy) - map(p - e.xyy), map(p + e.yxy) - map(p - e.yxy), map(p + e.yyx) - map(p - e.yyx))); }
|
|
50
|
+
void main() {
|
|
51
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
|
|
52
|
+
vec3 ro = vec3(0, .55, 4.2), rd = normalize(vec3(q, -2.2)); rd.yz *= rot(-.1);
|
|
53
|
+
ro.xz *= rot(.35 * sin(T * .17)); rd.xz *= rot(.35 * sin(T * .17));
|
|
54
|
+
// a dark velvet display case under a spotlight
|
|
55
|
+
vec3 col = mix(vec3(.05, .03, .06), vec3(.012, .01, .016), length(q * vec2(.6, .9)));
|
|
56
|
+
float t = 0., mat = 0., side = 1.; bool hit = false;
|
|
57
|
+
for (int i = 0; i < 120; i++) { float d = map(ro + rd * t, mat, side); if (d < .0012) { hit = true; break; } t += d; if (t > 9.) break; }
|
|
58
|
+
if (hit) {
|
|
59
|
+
vec3 p = ro + rd * t, n = nrm(p), L = normalize(vec3(-.5, .9, .6)), V = -rd, H = normalize(L + V);
|
|
60
|
+
vec3 lp = local(p, side); float r = length(lp);
|
|
61
|
+
float dif = max(dot(n, L), 0.), amb = .18 + .12 * n.y;
|
|
62
|
+
vec3 base;
|
|
63
|
+
if (mat < .5) base = mix(vec3(.36, .33, .3), vec3(.5, .47, .42), fbm(lp * 6.)) * (.8 + .2 * n3(lp * 30.)); // the stone's skin
|
|
64
|
+
else if (mat < 1.5) {
|
|
65
|
+
vec2 v = vor(normalize(lp)); float tip = smoothstep(.62, .48, r);
|
|
66
|
+
base = mix(vec3(.24, .06, .36), vec3(.82, .62, .95), tip) * (.75 + .5 * smoothstep(0., .08, v.y)); // amethyst: deep at the root, pale at the tip
|
|
67
|
+
} else {
|
|
68
|
+
// the cut face: rind, stone, agate bands, then the crystal layer
|
|
69
|
+
float band = sin(r * 140. + fbm(lp * 7.) * 6.);
|
|
70
|
+
base = r > .95 ? vec3(.42, .38, .34) : r > .8 ? vec3(.58, .55, .5) * (.85 + .15 * n3(lp * 25.)) : r > .72 ? mix(vec3(.78, .8, .86), vec3(.55, .6, .72), .5 + .5 * band) : vec3(.5, .3, .62);
|
|
71
|
+
}
|
|
72
|
+
col = base * (amb + dif * .95);
|
|
73
|
+
float sp = pow(max(dot(n, H), 0.), mat > .5 && mat < 1.5 ? 90. : 18.) * (mat > .5 && mat < 1.5 ? 2.2 * SP : .25);
|
|
74
|
+
if (mat > .5 && mat < 1.5) sp *= .4 + 1.6 * step(.82, h3(floor(lp * 60.)).x); // facets glint one by one
|
|
75
|
+
col += vec3(1., .96, 1.) * sp;
|
|
76
|
+
col *= 1. - .5 * smoothstep(.0, .6, -dot(n, L)) * step(mat, .5);
|
|
77
|
+
}
|
|
78
|
+
o = vec4(pow(col, vec3(1. / 2.2)), 1);
|
|
79
|
+
}`;
|
|
80
|
+
const VS = `#version 300 es
|
|
81
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
82
|
+
const sh = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s)); return s; };
|
|
83
|
+
const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, VS)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, FS)); gl.bindAttribLocation(p, 0, 'p'); gl.linkProgram(p);
|
|
84
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
|
|
85
|
+
gl.useProgram(p); const U = n => gl.getUniformLocation(p, n);
|
|
86
|
+
gl.bindVertexArray(gl.createVertexArray()); gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
|
|
87
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
88
|
+
let W = 1, H = 1;
|
|
89
|
+
const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, 2, Math.max(0.5, Math.sqrt(290000 / (w * h)))); W = canvas.width = Math.max(1, (w * d) | 0); H = canvas.height = Math.max(1, (h * d) | 0); gl.viewport(0, 0, W, H); };
|
|
90
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
91
|
+
let raf, last = performance.now(), t = 0;
|
|
92
|
+
function frame(now) {
|
|
93
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; t += dt * PARAMS.speed;
|
|
94
|
+
// closed, then it splits and swings open, holds, and closes again
|
|
95
|
+
const c = t % 11, open = c < 1.5 ? 0 : c < 3.6 ? (1 - Math.cos(Math.PI * (c - 1.5) / 2.1)) / 2 : c < 9.2 ? 1 : c < 10.6 ? (1 + Math.cos(Math.PI * (c - 9.2) / 1.4)) / 2 : 0;
|
|
96
|
+
gl.uniform2f(U('R'), W, H); gl.uniform1f(U('T'), t); gl.uniform1f(U('OPEN'), open); gl.uniform1f(U('CR'), PARAMS.crystals); gl.uniform1f(U('SP'), PARAMS.sparkle);
|
|
97
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
98
|
+
raf = requestAnimationFrame(frame);
|
|
99
|
+
}
|
|
100
|
+
raf = requestAnimationFrame(frame);
|
|
101
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
102
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Glass lens refraction: inside the lens each pixel samples the scene behind it through a domed surface,
|
|
2
|
+
// pulled toward the centre (magnification). Red, green and blue bend by slightly different amounts: dispersion.
|
|
3
|
+
const PARAMS = { ior: 1.3, dispersion: 0.02 };
|
|
4
|
+
|
|
5
|
+
export default function (stage) {
|
|
6
|
+
const canvas = document.createElement('canvas');
|
|
7
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
8
|
+
stage.append(canvas);
|
|
9
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
10
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
11
|
+
|
|
12
|
+
// The scene behind the glass, painted once.
|
|
13
|
+
const pic = document.createElement('canvas'); pic.width = 1200; pic.height = 900;
|
|
14
|
+
const g = pic.getContext('2d');
|
|
15
|
+
g.fillStyle = '#efe7dc'; g.fillRect(0, 0, 1200, 900);
|
|
16
|
+
g.fillStyle = '#d9c8b4'; for (let x = 0; x < 1200; x += 60) for (let y = 0; y < 900; y += 60) { g.beginPath(); g.arc(x + 30, y + 30, 6, 0, 7); g.fill(); }
|
|
17
|
+
g.fillStyle = '#8a6a4f'; g.font = '900 210px "Mona Sans", system-ui, sans-serif'; g.textAlign = 'center'; g.textBaseline = 'middle';
|
|
18
|
+
g.fillText('REFRACT', 600, 330);
|
|
19
|
+
g.fillStyle = '#e8590c'; g.fillText('LIGHT', 600, 580);
|
|
20
|
+
g.strokeStyle = '#15171a'; g.lineWidth = 6; for (let i = 0; i < 8; i++) { g.beginPath(); g.moveTo(80 + i * 150, 760); g.lineTo(160 + i * 150, 840); g.stroke(); }
|
|
21
|
+
|
|
22
|
+
const vs = `#version 300 es
|
|
23
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
24
|
+
const fs = `#version 300 es
|
|
25
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
26
|
+
uniform sampler2D img; uniform vec2 R, M; uniform float IOR, DISP;
|
|
27
|
+
vec3 scene(vec2 st) {
|
|
28
|
+
vec2 a = vec2(R.x / R.y, 1.), cover = a.x > 4. / 3. ? vec2(1., (4. / 3.) / a.x) : vec2(a.x / (4. / 3.), 1.);
|
|
29
|
+
vec2 tc = (st - .5) * cover + .5; return texture(img, vec2(tc.x, 1. - tc.y)).rgb;
|
|
30
|
+
}
|
|
31
|
+
void main() {
|
|
32
|
+
vec2 a = vec2(R.x / R.y, 1.), q = uv * a, m = M * a;
|
|
33
|
+
float r = .3, d = length(q - m) / r;
|
|
34
|
+
vec3 col = scene(uv);
|
|
35
|
+
float shadow = smoothstep(1.25, .95, length(q - m - vec2(.02, -.03)) / r) * .18;
|
|
36
|
+
col *= 1. - shadow;
|
|
37
|
+
if (d < 1.) {
|
|
38
|
+
float h = sqrt(1. - d * d); // dome height
|
|
39
|
+
float mag = (IOR - 1.) * 1.1;
|
|
40
|
+
vec2 dir = (q - m);
|
|
41
|
+
vec3 c;
|
|
42
|
+
c.r = scene((m + dir * (1. - mag * (1. + DISP * 6.) * h)) / a).r;
|
|
43
|
+
c.g = scene((m + dir * (1. - mag * h)) / a).g;
|
|
44
|
+
c.b = scene((m + dir * (1. - mag * (1. - DISP * 6.) * h)) / a).b;
|
|
45
|
+
float rim = smoothstep(.82, 1., d);
|
|
46
|
+
float hi = pow(max(0., dot(normalize(vec3(dir / r, h)), normalize(vec3(-.5, .6, .8)))), 40.);
|
|
47
|
+
col = mix(c * 1.04, vec3(1.), rim * .35) + hi * .6;
|
|
48
|
+
}
|
|
49
|
+
o = vec4(col, 1);
|
|
50
|
+
}`;
|
|
51
|
+
const sh = (t, s) => { const x = gl.createShader(t); gl.shaderSource(x, s); gl.compileShader(x); if (!gl.getShaderParameter(x, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(x)); return x; };
|
|
52
|
+
const prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, fs)); gl.bindAttribLocation(prog, 0, 'p'); gl.linkProgram(prog); gl.useProgram(prog);
|
|
53
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
54
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
55
|
+
const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, pic);
|
|
56
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
57
|
+
const u = n => gl.getUniformLocation(prog, n), uR = u('R'), uM = u('M'), uI = u('IOR'), uD = u('DISP');
|
|
58
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 1.5); canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
59
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
60
|
+
let mx = 0.5, my = 0.5, tx = 0.5, ty = 0.5, lastMove = -1e9, raf;
|
|
61
|
+
const onMove = e => { const b = canvas.getBoundingClientRect(); tx = (e.clientX - b.left) / b.width; ty = 1 - (e.clientY - b.top) / b.height; lastMove = performance.now(); };
|
|
62
|
+
canvas.addEventListener('pointermove', onMove);
|
|
63
|
+
function frame(now) {
|
|
64
|
+
const t = now / 1000;
|
|
65
|
+
if (now - lastMove > 1500) { tx = 0.5 + Math.cos(t * 0.6) * 0.28; ty = 0.5 + Math.sin(t * 0.9) * 0.2; }
|
|
66
|
+
mx += (tx - mx) * 0.1; my += (ty - my) * 0.1;
|
|
67
|
+
gl.uniform2f(uR, canvas.width, canvas.height); gl.uniform2f(uM, mx, my); gl.uniform1f(uI, PARAMS.ior); gl.uniform1f(uD, PARAMS.dispersion);
|
|
68
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
69
|
+
raf = requestAnimationFrame(frame);
|
|
70
|
+
}
|
|
71
|
+
raf = requestAnimationFrame(frame);
|
|
72
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', onMove); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
73
|
+
}
|