developer playground

The Amplify Lab

Nine working experiments in what a website can feel like, running live in your browser. Built in-house at OJ Amplify.

Everything here is safe to touch. Move, click, drag, play. There's a mini game at the end.

Heavy exhibits are GPU-gated with designed CSS fallbacks; older hardware gets a calmer version automatically. That discipline is part of the demo.

exhibit 01 · this background is being painted live · follow it with your cursorexhibit 01 · signal field · hand-written GLSL · raw WebGL · static

exhibit 00

The gate

Every heavy exhibit on this page passes through one decision tree before a single byte of three.js is fetched. A detection miss means "calmer page," never "broken page."

eager lab JS ≈ 13 KB · three.js ≈ 170 KB gz, lazy, gpu path only · DPR capped 1.75 / 1.5 mobile · everything pauses offscreen

probe WEBGL rendererUNMASKED_RENDERER_WEBGLsoftware renderer?swiftshader · llvmpipe · mesaprefers-reduced-motion?os-level setting winssaveData on?navigator.connectionlive rendercss fallbackstatic / reducedhw + motion + data okyes → fallbackreduce → static
view source: the hero's fragment shader (runs as-is)
precision highp float;
uniform vec2 uRes;
uniform float uT;
uniform vec2 uP;
uniform float uE;

float h(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453123);}
float n2(vec2 p){
  vec2 i=floor(p),f=fract(p);
  f=f*f*(3.-2.*f);
  return mix(mix(h(i),h(i+vec2(1.,0.)),f.x),mix(h(i+vec2(0.,1.)),h(i+vec2(1.,1.)),f.x),f.y);
}
float fbm(vec2 p){
  float v=0.,a=.5;
  for(int i=0;i<5;i++){v+=a*n2(p);p=p*2.03+vec2(1.7,4.1);a*=.5;}
  return v;
}

void main(){
  vec2 uv=(gl_FragCoord.xy*2.-uRes)/min(uRes.x,uRes.y);
  float t=uT*.055;
  vec2 p=uv*1.25;

  float d=length(uv-uP);
  float push=uE*exp(-d*d*2.6);

  vec2 q=vec2(fbm(p+t),fbm(p+vec2(5.2,1.3)-t*.8));
  vec2 r=vec2(fbm(p+2.2*q+vec2(1.7,9.2)+t*1.4+push*.6),fbm(p+2.2*q+vec2(8.3,2.8)-t));
  float f=fbm(p+2.4*r+push*.8);

  vec3 ink=vec3(.071,.075,.086);
  vec3 ember=vec3(.36,.10,.015);
  vec3 orange=vec3(1.,.404,.118);
  vec3 pale=vec3(1.,.72,.5);

  vec3 col=mix(ink,ember,smoothstep(.18,.62,f));
  col=mix(col,orange,smoothstep(.58,.88,f)*.9);
  col=mix(col,pale,smoothstep(.88,1.05,f)*.7);
  col+=orange*push*.25;
  col*=1.-.32*clamp(dot(uv*.62,uv*.62),0.,1.);

  gl_FragColor=vec4(col,1.);
}

exhibit 02

Liquid chrome

Your product, staged in living 3D. No photo shoot, no video crew, right in the browser. Point at it, then grab the dials: melt it, stir it.

An icosphere displaced by simplex noise in its vertex shader, physical material lit by a generated studio environment. The dials write straight into the shader's uniforms.

Three.js · physical materials · PMREM lighting · static

live uniforms: you're editing the vertex shader's inputs

exhibit 03

Terminal dimension

We can make plain text do this. Grab the spin dial and take it for a ride.

A 3D engine in sixty lines of math, drawn entirely in text. No WebGL, no graphics library: a torus, projected and lit on the CPU, printed in luminance glyphs thirty times a second.

pure JavaScript · CPU only · text glyphs · static

