Baktiyar Ablaikhan

Pixel molecule

Source
arlan.me/vault ↗
Author
Arlan
Licence
MIT

Loading…

'Arlan Marat' in a white pixel face on red drifts apart into molecule diagrams bonded by stair-stepped pixels, then sets again.

Code

"use client";

import { CanvasStudy, clamp, easeInOut, hash, rng, type Frame, type Painter } from "./stage";

export const BG = "#f5333f";
const INK = "#ffffff";

// 5×7 pixel glyphs
const GLYPHS: Record<string, string[]> = {
  A: [".###.", "#...#", "#...#", "#####", "#...#", "#...#", "#...#"],
  M: ["#...#", "##.##", "#.#.#", "#.#.#", "#...#", "#...#", "#...#"],
  r: [".....", ".....", "#.##.", "##..#", "#....", "#....", "#...."],
  l: [".##..", "..#..", "..#..", "..#..", "..#..", "..#..", ".###."],
  a: [".....", ".....", ".###.", "....#", ".####", "#...#", ".####"],
  n: [".....", ".....", "#.##.", "##..#", "#...#", "#...#", "#...#"],
  t: ["..#..", "..#..", "#####", "..#..", "..#..", "..#.#", "...#."],
};
const NAME = ["Arlan", "Marat"];
const LETTERS = NAME.flatMap((word, wi) => [...word].map((ch, i) => ({ ch, word: wi, i })));
const COLS = 5 * 10 + 9 + 3; // glyph columns, letter gaps, word gap

type Pt = [number, number];
const HOLD = 1.0;
const TRAVEL = 1.3;
const SHAPES = 3; // molecule shapes per sequence

function typeset(P: number, w: number, h: number): Pt[] {
  let x = (w - COLS * P) / 2;
  return LETTERS.map((l, k) => {
    if (k > 0 && l.i === 0) x += 3 * P;
    const c: Pt = [x + 2.5 * P, h / 2];
    x += 6 * P;
    return c;
  });
}

function molecule(seed: number, P: number, w: number, h: number): Pt[] {
  const r = rng(seed);
  const L = P * 12;
  const pts: Pt[] = [];
  const free = (x: number, y: number, min: number) => pts.every(([px, py]) => Math.hypot(px - x, py - y) > min);
  let x = 0;
  let y = 0;
  let dir = Math.floor(r() * 6);
  LETTERS.forEach((l) => {
    if (l.i === 0 && l.word > 0) {
      // the second word starts clear of the first chain
      for (let tries = 0; tries < 40; tries++) {
        const a = r() * Math.PI * 2;
        const nx = x + Math.cos(a) * L * (1.4 + r());
        const ny = y + Math.sin(a) * L * (1.4 + r());
        if (free(nx, ny, L * 1.3) || tries === 39) {
          x = nx;
          y = ny;
          break;
        }
      }
      dir = Math.floor(r() * 6);
    } else if (l.i > 0) {
      // hexagonal turns that avoid landing on another atom
      const turns = r() < 0.5 ? [1, 5, 0, 2, 4] : [5, 1, 0, 4, 2];
      for (const tr of turns) {
        const d = (dir + tr) % 6;
        const nx = x + Math.cos((d * Math.PI) / 3) * L;
        const ny = y + Math.sin((d * Math.PI) / 3) * L;
        if (free(nx, ny, L * 0.8) || tr === turns[turns.length - 1]) {
          dir = d;
          x = nx;
          y = ny;
          break;
        }
      }
    }
    pts.push([x, y]);
  });
  const xs = pts.map((p) => p[0]);
  const ys = pts.map((p) => p[1]);
  const bw = Math.max(...xs) - Math.min(...xs) || 1;
  const bh = Math.max(...ys) - Math.min(...ys) || 1;
  const s = Math.min(1, (w * 0.84) / bw, (h * 0.72) / bh);
  const cx = (Math.max(...xs) + Math.min(...xs)) / 2;
  const cy = (Math.max(...ys) + Math.min(...ys)) / 2;
  return pts.map(([px, py]) => [w / 2 + (px - cx) * s, h / 2 + (py - cy) * s]);
}

