a tiny lookbook
drag
developer playground
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
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
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
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
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…
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 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
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.
we design and build brands like this →
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 numbersTONIGHT ONLY
BASEMENT FREQUENCIES
Doors at midnight. Lineup secret. Bring earplugs, lose them immediately.
GRAB TICKETSfrom the archive
On leaving well
Some essays end quietly; the best ones simply stop.
Continue readingrun complete
build 4021 passed
0 errors. 0 warnings. 47ms. nothing else to report.
cat changelogexhibit 06
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
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
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.
details like these lift conversion →
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
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.
reporting comes with every retainer →
SVG · spring physics · no chart library · static
hover the chart
// 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
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
0 oz squeezed
0 buzzwords dodged · 0 hype swallowed
We squeeze the juice. We skip the hype.
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;
}
}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