Dither Logo

Your mark as ordered-dither stipple dots — each one a spring particle that scatters from the cursor and settles back.

  • Dither
  • Developer tools
  • React only · 0 deps
  • 1 file
Full screen

Browser support · WebGL2 / WebGL1 point sprites. Without WebGL a dotted CSS mask fallback is shown.

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/dither-logo.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/dither/dither-logo.tsx
1/**
2 * DitherLogo — a mark rendered as ordered-dither stipple dots. Every dot is a
3 * particle on a spring: the cursor scatters them, they settle back with a
4 * little overshoot. On first view the dots assemble from a loose cloud.
5 *
6 * Props
7 * - 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 dotted
21 * 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 rim
104 ctx.lineWidth = S * 0.022;
105 ctx.strokeStyle = color;
106 sq(S * 0.011);
107 ctx.stroke();
108
109 // planet
110 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 bottom
125 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 front
135 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 alpha
206 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 stability
390 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 home
420 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 Orbital
506 </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 edge
517 </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&nbsp;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`;

More Dither. Same file contract.

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.

Dither Gradient

A slow, flowing mesh gradient quantised through Bayer dithering into 2–4 inks — a hero backdrop with a light pool that follows the cursor.

Dither Text

Headlines printed as a Bayer halftone — dots ink in as the text scrolls into view and dissolve as it leaves.