Hero dot fieldDesign system
VisualSite

Hero dot field

Dot-pattern hero background with optional streams, labels, logo mark, and pointer interaction.

npx context-ds add hero-dot-field
On the site

Usage

Use it for

A Hero's background (<HeroDotField fine lively streams /> on the home page, fine lively streams miniStreams on a short page header, fine solidDots={false} for a quiet field). It clears around content marked with the pattern hole attribute, or, with none marked, around the hero's text, actions and media. Color it with a text class (text-brand/60 by default, text-white/40 on blue).

Avoid

  • A dot field behind body text
  • Two dot fields in one hero
  • Dots in a color other than the brand blue or white

Dependencies: hero-pattern-hole

Example

examples/hero-dot-field.tsx
import { Hero, HeroMedia, HeroMediaCard, HeroPill } from "@/components/site/hero";import { Button } from "@/components/ui/button";import { HeroDotField } from "@/components/visuals/hero-dot-field";export function Site() {  return (    <Hero      background={<HeroDotField fine lively streams fillFootOnPhones />}      eyebrow={<HeroPill>Backed by Y Combinator</HeroPill>}      title="The web {context} API for agents"      accent={["{context}", "agents"]}      description="One API for agents to scrape, search, monitor, and collect data from the web."      actions={        <>          <Button asChild variant="pill">            <a href="https://context.dev/signup">Start for free</a>          </Button>          <Button asChild variant="pill-outline">            <a href="https://docs.context.dev">Read the docs</a>          </Button>        </>      }      media={        <HeroMedia className="max-w-160">          <HeroMediaCard>            <div className="flex h-56 w-full items-end justify-center p-4 font-mono text-caption text-fg-muted">POST /v1/scrape</div>          </HeroMediaCard>        </HeroMedia>      }    />  );}

Source

