import type { ThemeColor } from "@earendil-works/pi-coding-agent"; interface AnimationCellContext { char: string; x: number; y: number; row: string; rows: string[]; width: number; height: number; frameCount: number; } export interface AnimationRenderInput { logoLines: string[]; frameCount: number; colorize?: (color: ThemeColor, value: string) => string; } export interface LogoAnimation { id: string; getColorToken(context: AnimationCellContext): ThemeColor; } type CellColorResolver = (context: AnimationCellContext) => ThemeColor; function renderCellAnimation( rows: string[], frameCount: number, getColorToken: CellColorResolver, colorize?: (color: ThemeColor, value: string) => string, ): string[] { if (!colorize) return rows; const width = Math.max(...rows.map((row) => row.length), 0); const height = rows.length; return rows.map((row, y) => [...row] .map((char, x) => { if (char === " ") return char; const color = getColorToken({ char, x, y, row, rows, width, height, frameCount, }); return color ? colorize(color, char) : char; }) .join(""), ); } export function renderAnimation(animation: LogoAnimation, input: AnimationRenderInput): string[] { return renderCellAnimation( input.logoLines, input.frameCount, animation.getColorToken, input.colorize, ); } export const animations: LogoAnimation[] = [ { id: "static", getColorToken(): ThemeColor { return "text"; }, }, { id: "shimmer", getColorToken({ x, y, frameCount }): ThemeColor { const TOKENS: ThemeColor[] = [ "accent", "success", "warning", "mdHeading", "mdLink", "syntaxKeyword", "syntaxFunction", "syntaxString", "syntaxNumber", "syntaxType", "toolTitle", "bashMode", "thinkingHigh", ]; return TOKENS[(x + y + frameCount) % TOKENS.length] ?? "text"; }, }, { id: "shimmerSimple", getColorToken({ x, width, frameCount }): ThemeColor { const shimmerWidth = 2; const cycle = width + shimmerWidth * 2; const head = frameCount % cycle; const start = head - shimmerWidth; const end = head; if (x >= start && x <= end) return "accent"; if (x === start - 1 || x === end + 1) return "muted"; return "dim"; }, }, { id: "scanline", getColorToken({ x, y, width, height, frameCount }): ThemeColor { const BEAM: ThemeColor[] = [ "borderAccent", // head: bright cyan "accent", // core ]; const AMBIENT: ThemeColor[] = [ "thinkingOff", // very dim pulse "muted", ]; const tailLength = BEAM.length - 1; const totalFrames = width + tailLength; const wobble = Math.round(Math.sin((frameCount * 0.15) + y * 0.8) * 1.5); const scanX = (frameCount % totalFrames) - tailLength + wobble; const distance = x - scanX; if (distance >= 0 && distance < BEAM.length) { return BEAM[distance]!; } const ambientPhase = Math.floor((x + y * 3 + frameCount * 0.3) * 0.4) % AMBIENT.length; return AMBIENT[ambientPhase] ?? "dim"; }, }, { id: "scaryRed", getColorToken({ x, y, height, frameCount }): ThemeColor { const FIRE: ThemeColor[] = [ "error", // deep red base "warning", // orange mid "mdHeading", // gold flicker "warning", // orange mid "error", // deep red base "mdHeading", // gold flicker "error", // deep red base // "mdHeading", // gold flicker // "syntaxString", // salmon/amber tip // "muted", // dying ember // "dim", // ash ]; const flicker = Math.sin(frameCount * 0.4 + x * 0.9 + y * 1.3) * 2 + Math.sin(frameCount * 0.7 + x * 0.5) * 1.5; const band = (y * 1.8 - (frameCount * 0.8) + flicker + x * 0.15) % (FIRE.length * 2); const idx = Math.abs(Math.floor(band)) % FIRE.length; const distFromBottom = height - 1 - y; if (distFromBottom < 1) return FIRE[0]!; if (distFromBottom < 2) return FIRE[Math.floor(idx * 0.5) % 2]!; return FIRE[idx] ?? "dim"; }, }, ] // Rerence color list // const AVAILABLE_COLORS = [ // "accent", // teal / cyan-green // "border", // blue // "borderAccent", // bright cyan // "borderMuted", // dark gray // "success", // green // "error", // red // "warning", // yellow // "muted", // gray // "dim", // dim gray // "text", // terminal default text // "thinkingText", // gray // // "userMessageText", // terminal default text // "customMessageText", // terminal default text // "customMessageLabel",// purple // "toolTitle", // terminal default text // "toolOutput", // gray // // "mdHeading", // gold / yellow-orange // "mdLink", // blue // "mdLinkUrl", // dim gray // "mdCode", // teal / accent // "mdCodeBlock", // green // "mdCodeBlockBorder", // gray // "mdQuote", // gray // "mdQuoteBorder", // gray // "mdHr", // gray // "mdListBullet", // teal / accent // // "toolDiffAdded", // green // "toolDiffRemoved", // red // "toolDiffContext", // gray // // "syntaxComment", // green // "syntaxKeyword", // blue // "syntaxFunction", // pale yellow // "syntaxVariable", // light blue // "syntaxString", // orange / salmon // "syntaxNumber", // pale green // "syntaxType", // teal // "syntaxOperator", // light gray // "syntaxPunctuation", // light gray // // "thinkingOff", // dark gray // "thinkingMinimal", // gray // "thinkingLow", // muted blue // "thinkingMedium", // blue-gray // "thinkingHigh", // purple // "thinkingXhigh", // bright purple / magenta // // "bashMode", // green // ] as const satisfies readonly ThemeColor[];