import type { ColorInput } from "bun"; import type { Effect } from "./animated.tsx"; import { lerpColor, packColor } from "./utils.ts"; const TAU = Math.PI * 2; /** A value, or an accessor for one — so effect colors can themselves animate. */ export type MaybeAccessor = T | (() => T); function read(v: MaybeAccessor): T { return typeof v === "function" ? (v as () => T)() : v; } export type ShimmerOptions = { /** Resting color. */ baseColor: MaybeAccessor; /** Color at the crest of the sweep. */ highlightColor: MaybeAccessor; /** Milliseconds per cell. Lower = faster. Default 200. */ speed?: number; /** Sweep direction. Default "ltr". */ direction?: "ltr" | "rtl"; }; /** * A highlight crest that sweeps across the region by column, with a soft * two-cell falloff. The classic "thinking" shimmer. */ export function shimmer(opts: ShimmerOptions): Effect { const speed = opts.speed ?? 200; const pad = 10; const ltr = (opts.direction ?? "ltr") === "ltr"; return ({ t, paused }) => { // Colors are sampled once per frame, not per cell. const base = packColor(read(opts.baseColor)); if (paused) return () => base; const hi = packColor(read(opts.highlightColor)); const near = lerpColor(base, hi, 0.5); const far = lerpColor(base, hi, 0.2); return (x, _y, width) => { const len = width + pad * 2; const pos = Math.floor(t / speed) % len; const glimmer = ltr ? pos - pad : width + pad - pos; const dist = Math.abs(x - glimmer); if (dist === 0) return hi; if (dist === 1) return near; if (dist === 2) return far; return base; }; }; } export type WaveOptions = { from: MaybeAccessor; to: MaybeAccessor; /** Milliseconds per full cycle. Default 1500. */ speed?: number; /** Cells per wave. Lower = tighter bands. Default 6. */ wavelength?: number; }; /** * A smooth color gradient that scrolls along the region by column — each cell * sits at a different point on a sine between `from` and `to`, so color "flows" * through the content. A gentler, continuous alternative to {@link shimmer}. */ export function wave(opts: WaveOptions): Effect { const speed = opts.speed ?? 1500; const wavelength = opts.wavelength ?? 6; return ({ t, paused }) => { const from = packColor(read(opts.from)); if (paused) return () => from; const to = packColor(read(opts.to)); const tPhase = (t / speed) * TAU; return (x) => { const phase = (Math.sin((x / wavelength) * TAU - tPhase) + 1) / 2; return lerpColor(from, to, phase); }; }; } export type PulseOptions = { baseColor: MaybeAccessor; flashColor: MaybeAccessor; /** Full cycle period in ms. Default 2000. */ period?: number; }; /** * A uniform pulse — the whole region breathes between two colors in unison * (position-independent). */ export function pulse(opts: PulseOptions): Effect { const period = opts.period ?? 2000; return ({ t, paused }) => { // Position-independent: the whole region is one color per frame. const base = packColor(read(opts.baseColor)); if (paused) return () => base; const phase = (Math.sin((t / period) * TAU) + 1) / 2; const color = lerpColor(base, read(opts.flashColor), phase); return () => color; }; }