Motion Lab / Galleries / 3D · playful
Een live WebGL-blob — geen Spline scene-URL nodig. Pastel multi-color, zachte bounce, mouse-tracked rotatie.
// Mechanisme: gallery-spline-embedded-playful
// WebGL2 fragment-shader blob via OGL Triangle pass — pastel multi-color (sky+mint+rose), bounce.
import { Renderer, Triangle, Program, Mesh } from "https://esm.sh/[email protected]";
const canvas = document.querySelector("[data-orb='playful']");
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;
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 blob(vec3 p, float t){
float r = 0.7 + 0.06*sin(t*1.7) + 0.05*sin(p.y*4.0 + t*1.3) + 0.04*sin(p.x*5.0 + t*0.8);
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);}
vec3 rotX(vec3 p, float a){ float c=cos(a),s=sin(a); return vec3(p.x,c*p.y-s*p.z,s*p.y+c*p.z);}
void main(){
vec2 uv = (vUv - 0.5) * vec2(uRes.x/uRes.y, 1.0);
// bounce
float bounce = abs(sin(uTime*1.6)) * 0.12;
vec3 ro = vec3(0.0, 0.0, -2.4);
vec3 rd = normalize(vec3(uv, 1.4));
float t=0.0,d=0.0,hit=0.0; vec3 p;
for(int i=0;i<56;i++){
p = ro + rd*t;
p.y -= bounce;
p = rotY(p, uTime*0.5 + uMouse.x*0.8);
p = rotX(p, uTime*0.3 + uMouse.y*0.5);
d = blob(p, uTime);
if(d<0.001){ hit=1.0; break; }
t += d;
if(t>4.0) break;
}
vec3 sky = vec3(0.62, 0.84, 0.97);
vec3 mint = vec3(0.69, 0.94, 0.82);
vec3 rose = vec3(0.99, 0.74, 0.78);
vec3 nrm = normalize(p);
vec3 col = sky;
col = mix(col, mint, smoothstep(-0.3, 0.5, nrm.y));
col = mix(col, rose, smoothstep(-0.4, 0.7, nrm.x));
float light = clamp(dot(nrm, normalize(vec3(0.4,0.7,-0.6))), 0.0, 1.0);
col = col * (0.55 + 0.55*light);
float rim = pow(1.0 - clamp(dot(nrm, normalize(-rd)),0.0,1.0), 2.5);
col += rim * vec3(1.0,1.0,1.0) * 0.35;
vec3 bg = mix(vec3(1.0,0.97,0.95), vec3(0.95,0.94,1.0), vUv.y);
col = mix(bg, col, hit);
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;
});
const t0 = performance.now();
const tick = (now) => {
const t = (now - t0) / 1000;
mouse.x += (mouse.tx - mouse.x) * 0.08;
mouse.y += (mouse.ty - mouse.y) * 0.08;
program.uniforms.uTime.value = reduce ? 0.6 : t;
program.uniforms.uMouse.value = [mouse.x, mouse.y];
program.uniforms.uRes.value = [canvas.clientWidth, canvas.clientHeight];
renderer.render({ scene: mesh });
if (!reduce) requestAnimationFrame(tick);
};
requestAnimationFrame(tick); <!-- Skeleton: gallery-spline-embedded-playful — WebGL2 pastel blob -->
<section class="pblob-stage">
<header class="pblob-meta">
<p class="pblob-eyebrow">Motion Lab / Galleries / 3D · playful</p>
<h2 class="pblob-title">Speel met<br/><em>dimensie.</em></h2>
<p class="pblob-sub">Een live WebGL-blob — geen Spline scene-URL nodig. Pastel multi-color, zachte bounce, mouse-tracked rotatie.</p>
</header>
<div class="pblob-frame">
<canvas data-orb="playful"></canvas>
<div class="pblob-tag">scene.glsl</div>
</div>
</section> /* Styling: gallery-spline-embedded-playful — pastel sky+mint+rose */
.pblob-stage{
--bg:#FFF7F3; --fg:#1A1410; --accent:#FF4A1C; --soft:#FFB5A7;
background:var(--bg); color:var(--fg);
min-height:80vh; padding:clamp(2.5rem,6vw,5rem) clamp(1.25rem,4vw,4rem);
display:grid; gap:clamp(1.5rem,3vw,3rem);
font-family:"Inter",system-ui,sans-serif;
}
@media (min-width:900px){ .pblob-stage{ grid-template-columns:minmax(260px,420px) 1fr; align-items:center; } }
.pblob-eyebrow{ font-family:"JetBrains Mono",monospace; font-size:.7rem; letter-spacing:.18em; text-transform:uppercase; color:rgba(26,20,16,.55); margin:0 0 1rem; }
.pblob-title{ font-family:"Fraunces",Georgia,serif; font-weight:400; font-size:clamp(2.4rem,5.5vw,4.4rem); line-height:.98; letter-spacing:-.02em; margin:0 0 1.25rem; }
.pblob-title em{ font-style:italic; background:linear-gradient(120deg,var(--accent),#ff7a4a); -webkit-background-clip:text; background-clip:text; color:transparent; }
.pblob-sub{ font-size:1rem; line-height:1.6; max-width:38ch; color:rgba(26,20,16,.72); margin:0; }
.pblob-frame{ position:relative; aspect-ratio:4/3; width:100%; border-radius:32px; overflow:hidden;
background:#FFE9E1;
box-shadow:0 1px 0 rgba(255,255,255,.8) inset, 0 30px 60px -20px rgba(255,74,28,.25), 0 8px 20px -8px rgba(26,20,16,.12); }
.pblob-frame canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }
.pblob-tag{ position:absolute; left:18px; bottom:14px; z-index:3;
font-family:"JetBrains Mono",monospace; font-size:.65rem; letter-spacing:.14em; text-transform:uppercase;
color:var(--fg); background:rgba(255,255,255,.7); padding:.45rem .7rem; border-radius:999px; backdrop-filter:blur(6px); }