SPB Git

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%
3.1 KB · 104 lines typescript
Raw Blame History
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