NEMAWASHI LAB

Design Tiles

SVGCanvas

Two takes on the same measuring rig. The reference bar gives every word a colour tile, but the tile is not the line box: canvas measureText's actualBoundingBoxAscent/Descent gives each character its true ink box (getExtentOfChar would return an identical em box for every glyph), so a rect hugs the skyline of the letters above it while x positions come from cumulative substring widths to keep kerning honest. Tiles fly in 130 ms apart over 760 ms once the bar is in view, then idle-shuffle swatches every 1.3-3.2 s with a 520 ms colour lerp and re-roll on hover. The lower bar fuses that with the particle spoiler: the text lives inside the SVG beside its rects, hidden under a cloud sampled from its own coverage, and a tap drives one shared screen-space radius through a per-word SVG mask so the iris opens across every tile at once.

reference · variable bounding box per character
fusion · same tiles + particle blur (tap to open the iris)

Source

DesignTilesDemo.tsx77 lines
"use client";

import { useState } from "react";
import DesignTiles from "@/components/DesignTiles";
import TileSpoiler from "@/components/TileSpoiler";
import ScrambleLink from "../ScrambleLink";

/* The /tiles evaluation route, re-homed in the lab — same two components, same
   pairing. Top: the reference bar, one colour tile per word, each tile's box
   measured from the glyphs it actually sits under. Bottom: the same tiles fused
   with the particle spoiler, so the phrase arrives hidden and a tap opens the
   iris. `run` remounts both, because each only plays its entrance once. */

const WORDS = ["design", "is", "how", "it", "works"];

const label: React.CSSProperties = {
  fontSize: 10,
  letterSpacing: "0.14em",
  textTransform: "uppercase",
  color: "var(--faint)",
  fontFamily: "var(--flux)",
  fontVariationSettings: '"wght" 400, "SRIF" 100',
};

export default function DesignTilesDemo() {
  const [run, setRun] = useState(0);

  return (
    <div
      className="fixed inset-0 flex flex-col items-center justify-center overflow-hidden px-6 select-none"
      style={{ background: "var(--paper)" }}
    >
      <span style={label}>reference &middot; variable bounding box per character</span>
      <DesignTiles
        key={`tiles-${run}`}
        words={WORDS}
        style={{
          width: "min(88%, 720px)",
          minHeight: "clamp(3.5rem, 11vh, 6.5rem)",
          marginTop: 14,
        }}
      />

      <span style={{ ...label, marginTop: 34 }}>
        fusion &middot; same tiles + particle blur (tap to open the iris)
      </span>
      <TileSpoiler
        key={`spoiler-${run}`}
        text={WORDS.join(" ")}
        style={{ fontSize: "clamp(1.3rem, 3.1vw, 2.5rem)", marginTop: 16 }}
      />

      <button
        onClick={() => setRun((n) => n + 1)}
        className="mt-10 text-[11px] tracking-[0.1em] uppercase transition-colors"
        style={{
          color: "var(--faint)",
          fontFamily: "var(--flux)",
          fontVariationSettings: '"wght" 400, "SRIF" 100',
        }}
      >
        replay
      </button>

      <div className="fixed bottom-6 left-6 z-10">
        <ScrambleLink
          from="DESIGN TILES"
          to="← LAB"
          href="/lab"
          className="text-[11px] tracking-[0.1em] text-foreground/30 uppercase hover:text-foreground/60 transition-colors"
          style={{ fontVariationSettings: "'wght' 400, 'SRIF' 100" }}
        />
      </div>
    </div>
  );
}

NEMAWASHI — Kotaro Abe