booting…
view source: the projection + z-buffer loop
function render() {
    const out = new Array(W * H).fill(" ");
    const zbuf = new Float32Array(W * H);
    const cA = Math.cos(A), sA = Math.sin(A);
    const cB = Math.cos(B), sB = Math.sin(B);
    for (let th = 0; th < 6.28; th += 0.07) {
      const ct = Math.cos(th), st = Math.sin(th);
      const cx = R2 + R1 * ct, cy = R1 * st;
      for (let ph = 0; ph < 6.28; ph += 0.02) {
        const cp = Math.cos(ph), sp = Math.sin(ph);
        const x = cx * (cB * cp + sA * sB * sp) - cy * cA * sB;
        const y = cx * (sB * cp - sA * cB * sp) + cy * cA * cB;
        const z = K2 + cA * cx * sp + cy * sA;
        const ooz = 1 / z;
        const xp = (W / 2 + K1 * ooz * x) | 0;
        const yp = (H / 2 - (K1 * 0.5) * ooz * y) | 0;
        const idx = xp + W * yp;
        if (yp >= 0 && yp < H && xp >= 0 && xp < W && ooz > zbuf[idx]) {
          const L =
            cp * ct * sB - cA * ct * sp - sA * st + cB * (cA * st - ct * sA * sp);
          zbuf[idx] = ooz;
          out[idx] = CHARS[L > 0 ? Math.min((L * 8) | 0, 12) : 0];
        }
      }
    }
    let s = "";
    for (let r = 0; r < H; r++) s += out.slice(r * W, r * W + W).join("") + "\n";
    pre.textContent = s;
  }

exhibit 04

Type gymnasium

Type that moves the way your brand sounds. Scroll fast and watch the middle line flex.

Type that performs instead of sitting still. Variable fonts expose their design axes to code, so letterforms can breathe, react, and travel.

variable fonts · scroll velocity · SVG paths · static

Fraunces: opsz · wght · SOFT · WONK, on a loop

a serif that breathes

Otherworldly, on demand

Archivo: weight and width follow your scroll speed

this one reacts to how fast you scroll

Scroll fast. Watch me flex.

decode on arrival

SIGNAL OVER NOISE

exhibit 05

Spectrum shift

Four brands, one room. Tap a chip (or the big word) and everything repaints. Even the card changes its voice. Rebrands don't have to be scary.

Background, text, and accent are live CSS variables interpolated in real time as you scroll. The sample card swaps persona per palette, and the headline glyphs split near your pointer.

CSS custom properties · live interpolation · static

Amplify

sample report · demo copy

Traffic, translated

Organic sessions climbed all year after the rebuild. Steady work, measured monthly.

View the numbers

TONIGHT ONLY

BASEMENT FREQUENCIES

Doors at midnight. Lineup secret. Bring earplugs, lose them immediately.

GRAB TICKETS

from the archive

On leaving well

Some essays end quietly; the best ones simply stop.

Continue reading

run complete

build 4021 passed

0 errors. 0 warnings. 47ms. nothing else to report.

cat changelog

exhibit 06

Scroll cinema

A little lookbook. Grab it and drag sideways, like flipping pages. Every page is still a toy: the spotlight, the loose numbers, the satellites.

A lerp-smoothed horizontal track: pointer drag with velocity coasting, wheel support, snap-to-panel, and a progress rail. Reveals ride the track's own progress. On phones it collapses to a vertical column and the entrances go back to pure-CSS animation-timeline.

lerp 0.09 · inertial flick ~260ms · pointer capture · vertical fallback · static

on narrow screens (or without animation-timeline support) the cards stack vertically. that's the designed fallback, not a bug

a tiny lookbook

drag

