NEMAWASHI LAB

Inline

FM

Running text is interleaved with animated gradient chips -- small inline-block colour swatches that expand from zero width on mount with staggered CSS transitions (each chip delayed by index * 100 ms). Each chip renders a canvas with four slowly orbiting radial gradients composited in overlay blend mode over a static linear-gradient background, giving the colour blocks a slow lava-lamp shimmer. A toggle switches between "with colour" (chips visible) and "text only" (chips collapse back to zero width with a reversed stagger), demonstrating how inline media can punctuate editorial text flow.

GIRAGIRA isnew. Theexperiments aren't.
We exploretypeandin
things thatcatchlight.

Source

InlineText.tsx246 lines
"use client";

import { useEffect, useRef, useState, useCallback } from "react";
import ScrambleLink from "../ScrambleLink";

// GRGR pastel palette for inline chips
const CHIPS = [
  { gradient: "linear-gradient(135deg, #f99a9e, #fbd9da)", label: "glare" },
  { gradient: "linear-gradient(135deg, #ecdca7, #f7f1de)", label: "light" },
  { gradient: "linear-gradient(135deg, #afcae4, #daebfb)", label: "colour" },
  { gradient: "linear-gradient(135deg, #a39af8, #dcd9fb)", label: "motion" },
  { gradient: "linear-gradient(135deg, #f0bfa3, #f8e6dc)", label: "type" },
  { gradient: "linear-gradient(135deg, #9dcdf6, #daebfb)", label: "play" },
  { gradient: "linear-gradient(135deg, #f99a9e, #a39af8)", label: "depth" },
];

interface Segment {
  type: "text" | "chip";
  value: string;
  chipIdx?: number;
}

const SEGMENTS: Segment[] = [
  { type: "text", value: "GIRAGIRA is" },
  { type: "chip", value: "glare", chipIdx: 0 },
  { type: "text", value: "new. The" },
  { type: "chip", value: "light", chipIdx: 1 },
  { type: "text", value: "experiments aren't." },
  { type: "text", value: "\n" },
  { type: "text", value: "We explore" },
  { type: "chip", value: "colour", chipIdx: 2 },
  { type: "text", value: "type" },
  { type: "chip", value: "motion", chipIdx: 3 },
  { type: "text", value: "and" },
  { type: "chip", value: "play", chipIdx: 5 },
  { type: "text", value: "in" },
  { type: "text", value: "\n" },
  { type: "text", value: "things that" },
  { type: "chip", value: "type", chipIdx: 4 },
  { type: "text", value: "catch" },
  { type: "chip", value: "depth", chipIdx: 6 },
  { type: "text", value: "light." },
];

function AnimatedChip({
  gradient,
  show,
  delay,
}: {
  gradient: string;
  show: boolean;
  delay: number;
}) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const rafRef = useRef<number | null>(null);

  useEffect(() => {
    const cvs = canvasRef.current;
    if (!cvs) return;
    const ctx = cvs.getContext("2d")!;
    const w = cvs.width;
    const h = cvs.height;

    function draw(t: number) {
      ctx.clearRect(0, 0, w, h);

      for (let i = 0; i < 4; i++) {
        const x = w * 0.2 + Math.sin(t * 0.0008 + i * 1.8) * w * 0.3;
        const y = h * 0.5 + Math.cos(t * 0.0012 + i * 2.1) * h * 0.25;
        const r = 20 + Math.sin(t * 0.001 + i) * 8;
        const hue = (t * 0.02 + i * 60) % 360;
        const grad = ctx.createRadialGradient(x, y, 0, x, y, r);
        grad.addColorStop(0, `hsla(${hue}, 70%, 82%, 0.6)`);
        grad.addColorStop(1, `hsla(${hue}, 70%, 82%, 0)`);
        ctx.fillStyle = grad;
        ctx.beginPath();
        ctx.arc(x, y, r, 0, Math.PI * 2);
        ctx.fill();
      }

      rafRef.current = requestAnimationFrame(draw);
    }

    rafRef.current = requestAnimationFrame(draw);
    return () => {
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
    };
  }, []);

  return (
    <span
      className="inline-block overflow-hidden align-middle"
      style={{
        width: show ? "clamp(50px, 7vw, 110px)" : "0",
        opacity: show ? 1 : 0,
        marginLeft: show ? "0.35em" : "0",
        marginRight: show ? "0.35em" : "0.25em",
        borderRadius: "6px",
        cursor: "pointer",
        transform: "scale(1)",
        transitionProperty: "width, opacity, margin-left, margin-right, transform",
        transitionDuration: "0.5s, 0.35s, 0.5s, 0.5s, 0.2s",
        transitionTimingFunction:
          "cubic-bezier(0.4, 0, 0.2, 1), ease, cubic-bezier(0.4, 0, 0.2, 1), cubic-bezier(0.4, 0, 0.2, 1), ease",
        transitionDelay: `${delay}s`,
      }}
      onMouseEnter={(e) => {
        (e.currentTarget as HTMLElement).style.transform = "scale(1.08)";
      }}
      onMouseLeave={(e) => {
        (e.currentTarget as HTMLElement).style.transform = "scale(1)";
      }}
    >
      <span
        className="block relative"
        style={{
          height: "clamp(35px, 4.95vw, 80px)",
          background: gradient,
          borderRadius: "6px",
          overflow: "hidden",
        }}
      >
        <canvas
          ref={canvasRef}
          width={120}
          height={80}
          className="absolute inset-0 w-full h-full"
          style={{ mixBlendMode: "overlay", opacity: 0.7 }}
        />
      </span>
    </span>
  );
}

