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,129 @@
|
|
|
1
|
+
// SDF morph: one chrome object that turns into five shapes: sphere, rounded cube, torus, octahedron and a
|
|
2
|
+
// sphere carved by a gyroid lattice. Each shape is a distance function; during a morph the two distances are
|
|
3
|
+
// blended with an ease-in-out, and the object twists around its axis at the midpoint. Rays reflect once, so
|
|
4
|
+
// the carved lattice mirrors itself, and the floor gets a soft shadow marched toward the light.
|
|
5
|
+
const PARAMS = { hold: 1.4, morph: 1.3, twist: 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 gl = canvas.getContext('webgl2', { antialias: false });
|
|
12
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
13
|
+
const vs = `#version 300 es
|
|
14
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
15
|
+
const fs = `#version 300 es
|
|
16
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
17
|
+
uniform vec2 R; uniform float T, E, TW, BOB, K; uniform int A, B; uniform mat3 ROT; uniform vec3 CE, CF, CR, CU;
|
|
18
|
+
const vec3 C = vec3(0, 1.15, 0), LD = vec3(.35, 1., .25);
|
|
19
|
+
float sdBox(vec3 p, vec3 b, float r) { vec3 q = abs(p) - b; return length(max(q, 0.)) + min(max(q.x, max(q.y, q.z)), 0.) - r; }
|
|
20
|
+
float sdOct(vec3 p, float s) {
|
|
21
|
+
p = abs(p); float m = p.x + p.y + p.z - s; vec3 q;
|
|
22
|
+
if (3. * p.x < m) q = p.xyz; else if (3. * p.y < m) q = p.yzx; else if (3. * p.z < m) q = p.zxy; else return m * .57735027;
|
|
23
|
+
float k = clamp(.5 * (q.z - q.y + s), 0., s); return length(vec3(q.x, q.y - s + k, q.z - k));
|
|
24
|
+
}
|
|
25
|
+
float gyroid(vec3 p) { return dot(sin(p), cos(p.yzx)); }
|
|
26
|
+
float shape(int id, vec3 p) {
|
|
27
|
+
if (id == 0) return length(p) - .9;
|
|
28
|
+
if (id == 1) return sdBox(p, vec3(.58), .16);
|
|
29
|
+
if (id == 2) { vec2 q = vec2(length(p.xz) - .66, p.y); return length(q) - .28; }
|
|
30
|
+
if (id == 3) return sdOct(p, 1.12) - .05;
|
|
31
|
+
float k = 4.4; return max(length(p) - .95, (abs(gyroid(p * k)) - .38) / (k * 1.55));
|
|
32
|
+
}
|
|
33
|
+
float obj(vec3 p) {
|
|
34
|
+
vec3 q = ROT * (p - C - vec3(0, BOB, 0));
|
|
35
|
+
float a = TW * q.y, c = cos(a), s = sin(a); q.xz = mat2(c, -s, s, c) * q.xz; // twist while it changes
|
|
36
|
+
return mix(shape(A, q), shape(B, q), E);
|
|
37
|
+
}
|
|
38
|
+
vec3 nrm(vec3 p) { vec2 e = vec2(.0012, -.0012); return normalize(e.xyy * obj(p + e.xyy) + e.yyx * obj(p + e.yyx) + e.yxy * obj(p + e.yxy) + e.xxx * obj(p + e.xxx)); }
|
|
39
|
+
float march(vec3 ro, vec3 rd, float far, out bool isObj) {
|
|
40
|
+
float t = 0.; isObj = false;
|
|
41
|
+
for (int i = 0; i < 170; i++) {
|
|
42
|
+
vec3 p = ro + rd * t; float dO = obj(p) * K, dF = p.y, d = min(dO, dF);
|
|
43
|
+
if (d < .0008 * (1. + t)) { isObj = dO < dF; return t; }
|
|
44
|
+
t += d; if (t > far) break;
|
|
45
|
+
}
|
|
46
|
+
return -1.;
|
|
47
|
+
}
|
|
48
|
+
float shadow(vec3 ro, vec3 rd) { // soft shadow: how close the ray grazes the object
|
|
49
|
+
float res = 1., t = .02;
|
|
50
|
+
for (int i = 0; i < 40; i++) { float h = obj(ro + rd * t) * K; res = min(res, 9. * h / t); t += clamp(h, .02, .25); if (res < .002 || t > 6.) break; }
|
|
51
|
+
return clamp(res, 0., 1.);
|
|
52
|
+
}
|
|
53
|
+
vec3 backdrop(vec3 d) { return mix(vec3(.2, .19, .185), vec3(.035, .035, .045), smoothstep(-.1, .4, d.y)); }
|
|
54
|
+
vec3 env(vec3 d) {
|
|
55
|
+
float y = d.y, az = atan(d.x, -d.z);
|
|
56
|
+
vec3 c = mix(vec3(.05, .05, .055), vec3(.55, .54, .52), smoothstep(-.25, .0, y)); // dark floor up to a lit horizon
|
|
57
|
+
c = mix(c, vec3(.04, .04, .05), smoothstep(.04, .35, y)); // black ceiling: chrome needs dark to read
|
|
58
|
+
vec2 q = d.xz / max(y, .05) - vec2(.35, .25);
|
|
59
|
+
vec2 b = abs(q) - vec2(.55, .38); float sb = length(max(b, 0.)) + min(max(b.x, b.y), 0.) - .1;
|
|
60
|
+
c += vec3(3.) * smoothstep(.04, -.04, sb) * step(0., y); // overhead softbox
|
|
61
|
+
float band = smoothstep(-.1, .2, y) * smoothstep(.85, .4, y);
|
|
62
|
+
c += vec3(2.2) * smoothstep(.05, 0., abs(az - 2.2)) * band; // white strip
|
|
63
|
+
c += vec3(2.6, 1.2, .4) * smoothstep(.13, 0., abs(az + 1.2)) * band; // warm amber strip
|
|
64
|
+
c += vec3(.5, .8, 1.6) * smoothstep(.04, 0., abs(az - .3)) * smoothstep(.0, .12, y) * smoothstep(.35, .15, y) * .8; // a cool kicker behind
|
|
65
|
+
return c;
|
|
66
|
+
}
|
|
67
|
+
vec3 floorCol(vec3 p) {
|
|
68
|
+
vec3 c = vec3(.3, .29, .28) * (.25 + 1.1 * exp(-dot(p.xz, p.xz) * .18)); // a pool of light on a dark floor
|
|
69
|
+
float s = shadow(p + vec3(0, .002, 0), normalize(LD));
|
|
70
|
+
float ao = 1. - .45 * exp(-max(obj(p), 0.) * 2.2); // contact darkening from the object above
|
|
71
|
+
return c * mix(.55, 1., s) * ao;
|
|
72
|
+
}
|
|
73
|
+
vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
|
|
74
|
+
void main() {
|
|
75
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
|
|
76
|
+
vec3 ro = CE, rd = normalize(CF * 1.9 + CR * q.x + CU * q.y);
|
|
77
|
+
vec3 col = backdrop(rd);
|
|
78
|
+
bool isObj; float t = march(ro, rd, 30., isObj);
|
|
79
|
+
if (t > 0.) {
|
|
80
|
+
vec3 p = ro + rd * t;
|
|
81
|
+
if (isObj) {
|
|
82
|
+
vec3 n = nrm(p), r = reflect(rd, n);
|
|
83
|
+
float fr = .8 + .2 * pow(1. - max(dot(n, -rd), 0.), 5.);
|
|
84
|
+
bool o2; float t2 = march(p + n * .01, r, 12., o2);
|
|
85
|
+
vec3 refl = env(r);
|
|
86
|
+
if (t2 > 0.) { vec3 p2 = p + n * .01 + r * t2; refl = o2 ? env(reflect(r, nrm(p2))) * .7 : floorCol(p2); } // second surface: itself or the floor
|
|
87
|
+
float occ = clamp(.4 + .6 * obj(p + n * .12) / .12, 0., 1.); // creases catch less light
|
|
88
|
+
col = refl * fr * mix(.55, 1., occ) * vec3(.96, .97, 1.);
|
|
89
|
+
} else {
|
|
90
|
+
col = mix(floorCol(p), backdrop(rd), smoothstep(5., 15., t));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
col = aces(col * .9);
|
|
94
|
+
col *= 1. - .25 * pow(length(uv - .5) * 1.3, 2.);
|
|
95
|
+
o = vec4(pow(col, vec3(1. / 2.2)), 1);
|
|
96
|
+
}`;
|
|
97
|
+
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; };
|
|
98
|
+
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);
|
|
99
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
100
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
101
|
+
const U = {}; ['R', 'T', 'E', 'TW', 'BOB', 'K', 'A', 'B', 'ROT', 'CE', 'CF', 'CR', 'CU'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
102
|
+
const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, Math.max(1, Math.sqrt(360000 / (w * h)))); // 1x on big views, sharper on small cards; ~360k pixels at most
|
|
103
|
+
canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
104
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
105
|
+
// strong ease-in-out, cubic-bezier(0.77, 0, 0.175, 1), solved by Newton steps
|
|
106
|
+
const bez = (x1, y1, x2, y2) => x => { let t = x; for (let i = 0; i < 8; i++) { const fx = 3 * x1 * t * (1 - t) ** 2 + 3 * x2 * t * t * (1 - t) + t ** 3 - x, dx = 3 * x1 * (1 - t) ** 2 + 6 * (x2 - x1) * t * (1 - t) + 3 * (1 - x2) * t * t; if (Math.abs(fx) < 1e-5 || dx < 1e-6) break; t = Math.min(1, Math.max(0, t - fx / dx)); } return 3 * y1 * t * (1 - t) ** 2 + 3 * y2 * t * t * (1 - t) + t ** 3; };
|
|
107
|
+
const ease = bez(0.77, 0, 0.175, 1), nrm = a => { const l = Math.hypot(...a); return a.map(v => v / l); };
|
|
108
|
+
let raf; const t0 = performance.now();
|
|
109
|
+
function frame(now) {
|
|
110
|
+
const t = (now - t0) / 1000, H = Math.max(0.2, PARAMS.hold), M = Math.max(0.2, PARAMS.morph), cyc = H + M;
|
|
111
|
+
const k = Math.floor(t / cyc), local = t - k * cyc, x = Math.min(1, Math.max(0, (local - H) / M)), e = ease(x);
|
|
112
|
+
const a = k % 5, b = (k + 1) % 5;
|
|
113
|
+
// slow tumble: yaw keeps turning, pitch rocks
|
|
114
|
+
const yw = t * 0.45, pt = 0.35 + Math.sin(t * 0.3) * 0.25, cy = Math.cos(yw), sy = Math.sin(yw), cp = Math.cos(pt), sp = Math.sin(pt);
|
|
115
|
+
const rot = new Float32Array([cy, sp * sy, -cp * sy, 0, cp, sp, sy, -sp * cy, cp * cy]);
|
|
116
|
+
const asp = canvas.width / canvas.height, fit = Math.max(1, 1.2 / asp), ang = 0.35 + Math.sin(t * 0.07) * 0.25;
|
|
117
|
+
const eye = [Math.sin(ang) * 3.7 * fit, 1.8 + 0.4 * (fit - 1), Math.cos(ang) * 3.7 * fit], ta = [0, 1.0, 0];
|
|
118
|
+
const f = nrm(ta.map((v, i) => v - eye[i])), r = nrm([-f[2], 0, f[0]]), 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]];
|
|
119
|
+
gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, t); gl.uniform1f(U.E, e);
|
|
120
|
+
const tw = PARAMS.twist * 2.4 * Math.sin(Math.PI * e);
|
|
121
|
+
gl.uniform1f(U.TW, tw); gl.uniform1f(U.K, 0.75 / Math.sqrt(1 + (tw * 0.95) ** 2)); // a twist stretches distances: step shorter while it lasts gl.uniform1f(U.BOB, Math.sin(t * 1.1) * 0.06);
|
|
122
|
+
gl.uniform1i(U.A, a); gl.uniform1i(U.B, b); gl.uniformMatrix3fv(U.ROT, false, rot);
|
|
123
|
+
gl.uniform3fv(U.CE, eye); gl.uniform3fv(U.CF, f); gl.uniform3fv(U.CR, r); gl.uniform3fv(U.CU, u);
|
|
124
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
125
|
+
raf = requestAnimationFrame(frame);
|
|
126
|
+
}
|
|
127
|
+
raf = requestAnimationFrame(frame);
|
|
128
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
129
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Shatter & rewind: a pane of glass takes a hit, cracks into shards and flies apart, then time runs backwards and it
|
|
2
|
+
// puts itself back together. The cracks are a Voronoi diagram: seed points scattered densely near the impact and
|
|
3
|
+
// sparsely far from it, each owning the part of the pane closest to it; every cell is cut out by clipping the pane
|
|
4
|
+
// against the halfway line to each other seed. Each shard then follows a ballistic path with its own spin, which is
|
|
5
|
+
// easy to run in reverse. The glass bends the scene behind it, a little differently for red, green and blue.
|
|
6
|
+
const PARAMS = { shards: 90, force: 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 PW = 1.7, PH = 1.1; // half-size of the pane
|
|
15
|
+
let shards = [];
|
|
16
|
+
// the Voronoi cell of each seed: the pane, clipped by the halfway line to every other seed
|
|
17
|
+
function fracture() {
|
|
18
|
+
const n = Math.max(12, Math.round(PARAMS.shards)), ix = (Math.random() - 0.5) * PW, iy = (Math.random() - 0.5) * PH * 0.8, seeds = [];
|
|
19
|
+
for (let i = 0; i < n; i++) { const r = Math.pow(Math.random(), 1.8) * 2.6, a = Math.random() * 6.283; seeds.push([ix + Math.cos(a) * r, iy + Math.sin(a) * r * 0.8]); }
|
|
20
|
+
shards = [];
|
|
21
|
+
for (let i = 0; i < n; i++) {
|
|
22
|
+
let poly = [[-PW, -PH], [PW, -PH], [PW, PH], [-PW, PH]]; const [sx, sy] = seeds[i];
|
|
23
|
+
for (let j = 0; j < n && poly.length; j++) {
|
|
24
|
+
if (j === i) continue; const [tx, ty] = seeds[j], nx = tx - sx, ny = ty - sy, mx = (sx + tx) / 2, my = (sy + ty) / 2;
|
|
25
|
+
const side = p => (p[0] - mx) * nx + (p[1] - my) * ny, out = [];
|
|
26
|
+
for (let k = 0; k < poly.length; k++) {
|
|
27
|
+
const a = poly[k], b = poly[(k + 1) % poly.length], da = side(a), db = side(b);
|
|
28
|
+
if (da <= 0) out.push(a);
|
|
29
|
+
if (da * db < 0) { const t = da / (da - db); out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]); }
|
|
30
|
+
}
|
|
31
|
+
poly = out;
|
|
32
|
+
}
|
|
33
|
+
if (poly.length < 3) continue;
|
|
34
|
+
const cx = poly.reduce((a, p) => a + p[0], 0) / poly.length, cy = poly.reduce((a, p) => a + p[1], 0) / poly.length;
|
|
35
|
+
const dx = cx - ix, dy = cy - iy, d = Math.hypot(dx, dy) + 0.05, kick = PARAMS.force * (1.4 / (0.4 + d));
|
|
36
|
+
const ax = Math.random() - 0.5, ay = Math.random() - 0.5, az = Math.random() - 0.5, al = Math.hypot(ax, ay, az);
|
|
37
|
+
shards.push({ poly: poly.map(([x, y]) => [x - cx, y - cy]), cx, cy, v: [dx / d * kick * 0.9, dy / d * kick * 0.9 + 0.6, -(1.2 + Math.random() * 1.6) * kick], axis: [ax / al, ay / al, az / al], spin: (Math.random() * 6 + 2) * Math.min(2, kick) });
|
|
38
|
+
}
|
|
39
|
+
return [ix, iy];
|
|
40
|
+
}
|
|
41
|
+
let impact = fracture();
|
|
42
|
+
|
|
43
|
+
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; };
|
|
44
|
+
const prog = (vs, fs) => {
|
|
45
|
+
const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs)); gl.linkProgram(p);
|
|
46
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
|
|
47
|
+
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); }
|
|
48
|
+
return { p, u };
|
|
49
|
+
};
|
|
50
|
+
const SCENE = `
|
|
51
|
+
vec3 scene(vec2 s) { // what is behind the glass: a sunset, a sun, bold bands and discs
|
|
52
|
+
vec3 c = mix(vec3(.98, .55, .32), vec3(.25, .18, .5), smoothstep(-.6, .9, s.y));
|
|
53
|
+
c = mix(c, vec3(1., .9, .6), smoothstep(.36, .33, length(s - vec2(.35, .05))));
|
|
54
|
+
c *= .85 + .15 * step(.5, fract(s.y * 7.));
|
|
55
|
+
c = mix(c, vec3(.1, .08, .2), smoothstep(-.4, -.42, s.y + .08 * sin(s.x * 3.)));
|
|
56
|
+
c = mix(c, vec3(.2, .7, .75), smoothstep(.2, .18, length(s - vec2(-.75, .35))));
|
|
57
|
+
return c;
|
|
58
|
+
}`;
|
|
59
|
+
const bgP = prog(`#version 300 es
|
|
60
|
+
layout(location = 0) in vec2 p; out vec2 v; void main() { v = p; gl_Position = vec4(p, .9999, 1); }`, `#version 300 es
|
|
61
|
+
precision highp float; in vec2 v; out vec4 o; uniform float A; ${SCENE} void main() { o = vec4(scene(v * vec2(A, 1.)), 1); }`);
|
|
62
|
+
const glassP = prog(`#version 300 es
|
|
63
|
+
layout(location = 0) in vec2 P; uniform mat3 RT; uniform vec3 C; uniform float A, F; out vec3 n; out vec3 w; out vec2 sp;
|
|
64
|
+
void main() {
|
|
65
|
+
vec3 p = RT * vec3(P, 0.) + C; n = RT * vec3(0, 0, 1); w = p;
|
|
66
|
+
vec3 c = p - vec3(0, 0, 4.); gl_Position = vec4(c.x * F / (-c.z * A), c.y * F / -c.z, 0, 1); sp = gl_Position.xy;
|
|
67
|
+
}`, `#version 300 es
|
|
68
|
+
precision highp float; in vec3 n; in vec3 w; in vec2 sp; out vec4 o; uniform float A, EDGE; ${SCENE}
|
|
69
|
+
void main() {
|
|
70
|
+
vec3 N = normalize(n); if (N.z < 0.) N = -N;
|
|
71
|
+
vec3 V = normalize(vec3(0, 0, 4.) - w);
|
|
72
|
+
vec2 off = N.xy * .22; // tilted glass shifts what you see through it, per colour
|
|
73
|
+
vec2 s = sp * vec2(A, 1.);
|
|
74
|
+
vec3 c = vec3(scene(s + off * 1.06).r, scene(s + off).g, scene(s + off * .94).b) * vec3(.8, .92, .9); // a faint green, as real glass has
|
|
75
|
+
float fr = .05 + .95 * pow(1. - max(dot(N, V), 0.), 5.);
|
|
76
|
+
vec3 refl = mix(vec3(.15, .16, .2), vec3(1.), smoothstep(.2, .9, reflect(-V, N).y * .5 + .5));
|
|
77
|
+
c = mix(c, refl, .1 + fr * .85) + EDGE;
|
|
78
|
+
o = vec4(c, 1);
|
|
79
|
+
}`);
|
|
80
|
+
const edgeP = prog(`#version 300 es
|
|
81
|
+
layout(location = 0) in vec2 P; uniform mat3 RT; uniform vec3 C; uniform float A, F;
|
|
82
|
+
void main() { vec3 c = RT * vec3(P, 0.) + C - vec3(0, 0, 4.); gl_Position = vec4(c.x * F / (-c.z * A), c.y * F / -c.z, -.001, 1); }`, `#version 300 es
|
|
83
|
+
precision mediump float; out vec4 o; uniform float K; void main() { o = vec4(vec3(.75, 1., .9) * K, 1); }`);
|
|
84
|
+
const qv = gl.createVertexArray(); gl.bindVertexArray(qv); gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
|
|
85
|
+
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);
|
|
86
|
+
const sv = gl.createVertexArray(); gl.bindVertexArray(sv); const sb = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, sb); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); gl.bindVertexArray(null);
|
|
87
|
+
|
|
88
|
+
let W = 1, H = 1;
|
|
89
|
+
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); };
|
|
90
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
91
|
+
const rotm = (ax, a) => { const [x, y, z] = ax, c = Math.cos(a), s = Math.sin(a), k = 1 - c; return new Float32Array([c + x * x * k, y * x * k + z * s, z * x * k - y * s, x * y * k - z * s, c + y * y * k, z * y * k + x * s, x * z * k + y * s, y * z * k - x * s, c + z * z * k]); };
|
|
92
|
+
let raf, last = performance.now(), t = 0, cycle = 0;
|
|
93
|
+
function frame(now) {
|
|
94
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; t += dt * PARAMS.speed;
|
|
95
|
+
// whole (1.4 s), the hit and the flight (2.2 s), a beat, the rewind (2.2 s), whole again
|
|
96
|
+
const c = t % 7;
|
|
97
|
+
if (Math.floor(t / 7) !== cycle) { cycle = Math.floor(t / 7); impact = fracture(); }
|
|
98
|
+
const fly = c < 1.4 ? 0 : c < 3.6 ? c - 1.4 : c < 4.4 ? 2.2 : c < 6.6 ? 2.2 * (1 - (c - 4.4) / 2.2) ** 1 : 0;
|
|
99
|
+
const tt = fly < 2.2 && c > 4.4 ? 2.2 * (1 - Math.pow(1 - fly / 2.2, 2)) : fly; // rewind decelerates into place
|
|
100
|
+
const cracks = c > 1.25 && c < 1.4 ? 1 : 0;
|
|
101
|
+
const A = W / H, F = 2.6;
|
|
102
|
+
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);
|
|
103
|
+
gl.enable(gl.DEPTH_TEST);
|
|
104
|
+
gl.useProgram(bgP.p); gl.uniform1f(bgP.u.A, A); gl.bindVertexArray(qv); gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
105
|
+
gl.bindVertexArray(sv);
|
|
106
|
+
for (const s of shards) {
|
|
107
|
+
const T = tt, pos = [s.cx + s.v[0] * T, s.cy + s.v[1] * T - 2.4 * T * T, s.v[2] * T], R = rotm(s.axis, s.spin * T);
|
|
108
|
+
const fan = []; s.poly.forEach(p => fan.push(p[0], p[1]));
|
|
109
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, sb); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(fan), gl.STREAM_DRAW);
|
|
110
|
+
gl.useProgram(glassP.p); gl.uniformMatrix3fv(glassP.u.RT, false, R); gl.uniform3f(glassP.u.C, ...pos); gl.uniform1f(glassP.u.A, A); gl.uniform1f(glassP.u.F, F); gl.uniform1f(glassP.u.EDGE, 0);
|
|
111
|
+
gl.drawArrays(gl.TRIANGLE_FAN, 0, s.poly.length);
|
|
112
|
+
// the edges: bright where the glass is cut (always, once broken; a flash of cracks just before it goes)
|
|
113
|
+
if (T > 0 || cracks) { gl.useProgram(edgeP.p); gl.uniformMatrix3fv(edgeP.u.RT, false, R); gl.uniform3f(edgeP.u.C, ...pos); gl.uniform1f(edgeP.u.A, A); gl.uniform1f(edgeP.u.F, F); gl.uniform1f(edgeP.u.K, cracks ? 1.4 : 1.05); gl.drawArrays(gl.LINE_LOOP, 0, s.poly.length); }
|
|
114
|
+
}
|
|
115
|
+
gl.bindVertexArray(null); gl.disable(gl.DEPTH_TEST);
|
|
116
|
+
raf = requestAnimationFrame(frame);
|
|
117
|
+
}
|
|
118
|
+
raf = requestAnimationFrame(frame);
|
|
119
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
120
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Soap bubble: its colours are thin-film interference. Light reflects off both the outer and inner surface of a film a
|
|
2
|
+
// few hundred nanometres thick; the two reflections add or cancel depending on the thickness, the viewing angle and
|
|
3
|
+
// the wavelength, so each spot shows a different colour. The film drains (thicker at the bottom, thin and dark at the
|
|
4
|
+
// top) and swirls, so the colours flow. Both the front and back of the bubble reflect a soft room, mostly at the edges
|
|
5
|
+
// (Fresnel), and the bubble wobbles as it floats.
|
|
6
|
+
const PARAMS = { thickness: 1, swirl: 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 vs = `#version 300 es
|
|
15
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
16
|
+
const fs = `#version 300 es
|
|
17
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
18
|
+
uniform vec2 R; uniform float T, TH, SW; uniform vec3 C;
|
|
19
|
+
// 3D noise sampled on the sphere itself, so there is no seam where an angle would wrap round
|
|
20
|
+
float hash(vec3 p) { p = fract(p * .3183099 + .1); p *= 17.; return fract(p.x * p.y * p.z * (p.x + p.y + p.z)); }
|
|
21
|
+
float noise(vec3 x) { vec3 i = floor(x), f = fract(x); f = f * f * (3. - 2. * f);
|
|
22
|
+
return mix(mix(mix(hash(i), hash(i + vec3(1, 0, 0)), f.x), mix(hash(i + vec3(0, 1, 0)), hash(i + vec3(1, 1, 0)), f.x), f.y),
|
|
23
|
+
mix(mix(hash(i + vec3(0, 0, 1)), hash(i + vec3(1, 0, 1)), f.x), mix(hash(i + vec3(0, 1, 1)), hash(i + 1.), f.x), f.y), f.z); }
|
|
24
|
+
float fbm(vec3 p) { float s = 0., a = .5; for (int i = 0; i < 5; i++) { s += a * noise(p); p = p * 2.03 + 1.7; a *= .5; } return s; }
|
|
25
|
+
// interference colour of a soap film (n = 1.33), thickness d in nm, for light at angle cosT inside the film
|
|
26
|
+
vec3 film(float d, float cosT) {
|
|
27
|
+
vec3 lam = vec3(650., 532., 450.);
|
|
28
|
+
vec3 ph = 4. * 3.14159 * 1.33 * d * cosT / lam;
|
|
29
|
+
return .5 - .5 * cos(ph + 3.14159); // the half-wave flip at the outer surface
|
|
30
|
+
}
|
|
31
|
+
vec3 room(vec3 d) { // a soft studio: two windows and a warm floor
|
|
32
|
+
vec3 c = mix(vec3(.05, .05, .05), vec3(.32, .34, .38), smoothstep(-.3, .5, d.y));
|
|
33
|
+
c += vec3(2.5) * smoothstep(.3, .0, length(d.xy - vec2(-.55, .45))) * step(0., -d.z);
|
|
34
|
+
c += vec3(1.6, 1.4, 1.1) * smoothstep(.22, .0, length(d.xy - vec2(.6, .3))) * step(0., -d.z);
|
|
35
|
+
return c;
|
|
36
|
+
}
|
|
37
|
+
vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
|
|
38
|
+
// the film's thickness over the bubble: drained toward the bottom, stirred by swirls that drift up
|
|
39
|
+
float thick(vec3 n) {
|
|
40
|
+
vec3 s = n * 2.2;
|
|
41
|
+
float sw = fbm(s + vec3(T * .05, -T * .14, T * .03) * SW + fbm(s * 1.6 - T * .08 * SW) * 1.8 * SW);
|
|
42
|
+
return TH * (90. + 430. * (.5 - .5 * n.y) * (.5 + .9 * sw)); // thin and vivid at the top, thicker below
|
|
43
|
+
}
|
|
44
|
+
void main() {
|
|
45
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
|
|
46
|
+
vec3 ro = vec3(0, 0, -4), rd = normalize(vec3(q, 2.4));
|
|
47
|
+
vec3 bg = mix(vec3(.025, .03, .045), vec3(.09, .1, .13), uv.y) + .05 * smoothstep(.9, .0, length(q - vec2(-.6, .5)));
|
|
48
|
+
vec3 col = bg;
|
|
49
|
+
// the bubble: a sphere whose radius wobbles a little with direction
|
|
50
|
+
vec3 oc = ro - C; float rad = .95, b = dot(oc, rd), c = dot(oc, oc) - rad * rad, h = b * b - c;
|
|
51
|
+
if (h > 0.) {
|
|
52
|
+
h = sqrt(h);
|
|
53
|
+
for (int s = 0; s < 2; s++) { // back wall first, then the front over it
|
|
54
|
+
float t = s == 0 ? -b + h : -b - h;
|
|
55
|
+
vec3 p = ro + rd * t, n = normalize(p - C);
|
|
56
|
+
n = normalize(n + .04 * vec3(sin(n.y * 5. + T * 1.7), sin(n.z * 4. + T * 1.3), sin(n.x * 6. + T * 2.1))); // wobble
|
|
57
|
+
vec3 nn = s == 0 ? -n : n;
|
|
58
|
+
float cosI = abs(dot(nn, rd)), cosT = sqrt(1. - (1. - cosI * cosI) / 1.77);
|
|
59
|
+
float F = .04 + .96 * pow(1. - cosI, 5.);
|
|
60
|
+
vec3 refl = room(reflect(rd, nn));
|
|
61
|
+
vec3 irid = film(thick(n), cosT);
|
|
62
|
+
vec3 layer = refl * irid * (.6 + 2.6 * F) + irid * .07 + refl * F * .25;
|
|
63
|
+
col = col * (1. - .06) + layer * (s == 0 ? .55 : 1.);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
col = aces(col * 1.1);
|
|
67
|
+
o = vec4(pow(col, vec3(1. / 2.2)), 1);
|
|
68
|
+
}`;
|
|
69
|
+
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; };
|
|
70
|
+
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);
|
|
71
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
72
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
73
|
+
const U = {}; ['R', 'T', 'TH', 'SW', 'C'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
74
|
+
const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, Math.max(1, Math.sqrt(400000 / (w * h)))); canvas.width = w * d | 0; canvas.height = h * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
75
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
76
|
+
let raf; const t0 = performance.now();
|
|
77
|
+
function frame(now) {
|
|
78
|
+
const t = (now - t0) / 1000;
|
|
79
|
+
gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, t); gl.uniform1f(U.TH, PARAMS.thickness); gl.uniform1f(U.SW, PARAMS.swirl);
|
|
80
|
+
gl.uniform3f(U.C, Math.sin(t * 0.31) * 0.25, Math.sin(t * 0.47) * 0.15, 0); // it floats about
|
|
81
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
82
|
+
raf = requestAnimationFrame(frame);
|
|
83
|
+
}
|
|
84
|
+
raf = requestAnimationFrame(frame);
|
|
85
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
86
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Terrain flyover: mountains that exist only as a function, height(x, z), built from layers of noise. Each layer adds
|
|
2
|
+
// finer detail, and layers are damped where the slope is already steep (the derivative trick), which carves sharp
|
|
3
|
+
// ridges and smooth valleys like real erosion. A ray steps forward until it drops below the ground. Snow sits on high,
|
|
4
|
+
// flat ground; rock shows on steep faces; a lake fills the lowest valleys; haze grows with distance and the sun is low.
|
|
5
|
+
const PARAMS = { speed: 1, sun: 0.35 };
|
|
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 gl = canvas.getContext('webgl2', { antialias: false });
|
|
12
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
13
|
+
const vs = `#version 300 es
|
|
14
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
15
|
+
const fs = `#version 300 es
|
|
16
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
17
|
+
uniform vec2 R; uniform float Z, SUN;
|
|
18
|
+
float hash(vec2 p) { p = 50. * fract(p * .3183099 + vec2(.71, .113)); return -1. + 2. * fract(p.x * p.y * (p.x + p.y)); }
|
|
19
|
+
vec3 noised(vec2 x) { // value noise with its analytic derivative
|
|
20
|
+
vec2 i = floor(x), f = fract(x), u = f * f * f * (f * (f * 6. - 15.) + 10.), du = 30. * f * f * (f * (f - 2.) + 1.);
|
|
21
|
+
float a = hash(i), b = hash(i + vec2(1, 0)), c = hash(i + vec2(0, 1)), d = hash(i + 1.);
|
|
22
|
+
return vec3(a + (b - a) * u.x + (c - a) * u.y + (a - b - c + d) * u.x * u.y, du * (vec2(b - a, c - a) + (a - b - c + d) * u.yx));
|
|
23
|
+
}
|
|
24
|
+
const mat2 M = mat2(.8, -.6, .6, .8);
|
|
25
|
+
float terrain(vec2 x, int oct) {
|
|
26
|
+
vec2 p = x * .0035; float a = 0., b = 1.; vec2 d = vec2(0);
|
|
27
|
+
for (int i = 0; i < 11; i++) { if (i >= oct) break; vec3 n = noised(p); d += n.yz; a += b * n.x / (1. + dot(d, d)); b *= .5; p = M * p * 2.; }
|
|
28
|
+
return 120. * a;
|
|
29
|
+
}
|
|
30
|
+
float ground(vec2 x) { return max(terrain(x, 8), -18.); } // a lake fills the lowest ground
|
|
31
|
+
vec3 nrm(vec3 p, float t) { vec2 e = vec2(.002 * t, 0); return normalize(vec3(terrain(p.xz - e.xy, 11) - terrain(p.xz + e.xy, 11), 2. * e.x, terrain(p.xz - e.yx, 11) - terrain(p.xz + e.yx, 11))); }
|
|
32
|
+
vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
|
|
33
|
+
void main() {
|
|
34
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
|
|
35
|
+
vec3 ro = vec3(sin(Z * .002) * 300., 0, Z); ro.y = terrain(ro.xz, 6) + 55.;
|
|
36
|
+
vec3 ta = vec3(sin((Z + 120.) * .002) * 300., ro.y - 18., Z + 120.);
|
|
37
|
+
vec3 f = normalize(ta - ro), r = normalize(cross(vec3(0, 1, 0), f)), u = cross(f, r), rd = normalize(f * 1.6 + r * q.x + u * q.y);
|
|
38
|
+
vec3 sd = normalize(vec3(-.8, SUN, .6));
|
|
39
|
+
vec3 sky = mix(vec3(.95, .72, .52), vec3(.32, .48, .72), pow(max(rd.y, 0.), .5)) + vec3(1., .7, .4) * pow(max(dot(rd, sd), 0.), 40.) * .8;
|
|
40
|
+
vec3 col = sky;
|
|
41
|
+
float t = 1.; bool hit = false;
|
|
42
|
+
for (int i = 0; i < 200; i++) { vec3 p = ro + rd * t; float h = p.y - ground(p.xz); if (h < .002 * t) { hit = true; break; } t += .45 * h; if (t > 2800.) break; }
|
|
43
|
+
if (hit) {
|
|
44
|
+
vec3 p = ro + rd * t;
|
|
45
|
+
bool water = p.y < -17.9;
|
|
46
|
+
vec3 n = water ? vec3(0, 1, 0) : nrm(p, t);
|
|
47
|
+
float dif = max(dot(n, sd), 0.);
|
|
48
|
+
// shadow: step from the ground toward the sun
|
|
49
|
+
float sh = 1.; for (float s = 4.; s < 300.; s *= 1.35) { vec3 sp = p + sd * s; float hh = sp.y - ground(sp.xz); sh = min(sh, 14. * hh / s); if (sh < 0.) break; } sh = clamp(sh, 0., 1.);
|
|
50
|
+
vec3 rock = mix(vec3(.18, .14, .11), vec3(.32, .27, .22), smoothstep(-1., 1., sin(p.y * .3 + p.x * .02)));
|
|
51
|
+
vec3 grass = vec3(.16, .2, .08);
|
|
52
|
+
vec3 mat = mix(grass, rock, smoothstep(.65, .85, 1. - n.y) + smoothstep(10., 40., p.y) * .6);
|
|
53
|
+
float snow = smoothstep(35., 55., p.y + 8. * n.y) * smoothstep(.55, .8, n.y);
|
|
54
|
+
mat = mix(mat, vec3(.92, .94, 1.), snow);
|
|
55
|
+
col = mat * (dif * sh * vec3(1.6, 1.25, .95) + (.5 + .5 * n.y) * vec3(.18, .24, .34));
|
|
56
|
+
if (water) col = mix(vec3(.04, .08, .1), sky * .9, .25 + .6 * pow(1. - max(-rd.y, 0.), 6.));
|
|
57
|
+
col = mix(col, sky * .95 + vec3(.05), 1. - exp(-t * .0011)); // aerial haze
|
|
58
|
+
}
|
|
59
|
+
col = aces(col);
|
|
60
|
+
o = vec4(pow(col, vec3(1. / 2.2)), 1);
|
|
61
|
+
}`;
|
|
62
|
+
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; };
|
|
63
|
+
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);
|
|
64
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
65
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
66
|
+
const U = {}; ['R', 'Z', 'SUN'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
67
|
+
const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, Math.max(0.6, Math.sqrt(260000 / (w * h)))); canvas.width = w * d | 0; canvas.height = h * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
68
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
69
|
+
let raf, last = performance.now(), z = 0;
|
|
70
|
+
function frame(now) {
|
|
71
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; z += dt * 45 * PARAMS.speed;
|
|
72
|
+
gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.Z, z); gl.uniform1f(U.SUN, PARAMS.sun);
|
|
73
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
74
|
+
raf = requestAnimationFrame(frame);
|
|
75
|
+
}
|
|
76
|
+
raf = requestAnimationFrame(frame);
|
|
77
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
78
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Tesseract: a cube in four dimensions, turning. Its 16 corners are every combination of ±1 in x, y, z and w, and
|
|
2
|
+
// its 32 edges join corners that differ in one coordinate. It rotates in the planes that involve w, then is projected
|
|
3
|
+
// into 3D the way a 3D object is projected onto a screen: things further along w come out smaller. That is why the
|
|
4
|
+
// inner cube grows, swallows the outer one and turns inside out; nothing bends, it is a rigid rotation seen from
|
|
5
|
+
// one dimension down. Edges are chrome tubes, gold on the near-w side and blue on the far side, over a glossy floor.
|
|
6
|
+
const PARAMS = { spin: 1, tube: 1, perspective: 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
|
+
// the 4D cube
|
|
15
|
+
const V4 = []; for (let i = 0; i < 16; i++) V4.push([i & 1 ? 1 : -1, i & 2 ? 1 : -1, i & 4 ? 1 : -1, i & 8 ? 1 : -1]);
|
|
16
|
+
const E = []; for (let i = 0; i < 16; i++) for (let k = 0; k < 4; k++) { const j = i ^ (1 << k); if (j > i) E.push([i, j]); }
|
|
17
|
+
|
|
18
|
+
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; };
|
|
19
|
+
const prog = (vs, fs) => {
|
|
20
|
+
const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs)); gl.linkProgram(p);
|
|
21
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
|
|
22
|
+
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); }
|
|
23
|
+
return { p, u };
|
|
24
|
+
};
|
|
25
|
+
const ENV = `
|
|
26
|
+
vec3 env(vec3 d) { // a dark studio: overhead softbox, warm and cool strips, a ring light
|
|
27
|
+
vec3 c = mix(vec3(.05, .055, .07), vec3(.26, .27, .31), smoothstep(-.1, .7, d.y));
|
|
28
|
+
vec2 q = d.xz / max(d.y, .05);
|
|
29
|
+
c += vec3(1.) * 2.6 * smoothstep(.06, .0, abs(q.x - .1) - .55) * smoothstep(.06, .0, abs(q.y + .2) - .16) * step(0., d.y);
|
|
30
|
+
float az = atan(d.x, d.z);
|
|
31
|
+
c += vec3(1., .55, .3) * 1.6 * smoothstep(.1, .02, abs(az - 1.9)) * smoothstep(-.05, .2, d.y) * smoothstep(.75, .3, d.y);
|
|
32
|
+
c += vec3(.35, .6, 1.) * 1.4 * smoothstep(.1, .02, abs(az + 1.6)) * smoothstep(-.05, .2, d.y) * smoothstep(.75, .3, d.y);
|
|
33
|
+
c += vec3(1., .97, .92) * 1.3 * smoothstep(.07, .0, abs(d.y - .34) - .05);
|
|
34
|
+
return c;
|
|
35
|
+
}
|
|
36
|
+
vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }`;
|
|
37
|
+
// tubes and balls share one shader: a mesh placed per instance, mirrored below the floor for the reflection pass
|
|
38
|
+
const meshVS = `#version 300 es
|
|
39
|
+
layout(location = 0) in vec3 P; layout(location = 1) in vec3 Nn;
|
|
40
|
+
layout(location = 2) in vec3 IA; layout(location = 3) in vec3 IB; layout(location = 4) in vec2 IT; // ends (or centre), tint, radius
|
|
41
|
+
uniform mat4 M; uniform float MIR, FLOOR; out vec3 n; out vec3 w; out float tint;
|
|
42
|
+
void main() {
|
|
43
|
+
vec3 ax = IB - IA; float L = length(ax); vec3 z = L > 1e-5 ? ax / L : vec3(0, 1, 0);
|
|
44
|
+
vec3 x = normalize(abs(z.y) < .9 ? cross(z, vec3(0, 1, 0)) : cross(z, vec3(1, 0, 0))), y = cross(z, x);
|
|
45
|
+
vec3 p = IA + (x * P.x + y * P.y) * IT.y + z * P.z * L, nn = x * Nn.x + y * Nn.y + z * Nn.z;
|
|
46
|
+
if (L < 1e-5) { p = IA + P * IT.y; nn = Nn; } // a ball
|
|
47
|
+
if (MIR > .5) { p.y = 2. * FLOOR - p.y; nn.y = -nn.y; }
|
|
48
|
+
n = nn; w = p; tint = IT.x; gl_Position = M * vec4(p, 1);
|
|
49
|
+
}`;
|
|
50
|
+
const meshFS = `#version 300 es
|
|
51
|
+
precision highp float; in vec3 n; in vec3 w; in float tint; out vec4 o; uniform vec3 E; uniform float MIR;
|
|
52
|
+
${ENV}
|
|
53
|
+
void main() {
|
|
54
|
+
vec3 N = normalize(n), V = normalize(E - w), R = reflect(-V, N);
|
|
55
|
+
vec3 metal = mix(vec3(.55, .72, 1.), vec3(1., .78, .42), tint); // blue on the far side of w, gold on the near side
|
|
56
|
+
float fr = .45 + .55 * pow(1. - max(dot(N, V), 0.), 4.);
|
|
57
|
+
vec3 c = env(R) * metal * fr + metal * .04;
|
|
58
|
+
if (MIR > .5) c *= .32;
|
|
59
|
+
o = vec4(aces(c), 1);
|
|
60
|
+
}`;
|
|
61
|
+
const floorVS = `#version 300 es
|
|
62
|
+
layout(location = 0) in vec2 P; uniform mat4 M; uniform float FLOOR; out vec3 w; void main() { w = vec3(P.x, FLOOR, P.y); gl_Position = M * vec4(w, 1); }`;
|
|
63
|
+
const floorFS = `#version 300 es
|
|
64
|
+
precision highp float; in vec3 w; out vec4 o; uniform vec3 E;
|
|
65
|
+
void main() { vec3 V = normalize(E - w); float fr = .25 + .75 * pow(1. - V.y, 5.), fade = smoothstep(9., 2., length(w.xz));
|
|
66
|
+
o = vec4(vec3(.01, .011, .014), mix(1., .55, fade * (1. - fr * .6))); }`;
|
|
67
|
+
const bgFS = `#version 300 es
|
|
68
|
+
precision highp float; in vec2 v; out vec4 o; void main() { vec3 c = mix(vec3(.03, .032, .04), vec3(.008, .009, .012), length(v * vec2(.6, 1.))); o = vec4(c, 1); }`;
|
|
69
|
+
const meshP = prog(meshVS, meshFS), floorP = prog(floorVS, floorFS), bgP = prog(`#version 300 es
|
|
70
|
+
layout(location = 0) in vec2 p; out vec2 v; void main() { v = p; gl_Position = vec4(p, .9999, 1); }`, bgFS);
|
|
71
|
+
|
|
72
|
+
// meshes: a unit tube along z (0..1) and a unit ball
|
|
73
|
+
const tube = [], ball = [], SEG = 18;
|
|
74
|
+
for (let i = 0; i < SEG; i++) {
|
|
75
|
+
const a0 = (i / SEG) * Math.PI * 2, a1 = ((i + 1) / SEG) * Math.PI * 2, c0 = Math.cos(a0), s0 = Math.sin(a0), c1 = Math.cos(a1), s1 = Math.sin(a1);
|
|
76
|
+
tube.push(c0, s0, 0, c0, s0, 0, c1, s1, 0, c1, s1, 0, c0, s0, 1, c0, s0, 0, c1, s1, 0, c1, s1, 0, c1, s1, 1, c1, s1, 0, c0, s0, 1, c0, s0, 0);
|
|
77
|
+
}
|
|
78
|
+
const LAT = 12, LON = 18;
|
|
79
|
+
const bp = (i, j) => { const th = (i / LAT) * Math.PI, ph = (j / LON) * Math.PI * 2; return [Math.sin(th) * Math.cos(ph), Math.cos(th), Math.sin(th) * Math.sin(ph)]; };
|
|
80
|
+
for (let i = 0; i < LAT; i++) for (let j = 0; j < LON; j++) { for (const [a, b] of [[i, j], [i + 1, j], [i, j + 1], [i, j + 1], [i + 1, j], [i + 1, j + 1]]) { const p = bp(a, b); ball.push(...p, ...p); } }
|
|
81
|
+
const mkVao = (mesh, inst) => {
|
|
82
|
+
const v = gl.createVertexArray(); gl.bindVertexArray(v);
|
|
83
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(mesh), gl.STATIC_DRAW);
|
|
84
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
|
|
85
|
+
const b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b); gl.bufferData(gl.ARRAY_BUFFER, inst.byteLength, gl.DYNAMIC_DRAW);
|
|
86
|
+
gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 3, gl.FLOAT, false, 32, 0); gl.vertexAttribDivisor(2, 1);
|
|
87
|
+
gl.enableVertexAttribArray(3); gl.vertexAttribPointer(3, 3, gl.FLOAT, false, 32, 12); gl.vertexAttribDivisor(3, 1);
|
|
88
|
+
gl.enableVertexAttribArray(4); gl.vertexAttribPointer(4, 2, gl.FLOAT, false, 32, 24); gl.vertexAttribDivisor(4, 1);
|
|
89
|
+
gl.bindVertexArray(null); return { v, b, n: mesh.length / 6 };
|
|
90
|
+
};
|
|
91
|
+
const tubeI = new Float32Array(E.length * 8), ballI = new Float32Array(16 * 8);
|
|
92
|
+
const tubeV = mkVao(tube, tubeI), ballV = mkVao(ball, ballI);
|
|
93
|
+
const fv = gl.createVertexArray(); gl.bindVertexArray(fv); gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
|
|
94
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-30, -30, 30, -30, -30, 30, 30, -30, 30, 30, -30, 30]), gl.STATIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
95
|
+
const qv = gl.createVertexArray(); gl.bindVertexArray(qv); gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
|
|
96
|
+
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); gl.bindVertexArray(null);
|
|
97
|
+
|
|
98
|
+
let W = 1, H = 1;
|
|
99
|
+
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); };
|
|
100
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
101
|
+
const FLOOR = -1.95;
|
|
102
|
+
let EYE = [0, 0, 0];
|
|
103
|
+
const camera = t => {
|
|
104
|
+
const az = t * 0.1 + 0.6, el = 0.26, dist = 6.4; EYE = [Math.sin(az) * Math.cos(el) * dist, Math.sin(el) * dist, Math.cos(az) * Math.cos(el) * dist];
|
|
105
|
+
const T = [0, -0.1, 0], f = T.map((v, i) => v - EYE[i]), fl = Math.hypot(...f); for (let i = 0; i < 3; i++) f[i] /= fl;
|
|
106
|
+
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]];
|
|
107
|
+
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] * EYE[0] + r[1] * EYE[1] + r[2] * EYE[2]), -(u[0] * EYE[0] + u[1] * EYE[1] + u[2] * EYE[2]), f[0] * EYE[0] + f[1] * EYE[1] + f[2] * EYE[2], 1];
|
|
108
|
+
const F = 1 / Math.tan(0.4), A = W / H, n = 0.1, fa = 100, 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);
|
|
109
|
+
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; }
|
|
110
|
+
return out;
|
|
111
|
+
};
|
|
112
|
+
let raf, last = performance.now(), t = 0;
|
|
113
|
+
function frame(now) {
|
|
114
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; t += dt * PARAMS.spin;
|
|
115
|
+
// rotate in the xw and zw planes (the ones that turn it inside out) and a little in yw
|
|
116
|
+
const a = t * 0.42, b = t * 0.27, c = t * 0.13, ca = Math.cos(a), sa = Math.sin(a), cb = Math.cos(b), sb = Math.sin(b), cc = Math.cos(c), scn = Math.sin(c);
|
|
117
|
+
const D4 = 2.6 / PARAMS.perspective + 0.4;
|
|
118
|
+
const P3 = V4.map(([x, y, z, w]) => {
|
|
119
|
+
let X = x * ca - w * sa, Wv = x * sa + w * ca;
|
|
120
|
+
let Z = z * cb - Wv * sb; Wv = z * sb + Wv * cb;
|
|
121
|
+
let Y = y * cc - Wv * scn; Wv = y * scn + Wv * cc;
|
|
122
|
+
const k = D4 / (D4 - Wv); // perspective from 4D: further along w is smaller
|
|
123
|
+
return [X * k * 0.78, Y * k * 0.78, Z * k * 0.78, (Wv + 1.8) / 3.6];
|
|
124
|
+
});
|
|
125
|
+
const R = 0.055 * PARAMS.tube;
|
|
126
|
+
E.forEach(([i, j], e) => { const p = P3[i], q = P3[j]; tubeI.set([p[0], p[1], p[2], q[0], q[1], q[2], Math.min(1, Math.max(0, (p[3] + q[3]) / 2)), R], e * 8); });
|
|
127
|
+
P3.forEach((p, i) => ballI.set([p[0], p[1], p[2], p[0], p[1], p[2], Math.min(1, Math.max(0, p[3])), R * 1.7], i * 8));
|
|
128
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, W, H);
|
|
129
|
+
gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
130
|
+
gl.enable(gl.DEPTH_TEST); gl.disable(gl.BLEND);
|
|
131
|
+
gl.useProgram(bgP.p); gl.bindVertexArray(qv); gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
132
|
+
const M = camera(t / PARAMS.spin);
|
|
133
|
+
const draw = mir => {
|
|
134
|
+
gl.useProgram(meshP.p); gl.uniformMatrix4fv(meshP.u.M, false, M); gl.uniform3f(meshP.u.E, ...EYE); gl.uniform1f(meshP.u.MIR, mir); gl.uniform1f(meshP.u.FLOOR, FLOOR);
|
|
135
|
+
for (const [V, data, count] of [[tubeV, tubeI, E.length], [ballV, ballI, 16]]) { gl.bindVertexArray(V.v); gl.bindBuffer(gl.ARRAY_BUFFER, V.b); gl.bufferSubData(gl.ARRAY_BUFFER, 0, data); gl.drawArraysInstanced(gl.TRIANGLES, 0, V.n, count); }
|
|
136
|
+
};
|
|
137
|
+
draw(1); // the reflection, under the floor
|
|
138
|
+
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
139
|
+
gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
140
|
+
gl.useProgram(floorP.p); gl.uniformMatrix4fv(floorP.u.M, false, M); gl.uniform1f(floorP.u.FLOOR, FLOOR); gl.uniform3f(floorP.u.E, ...EYE); gl.bindVertexArray(fv); gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
141
|
+
gl.disable(gl.BLEND);
|
|
142
|
+
draw(0);
|
|
143
|
+
gl.bindVertexArray(null); gl.disable(gl.DEPTH_TEST);
|
|
144
|
+
raf = requestAnimationFrame(frame);
|
|
145
|
+
}
|
|
146
|
+
raf = requestAnimationFrame(frame);
|
|
147
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
148
|
+
}
|