pointer-driven radial mask · background-clip textyou're holding the spotlight. move it around
+38% boing1.2s hang time0 guesswork
pointer capture · spring settle on releasethe numbers are loose. toss them
rAF-driven orbits · IO-paused offscreenyour own little solar system. add satellites, crank the orbit
view source: the horizontal track engine (lerp · flick · snap)
function initTrack(root, motionOK) {
  const wide = matchMedia("(min-width: 64rem)").matches;
  const viewport = root.querySelector(".cine-viewport");
  const track = root.querySelector(".cine-track");
  if (!viewport || !track || !wide || !motionOK) return null;

  const panels = [...track.querySelectorAll(".cine-panel")];
  const rail = root.querySelector(".cine-rail");
  const railFill = rail?.querySelector("span");

  let target = 0, current = -1, raf = 0, active = false;
  let dragging = false, startX = 0, startTarget = 0, samples = [];
  let snapTimer = 0;

  const max = () => Math.max(0, track.scrollWidth - viewport.clientWidth);
  const clamp = (v) => Math.min(max(), Math.max(0, v));

  function offsets() {
    // normalize to the first panel so the track's centering pad cancels out
    const pad = panels[0].offsetLeft;
    return panels.map((p) => Math.min(p.offsetLeft - pad, max()));
  }
  function nearestPanel(v) {
    const os = offsets();
    let best = 0;
    os.forEach((o, i) => {
      if (Math.abs(o - v) < Math.abs(os[best] - v)) best = i;
    });
    return best;
  }
  function snap() {
    const i = nearestPanel(target);
    target = clamp(offsets()[i]);
    if (rail) rail.setAttribute("aria-valuenow", i);
    wake();
  }
  function seekPanel(i) {
    const os = offsets();
    target = clamp(os[Math.max(0, Math.min(panels.length - 1, i))]);
    if (rail) rail.setAttribute("aria-valuenow", i);
    wake();
  }

  function frame() {
    raf = 0;
    const d = target - current;
    current = Math.abs(d) < 0.4 && !dragging ? target : current + d * 0.09;
    track.style.transform = `translate3d(${(-current).toFixed(2)}px, 0, 0)`;
    // per-panel progress (−1..1) drives parallax + entrance
    const vpc = viewport.clientWidth / 2;
    panels.forEach((p) => {
      const pp = Math.max(-1, Math.min(1, (p.offsetLeft + p.offsetWidth / 2 - current - vpc) / viewport.clientWidth));
      p.style.setProperty("--pp", pp.toFixed(3));
      p.style.setProperty("--ppa", Math.abs(pp).toFixed(3));
    });
    if (railFill) {
      const m = max();
      railFill.style.transform = `scaleX(${(m ? current / m : 0).toFixed(4)})`;
    }
    if (active && (dragging || Math.abs(target - current) >= 0.4)) {
      raf = requestAnimationFrame(frame);
    }
  }
  function wake() {
    if (active && !raf) raf = requestAnimationFrame(frame);
  }

  // Drag with inertial flick — toys win any pointer dispute.
  viewport.addEventListener("pointerdown", (e) => {
    if (e.target.closest("button, input, a, .cine-chips span, .cine-sat-ui, details")) return;
    dragging = true;
    viewport.classList.add("cine-grabbing");
    startX = e.clientX;
    startTarget = target;
    samples = [{ x: e.clientX, t: performance.now() }];
    clearTimeout(snapTimer);
    try { viewport.setPointerCapture(e.pointerId); } catch {}
    wake();
  });
  viewport.addEventListener("pointermove", (e) => {
    if (!dragging) return;
    target = clamp(startTarget - (e.clientX - startX));
    samples.push({ x: e.clientX, t: performance.now() });
    if (samples.length > 6) samples.shift();
  }, { passive: true });
  function release() {
    if (!dragging) return;
    dragging = false;
    viewport.classList.remove("cine-grabbing");
    // velocity over the last ~80ms → coast ~260ms further, then snap
    const now = performance.now();
    const old = samples.find((s) => now - s.t < 90) || samples[0];
    const last = samples[samples.length - 1];
    const dt = last.t - old.t;
    if (dt > 0) {
      const v = (last.x - old.x) / dt; // px per ms
      target = clamp(target - v * 260);
    }
    snapTimer = setTimeout(snap, 60);
    wake();
  }
  viewport.addEventListener("pointerup", release);
  viewport.addEventListener("pointercancel", release);

  // Horizontal wheel / shift+wheel — plain vertical wheel keeps scrolling the page.
  viewport.addEventListener(
    "wheel",
    (e) => {
      const dx = e.shiftKey ? e.deltaY : e.deltaX;
      if (Math.abs(dx) <= Math.abs(e.deltaY) && !e.shiftKey) return;
      e.preventDefault();
      target = clamp(target + dx);
      clearTimeout(snapTimer);
      snapTimer = setTimeout(snap, 160);
      wake();
    },
    { passive: false }
  );

  // Keyboard: arrows page through panels; focus inside a panel brings it into view.
  viewport.addEventListener("keydown", (e) => {
    if (e.key === "ArrowRight") { seekPanel(nearestPanel(target) + 1); e.preventDefault(); }
    else if (e.key === "ArrowLeft") { seekPanel(nearestPanel(target) - 1); e.preventDefault(); }
  });
  track.addEventListener("focusin", (e) => {
    const p = e.target.closest(".cine-panel");
    if (p) seekPanel(panels.indexOf(p));
  });

  // Progress rail: click or drag to seek.
  if (rail) {
    let railDrag = false;
    const seekFromEvent = (e) => {
      const r = rail.getBoundingClientRect();
      target = clamp(((e.clientX - r.left) / r.width) * max());
      wake();
    };
    rail.addEventListener("pointerdown", (e) => {
      railDrag = true;
      try { rail.setPointerCapture(e.pointerId); } catch {}
      seekFromEvent(e);
    });
    rail.addEventListener("pointermove", (e) => railDrag && seekFromEvent(e), { passive: true });
    const railDrop = () => {
      if (!railDrag) return;
      railDrag = false;
      snap();
    };
    rail.addEventListener("pointerup", railDrop);
    rail.addEventListener("pointercancel", railDrop);
    rail.addEventListener("keydown", (e) => {
      if (e.key === "ArrowRight" || e.key === "ArrowUp") { seekPanel(nearestPanel(target) + 1); e.preventDefault(); }
      else if (e.key === "ArrowLeft" || e.key === "ArrowDown") { seekPanel(nearestPanel(target) - 1); e.preventDefault(); }
    });
  }

  addEventListener("resize", () => { target = clamp(target); wake(); }, { passive: true });

  return {
    start() { active = true; wake(); },
    stop() { active = false; cancelAnimationFrame(raf); raf = 0; },
  };
}

