Dither Image
Bayer and blue-noise dithering of any image into a 2–4 ink palette, with a cursor loupe that sharpens, inverts or reveals.
Your mark as ordered-dither stipple dots — each one a spring particle that scatters from the cursor and settles back.
Browser support · WebGL2 / WebGL1 point sprites. Without WebGL a dotted CSS mask fallback is shown.
npx shadcn@latest add https://freedesignmd.com/r/dither-logo.json1/**2 * DitherLogo — a mark rendered as ordered-dither stipple dots. Every dot is a3 * particle on a spring: the cursor scatters them, they settle back with a4 * little overshoot. On first view the dots assemble from a loose cloud.5 *6 * Props7 * - path?: string SVG path data (any viewBox), filled with `color`. Overrides the built-in mark.8 * - viewBox?: number Size of the path's square viewBox. Default 24.9 * - text?: string Alternative: a word/glyph drawn as the mark.10 * - color?: string Dot colour. Default "#EDEDF2".11 * - accent?: string Second ink used by the built-in mark's planet. Default "#FF7A45".12 * - spacing?: number Dot grid in CSS px. Default 4.13 * - dotScale?: number Dot diameter relative to spacing. Default 0.7.14 * - radius?: number Cursor repel radius in CSS px. Default 90.15 * - strength?: number Cursor force. Default 1.16 * - assemble?: boolean Intro assembly animation. Default true.17 * - label?: string Accessible name (canvas gets role="img"); omit for decorative.18 * CSS vars: --fd-dlogo-fallback.19 *20 * Browser support: WebGL2 / WebGL1 (point sprites). Without WebGL a CSS dotted21 * mask fallback is shown. Reduced motion: no assembly, no scatter, still dots.22 */23import { useEffect, useRef, useState } from "react";24import type { CSSProperties } from "react";25 26type GL = WebGLRenderingContext | WebGL2RenderingContext;27 28const VERT = `29precision highp float;30attribute vec2 aPos;31attribute vec3 aCol;32attribute float aSize;33uniform vec2 uRes;34uniform float uPt;35varying vec3 vCol;36void main(){37 vec2 p = aPos / uRes * 2. - 1.;38 gl_Position = vec4(p.x, -p.y, 0., 1.);39 gl_PointSize = uPt * aSize;40 vCol = aCol;41}`;42const FRAG = `43precision highp float;44varying vec3 vCol;45uniform float uPt;46void main(){47 vec2 c = gl_PointCoord - .5;48 float d = length(c) * 2.;49 float aa = 2. / max(uPt, 1.);50 float a = 1. - smoothstep(1. - aa, 1., d);51 if (a <= 0.) discard;52 gl_FragColor = vec4(vCol * a, a);53}`;54 55/** Classic 8×8 Bayer matrix, normalised to (0,1). Base cell [[0,2],[3,1]], recursed per bit. */56const BAYER8 = (() => {57 const base = [0, 2, 3, 1];58 const m = new Float32Array(64);59 for (let y = 0; y < 8; y++)60 for (let x = 0; x < 8; x++) {61 let v = 0;62 for (let k = 0; k < 3; k++) v += base[(((y >> k) & 1) << 1) | ((x >> k) & 1)] * 4 ** (2 - k);63 m[y * 8 + x] = (v + 0.5) / 64;64 }65 return m;66})();67 68const hexToRgb = (hex: string): [number, number, number] => {69 let h = hex.replace("#", "").trim();70 if (h.length === 3) h = h.split("").map((c) => c + c).join("");71 const n = parseInt(h.slice(0, 6), 16) || 0;72 return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];73};74 75/** Built-in mark: squircle app icon with a lit planet sliced by horizon stripes. */76function drawMark(ctx: CanvasRenderingContext2D, S: number, color: string, accent: string) {77 const r = S * 0.24;78 const pad = S * 0.04;79 const sq = (inset: number) => {80 const a = pad + inset;81 const b = S - pad - inset;82 const rr = r - inset * 0.6;83 ctx.beginPath();84 ctx.moveTo(a + rr, a);85 ctx.lineTo(b - rr, a);86 ctx.quadraticCurveTo(b, a, b, a + rr);87 ctx.lineTo(b, b - rr);88 ctx.quadraticCurveTo(b, b, b - rr, b);89 ctx.lineTo(a + rr, b);90 ctx.quadraticCurveTo(a, b, a, b - rr);91 ctx.lineTo(a, a + rr);92 ctx.quadraticCurveTo(a, a, a + rr, a);93 ctx.closePath();94 };95 // icon body: soft top-lit tone (alpha encodes density)96 const body = ctx.createLinearGradient(0, pad, S * 0.3, S - pad);97 body.addColorStop(0, color + "88");98 body.addColorStop(0.55, color + "30");99 body.addColorStop(1, color + "12");100 ctx.fillStyle = body;101 sq(0);102 ctx.fill();103 // crisp rim104 ctx.lineWidth = S * 0.022;105 ctx.strokeStyle = color;106 sq(S * 0.011);107 ctx.stroke();108 109 // planet110 const cx = S * 0.5;111 const cy = S * 0.53;112 const R = S * 0.25;113 ctx.save();114 ctx.beginPath();115 ctx.arc(cx, cy, R, 0, Math.PI * 2);116 ctx.clip();117 const g = ctx.createRadialGradient(cx - R * 0.45, cy - R * 0.5, R * 0.05, cx, cy, R * 1.05);118 g.addColorStop(0, accent);119 g.addColorStop(0.45, accent + "f0");120 g.addColorStop(0.8, accent + "80");121 g.addColorStop(1, accent + "30");122 ctx.fillStyle = g;123 ctx.fillRect(cx - R, cy - R, R * 2, R * 2);124 // horizon stripes: cut gaps that grow toward the bottom125 ctx.globalCompositeOperation = "destination-out";126 let y = cy + R * 0.08;127 let gap = R * 0.07;128 while (y < cy + R) {129 ctx.fillRect(cx - R, y, R * 2, gap);130 y += gap + R * 0.15;131 gap *= 1.4;132 }133 ctx.restore();134 // orbit ring in front135 ctx.save();136 ctx.translate(cx, cy);137 ctx.rotate(-0.32);138 ctx.strokeStyle = color + "ee";139 ctx.lineWidth = S * 0.018;140 ctx.beginPath();141 ctx.ellipse(0, 0, R * 1.55, R * 0.36, 0, Math.PI * 0.02, Math.PI * 0.98);142 ctx.stroke();143 ctx.restore();144}145 146interface Particles {147 n: number;148 home: Float32Array;149 pos: Float32Array;150 vel: Float32Array;151 col: Float32Array;152 size: Float32Array;153}154 155function sample(156 W: number,157 H: number,158 spacing: number,159 opts: { path?: string; viewBox: number; text?: string; color: string; accent: string },160): Particles {161 const S = Math.max(16, Math.floor(Math.min(W, H)));162 const off = document.createElement("canvas");163 off.width = S;164 off.height = S;165 const ctx = off.getContext("2d", { willReadFrequently: true });166 const empty = { n: 0, home: new Float32Array(), pos: new Float32Array(), vel: new Float32Array(), col: new Float32Array(), size: new Float32Array() };167 if (!ctx) return empty;168 if (opts.path) {169 ctx.save();170 const k = (S * 0.92) / opts.viewBox;171 ctx.translate(S * 0.04, S * 0.04);172 ctx.scale(k, k);173 ctx.fillStyle = opts.color;174 ctx.fill(new Path2D(opts.path));175 ctx.restore();176 } else if (opts.text) {177 ctx.fillStyle = opts.color;178 ctx.textAlign = "center";179 ctx.textBaseline = "middle";180 let fs = S * 0.8;181 ctx.font = `800 ${fs}px ui-sans-serif, -apple-system, "SF Pro Display", "Inter", "Segoe UI", Roboto, sans-serif`;182 const tw = ctx.measureText(opts.text).width;183 if (tw > S * 0.92) fs *= (S * 0.92) / tw;184 ctx.font = `800 ${fs}px ui-sans-serif, -apple-system, "SF Pro Display", "Inter", "Segoe UI", Roboto, sans-serif`;185 ctx.fillText(opts.text, S / 2, S / 2);186 } else {187 drawMark(ctx, S, opts.color, opts.accent);188 }189 const data = ctx.getImageData(0, 0, S, S).data;190 const ox = (W - S) / 2;191 const oy = (H - S) / 2;192 const home: number[] = [];193 const col: number[] = [];194 const size: number[] = [];195 const cols = Math.floor(S / spacing);196 for (let gy = 0; gy < cols; gy++) {197 for (let gx = 0; gx < cols; gx++) {198 const px = Math.floor((gx + 0.5) * spacing);199 const py = Math.floor((gy + 0.5) * spacing);200 const i = (py * S + px) * 4;201 const a = data[i + 3] / 255;202 if (a < 0.02) continue;203 if (a < BAYER8[(gy & 7) * 8 + (gx & 7)]) continue;204 home.push(ox + px, oy + py);205 // un-premultiply is not needed: getImageData returns straight alpha206 col.push(data[i] / 255, data[i + 1] / 255, data[i + 2] / 255);207 size.push(a > 0.9 ? 1 : 0.86);208 }209 }210 const n = home.length / 2;211 return {212 n,213 home: new Float32Array(home),214 pos: new Float32Array(home),215 vel: new Float32Array(n * 2),216 col: new Float32Array(col),217 size: new Float32Array(size),218 };219}220 221export interface DitherLogoProps {222 path?: string;223 viewBox?: number;224 text?: string;225 color?: string;226 accent?: string;227 spacing?: number;228 dotScale?: number;229 radius?: number;230 strength?: number;231 assemble?: boolean;232 label?: string;233 className?: string;234 style?: CSSProperties;235}236 237export function DitherLogo({238 path,239 viewBox = 24,240 text,241 color = "#EDEDF2",242 accent = "#FF7A45",243 spacing = 4,244 dotScale = 0.7,245 radius = 90,246 strength = 1,247 assemble = true,248 label,249 className,250 style,251}: DitherLogoProps) {252 const canvasRef = useRef<HTMLCanvasElement>(null);253 const [failed, setFailed] = useState(false);254 const live = useRef({ dotScale, radius, strength, spacing });255 live.current = { dotScale, radius, strength, spacing };256 257 useEffect(() => {258 const canvas = canvasRef.current;259 if (!canvas) return;260 const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;261 const attrs: WebGLContextAttributes = { antialias: false, alpha: true, premultipliedAlpha: true };262 let gl: GL | null = canvas.getContext("webgl2", attrs) as WebGL2RenderingContext | null;263 if (!gl) gl = canvas.getContext("webgl", attrs) as WebGLRenderingContext | null;264 if (!gl) {265 setFailed(true);266 return;267 }268 const g = gl;269 const compile = (type: number, src: string) => {270 const s = g.createShader(type)!;271 g.shaderSource(s, src);272 g.compileShader(s);273 if (!g.getShaderParameter(s, g.COMPILE_STATUS)) console.warn("[DitherLogo]", g.getShaderInfoLog(s));274 return s;275 };276 const prog = g.createProgram()!;277 g.attachShader(prog, compile(g.VERTEX_SHADER, VERT));278 g.attachShader(prog, compile(g.FRAGMENT_SHADER, FRAG));279 g.linkProgram(prog);280 if (!g.getProgramParameter(prog, g.LINK_STATUS)) {281 console.warn("[DitherLogo]", g.getProgramInfoLog(prog));282 setFailed(true);283 return;284 }285 g.useProgram(prog);286 g.enable(g.BLEND);287 g.blendFunc(g.ONE, g.ONE_MINUS_SRC_ALPHA);288 const uRes = g.getUniformLocation(prog, "uRes");289 const uPt = g.getUniformLocation(prog, "uPt");290 const posBuf = g.createBuffer();291 const colBuf = g.createBuffer();292 const sizeBuf = g.createBuffer();293 const aPos = g.getAttribLocation(prog, "aPos");294 const aCol = g.getAttribLocation(prog, "aCol");295 const aSize = g.getAttribLocation(prog, "aSize");296 297 let P: Particles | null = null;298 let W = 1;299 let H = 1;300 let dpr = 1;301 let raf = 0;302 let last = 0;303 let visible = false;304 let assembled = !assemble || reduced;305 const ptr = { x: -9999, y: -9999, px: -9999, py: -9999, in: false };306 307 const upload = () => {308 if (!P) return;309 g.bindBuffer(g.ARRAY_BUFFER, colBuf);310 g.bufferData(g.ARRAY_BUFFER, P.col, g.STATIC_DRAW);311 g.enableVertexAttribArray(aCol);312 g.vertexAttribPointer(aCol, 3, g.FLOAT, false, 0, 0);313 g.bindBuffer(g.ARRAY_BUFFER, sizeBuf);314 g.bufferData(g.ARRAY_BUFFER, P.size, g.STATIC_DRAW);315 g.enableVertexAttribArray(aSize);316 g.vertexAttribPointer(aSize, 1, g.FLOAT, false, 0, 0);317 g.bindBuffer(g.ARRAY_BUFFER, posBuf);318 g.bufferData(g.ARRAY_BUFFER, P.pos, g.DYNAMIC_DRAW);319 g.enableVertexAttribArray(aPos);320 g.vertexAttribPointer(aPos, 2, g.FLOAT, false, 0, 0);321 };322 323 const scatter = () => {324 if (!P) return;325 for (let i = 0; i < P.n; i++) {326 const a = Math.random() * Math.PI * 2;327 const d = Math.min(W, H) * (0.25 + Math.random() * 0.45);328 P.pos[i * 2] = P.home[i * 2] + Math.cos(a) * d;329 P.pos[i * 2 + 1] = P.home[i * 2 + 1] + Math.sin(a) * d;330 P.vel[i * 2] = 0;331 P.vel[i * 2 + 1] = 0;332 }333 };334 335 const step = (dt: number) => {336 if (!P) return false;337 const { radius: R, strength: F } = live.current;338 const k = 120;339 const c = 11;340 const R2 = R * R;341 const pvx = ptr.x - ptr.px;342 const pvy = ptr.y - ptr.py;343 ptr.px = ptr.x;344 ptr.py = ptr.y;345 let energy = 0;346 const pos = P.pos;347 const vel = P.vel;348 const home = P.home;349 for (let i = 0; i < P.n * 2; i += 2) {350 let fx = (home[i] - pos[i]) * k - vel[i] * c;351 let fy = (home[i + 1] - pos[i + 1]) * k - vel[i + 1] * c;352 if (ptr.in) {353 const dx = pos[i] - ptr.x;354 const dy = pos[i + 1] - ptr.y;355 const d2 = dx * dx + dy * dy;356 if (d2 < R2) {357 const d = Math.sqrt(d2) + 0.001;358 const f = 1 - d / R;359 const push = f * f * 9000 * F;360 fx += (dx / d) * push + pvx * f * 40 * F;361 fy += (dy / d) * push + pvy * f * 40 * F;362 }363 }364 vel[i] += fx * dt;365 vel[i + 1] += fy * dt;366 pos[i] += vel[i] * dt;367 pos[i + 1] += vel[i + 1] * dt;368 energy += Math.abs(vel[i]) + Math.abs(vel[i + 1]) + Math.abs(home[i] - pos[i]) + Math.abs(home[i + 1] - pos[i + 1]);369 }370 return energy / Math.max(1, P.n) > 0.02;371 };372 373 const draw = () => {374 if (!P) return;375 g.viewport(0, 0, canvas.width, canvas.height);376 g.clearColor(0, 0, 0, 0);377 g.clear(g.COLOR_BUFFER_BIT);378 g.uniform2f(uRes, W, H);379 g.uniform1f(uPt, Math.max(1, live.current.spacing * live.current.dotScale * dpr));380 g.bindBuffer(g.ARRAY_BUFFER, posBuf);381 g.bufferSubData(g.ARRAY_BUFFER, 0, P.pos);382 g.drawArrays(g.POINTS, 0, P.n);383 };384 385 const loop = (now: number) => {386 raf = 0;387 const dt = last ? Math.min((now - last) / 1000, 1 / 30) : 1 / 60;388 last = now;389 // two half-steps for stability390 let moving = step(dt / 2);391 moving = step(dt / 2) || moving;392 draw();393 if ((moving || ptr.in) && visible && !document.hidden) raf = requestAnimationFrame(loop);394 else last = 0;395 };396 const request = () => {397 if (!raf && !reduced && visible) raf = requestAnimationFrame(loop);398 };399 400 const build = () => {401 const r = canvas.getBoundingClientRect();402 W = Math.max(1, r.width);403 H = Math.max(1, r.height);404 dpr = Math.min(window.devicePixelRatio || 1, 2);405 canvas.width = Math.round(W * dpr);406 canvas.height = Math.round(H * dpr);407 P = sample(W, H, live.current.spacing, { path, viewBox, text, color, accent });408 if (!assembled) scatter();409 upload();410 draw();411 request();412 };413 414 const ro = new ResizeObserver(() => build());415 ro.observe(canvas);416 const io = new IntersectionObserver(([e]) => {417 visible = e.isIntersecting;418 if (visible) {419 if (!assembled) assembled = true; // first reveal: particles fly home420 request();421 }422 }, { threshold: 0.25 });423 io.observe(canvas);424 425 const host = canvas.parentElement!;426 const move = (e: PointerEvent) => {427 const r = canvas.getBoundingClientRect();428 ptr.x = e.clientX - r.left;429 ptr.y = e.clientY - r.top;430 if (!ptr.in) {431 ptr.px = ptr.x;432 ptr.py = ptr.y;433 }434 ptr.in = !reduced;435 request();436 };437 const leave = () => {438 ptr.in = false;439 request();440 };441 const onVis = () => !document.hidden && request();442 const onLost = (e: Event) => {443 e.preventDefault();444 console.warn("[DitherLogo] context lost");445 cancelAnimationFrame(raf);446 raf = 0;447 setFailed(true);448 };449 host.addEventListener("pointermove", move);450 host.addEventListener("pointerdown", move);451 host.addEventListener("pointerleave", leave);452 document.addEventListener("visibilitychange", onVis);453 canvas.addEventListener("webglcontextlost", onLost);454 return () => {455 cancelAnimationFrame(raf);456 ro.disconnect();457 io.disconnect();458 host.removeEventListener("pointermove", move);459 host.removeEventListener("pointerdown", move);460 host.removeEventListener("pointerleave", leave);461 document.removeEventListener("visibilitychange", onVis);462 canvas.removeEventListener("webglcontextlost", onLost);463 g.deleteBuffer(posBuf);464 g.deleteBuffer(colBuf);465 g.deleteBuffer(sizeBuf);466 g.deleteProgram(prog);467 };468 }, [path, viewBox, text, color, accent, spacing, assemble]);469 470 return (471 <div className={`fd-dlogo${className ? " " + className : ""}`} style={style}>472 <style>{css}</style>473 {failed ? (474 <div className="fd-dlogo-fallback" role={label ? "img" : undefined} aria-label={label} aria-hidden={label ? undefined : true} style={{ ["--fd-dlogo-c" as string]: color, ["--fd-dlogo-a" as string]: accent }} />475 ) : (476 <canvas ref={canvasRef} className="fd-dlogo-canvas" role={label ? "img" : undefined} aria-label={label} aria-hidden={label ? undefined : true} />477 )}478 </div>479 );480}481 482const css = `483.fd-dlogo, .fd-dlogo *, .fd-dlogo *::before, .fd-dlogo *::after { box-sizing: border-box; }484.fd-dlogo { position: relative; display: block; margin: 0; padding: 0; border: 0; background: transparent; aspect-ratio: 1; touch-action: pan-y; }485.fd-dlogo-canvas { position: absolute; inset: 0; display: block; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; }486.fd-dlogo-fallback {487 position: absolute; inset: 4%; margin: 0; padding: 0; border: 0; border-radius: 24%;488 background:489 radial-gradient(circle at 42% 44%, var(--fd-dlogo-a) 0 14%, transparent 26%),490 linear-gradient(160deg, var(--fd-dlogo-c), transparent 85%);491 -webkit-mask: radial-gradient(circle, #000 1.3px, transparent 1.8px) 0 0 / 4px 4px;492 mask: radial-gradient(circle, #000 1.3px, transparent 1.8px) 0 0 / 4px 4px;493}494`;495 496/* ───────────────────────── demo ───────────────────────── */497 498export function Demo() {499 return (500 <section className="fd-dlogo-demo">501 <style>{demoCss}</style>502 <header className="fd-dlogo-demo-nav">503 <span className="fd-dlogo-demo-brand">504 <DitherLogo className="fd-dlogo-demo-brandmark" spacing={1.5} dotScale={1.15} assemble={false} radius={12} strength={0.4} />505 Orbital506 </span>507 <nav className="fd-dlogo-demo-links" aria-label="Primary">508 <a href="#docs" onClick={(e) => e.preventDefault()}>Docs</a>509 <a href="#changelog" onClick={(e) => e.preventDefault()}>Changelog</a>510 <a href="#pricing" onClick={(e) => e.preventDefault()}>Pricing</a>511 </nav>512 </header>513 <div className="fd-dlogo-demo-grid">514 <div className="fd-dlogo-demo-copy">515 <p className="fd-dlogo-demo-tag">516 <span className="fd-dlogo-demo-dot" aria-hidden="true" /> Orbital 2.0 · Inference at the edge517 </p>518 <h1 className="fd-dlogo-demo-title">Ship models,<br />not servers.</h1>519 <p className="fd-dlogo-demo-body">520 Deploy any open-weight model to 38 regions with one command. Cold starts under 240 ms, billed per token, observable down to the kernel.521 </p>522 <div className="fd-dlogo-demo-cmd">523 <span className="fd-dlogo-demo-prompt" aria-hidden="true">$</span>524 <code>npx orbital deploy llama-4-scout --regions all</code>525 </div>526 <dl className="fd-dlogo-demo-stats">527 <div><dt>p50 latency</dt><dd>41 ms</dd></div>528 <div><dt>Regions</dt><dd>38</dd></div>529 <div><dt>Uptime, 90d</dt><dd>99.995%</dd></div>530 </dl>531 </div>532 <div className="fd-dlogo-demo-stage">533 <DitherLogo className="fd-dlogo-demo-mark" label="Orbital logo" />534 <p className="fd-dlogo-demo-caption">Move your cursor through the mark</p>535 </div>536 </div>537 </section>538 );539}540 541const demoCss = `542.fd-dlogo-demo, .fd-dlogo-demo *, .fd-dlogo-demo *::before, .fd-dlogo-demo *::after { box-sizing: border-box; }543.fd-dlogo-demo {544 min-height: 100vh; margin: 0; padding: 0 clamp(20px, 5vw, 72px) 56px;545 background: radial-gradient(1200px 700px at 75% 45%, #15151C 0%, #0B0B0F 60%); color: #EDEDF2;546 font: 400 15px/1.5 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;547}548.fd-dlogo-demo-nav { display: flex; align-items: center; justify-content: space-between; height: 72px; margin: 0; padding: 0; border-bottom: 1px solid rgba(255,255,255,.07); }549.fd-dlogo-demo-brand { display: inline-flex; align-items: center; gap: 10px; margin: 0; padding: 0; color: #EDEDF2; font: 600 17px/1 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.01em; }550.fd-dlogo-demo-brandmark.fd-dlogo { width: 28px; height: 28px; }551.fd-dlogo-demo-links { display: flex; gap: 28px; margin: 0; padding: 0; }552.fd-dlogo-demo-links a { margin: 0; padding: 0; color: #9A9AA6; text-decoration: none; font: 500 14px/1 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; transition: color 120ms cubic-bezier(0.22, 1, 0.36, 1); }553.fd-dlogo-demo-links a:hover { color: #EDEDF2; }554.fd-dlogo-demo-links a:focus-visible { outline: 2px solid rgba(10,132,255,.7); outline-offset: 2px; border-radius: 4px; }555.fd-dlogo-demo-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: clamp(24px, 5vw, 80px); align-items: center; max-width: 1240px; margin: 0 auto; padding: clamp(40px, 7vh, 96px) 0 0; }556.fd-dlogo-demo-tag { display: inline-flex; align-items: center; gap: 8px; margin: 0 0 24px; padding: 6px 12px; border: 1px solid rgba(255,255,255,.1); border-radius: 9999px; background: rgba(255,255,255,.03); color: #C7C7D1; font: 500 13px/1.2 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: 0; }557.fd-dlogo-demo-dot { display: block; width: 6px; height: 6px; margin: 0; padding: 0; border-radius: 50%; background: #FF7A45; box-shadow: 0 0 0 3px rgba(255,122,69,.2); }558.fd-dlogo-demo-title { margin: 0 0 20px; padding: 0; color: #F5F5F8; letter-spacing: -0.045em; font: 650 clamp(2.75rem, 5.6vw, 5.5rem)/0.98 "SF Pro Display", ui-sans-serif, -apple-system, "Inter", "Segoe UI", Roboto, sans-serif; }559.fd-dlogo-demo-body { max-width: 44ch; margin: 0 0 28px; padding: 0; color: #9A9AA6; font: 400 1.0625rem/1.55 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.01em; }560.fd-dlogo-demo-cmd { display: inline-flex; align-items: center; gap: 12px; max-width: 100%; margin: 0 0 36px; padding: 14px 18px; border: 1px solid rgba(255,255,255,.09); border-radius: 14px; background: #121218; overflow-x: auto; }561.fd-dlogo-demo-cmd code { margin: 0; padding: 0; background: none; color: #EDEDF2; font: 500 14px/1.2 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; white-space: nowrap; letter-spacing: 0; }562.fd-dlogo-demo-prompt { margin: 0; padding: 0; color: #FF7A45; font: 600 14px/1.2 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; }563.fd-dlogo-demo-stats { display: flex; gap: clamp(20px, 4vw, 48px); margin: 0; padding: 24px 0 0; border-top: 1px solid rgba(255,255,255,.07); }564.fd-dlogo-demo-stats div { margin: 0; padding: 0; }565.fd-dlogo-demo-stats dt { margin: 0 0 6px; padding: 0; color: #6B6B76; font: 500 12px/1.2 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: 0.04em; text-transform: uppercase; }566.fd-dlogo-demo-stats dd { margin: 0; padding: 0; color: #EDEDF2; font: 600 24px/1.1 "SF Pro Display", ui-sans-serif, -apple-system, "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }567.fd-dlogo-demo-copy { min-width: 0; margin: 0; padding: 0; }568.fd-dlogo-demo-stage { display: flex; flex-direction: column; align-items: center; gap: 12px; margin: 0; padding: 0; }569.fd-dlogo-demo-mark.fd-dlogo { width: min(100%, 480px); cursor: crosshair; }570.fd-dlogo-demo-caption { margin: 0; padding: 0; color: #55555F; font: 500 12px/1.2 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: 0.04em; }571@media (max-width: 860px) {572 .fd-dlogo-demo-grid { grid-template-columns: minmax(0, 1fr); }573 .fd-dlogo-demo-stage { order: -1; }574 .fd-dlogo-demo-mark.fd-dlogo { width: min(100%, 320px); }575 .fd-dlogo-demo-links { display: none; }576 .fd-dlogo-demo-cmd { display: flex; }577 .fd-dlogo-demo-stats { flex-wrap: wrap; }578}579`;Bayer and blue-noise dithering of any image into a 2–4 ink palette, with a cursor loupe that sharpens, inverts or reveals.
A slow, flowing mesh gradient quantised through Bayer dithering into 2–4 inks — a hero backdrop with a light pool that follows the cursor.
Headlines printed as a Bayer halftone — dots ink in as the text scrolls into view and dissolve as it leaves.