spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: src/tui/motion.ts4 * Description: Motion doctrine — the six sanctioned animations as a tick-driven state machine, `--motion off` aware (TUI v2 §6).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910/**11 * The complete catalog — six effects, each carrying meaning, nothing else12 * (TUI v2 §6): splash sweep · phase-transition flash · approval-panel13 * breathing · gauge drain on compact · post-stream code colorization ·14 * verify-strip contraction.15 */16export type MotionEffect =17 | "splash-sweep"18 | "phase-flash"19 | "panel-breath"20 | "gauge-drain"21 | "code-colorize"22 | "verify-contract";2324/** Frames each transition runs (~16 ms ticks; ≤ 300 ms per rule 2). */25const EFFECT_FRAMES: Record<MotionEffect, number> = {26 "splash-sweep": 10,27 "phase-flash": 3,28 "panel-breath": Number.POSITIVE_INFINITY, // waiting indicator: ≤ 1 Hz, runs until decided29 "gauge-drain": 18,30 "code-colorize": 1,31 "verify-contract": 6,32};3334interface ActiveEffect {35 effect: MotionEffect;36 frame: number;37}3839/**40 * Rules (TUI v2 §6):41 * 1. every animation advances ONLY inside the existing render tick — zero42 * extra timers, zero re-renders outside the frame budget;43 * 2. transitions ≤ 300 ms; waiting indicators ≤ 1 Hz;44 * 3. `enabled: false` (--motion off, NVIM, screen readers) reduces everything45 * to its final static state instantly.46 */47export class MotionController {48 #enabled: boolean;49 readonly #active = new Map<string, ActiveEffect>();5051 constructor(options: { enabled: boolean }) {52 this.#enabled = options.enabled;53 }5455 get enabled(): boolean {56 return this.#enabled;57 }5859 setEnabled(enabled: boolean): void {60 this.#enabled = enabled;61 if (!enabled) this.#active.clear();62 }6364 /** Begin an effect under a stable key (e.g. `phase-flash:implement`). */65 start(key: string, effect: MotionEffect): void {66 if (!this.#enabled) return;67 this.#active.set(key, { effect, frame: 0 });68 }6970 stop(key: string): void {71 this.#active.delete(key);72 }7374 /**75 * Advance every active effect by one render tick; finished transitions are76 * dropped. Returns whether anything is still animating (the renderer may77 * skip scheduling extra frames when false).78 */79 tick(): boolean {80 for (const [key, active] of this.#active) {81 active.frame += 1;82 if (active.frame >= EFFECT_FRAMES[active.effect]) this.#active.delete(key);83 }84 return this.#active.size > 0;85 }8687 /** Current frame of an effect, or null when inactive/off (render static). */88 frame(key: string): number | null {89 const active = this.#active.get(key);90 return active !== undefined ? active.frame : null;91 }9293 /**94 * Breathing intensity for the approval panel: ±8% luminance over ~2 s.95 * Returns 0 when motion is off — the panel renders at rest.96 */97 breathIntensity(key: string, ticksPerSecond = 60): number {98 const active = this.#active.get(key);99 if (active === undefined || active.effect !== "panel-breath") return 0;100 const period = 2 * ticksPerSecond;101 return 0.08 * Math.sin((2 * Math.PI * active.frame) / period);102 }103}104