NEMAWASHI LAB

Wave Scramble

CSS

Text swaps between English and Japanese as a wave, not a per-character shuffle: the span renders as [resolved new text][churn band][untouched old text], and the front travels the string's length plus the band width over 320 + 9n ms, capped at 1.2 s. The band is 12% of the string clamped to 3-9 characters, reshuffled every 55 ms from a pool chosen by the TARGET language -- katakana plus Latin when the destination contains CJK, Latin alone otherwise. Head and churn carry the end-goal face from the first frame while the tail keeps the previous one, so no glyph ever passes through an intermediate typeface. React owns a single text node inside the span; the effect swaps in two styled children for the duration and restores that node on settle, so later React updates still land on a live node.

Before code, we tend the roots — people aligned, viability proven.

Tokyo based design studio, specialised in experience design & design interactions.

Let's work together

Source

WaveScramble.tsx115 lines
"use client";

import { useEffect, useState } from "react";
import Scramble from "@/components/Scramble";
import ScrambleLink from "../ScrambleLink";

/* The site's EN ⇄ JP scramble. Not a per-character shuffle: a wave travels
   front-to-back through the sentence as [resolved new text][churn band][old
   text], and the churn pool is drawn from the TARGET language (kana once the
   destination is Japanese). Head and churn render in the end-goal face from
   frame one while the untouched tail keeps the previous one, so no glyph ever
   passes through an intermediate typeface. Auto-toggles so the wave keeps
   running; the buttons force a direction. */

type Lang = "en" | "jp";

const FLUX_FACE = "font-[family-name:var(--flux)]";
const MINCHO_FACE =
  "font-[family-name:var(--mincho)] [font-variation-settings:normal] tracking-[0.04em]";

const CYCLE_MS = 3800;

const TAGLINE = {
  en: "Tokyo based design studio, specialised in experience design & design interactions.",
  jp: "東京のデザインスタジオ。エクスペリエンスデザインと、デザインインタラクションを専門としています。",
};

const LEAD = {
  en: "Before code, we tend the roots — people aligned, viability proven.",
  jp: "コードの前に、根をととのえる。人を揃え、勝算を確かめる。",
};

const LABEL = { en: "Let's work together", jp: "一緒に、はじめませんか" };

export default function WaveScramble() {
  const [lang, setLang] = useState<Lang>("en");
  const [auto, setAuto] = useState(true);

  useEffect(() => {
    if (!auto) return;
    const id = setInterval(
      () => setLang((l) => (l === "en" ? "jp" : "en")),
      CYCLE_MS,
    );
    return () => clearInterval(id);
  }, [auto]);

  const face = (l: Lang) => (l === "jp" ? MINCHO_FACE : FLUX_FACE);
  // end-goal face for the head + churn band, previous face for the old tail
  const wave = {
    headClass: face(lang),
    tailClass: face(lang === "jp" ? "en" : "jp"),
  };

  const pick = (l: Lang) => {
    setAuto(false);
    setLang(l);
  };

  return (
    <div
      className="fixed inset-0 flex flex-col items-center justify-center overflow-hidden px-8"
      style={{ background: "var(--paper)", color: "var(--ink)" }}
    >
      <div className="max-w-[46ch] text-center">
        <p className="text-[clamp(17px,2.1vw,26px)] leading-[1.6]">
          <Scramble {...wave} text={LEAD[lang]} />
        </p>
        <p className="mt-6 text-[clamp(12px,1.2vw,15px)] leading-[1.7] opacity-60">
          <Scramble {...wave} text={TAGLINE[lang]} />
        </p>
        <p className="mt-8 text-[11px] tracking-[0.12em] uppercase opacity-40">
          <Scramble {...wave} text={LABEL[lang]} />
        </p>
      </div>

      <div className="mt-12 flex items-center gap-4 text-[11px] tracking-[0.1em] uppercase">
        {(["en", "jp"] as const).map((l) => (
          <button
            key={l}
            onClick={() => pick(l)}
            style={{
              color: lang === l ? "var(--ink)" : "var(--faint)",
              fontFamily: "var(--flux)",
              fontVariationSettings: '"wght" 400, "SRIF" 100',
            }}
          >
            {l === "en" ? "EN" : "日本語"}
          </button>
        ))}
        <button
          onClick={() => setAuto((a) => !a)}
          style={{
            color: auto ? "var(--ink)" : "var(--faint)",
            fontFamily: "var(--flux)",
            fontVariationSettings: '"wght" 400, "SRIF" 100',
          }}
        >
          auto
        </button>
      </div>

      <div className="fixed bottom-6 left-6 z-10">
        <ScrambleLink
          from="WAVE SCRAMBLE"
          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