exhibit 07

Microcosm

The small touches that make a site feel expensive. Visitors trust a site that sweats details like these. Try all eight.

The small interactions that make a site feel expensive. Each one is a few lines of code and runs entirely on the compositor, so nothing here can make a page stutter.

vanilla JS · compositor-only transforms · static

magnetic pull

sample card

±8°

its own tilt range, live

spring tilt

repelling field

spring easing, no JS

HOVER TO DECODE

text scramble

click to copy

confetti, earned

pluck the string

exhibit 08

Live data

Reports you'll actually want to open. Grab any dot and drag it: the whole report redraws around you. Demo data, real physics.

The line draws in with real spring physics, the scrubber follows your pointer, and every data point is draggable: the smoothed path rebuilds live. Demo data. Clients get the genuine article.

SVG · spring physics · no chart library · static

hover the chart

JFMAMJJASOND
12months you can drag
60fpsspring-powered redraws
0spreadsheets involved
view source: the spring integrator on the dash offset
// Critically-damped-ish spring on the dash offset.
    let v = 0, p = len;
    const k = K, c = C;
    let last = performance.now();
    (function step(now) {
      const dt = Math.min(0.05, (now - last) / 1000);
      last = now;
      const a = -k * p - c * v;
      v += a * dt;
      p += v * dt;
      if (Math.abs(p) < 0.5 && Math.abs(v) < 0.5) {
        line.style.strokeDashoffset = "0";
        if (area) { area.style.transition = "opacity 900ms ease"; area.style.opacity = "1"; }
        return;
      }
      line.style.strokeDashoffset = `${Math.max(0, p)}`;
      requestAnimationFrame(step);
    })(last);

exhibit 09

Fresh squeezed

Catch the real stuff. Dodge the hype. Twenty seconds. That's the whole business, as a game.

A 20-second arcade loop in canvas 2D: pooled particles, squash-and-stretch, hit-stop, screen shake. The full juice inventory, at 60fps, in the brand palette.

canvas 2D · pooled particles · one-thumb input · static

exhibit 09

Fresh squeezed

catch the oranges · dodge the buzzwords

tap to squeeze

view source: pooled particle burst (no allocations mid-game)
function burst(x, y, n, sour) {
    let made = 0;
    for (const p of parts) {
      if (p.live) continue;
      p.live = true;
      p.x = x; p.y = y;
      const a = Math.random() * Math.PI * 2, sp = 60 + Math.random() * 160;
      p.vx = Math.cos(a) * sp; p.vy = Math.sin(a) * sp - 80;
      p.r = 2 + Math.random() * 3.5;
      p.t = 0;
      p.sour = sour;
      if (++made >= n) break;
    }
  }

Your website could feel like this.

Every exhibit here ships at production quality: accessible, fast, and graceful on older devices. That's the bar we hold for client work, too.

designed and engineered in-house at OJ Amplify: humans plus our AI-assisted workflow