export default function InlineText() {
  const [mode, setMode] = useState<"chips" | "text">("chips");
  const [revealed, setRevealed] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => setRevealed(true), 300);
    return () => clearTimeout(timer);
  }, []);

  const showChips = mode === "chips" && revealed;
  const chipCount = SEGMENTS.filter((s) => s.type === "chip").length;

  let chipSeq = 0;

  return (
    <div
      className="fixed inset-0 flex flex-col items-start justify-center select-none"
      style={{
        background: "#f5f3f0",
        padding: "clamp(24px, 5vw, 80px)",
      }}
    >
      <div className="flex flex-col">
        <p
          style={{
            fontFamily: "var(--font-flux), system-ui, sans-serif",
            fontSize: "clamp(32px, 4.5vw, 72px)",
            fontVariationSettings: "'wght' 300, 'SRIF' 0",
            lineHeight: 1.4,
            color: "#1a1a2e",
          }}
        >
          {SEGMENTS.map((seg, i) => {
            if (seg.type === "text" && seg.value === "\n") {
              return <br key={i} />;
            }
            if (seg.type === "chip" && seg.chipIdx != null) {
              const chip = CHIPS[seg.chipIdx];
              const idx = chipSeq++;
              const delay = showChips
                ? idx * 0.1
                : (chipCount - 1 - idx) * 0.05;
              return (
                <AnimatedChip
                  key={i}
                  gradient={chip.gradient}
                  show={showChips}
                  delay={delay}
                />
              );
            }
            return (
              <span key={i} className="whitespace-nowrap">
                {seg.value}
              </span>
            );
          })}
        </p>

        <div
          className="flex gap-6"
          style={{
            marginTop: "clamp(40px, 5vw, 80px)",
            fontFamily: "var(--font-flux), system-ui, sans-serif",
            fontSize: "clamp(16px, 2vw, 32px)",
            fontVariationSettings: "'wght' 300, 'SRIF' 0",
          }}
        >
          <button
            onClick={() => setMode("chips")}
            className="border-0 bg-transparent cursor-pointer p-0"
            style={{
              fontFamily: "inherit",
              fontSize: "inherit",
              fontVariationSettings: "inherit",
              letterSpacing: "-0.01em",
              color: mode === "chips" ? "#1a1a2e" : "rgba(26,26,46,0.3)",
              transition: "color 0.2s",
            }}
          >
            with colour
          </button>
          <button
            onClick={() => setMode("text")}
            className="border-0 bg-transparent cursor-pointer p-0"
            style={{
              fontFamily: "inherit",
              fontSize: "inherit",
              fontVariationSettings: "inherit",
              letterSpacing: "-0.01em",
              color: mode === "text" ? "#1a1a2e" : "rgba(26,26,46,0.3)",
              transition: "color 0.2s",
            }}
          >
            text only
          </button>
        </div>
      </div>

      <div className="fixed bottom-6 left-6" style={{ zIndex: 10 }}>
        <ScrambleLink
          from="INLINE"
          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