export function create(): Painter {
  return ({ ctx, w, h, t }: Frame) => {
    ctx.fillStyle = BG;
    ctx.fillRect(0, 0, w, h);
    const P = Math.max(2, Math.floor(Math.min((w * 0.7) / COLS, (h * 0.42) / 7)));
    const stages = SHAPES + 1;
    const seqLen = stages * (HOLD + TRAVEL);
    const seq = Math.floor(t / seqLen);
    const u = t - seq * seqLen;
    const layouts: Pt[][] = [typeset(P, w, h)];
    for (let s = 0; s < SHAPES; s++) layouts.push(molecule(seq * 31 + s * 7 + 1, P, w, h));
    const stage = Math.floor(u / (HOLD + TRAVEL));
    const within = u - stage * (HOLD + TRAVEL);
    const from = layouts[stage];
    const to = layouts[(stage + 1) % layouts.length];
    const k = easeInOut(clamp((within - HOLD) / TRAVEL));
    const pos: Pt[] = from.map((a, i) => [a[0] + (to[i][0] - a[0]) * k, a[1] + (to[i][1] - a[1]) * k]);
    const home = layouts[0];

    ctx.fillStyle = INK;
    // bonds: fine stair-stepped runs of square pixels
    const q = Math.max(1, Math.round(P / 2));
    for (let i = 1; i < LETTERS.length; i++) {
      if (LETTERS[i].word !== LETTERS[i - 1].word) continue;
      const [ax, ay] = pos[i - 1];
      const [bx, by] = pos[i];
      const d = Math.hypot(bx - ax, by - ay);
      const rest = Math.hypot(home[i][0] - home[i - 1][0], home[i][1] - home[i - 1][1]);
      const show = clamp((d - rest * 1.05) / (P * 3));
      if (show <= 0) continue;
      const ux = (bx - ax) / d;
      const uy = (by - ay) / d;
      // leave the letter boxes clear
      const pad = Math.min(3.1 * P / Math.max(Math.abs(ux), 1e-3), 4.1 * P / Math.max(Math.abs(uy), 1e-3));
      const len = (d - pad * 2) * show;
      if (len <= 0) continue;
      const lines = hash(i, seq) > 0.6 ? [-1, 1] : [0];
      for (const o of lines) {
        const ox = -uy * o * q * 1.6;
        const oy = ux * o * q * 1.6;
        const steps = Math.ceil(len / q);
        for (let s = 0; s <= steps; s++) {
          const px = ax + ux * (pad + s * q) + ox;
          const py = ay + uy * (pad + s * q) + oy;
          ctx.fillRect(Math.round(px / q) * q, Math.round(py / q) * q, q, q);
        }
      }
    }
    // letters
    LETTERS.forEach((l, i) => {
      const g = GLYPHS[l.ch];
      const x0 = Math.round(pos[i][0] - 2.5 * P);
      const y0 = Math.round(pos[i][1] - 3.5 * P);
      for (let r = 0; r < 7; r++)
        for (let c = 0; c < 5; c++) if (g[r][c] === "#") ctx.fillRect(x0 + c * P, y0 + r * P, P, P);
    });
  };
}

export default function PixelMolecule() {
  return (
    <CanvasStudy
      create={create}
      background={BG}
      still={3}
      label="'Arlan Marat' in a white pixel face on red drifts apart into a molecule diagram bonded by stair-stepped pixel runs, re-forms in new shapes and sets again."
    />
  );
}
Prompt

Build "Pixel molecule", a text animation. What it does: 'Arlan Marat' in a white pixel face on red drifts apart into molecule diagrams bonded by stair-stepped pixels, then sets again. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS. Technique: an HTML canvas, a requestAnimationFrame loop, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://www.arlan.me/vault (Arlan, MIT).