Motion Lab / Galleries / 3D · minimal
// Mechanisme: gallery-spline-embedded-minimal
// WebGL2 fragment-shader orb via OGL Triangle pass — geen Spline scene-URL nodig.
// Cream/ink monochroom, slow rotate, mouse-tracked yaw, soft-shadow ambient.
import { Renderer, Triangle, Program, Mesh } from "https://esm.sh/[email protected]";
const canvas = document.querySelector("[data-orb='minimal']");
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!canvas) throw new Error("orb canvas missing");
const renderer = new Renderer({ canvas, dpr: Math.min(devicePixelRatio, 2), alpha: true });
const gl = renderer.gl;
gl.clearColor(0,0,0,0);
const resize = () => {
const r = canvas.getBoundingClientRect();
renderer.setSize(r.width, r.height);
};
resize(); window.addEventListener("resize", resize);
const geometry = new Triangle(gl);
const program = new Program(gl, {
vertex: `attribute vec2 position; varying vec2 vUv;
void main(){ vUv = position * 0.5 + 0.5; gl_Position = vec4(position, 0.0, 1.0); }`,
fragment: `precision highp float;
uniform float uTime; uniform vec2 uRes; uniform vec2 uMouse;
varying vec2 vUv;
float sphere(vec3 p, float r){ return length(p) - r; }
vec3 rotY(vec3 p, float a){ float c=cos(a),s=sin(a); return vec3(c*p.x+s*p.z,p.y,-s*p.x+c*p.z);}
void main(){
vec2 uv = (vUv - 0.5) * vec2(uRes.x/uRes.y, 1.0);
vec3 ro = vec3(0.0, 0.0, -2.4);
vec3 rd = normalize(vec3(uv, 1.5));
float t = 0.0; float d = 0.0; vec3 p; float hit = 0.0;
for(int i=0;i<48;i++){
p = ro + rd*t;
p = rotY(p, uTime*0.12 + uMouse.x*0.6);
d = sphere(p, 0.85);
if(d<0.001){ hit = 1.0; break; }
t += d;
if(t>4.0) break;
}
vec3 paper = vec3(0.957, 0.945, 0.921);
vec3 ink = vec3(0.039);
float light = clamp(dot(normalize(p), normalize(vec3(0.4,0.6,-0.5))), 0.0, 1.0);
float rim = pow(1.0 - clamp(dot(normalize(p), normalize(-rd)),0.0,1.0), 2.0);
vec3 col = mix(ink*0.9, ink*0.3 + paper*0.15, light);
col += rim*0.18;
// soft shadow halo
float halo = smoothstep(1.05, 0.4, length(uv));
col = mix(paper, col, hit*halo);
// vignette
col *= 1.0 - 0.18*length(uv);
gl_FragColor = vec4(col, 1.0);
}`,
uniforms: {
uTime: { value: 0 },
uRes: { value: [canvas.clientWidth, canvas.clientHeight] },
uMouse: { value: [0, 0] },
}
});
const mesh = new Mesh(gl, { geometry, program });
const mouse = { x: 0, y: 0, tx: 0, ty: 0 };
canvas.addEventListener("pointermove", (e) => {
if (reduce) return;
const r = canvas.getBoundingClientRect();
mouse.tx = ((e.clientX - r.left) / r.width - 0.5) * 2;
mouse.ty = ((e.clientY - r.top) / r.height - 0.5) * 2;
});
let raf, t0 = performance.now();
const tick = (now) => {
const t = (now - t0) / 1000;
mouse.x += (mouse.tx - mouse.x) * 0.06;
mouse.y += (mouse.ty - mouse.y) * 0.06;
program.uniforms.uTime.value = reduce ? 0 : t;
program.uniforms.uMouse.value = [mouse.x, mouse.y];
program.uniforms.uRes.value = [canvas.clientWidth, canvas.clientHeight];
renderer.render({ scene: mesh });
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick); <!-- Skeleton: gallery-spline-embedded-minimal — WebGL2 orb, cream/ink -->
<section class="orb-stage">
<p class="orb-eyebrow">Motion Lab / Galleries / 3D · minimal</p>
<div class="orb-frame">
<canvas data-orb="minimal"></canvas>
<noscript><div class="orb-fallback orb-fallback-minimal" aria-hidden="true"></div></noscript>
</div>
<h2 class="orb-h">Stillness, rendered<br/>in three dimensions.</h2>
</section> /* Styling: gallery-spline-embedded-minimal — cream paper, ink orb */
.orb-stage{
--paper:#F4F1EB; --ink:#0A0A0A; --muted:rgba(10,10,10,.45);
background:var(--paper); color:var(--ink);
min-height:100vh; display:grid; place-items:center;
padding:clamp(3rem,8vw,8rem) clamp(1.5rem,4vw,4rem);
}
.orb-eyebrow{ font-family:"JetBrains Mono",monospace; font-size:.7rem; letter-spacing:.18em; text-transform:uppercase; color:var(--muted); margin:0 0 2rem; }
.orb-frame{ position:relative; width:min(1100px,100%); aspect-ratio:16/10; overflow:hidden; border-radius:2px; background:var(--paper); }
.orb-frame canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }
.orb-fallback-minimal{ position:absolute; inset:0;
background: radial-gradient(circle at 50% 50%, #0A0A0A 0%, rgba(10,10,10,.4) 25%, rgba(10,10,10,0) 55%); }
.orb-h{ font-family:"Fraunces",serif; font-weight:300; font-size:clamp(2rem,5vw,4rem); line-height:1.05; letter-spacing:-.02em; margin:2rem 0 0; max-width:40ch; }
@media (prefers-reduced-motion: reduce){ .orb-frame canvas{ filter:none; } }