← Blurr Motion gallery-spline-embedded-playful
Categorie galleries Tier 3 Techniek #43 Deps ogl

Motion Lab / Galleries / 3D · playful

Speel met
dimensie.

Een live WebGL-blob — geen Spline scene-URL nodig. Pastel multi-color, zachte bounce, mouse-tracked rotatie.

scene.glsl
1. Mechanisme — kopieer 1-op-1, geen styling-keuzes
// 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);
2. Skeleton — DOM + class-namen, mag herschikken
<!-- 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>
3. Styling-template — verplicht eigen invulling per merk
/* 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); }