components/visuals/hero-dot-field.tsx
"use client";import * as React from "react";import { cn } from "@/lib/utils";import { LOGO_MARK_HEIGHT, LOGO_MARK_WIDTH, logoMarkKindAt } from "./hero-dot-logo-shape";import { HERO_PATTERN_HOLE_ATTRIBUTE } from "@/components/visuals/hero-pattern-hole";const DESKTOP_PITCH = 16;const MOBILE_PITCH = 12;const MOBILE_BREAKPOINT = 640;const DOT_RADIUS_RATIO = 0.32;const NOISE_CELL_SIZE_IN_DOTS = 11;const BAND_LEVELS = [0.32, 0.5, 0.68];const BAND_HALF_WIDTH = 0.045;// A lively field sits behind a page's main content, so its bands run thinner and it fades toward the middle.const LIVELY_BAND_HALF_WIDTH = 0.028;const LIVELY_CENTER_FADE =  "radial-gradient(ellipse 42% 48% at 50% 48%, color-mix(in srgb, var(--ds-color-black) 20%, transparent), color-mix(in srgb, var(--ds-color-black) 55%, transparent) 60%, var(--ds-color-black) 100%)";// A stream field leaves the middle bare for the content: curved streams of dots run in from the edges, wide where they// enter and tapering to a thread as they near the content, with light pulsing along them toward it.const STREAM_ENTRY_HALF_WIDTH_IN_DOTS = 5.5;const STREAM_TAIL_HALF_WIDTH_IN_DOTS = 0.5;const STREAM_SAMPLES = 120;const STREAM_RING_SHARE = 0.3;const STREAM_LIT_SHARE = 0.12;const STREAM_TAIL_OPACITY = 0.5;// A mini stream field draws each side's streams in a box as tall as the field and pinned to its own edge, so a short// band keeps the streams' curves instead of flattening them across its whole width. The streams thin only by half,// because at a true scale a band's corners hold too few dots to read.const MINI_STREAM_BOX_ASPECT = 3.2;const MINI_STREAM_MIN_WIDTH_SCALE = 0.5;const FULL_STREAM_FIELD_HEIGHT_PX = 900;const PULSE_SPEED_PX_PER_S = 320;const PULSE_HALF_LENGTH_IN_DOTS = 1.6;const PULSE_TICK_MS = 50;// Longer than the gap between a staggered entrance's steps (60ms), so their endings count as one.const ENTRANCE_SETTLE_MS = 150;const PAGE_SETTLE_TIMEOUT_MS = 3000;const PULSE_TRANSITION_MS = 120;const PULSE_REST_MIN_MS = 5000;const PULSE_REST_MAX_MS = 12000;const LIT_DOT_SHARE = 0.07;// A lively field lights about a tenth of its dots, so the blue carries across the whole surface.const LIVELY_LIT_DOT_SHARE = 0.09;const RING_DOT_SHARE = 0.1;const DIM_OPACITY = 0.14;const RING_OPACITY = 0.55;const HOLE_CLEARANCE_RATIO = 1;// How far an oval hole's edge wanders in or out, as a share of its radius, so the pattern frays around it.const OVAL_EDGE_JITTER = 0.3;const SWAP_INTERVAL_MS = 900;const SWAPS_PER_TICK = 2;const SWAP_REACH_IN_DOTS = 2;// A lively field moves its light around faster and has dots flicker between kinds on their own.const LIVELY_SWAP_INTERVAL_MS = 600;const LIVELY_SWAPS_PER_TICK = 3;const LIVELY_TWINKLE_INTERVAL_MS = 450;const LIVELY_TWINKLES_PER_TICK = 1;const TRAIL_HOLD_MS = 1200;const TRAIL_TRANSITION_MS = 150;const TRAIL_SPAWN_COUNT = 4;const TRAIL_SPAWN_REACH_IN_DOTS = 2;const TRAIL_MAX_SPAWNED = 160;const SEED = 7;const WORD_SIZE = 12;// Doto is monospaced, each letter three fifths of its size wide.const WORD_ADVANCE = 0.6;const WORD_MARGIN_CELLS = 1;// Doto sets a tenth of its size of empty space after the last letter, which would leave the border wider on the right.const WORD_TRAILING_BEARING = 0.1;const WORD_BORDER = { paddingX: 5, height: 18, radius: 2, opacity: 0.35 };// With no element marked as a hole, the field clears around the content it sits behind (a library Hero's title,// description, actions and media), so it works without any markup.const AUTO_TEXT_HOLES = "h1, h2, h3, h4, p";const AUTO_BOX_HOLES = "a, button, img, input, [data-slot='hero-media'], [data-slot='hero-pill']";type DotKind = "dim" | "lit" | "ring";const KINDS: DotKind[] = ["dim", "lit", "ring"];const INVERTED_KIND: Record<DotKind, DotKind> = { dim: "ring", ring: "lit", lit: "ring" };interface Rect {  left: number;  top: number;  right: number;  bottom: number;  /** An opaque box (the hole kind `box`): the logo mark may run on underneath it, where it is simply hidden. */  opaque?: boolean;  /** The ellipse inscribed in the box, with a frayed edge (the hole kind `oval`). */  oval?: boolean;}interface Dot {  key: string;  cx: number;  cy: number;  kind: DotKind;  opacity?: number;}interface PlacedWord {  text: string;  x: number;  y: number;}interface FieldLayout {  pitch: number;  radius: number;  dots: Dot[];  words: PlacedWord[];  clearedCells: Set<string>;  /** Dots of the logo mark: the pointer trail flips them like any other, but the drifting light leaves them be. */  markCells: Set<string>;  streams: StreamLayout[];}// The mark in the field's bottom-right corner, running off the right and bottom edges. Only shown on wide fields,// where it clears the hero's content.const LOGO_MARK = { widthPx: 640, overhangRightPx: 64, overhangBottomPx: 96, minFieldWidth: 1024 };function hash(x: number, y: number) {  let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(SEED, 2147483647);  h = Math.imul(h ^ (h >>> 13), 1274126177);  return ((h ^ (h >>> 16)) >>> 0) / 4294967296;}function smoothstep(t: number) {  return t * t * (3 - 2 * t);}function valueNoise(x: number, y: number) {  const x0 = Math.floor(x);  const y0 = Math.floor(y);  const tx = smoothstep(x - x0);  const ty = smoothstep(y - y0);  const top = hash(x0, y0) + (hash(x0 + 1, y0) - hash(x0, y0)) * tx;  const bottom = hash(x0, y0 + 1) + (hash(x0 + 1, y0 + 1) - hash(x0, y0 + 1)) * tx;  return top + (bottom - top) * ty;}function layeredNoise(x: number, y: number) {  return (valueNoise(x, y) * 2 + valueNoise(x * 2 + 17, y * 2 + 31)) / 3;}// Dots sit on a few of the noise's contour levels, so they gather into winding bands rather than scattered blobs.function isOnBand(column: number, row: number, halfWidth = BAND_HALF_WIDTH) {  const level = layeredNoise(column / NOISE_CELL_SIZE_IN_DOTS, row / NOISE_CELL_SIZE_IN_DOTS);  return BAND_LEVELS.some((bandLevel) => Math.abs(level - bandLevel) < halfWidth);}type Point = [number, number];/** A quadratic curve in shares of the field's width and height: where it enters, where it bends toward, where it ends. */type StreamCurve = [Point, Point, Point];// Each stream points at the headline or the demo, so the web reads as flowing into the product. The two sides differ// on purpose, and some streams enter through the top or bottom edge, so the whole reads as one current.const STREAM_CURVES: StreamCurve[] = [  [    [-0.04, 0.04],    [0.12, 0.08],    [0.23, 0.36],  ],  [    [-0.04, 0.42],    [0.08, 0.54],    [0.22, 0.62],  ],  [    [0.04, 1.05],    [0.08, 0.82],    [0.22, 0.78],  ],  [    [0.9, -0.05],    [0.9, 0.16],    [0.78, 0.3],  ],  [    [1.04, 0.3],    [0.9, 0.3],    [0.77, 0.46],  ],  [    [1.04, 0.62],    [0.92, 0.7],    [0.78, 0.7],  ],  [    [0.97, 1.05],    [0.94, 0.9],    [0.8, 0.82],  ],];// On a phone the side streams are only slivers at the edges, so these sweep in across the foot from both sides.const PHONE_FOOT_STREAM_CURVES: StreamCurve[] = [  [    [-0.06, 0.74],    [0.28, 0.84],    [0.46, 1.05],  ],  [    [1.06, 0.82],    [0.72, 0.88],    [0.58, 1.05],  ],];interface StreamLayout {  dots: StreamDot[];  lengthPx: number;}interface StreamDot {  key: string;  /** How far along its stream the dot sits, from where it enters (0) to its tail (1). */  progress: number;}interface StreamSet {  curves: StreamCurve[];  /** Shrinks the streams' width along with a mini field, so they stay threads rather than bands. */  widthScale: number;}function streamSet(width: number, height: number, mini: boolean, fillFootOnPhones: boolean): StreamSet {  if (!mini) return { curves: fillFootOnPhones && width < MOBILE_BREAKPOINT ? [...STREAM_CURVES, ...PHONE_FOOT_STREAM_CURVES] : STREAM_CURVES, widthScale: 1 };  const boxShare = Math.min(1, (height * MINI_STREAM_BOX_ASPECT) / width);  const toFieldShare = (x: number, isLeftSide: boolean) => (isLeftSide ? x * boxShare : 1 - (1 - x) * boxShare);  return {    curves: STREAM_CURVES.map((curve) => {      const isLeftSide = curve[2][0] < 0.5;      return curve.map(([x, y]) => [toFieldShare(x, isLeftSide), y]) as StreamCurve;    }),    widthScale: Math.min(1, Math.max(MINI_STREAM_MIN_WIDTH_SCALE, height / FULL_STREAM_FIELD_HEIGHT_PX)),  };}function pointOnCurve([start, bend, end]: StreamCurve, t: number, width: number, height: number): Point {  const inverse = 1 - t;  const x = inverse * inverse * start[0] + 2 * inverse * t * bend[0] + t * t * end[0];  const y = inverse * inverse * start[1] + 2 * inverse * t * bend[1] + t * t * end[1];  return [x * width, y * height];}function curveLength(curve: StreamCurve, width: number, height: number) {  let length = 0;  let previous = pointOnCurve(curve, 0, width, height);  for (let sample = 1; sample <= STREAM_SAMPLES; sample++) {    const point = pointOnCurve(curve, sample / STREAM_SAMPLES, width, height);    length += Math.hypot(point[0] - previous[0], point[1] - previous[1]);    previous = point;  }  return length;}function cellKey(column: number, row: number) {  return `${column}:${row}`;}// Stamps each stream onto the grid by walking along it and claiming the cells within its width; a cell two streams// cross keeps the first.function layOutStreams(columns: number, rows: number, width: number, height: number, pitch: number, { curves, widthScale }: StreamSet) {  const progressByCell = new Map<string, { stream: number; progress: number; distance: number }>();  const entryHalfWidth = Math.max(STREAM_TAIL_HALF_WIDTH_IN_DOTS, STREAM_ENTRY_HALF_WIDTH_IN_DOTS * widthScale);  curves.forEach((curve, stream) => {    for (let sample = 0; sample <= STREAM_SAMPLES; sample++) {      const t = sample / STREAM_SAMPLES;      const [x, y] = pointOnCurve(curve, t, width, height);      const halfWidth = (entryHalfWidth + (STREAM_TAIL_HALF_WIDTH_IN_DOTS - entryHalfWidth) * t) * pitch;      const reach = Math.ceil(halfWidth / pitch);      const centerColumn = Math.floor(x / pitch);      const centerRow = Math.floor(y / pitch);      for (let row = centerRow - reach; row <= centerRow + reach; row++) {        for (let column = centerColumn - reach; column <= centerColumn + reach; column++) {          if (column < 0 || row < 0 || column >= columns || row >= rows) continue;          const distance = Math.hypot(column * pitch + pitch / 2 - x, row * pitch + pitch / 2 - y);          if (distance > halfWidth) continue;          const key = cellKey(column, row);          const claimed = progressByCell.get(key);          if (claimed && (claimed.stream !== stream || claimed.distance <= distance)) continue;          progressByCell.set(key, { stream, progress: t, distance });        }      }    }  });  return progressByCell;}// The stream breaks up toward its tail, so it frays into the open middle rather than ending in a blunt point.function streamDotKind(column: number, row: number, progress: number): DotKind | null {  if (hash(column + 71, row + 37) < progress ** 2 * 0.7) return null;  const roll = hash(column + 101, row + 211);  return roll < STREAM_RING_SHARE ? "ring" : roll > 1 - STREAM_LIT_SHARE ? "lit" : "dim";}function textLineBoxes(element: Element) {  const range = document.createRange();  range.selectNodeContents(element);  return Array.from(range.getClientRects());}function holeRect(box: DOMRect, origin: DOMRect, kind: string | null): Rect {  return {    left: box.left - origin.left,    top: box.top - origin.top,    right: box.right - origin.left,    bottom: box.bottom - origin.top,    opaque: kind === "box",    oval: kind === "oval",  };}// Holes are looked up across the whole hero (the field may sit in a background layer beside the content) and measured// against the field's own box.function measureContentHoles(scope: HTMLElement, field: HTMLElement, svg: SVGSVGElement): Rect[] {  const origin = field.getBoundingClientRect();  const holes: Rect[] = [];  const marked = Array.from(scope.querySelectorAll(`[${HERO_PATTERN_HOLE_ATTRIBUTE}]`));  // Layers beside the field (a Hero's background slot) are not content, so they never cut holes.  const isOwn = (element: Element) => svg.contains(element) || (field !== scope && field.contains(element));  const elements: [Element, string | null][] = marked.length    ? marked.map((element) => [element, element.getAttribute(HERO_PATTERN_HOLE_ATTRIBUTE)])    : [        ...Array.from(scope.querySelectorAll(AUTO_TEXT_HOLES)).map((element) => [element, "text"] as [Element, string]),        ...Array.from(scope.querySelectorAll(AUTO_BOX_HOLES)).map((element) => [element, "snug-box"] as [Element, string]),      ];  for (const [element, kind] of elements) {    if (isOwn(element)) continue;    const boxes = kind === "text" ? textLineBoxes(element) : [element.getBoundingClientRect()];    for (const box of boxes) if (box.width > 0 && box.height > 0) holes.push(holeRect(box, origin, kind));  }  return holes;}function buildLayout(  width: number,  height: number,  holes: Rect[],  words: string[],  fixedPitch: number | undefined,  litShare: number,  logoMark: boolean,  streams: boolean,  miniStreams: boolean,  fillFootOnPhones: boolean,): FieldLayout {  const pitch = fixedPitch ?? (width < MOBILE_BREAKPOINT ? MOBILE_PITCH : DESKTOP_PITCH);  const clearance = pitch * HOLE_CLEARANCE_RATIO;  const isInHole = (x: number, y: number, hole: Rect) => {    if (hole.oval) {      const radiusX = (hole.right - hole.left) / 2;      const radiusY = (hole.bottom - hole.top) / 2;      const distance = Math.hypot((x - hole.left - radiusX) / radiusX, (y - hole.top - radiusY) / radiusY);      return distance + (hash(Math.round(x), Math.round(y)) - 0.5) * OVAL_EDGE_JITTER < 1;    }    return x > hole.left - clearance && x < hole.right + clearance && y > hole.top - clearance && y < hole.bottom + clearance;  };  const isCleared = (x: number, y: number) => holes.some((hole) => isInHole(x, y, hole));  // The mark passes under opaque content such as a demo panel rather than stopping short of it, but still keeps clear  // of text, so it never runs behind a label.  const isClearedForMark = (x: number, y: number) => holes.some((hole) => !hole.opaque && isInHole(x, y, hole));  const dots: Dot[] = [];  const clearedCells = new Set<string>();  const markCells = new Set<string>();  const streamShapes = streamSet(width, height, miniStreams, fillFootOnPhones);  const columns = Math.floor(width / pitch);  const rows = Math.floor(height / pitch);  const streamCells = streams ? layOutStreams(columns, rows, width, height, pitch, streamShapes) : null;  const streamLayouts: StreamLayout[] = streamShapes.curves.map((curve) => ({ dots: [], lengthPx: curveLength(curve, width, height) }));  const showMark = logoMark && width >= LOGO_MARK.minFieldWidth;  const markUnitsPerPx = LOGO_MARK_WIDTH / LOGO_MARK.widthPx;  const markLeft = width - LOGO_MARK.widthPx + LOGO_MARK.overhangRightPx;  const markTop = height + LOGO_MARK.overhangBottomPx - LOGO_MARK.widthPx / (LOGO_MARK_WIDTH / LOGO_MARK_HEIGHT);  const bandHalfWidth = litShare === LIVELY_LIT_DOT_SHARE ? LIVELY_BAND_HALF_WIDTH : BAND_HALF_WIDTH;  for (let row = 0; row < rows; row++) {    for (let column = 0; column < columns; column++) {      const key = cellKey(column, row);      const cx = column * pitch + pitch / 2;      const cy = row * pitch + pitch / 2;      const inMarkBox = showMark && cx >= markLeft - clearance && cy >= markTop - clearance;      if (inMarkBox ? isClearedForMark(cx, cy) : isCleared(cx, cy)) {        clearedCells.add(key);        continue;      }      if (inMarkBox) {        const markKind = logoMarkKindAt((cx - markLeft) * markUnitsPerPx, (cy - markTop) * markUnitsPerPx, pitch * DOT_RADIUS_RATIO * markUnitsPerPx);        if (markKind) {          markCells.add(key);          dots.push({ key, cx, cy, kind: markKind });        }        continue;      }      if (streamCells) {        const streamCell = streamCells.get(key);        const kind = streamCell && streamDotKind(column, row, streamCell.progress);        if (!streamCell || !kind) continue;        streamLayouts[streamCell.stream].dots.push({ key, progress: streamCell.progress });        dots.push({ key, cx, cy, kind, opacity: 1 - (1 - STREAM_TAIL_OPACITY) * streamCell.progress });        continue;      }      if (!isOnBand(column, row, bandHalfWidth)) continue;      const roll = hash(column + 101, row + 211);      const kind: DotKind = roll < RING_DOT_SHARE ? "ring" : roll > 1 - litShare ? "lit" : "dim";      dots.push({ key, cx, cy, kind });    }  }  return {    pitch,    radius: pitch * DOT_RADIUS_RATIO,    dots,    words: placeWords(words, columns, rows, pitch, dots, clearedCells),    clearedCells,    markCells,    streams: streamCells ? streamLayouts.filter((stream) => stream.dots.length > 0) : [],  };}// Each word takes a run of open cells, with a cell of open space around it, so it reads as sitting in a gap of the// pattern. Each goes to the open spot farthest from those already placed. Their cells are then cleared, so the// pointer's trail never scatters dots over them.function placeWords(words: string[], columns: number, rows: number, pitch: number, dots: Dot[], clearedCells: Set<string>): PlacedWord[] {  if (words.length === 0) return [];  const taken = new Set([...dots.map((dot) => dot.key), ...clearedCells]);  const isOpen = (column: number, row: number) => column >= 0 && row >= 0 && column < columns && row < rows && !taken.has(cellKey(column, row));  const placed: { word: PlacedWord; column: number; row: number }[] = [];  for (const text of words) {    const span = Math.ceil((text.length * WORD_ADVANCE * WORD_SIZE) / pitch);    const fits = (column: number, row: number) => {      for (let dy = -WORD_MARGIN_CELLS; dy <= WORD_MARGIN_CELLS; dy++) {        for (let dx = -WORD_MARGIN_CELLS; dx < span + WORD_MARGIN_CELLS; dx++) if (!isOpen(column + dx, row + dy)) return false;      }      return true;    };    let best: { column: number; row: number; score: number } | null = null;    for (let row = WORD_MARGIN_CELLS; row < rows - WORD_MARGIN_CELLS; row++) {      for (let column = WORD_MARGIN_CELLS; column < columns - span - WORD_MARGIN_CELLS; column++) {        if (!fits(column, row)) continue;        const distance = placed.length ? Math.min(...placed.map((other) => Math.hypot(other.column - column, other.row - row))) : hash(column + 17, row + 29);        if (!best || distance > best.score) best = { column, row, score: distance };      }    }    if (!best) continue;    for (let dx = 0; dx < span; dx++) taken.add(cellKey(best.column + dx, best.row));    for (let dx = -WORD_MARGIN_CELLS; dx < span + WORD_MARGIN_CELLS; dx++) {      for (let dy = -WORD_MARGIN_CELLS; dy <= WORD_MARGIN_CELLS; dy++) clearedCells.add(cellKey(best.column + dx, best.row + dy));    }    placed.push({ word: { text, x: best.column * pitch, y: (best.row + 0.5) * pitch }, column: best.column, row: best.row });  }  return placed.map(({ word }) => word);}function kindStyle(kind: DotKind) {  return {    fillOpacity: kind === "ring" ? 0 : kind === "lit" ? 1 : DIM_OPACITY,    strokeOpacity: kind === "ring" ? RING_OPACITY : 0,  };}function paint(circle: SVGCircleElement | undefined, kind: DotKind) {  if (!circle) return;  const style = kindStyle(kind);  circle.style.fillOpacity = String(style.fillOpacity);  circle.style.strokeOpacity = String(style.strokeOpacity);}function pickRandom<T>(items: T[]) {  return items[Math.floor(Math.random() * items.length)];}/** * Brings the field to life: lit dots hand their light to a dim neighbor in the same band, and the pointer leaves a * trail, flipping the kind of each dot it crosses and scattering a few short-lived dots wherever it crosses open space. */function animateField(scope: HTMLElement, field: HTMLElement, svg: SVGSVGElement, spawnLayer: SVGGElement, layout: FieldLayout, lively: boolean, solidDots: boolean) {  // Without solid dots, any dot the trail or a scattered spawn would light shows as a ring instead.  const paintKind = (circle: SVGCircleElement | undefined, kind: DotKind) => paint(circle, solidDots || kind !== "lit" ? kind : "ring");  const circles = new Map<string, SVGCircleElement>();  for (const circle of svg.querySelectorAll<SVGCircleElement>("circle[data-dot]")) circles.set(circle.dataset.dot ?? "", circle);  const kinds = new Map(layout.dots.map((dot) => [dot.key, dot.kind]));  for (const [key, kind] of kinds) paintKind(circles.get(key), kind);  const inverted = new Map<string, number>();  const spawnedCells = new Set<string>();  const timers = new Set<number>();  const later = (callback: () => void, delay: number) => {    const timer = window.setTimeout(() => {      timers.delete(timer);      callback();    }, delay);    timers.add(timer);  };  let onScreen = true;  const visibility = new IntersectionObserver(([entry]) => {    onScreen = entry.isIntersecting;  });  visibility.observe(field);  // Streams carry their light as pulses toward the content, so it is not also handed around dot by dot.  const drift = window.setInterval(    () => {      if (document.hidden || !onScreen || layout.streams.length > 0) return;      const lit = Array.from(kinds).filter(([key, kind]) => kind === "lit" && !inverted.has(key) && !layout.markCells.has(key));      for (let swap = 0; swap < (lively ? LIVELY_SWAPS_PER_TICK : SWAPS_PER_TICK) && lit.length > 0; swap++) {        const [from] = pickRandom(lit);        const [column, row] = from.split(":").map(Number);        const candidates: string[] = [];        for (let dy = -SWAP_REACH_IN_DOTS; dy <= SWAP_REACH_IN_DOTS; dy++) {          for (let dx = -SWAP_REACH_IN_DOTS; dx <= SWAP_REACH_IN_DOTS; dx++) {            const key = cellKey(column + dx, row + dy);            if (kinds.get(key) === "dim" && !inverted.has(key)) candidates.push(key);          }        }        if (candidates.length === 0) continue;        const to = pickRandom(candidates);        kinds.set(from, "dim");        kinds.set(to, "lit");        paintKind(circles.get(from), "dim");        paintKind(circles.get(to), "lit");      }    },    lively ? LIVELY_SWAP_INTERVAL_MS : SWAP_INTERVAL_MS,  );  const invert = (key: string) => {    const kind = kinds.get(key);    if (!kind || inverted.has(key)) return;    const circle = circles.get(key);    inverted.set(key, 1);    circle?.style.setProperty("transition-duration", `${TRAIL_TRANSITION_MS}ms`);    paintKind(circle, INVERTED_KIND[kind]);    later(() => {      inverted.delete(key);      circle?.style.removeProperty("transition-duration");      paintKind(circle, kinds.get(key) ?? kind);    }, TRAIL_HOLD_MS);  };  const pulses = layout.streams.map((stream) => ({ stream, position: 0, restUntil: performance.now() + Math.random() * PULSE_REST_MAX_MS, lit: new Set<string>() }));  const pulseTimer =    pulses.length > 0      ? window.setInterval(() => {          if (document.hidden || !onScreen) return;          const now = performance.now();          for (const pulse of pulses) {            if (now < pulse.restUntil) continue;            pulse.position += (PULSE_SPEED_PX_PER_S * PULSE_TICK_MS) / 1000 / pulse.stream.lengthPx;            const halfLength = (PULSE_HALF_LENGTH_IN_DOTS * layout.pitch) / pulse.stream.lengthPx;            const nowLit = new Set<string>();            if (pulse.position <= 1 + halfLength) {              for (const dot of pulse.stream.dots) if (Math.abs(dot.progress - pulse.position) < halfLength) nowLit.add(dot.key);            } else {              pulse.position = -halfLength;              pulse.restUntil = now + PULSE_REST_MIN_MS + Math.random() * (PULSE_REST_MAX_MS - PULSE_REST_MIN_MS);            }            for (const key of pulse.lit) {              if (nowLit.has(key) || inverted.has(key)) continue;              const circle = circles.get(key);              circle?.style.removeProperty("transition-duration");              paint(circle, kinds.get(key) ?? "dim");            }            for (const key of nowLit) {              if (pulse.lit.has(key) || inverted.has(key)) continue;              const circle = circles.get(key);              circle?.style.setProperty("transition-duration", `${PULSE_TRANSITION_MS}ms`);              paint(circle, "lit");            }            pulse.lit = nowLit;          }        }, PULSE_TICK_MS)      : 0;  const dotKeys = Array.from(kinds.keys()).filter((key) => !layout.markCells.has(key));  const twinkle = lively    ? window.setInterval(() => {        if (document.hidden || !onScreen || dotKeys.length === 0) return;        for (let count = 0; count < LIVELY_TWINKLES_PER_TICK; count++) invert(pickRandom(dotKeys));      }, LIVELY_TWINKLE_INTERVAL_MS)    : 0;  const scatter = (column: number, row: number) => {    const origin = cellKey(column, row);    if (spawnedCells.has(origin) || spawnLayer.childElementCount >= TRAIL_MAX_SPAWNED) return;    spawnedCells.add(origin);    later(() => spawnedCells.delete(origin), TRAIL_HOLD_MS);    const open: [number, number][] = [];    for (let dy = -TRAIL_SPAWN_REACH_IN_DOTS; dy <= TRAIL_SPAWN_REACH_IN_DOTS; dy++) {      for (let dx = -TRAIL_SPAWN_REACH_IN_DOTS; dx <= TRAIL_SPAWN_REACH_IN_DOTS; dx++) {        const key = cellKey(column + dx, row + dy);        if (column + dx >= 0 && row + dy >= 0 && !kinds.has(key) && !layout.clearedCells.has(key) && !spawnedCells.has(key)) open.push([column + dx, row + dy]);      }    }    for (let count = 0; count < TRAIL_SPAWN_COUNT && open.length > 0; count++) {      const [spawnColumn, spawnRow] = open.splice(Math.floor(Math.random() * open.length), 1)[0];      const spawnKey = cellKey(spawnColumn, spawnRow);      spawnedCells.add(spawnKey);      const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");      circle.setAttribute("cx", String(spawnColumn * layout.pitch + layout.pitch / 2));      circle.setAttribute("cy", String(spawnRow * layout.pitch + layout.pitch / 2));      circle.setAttribute("r", String(layout.radius - 0.5));      circle.style.opacity = "0";      circle.style.transitionDuration = `${TRAIL_TRANSITION_MS}ms`;      paintKind(circle, pickRandom(KINDS));      spawnLayer.appendChild(circle);      requestAnimationFrame(() => (circle.style.opacity = "1"));      later(() => {        circle.style.removeProperty("transition-duration");        circle.style.opacity = "0";      }, TRAIL_HOLD_MS);      later(() => {        circle.remove();        spawnedCells.delete(spawnKey);      }, TRAIL_HOLD_MS * 2);    }  };  const visit = (column: number, row: number) => {    const key = cellKey(column, row);    if (layout.clearedCells.has(key)) return;    if (kinds.has(key)) invert(key);    else scatter(column, row);  };  // Steps along the segment since the last event, so a fast flick still marks every cell it crosses.  let last: { x: number; y: number } | null = null;  // Measured once per scroll or resize rather than on every pointer move, which would force a layout each time.  let origin: DOMRect | null = null;  const forgetOrigin = () => {    origin = null;  };  const onPointerMove = (event: PointerEvent) => {    if (event.pointerType !== "mouse") return;    origin ??= field.getBoundingClientRect();    const point = { x: event.clientX - origin.left, y: event.clientY - origin.top };    const from = last ?? point;    const steps = Math.max(1, Math.ceil(Math.hypot(point.x - from.x, point.y - from.y) / (layout.pitch / 2)));    let previousCell = "";    for (let step = 1; step <= steps; step++) {      const column = Math.floor((from.x + ((point.x - from.x) * step) / steps) / layout.pitch);      const row = Math.floor((from.y + ((point.y - from.y) * step) / steps) / layout.pitch);      const key = cellKey(column, row);      if (key === previousCell) continue;      previousCell = key;      visit(column, row);    }    last = point;  };  const onPointerLeave = () => {    last = null;  };  // Listens on the hero, not the field: the field never takes pointer events, so clicks go to the content.  scope.addEventListener("pointermove", onPointerMove, { passive: true });  scope.addEventListener("pointerleave", onPointerLeave);  window.addEventListener("scroll", forgetOrigin, { passive: true });  window.addEventListener("resize", forgetOrigin, { passive: true });  return () => {    window.clearInterval(drift);    window.clearInterval(twinkle);    window.clearInterval(pulseTimer);    visibility.disconnect();    for (const timer of timers) window.clearTimeout(timer);    scope.removeEventListener("pointermove", onPointerMove);    scope.removeEventListener("pointerleave", onPointerLeave);    window.removeEventListener("scroll", forgetOrigin);    window.removeEventListener("resize", forgetOrigin);    spawnLayer.replaceChildren();  };}// Every fading dot repaints the whole field, which on a phone competes with loading and hydrating the page. The field// shows at once and only starts moving after the page has loaded and the browser has gone idle.let pageHasSettled = false;function afterPageSettles(start: () => void): () => void {  if (pageHasSettled) {    start();    return () => {};  }  let cancelRun = () => {};  const settle = () => {    const run = () => {      pageHasSettled = true;      start();    };    if ("requestIdleCallback" in window) {      const idleCallback = window.requestIdleCallback(run, { timeout: PAGE_SETTLE_TIMEOUT_MS });      cancelRun = () => window.cancelIdleCallback(idleCallback);    } else {      const timer = setTimeout(run, 1);      cancelRun = () => clearTimeout(timer);    }  };  if (document.readyState === "complete") settle();  else window.addEventListener("load", settle, { once: true });  return () => {    window.removeEventListener("load", settle);    cancelRun();  };}// The field the dots belong to: the hero (or section) around it, where the content and its holes live.function scopeOf(field: HTMLElement) {  return field.closest<HTMLElement>("[data-slot='hero'], section") ?? field;}const DOT_TRANSITION_CLASS = "[&>circle]:transition-[fill-opacity,stroke-opacity,opacity] [&>circle]:duration-700 [&>circle]:ease-in-out motion-reduce:[&>circle]:transition-none";export interface HeroDotFieldProps {  className?: string;  /** Short labels set in open gaps of the pattern. */  words?: string[];  wordClassName?: string;  /** The smaller dots of a narrow field, at any width, so a wide field can match them. */  fine?: boolean;  /** More motion on its own: faster drifting light and dots that flicker without the pointer. */  lively?: boolean;  /** The Context.dev mark, large in the bottom-right corner, drawn in the field's own dots. */  logoMark?: boolean;  /** Curved streams of dots running in from the edges toward the content, with light pulsing along them. */  streams?: boolean;  /** With `streams`, draws them small in the corners, keeping their shapes, for a field much wider than it is tall. */  miniStreams?: boolean;  /** With `streams`, two more sweep across the bottom on a phone, for content that stops well above the field's foot. */  fillFootOnPhones?: boolean;  /** Off for only faint dots and rings, with no solid dots anywhere, even under the pointer. */  solidDots?: boolean;}/** * The hero's dot-grid backdrop: winding bands of faint dots, rings and a few lit dots, or with `streams`, curved streams * running in from the edges. It fills its parent (a Hero's `background` slot, or any positioned element) and clears * around every element of the hero marked with the hole attribute, or, with none marked, around the hero's text, * actions and media. The dots take `currentColor`; it never takes pointer events. */export function HeroDotField({  className,  words = [],  wordClassName,  fine = false,  lively = false,  logoMark = false,  streams = false,  miniStreams = false,  fillFootOnPhones = false,  solidDots = true,}: HeroDotFieldProps) {  const wordsKey = words.join(" ");  const svgRef = React.useRef<SVGSVGElement>(null);  const spawnLayerRef = React.useRef<SVGGElement>(null);  const [layout, setLayout] = React.useState<FieldLayout | null>(null);  React.useEffect(() => {    const svg = svgRef.current;    const field = svg?.parentElement;    if (!svg || !field) return;    const scope = scopeOf(field);    let frame = 0;    const relayout = () => {      cancelAnimationFrame(frame);      frame = requestAnimationFrame(() =>        setLayout(          buildLayout(            field.clientWidth,            field.clientHeight,            measureContentHoles(scope, field, svg),            wordsKey ? wordsKey.split(" ") : [],            fine ? MOBILE_PITCH : undefined,            !solidDots ? 0 : lively ? LIVELY_LIT_DOT_SHARE : LIT_DOT_SHARE,            logoMark,            streams,            miniStreams,            fillFootOnPhones,          ),        ),      );    };    const observer = new ResizeObserver(relayout);    observer.observe(field);    for (const hole of scope.querySelectorAll(`[${HERO_PATTERN_HOLE_ATTRIBUTE}]`)) observer.observe(hole);    void document.fonts.ready.then(relayout);    // An entrance animation moves the content without resizing it, so the holes are measured again once it settles.    // Steps end one after another, and each rebuild re-renders every dot, so it waits for the last of them.    let settleTimer = 0;    const relayoutOnceSettled = () => {      window.clearTimeout(settleTimer);      settleTimer = window.setTimeout(relayout, ENTRANCE_SETTLE_MS);    };    scope.addEventListener("animationend", relayoutOnceSettled);    return () => {      cancelAnimationFrame(frame);      window.clearTimeout(settleTimer);      observer.disconnect();      scope.removeEventListener("animationend", relayoutOnceSettled);    };  }, [wordsKey, fine, lively, logoMark, streams, miniStreams, fillFootOnPhones, solidDots]);  React.useEffect(() => {    const svg = svgRef.current;    const field = svg?.parentElement;    const spawnLayer = spawnLayerRef.current;    // Under reduced motion the field stays a still picture: no drifting light, pulses or pointer trail.    if (!svg || !field || !spawnLayer || !layout || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;    let stopAnimating: (() => void) | undefined;    const cancelStart = afterPageSettles(() => {      stopAnimating = animateField(scopeOf(field), field, svg, spawnLayer, layout, lively, solidDots);    });    return () => {      cancelStart();      stopAnimating?.();    };  }, [layout, lively, solidDots]);  return (    <svg      ref={svgRef}      aria-hidden="true"      data-dot-field=""      className={cn(        "pointer-events-none absolute inset-0 size-full text-brand/60 transition-opacity duration-700 ease-out motion-reduce:transition-none",        layout ? "opacity-100" : "opacity-0",        className,      )}      style={!streams && lively ? { maskImage: LIVELY_CENTER_FADE, WebkitMaskImage: LIVELY_CENTER_FADE } : undefined}    >      <g fill="currentColor" stroke="currentColor" strokeWidth="1" className={DOT_TRANSITION_CLASS}>        {layout?.dots.map((dot) => <circle key={dot.key} data-dot={dot.key} cx={dot.cx} cy={dot.cy} r={layout.radius - 0.5} opacity={dot.opacity} style={kindStyle(dot.kind)} />)}      </g>      <g ref={spawnLayerRef} fill="currentColor" stroke="currentColor" strokeWidth="1" className={DOT_TRANSITION_CLASS} />      <g fill="currentColor" fontSize={WORD_SIZE} fontWeight={800} className={cn("font-data", wordClassName)}>        {layout?.words.map((word) => (          <g key={word.text}>            <rect              x={word.x - WORD_BORDER.paddingX + 0.5}              y={Math.round(word.y - WORD_BORDER.height / 2) + 0.5}              width={Math.round(word.text.length * WORD_ADVANCE * WORD_SIZE - WORD_TRAILING_BEARING * WORD_SIZE + WORD_BORDER.paddingX * 2)}              height={WORD_BORDER.height}              rx={WORD_BORDER.radius}              fill="none"              stroke="currentColor"              strokeOpacity={WORD_BORDER.opacity}            />            <text x={word.x} y={word.y} dominantBaseline="central">              {word.text}            </text>          </g>        ))}      </g>    </svg>  );}
components/visuals/hero-dot-logo-shape.ts
// The Context.dev mark's geometry, in its own 772.32 x 531.2 units: a rounded square with a round hole, and the disc// beside it. The hero dot field lays these out as its own dots, so the mark shares the field's grid, dot sizes and// pointer trail exactly.const SQUARE = { left: 260.5, top: 0, right: 772.32, bottom: 511.83 };const HOLE = { cx: 414.05, cy: 358.28, r: 153.55 };const DISC = { cx: 153.55, cy: 377.64, r: 153.55 };export const LOGO_MARK_WIDTH = 772.32;export const LOGO_MARK_HEIGHT = 531.2;export type LogoMarkKind = "lit" | "ring";/** * Solid parts of the mark are lit dots and the square's hole is drawn in rings, in the site's two-dot language. A dot is * only placed where its whole disc (`dotRadius`, in mark units) lands inside that part, so the outline stays clean. */export function logoMarkKindAt(x: number, y: number, dotRadius: number): LogoMarkKind | null {  if (Math.hypot(x - DISC.cx, y - DISC.cy) <= DISC.r - dotRadius) return "lit";  // The disc's top rows run straight on into the square, filling the notch its curve would leave against the square's  // left edge, so the two shapes read as one joined line there.  const inJoin = x >= DISC.cx && x - dotRadius < SQUARE.left && y >= DISC.cy - DISC.r + dotRadius && y <= DISC.cy - Math.sqrt(DISC.r ** 2 - (SQUARE.left - DISC.cx) ** 2);  if (inJoin) return "lit";  const inSquare = x - dotRadius >= SQUARE.left && x + dotRadius <= SQUARE.right && y - dotRadius >= SQUARE.top && y + dotRadius <= SQUARE.bottom;  if (!inSquare) return null;  const fromHole = Math.hypot(x - HOLE.cx, y - HOLE.cy);  if (fromHole <= HOLE.r - dotRadius) return "ring";  return fromHole <= HOLE.r + dotRadius ? null : "lit";}

Reference: src/components/sections/shared/hero-dot-field.tsx.