Files
fn_registry/cpp/apps/shaders_lab/SPEC.md
T
2026-05-11 16:30:43 +02:00

36 KiB
Raw Blame History

Shader Playground — MVP Spec

Editor web de shaders GLSL con:

  • Auto-UI generada a partir de anotaciones en uniforms.
  • Integración con Claude API para generar y modificar shaders desde chat.
  • Registro mínimo de funciones reutilizables que el LLM puede consultar e inyectar.
  • Sistema de sidebars modulares estilo apps de VJing: canvas central protagonista, paneles acoplables/ocultables a los lados.
  • Output fullscreen para sesiones de VJing.

Pensado para completarse en un finde (fase A sábado + fase B domingo).


0. Filosofía y no-objetivos

Objetivos del MVP

  • Escribir GLSL en un editor web, ver el resultado en vivo, con el canvas como protagonista visual.
  • Declarar uniforms anotados → panel de controles se genera solo (sliders, color pickers, xy-pads, knobs).
  • Chat lateral con Claude que genera shaders, los modifica, y usa el fn-registry como herramienta para reutilizar código existente.
  • Biblioteca de funciones GLSL con búsqueda, tal que el usuario pueda "guardar este fbm" o "guardar este efecto de nube" y reutilizarlo en futuros shaders.
  • Guardar/cargar shaders y funciones GLSL en localStorage.
  • Modo fullscreen del canvas para usar en sesiones reales de VJ.

NO objetivos (explícitamente fuera del MVP)

  • Backend propio / base de datos / multi-usuario.
  • Visualizaciones matemáticas auxiliares (FFT, campos, derivadas).
  • MIDI / OSC / audio FFT / Syphon / Spout / NDI.
  • Multi-pass / buffers encadenados tipo Shadertoy.
  • Vertex shader custom (solo fullscreen quad fijo).
  • Compute shaders.
  • Fine-tuning del LLM, RAG elaborado, embeddings.
  • Categorías/taxonomía compleja del registry (flat namespace con tags es suficiente).
  • Múltiples shaders simultáneos con crossfade / capas estilo Photoshop.
  • Sidebars flotantes arrastrables tipo Ableton/Resolume (siempre acoplados a los bordes).

Si el MVP se usa de verdad durante un mes, las features de arriba entran en futuras iteraciones una a una.


1. Stack técnico

  • Package manager / runtime dev: Bun.
  • Build: Vite.
  • UI: React 18 + TypeScript strict.
  • Estilos: Tailwind + shadcn/ui.
  • Iconos: lucide-react.
  • Estado: Zustand.
  • Editor de código: CodeMirror 6 (paquetes @codemirror/state, @codemirror/view, @codemirror/legacy-modes para GLSL).
  • Renderer: WebGL2 directo (sin regl ni Three.js). Wrapper propio minimal en src/renderer/.
  • Layout: CSS Grid + react-resizable-panels para los sidebars acoplados.
  • Color picker: react-colorful.
  • LLM: Claude API (@anthropic-ai/sdk) usando claude-opus-4-7 con streaming.
  • Persistencia: localStorage directo, envuelto en módulo fino.

Por qué WebGL2 puro y no regl

  • Vamos a hacer cosas específicas (hot-swap de programas, introspección de uniforms activos, manejo fino de errores de compilación con números de línea) que regl abstrae de formas que más tarde querríamos revertir.
  • Aprender la API te deja preparado para WebGPU/wgpu en v2.
  • El wrapper que necesitamos son ~200 líneas de TypeScript. Aceptable.

Estructura de carpetas

src/
  editor/              # CodeMirror wrapper y modo GLSL
  renderer/            # WebGL2 wrapper, compile pipeline, fullscreen quad
  parser/              # Extracción de uniforms desde GLSL source
  registry/            # fn-registry: CRUD, búsqueda, inyección
  llm/                 # Cliente de Claude + tool definitions + prompt templates
  ui/
    layout/            # Icon rail, sidebar containers, canvas stage
    sidebars/          # CodeSidebar, ControlsSidebar, AgentSidebar, RegistrySidebar
    controls/          # Slider, ColorPicker, XYPad, Knob, Toggle (widgets individuales)
    components/        # shadcn/ui imports
  store/               # Zustand stores (uno dedicado a layout)
  storage/             # localStorage wrapper + schema
  seed/                # Shaders y funciones de ejemplo que se cargan la primera vez
  App.tsx
  main.tsx

2. Layout de la aplicación (sistema de sidebars)

Principios

  • El canvas del preview es siempre el protagonista visual. Ocupa el área central y nunca se reduce a menos de ~60% del viewport salvo en layouts atípicos. Nada de mandarlo a un rincón.
  • Dos sidebars visibles a la vez como configuración por defecto: uno a la izquierda (típicamente el Code), uno a la derecha (típicamente los Controls). El resto se invocan cuando hacen falta y reemplazan al que esté en ese lado.
  • Sidebars acoplados a los bordes, no flotantes ni arrastrables. Simplicidad > flexibilidad en MVP.
  • Ancho de sidebar arrastrable (min 240px, max ~600px), persistido por sidebar.
  • Toggle suave (show/hide con animación corta, 150ms).

Zonas y componentes

┌──┬────────────────────┬────────────────────────┬────────────────────┐
│  │                    │                        │                    │
│  │                    │                        │                    │
│I │   Left sidebar     │    Canvas (preview)    │   Right sidebar    │
│c │   (CODE o          │    WebGL2 fullscreen   │   (CONTROLS o      │
│o │    REGISTRY)       │    quad               │    AGENT)          │
│n │                    │                        │                    │
│  │                    │                        │                    │
│R │                    │                        │                    │
│a │                    │                        │                    │
│i │                    │                        │                    │
│l │                    │                        │                    │
│  │                    │                        │                    │
└──┴────────────────────┴────────────────────────┴────────────────────┘

Icon rail (columna vertical fija, siempre visible)

Ancho ~48px en el borde izquierdo. Iconos verticales con lucide-react:

  • 📄 Code (ícono FileCode2) — toggle del CodeSidebar.
  • 🎛️ Controls (ícono Sliders) — toggle del ControlsSidebar.
  • 💬 Agent (ícono Sparkles o MessageSquare) — toggle del AgentSidebar.
  • 📚 Registry (ícono Library o BookOpen) — toggle del RegistrySidebar.
  • ─── separador ───
  • 💾 Shaders (ícono Save) — abre el panel de shaders guardados (también es un sidebar, en el lado opuesto al que esté libre).
  • ⚙️ Settings (ícono Settings) — abre modal de settings (API key, modelo, tema).
  • ⏏️ Fullscreen (ícono Maximize2) — entra en modo fullscreen VJ.

Cada botón del rail muestra un indicador visual si su sidebar está activo (punto de color al lado del icono, o fondo resaltado).

Reglas de apertura de sidebars

Cada sidebar tiene un "lado preferido":

  • CODE → izquierda (preferente).
  • CONTROLS → derecha (preferente).
  • AGENT → derecha (preferente).
  • REGISTRY → izquierda (preferente).
  • SHADERS → izquierda (preferente).

Al pulsar el icono:

  1. Si ese sidebar ya está abierto → cerrarlo.
  2. Si no está abierto → abrirlo en su lado preferido, sustituyendo lo que hubiera en ese lado.
  3. Modificador Alt + click sobre el icono → abrirlo en el lado opuesto (forzar).

Solo puede haber un sidebar por lado. No se apilan, no hay tabs superpuestos en el MVP.

Estado del layout en Zustand

type SidebarId = 'code' | 'controls' | 'agent' | 'registry' | 'shaders';
type Side = 'left' | 'right';

interface LayoutState {
  sidebars: {
    left: SidebarId | null;
    right: SidebarId | null;
  };
  widths: Record<Side, number>;         // px, persistido
  fullscreen: boolean;

  toggle: (id: SidebarId, opts?: { forceSide?: Side }) => void;
  close: (side: Side) => void;
  setWidth: (side: Side, width: number) => void;
  enterFullscreen: () => void;
  exitFullscreen: () => void;
}

Layout por defecto al primer arranque

Left:  CODE
Right: CONTROLS

Esto es el "layout trabajo": editor + controles en vivo alrededor del canvas. Persiste en localStorage.

Modo fullscreen (modo VJ)

  • Icon rail, sidebars y topbar desaparecen completamente.
  • Canvas ocupa el 100% del viewport.
  • Atajos siguen funcionando: F o Esc salen.
  • Teclas 1..9 cargan shaders guardados por índice (útil para directo).
  • Overlay botón transparente en esquina inferior derecha (icono Maximize2 inverso, solo visible al mover el ratón en los últimos ~2 segundos, fade-out después). Click → salida de fullscreen. Esto es el "como en Resolume": para cuando estás tocando y quieres volver sin buscar tecla.
  • Para ajustar uniforms en fullscreen sin salir, el camino es salir con Esc, ajustar, y volver a F. El MVP no tiene edge panels translúcidos — eso se evaluará en v2 tras uso real.

Topbar (fuera de fullscreen)

Encima del canvas, arriba del todo, ~40px de alto:

  • Izq: nombre del shader actual (editable en línea con doble click).
  • Centro: botones Play / Pause / Reset time.
  • Der: indicador de compile (verde OK / rojo con error on line X), botón nuevo, selector de shader (dropdown).

3. Contenido de cada sidebar

CODE sidebar

  • Header: título "Code" + indicador de modo actual (sidebar / overlay).
  • Body: CodeMirror 6 con GLSL syntax highlight, números de línea, error underlining.
  • Footer: info line con bytes, líneas, último compile time, estado (OK / Error línea N).

Dos modos de visualización, elegibles en Settings:

  1. Sidebar mode (default): el editor vive acoplado al borde izquierdo, como cualquier otro sidebar. Coexiste con el preview y los controles. Es el modo "trabajo".

  2. Overlay mode (estilo apps VJ): al activar CODE, en lugar de abrir un sidebar, aparece un modal semitransparente (70% opacidad, fondo oscurecido) flotante sobre el canvas. El canvas sigue renderizando por debajo. Esc o click fuera cierra el modal. Es el modo "live coding / VJ" donde el canvas al máximo es lo importante y el código es una ventana que aparece y desaparece.

La elección vive en el modal de Settings (ver §9). El usuario puede cambiar entre modos en cualquier momento. El comportamiento del icono "Code" en el rail se adapta automáticamente: en sidebar mode abre el sidebar, en overlay mode abre el modal.

Atajo Cmd/Ctrl + / — independientemente del modo configurado, abre el editor en overlay temporalmente. Útil para un vistazo rápido sin cambiar preferencia.

CONTROLS sidebar

  • Header: "Controls" + botón "Reset to defaults" (devuelve todos los uniforms a su default del shader actual).
  • Body: lista vertical de widgets autogenerados desde los uniforms anotados. Cada uniform es una "card" con:
    • Nombre del uniform.
    • Widget (Slider / ColorPicker / XYPad / Knob / Toggle / Slider2D).
    • Valor actual formateado numéricamente.
  • Footer: contador "N uniforms detected".
  • Scroll vertical si no caben.

Si el shader no tiene uniforms anotados: mensaje placeholder "Declare uniforms with // @slider ... annotations to see controls here." con un link "See annotation format" que abre un popover con ejemplos.

AGENT sidebar

  • Header: "Agent" + selector de modelo (dropdown: Opus/Sonnet/Haiku) + botón "Clear conversation".
  • Body: lista de mensajes con markdown rendering, bloques de código GLSL con highlight, colapsables para tool_use y tool_result ("🔧 Searched registry: 3 results").
  • Botón "Apply this shader" junto a bloques de código en respuestas del LLM (ver §7).
  • Below body: chips con prompts de demostración.
  • Footer: textarea de input multilínea, Cmd/Ctrl + Enter envía, Shift + Enter nueva línea.

REGISTRY sidebar

  • Header: "Functions" + input de búsqueda (filtra en vivo por name/description/tags).
  • Body: lista de funciones registradas. Cada item:
    • Nombre + signature.
    • Tags como pills (color distinto por tag).
    • Descripción corta (2 líneas max, truncada).
    • Acciones hover: "Insert into current shader" (añade al @registry_inject_begin/end markers), "View code" (expande inline), "Edit", "Delete".
  • Footer: botón "+ New function" (abre modal de creación), "Import/Export JSON".

SHADERS sidebar

  • Header: "Saved shaders" + botón "+ New".
  • Body: lista de shaders guardados. Cada item:
    • Thumbnail pequeño (64x36 px) generado rasterizando el shader en un canvas offscreen al guardar.
    • Nombre + fecha de última edición.
    • Acciones hover: "Load", "Rename", "Duplicate", "Delete", "Export".
  • Search por nombre en la cabecera.

Los thumbnails son una mejora visual importante para VJing (reconocimiento instantáneo). Si no da tiempo, fallback a iconos/gradientes generados deterministicamente desde el nombre (tipo GitHub identicons).


4. Invocación de sidebars

Canal principal: icon rail

El icon rail vertical permanente en el borde izquierdo es el único camino de descubrimiento y uso habitual. Siempre visible (excepto en fullscreen VJ). Click en el icono → toggle del sidebar. Alt+click → abrir en lado opuesto al preferido.

El rail es la fuente de verdad del layout: cualquier usuario, sin leer documentación, sabe qué sidebars existen y puede abrirlos con un click.

Atajos de teclado (para usuarios avanzados)

Existen pero no son el canal principal — duplican funcionalidad del rail para quien quiera manos en el teclado:

  • F1..F5 — toggle de cada sidebar (ver §11 para la tabla completa).
  • Cmd/Ctrl + B — colapsar ambos sidebars (canvas máximo sin fullscreen).
  • F — fullscreen VJ.
  • Esc — cerrar lo que esté abierto en cascada.

Modo VJ: botón flotante para salir de fullscreen

En modo fullscreen, al mover el ratón aparece un único botón flotante translúcido en la esquina inferior derecha para salir. Fade-out tras 2s. No hay más invocaciones flotantes — todo lo demás via Esc o teclas de atajo.


5. Renderer (WebGL2 puro)

Fullscreen quad fijo

Vertex shader no editable:

#version 300 es
in vec2 a_position;
void main() {
  gl_Position = vec4(a_position, 0.0, 1.0);
}

Geometría: dos triángulos cubriendo [-1,1]².

Fragment shader — lo escribe el usuario

El wrapper antepone automáticamente:

#version 300 es
precision highp float;
out vec4 fragColor;

uniform vec2  u_resolution;
uniform float u_time;
uniform vec2  u_mouse;

Estos tres uniforms siempre están disponibles, el parser los ignora (no aparecen en controls).

Render loop

  • requestAnimationFrame, performance.now()u_time en segundos.
  • Play/pause congela/descongela u_time.
  • Reset pone u_time = 0.
  • Canvas se redimensiona vía ResizeObserver al tamaño del stage central (cambia cuando se abren/cierran sidebars).

Compile pipeline

  • Al cambiar source: debounce 250 ms.
  • gl.createShadergl.shaderSourcegl.compileShader.
  • Si COMPILE_STATUS es false: gl.getShaderInfoLog(), parsear línea (formato ERROR: 0:<line>: <msg>), propagar al editor.
  • Si compila: link program, swap atómico con el anterior, delete del anterior.
  • Si el programa nuevo falla: mantener el anterior, canvas NUNCA se queda en negro por un error.
  • Introspección post-link: gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS) para validar que el parser encontró lo mismo que GLSL realmente expone. Discrepancias → warning en consola (no error).

Wrapper API

interface Renderer {
  compile(source: string): Promise<CompileResult>;
  setUniform(name: string, value: number | number[] | boolean): void;
  setPlaying(playing: boolean): void;
  resetTime(): void;
  resize(w: number, h: number): void;
  snapshot(w: number, h: number): Promise<ImageBitmap>;  // para thumbnails
  dispose(): void;
}

type CompileResult =
  | { ok: true; activeUniforms: string[] }
  | { ok: false; line: number; message: string };

6. Parser de uniforms

Formato de anotación

uniform float u_speed;      // @slider min=0 max=5 default=1
uniform float u_freq;       // @slider min=0.1 max=100 default=10 log=true
uniform vec3  u_colorA;     // @color default=0.1,0.2,0.5
uniform vec4  u_tint;       // @color default=1,0.5,0,1
uniform vec2  u_origin;     // @xy min=-1 max=1 default=0,0
uniform vec2  u_offset;     // @slider2d min=-10,-10 max=10,10 default=0,0
uniform float u_angle;      // @knob min=0 max=6.283 default=0
uniform int   u_iter;       // @slider min=1 max=50 default=10 step=1
uniform bool  u_debug;      // @toggle default=false

Algoritmo (regex, suficiente para MVP)

Para cada línea:

  1. Match ^\s*uniform\s+(\w+)\s+(\w+)\s*;\s*(?:\/\/\s*@(\w+)(.*))?$
  2. Grupo 1: tipo GLSL. Grupo 2: nombre. Grupo 3: widget kind (opcional). Grupo 4: resto de props.
  3. Si no hay widget kind, usar defaults:
    • floatslider(min=0, max=1, default=0)
    • vec2xy(min=0,0 max=1,1 default=0.5,0.5)
    • vec3color(default=1,1,1)
    • vec4color(default=1,1,1,1)
    • intslider(step=1, min=0, max=10, default=0)
    • booltoggle(default=false)
  4. Parse props key=value separados por whitespace. Números: parseFloat. Vectores: split por ,. Bools: "true"/"false".
  5. Ignorar uniforms con nombre en {u_resolution, u_time, u_mouse} (reservados).
  6. Ignorar uniforms cuyo tipo sea sampler2D (no soportados, warning en consola).

Tipos TS

type GLSLType = 'float' | 'vec2' | 'vec3' | 'vec4' | 'int' | 'bool';

type WidgetKind = 'slider' | 'slider2d' | 'color' | 'xy' | 'knob' | 'toggle';

interface UniformDescriptor {
  name: string;
  glslType: GLSLType;
  widget: WidgetKind;
  props: Record<string, number | number[] | boolean>;
  defaultValue: number | number[] | boolean;
}

type ParseResult = { uniforms: UniformDescriptor[]; warnings: string[] };

Tests (Vitest, en src/parser/parser.test.ts)

  1. Uniform sin anotación → defaults por tipo.
  2. @slider min=0 max=10 default=5.
  3. @color default=1,0,0 sobre vec3.
  4. @xy default=0.5,0.5 sobre vec2.
  5. Uniforms reservados ignorados.
  6. Comentario malformado → fallback a defaults, warning.
  7. Múltiples uniforms en el mismo source, orden preservado.
  8. Comentarios de bloque /* ... */ no interfieren.

7. Widgets de control

Todos reciben {value, onChange, descriptor}, controlados, sin estado interno.

Slider (float, int)

  • shadcn/ui <Slider>.
  • Label con valor numérico, click para editar.
  • Si log=true: mapeo logarítmico.
  • Si step definido: respetarlo.

ColorPicker (vec3, vec4)

  • react-colorful RgbaColorPicker.
  • Almacena internamente como array [r,g,b] o [r,g,b,a] en [0,1].

XYPad (vec2 con @xy)

  • Cuadrado ~150×150 px.
  • Drag con pointerdown/move/up.
  • Mapea [0,1]² del DOM a [min.x, max.x] × [min.y, max.y] con Y invertida.
  • Valores numéricos debajo.

Slider2D (vec2 con @slider2d)

  • Dos sliders apilados con labels x / y.

Knob (float con @knob)

  • Círculo SVG con marca.
  • Drag vertical u horizontal cambia el valor.
  • Visual distinto al slider para que se distinga.

Toggle (bool)

  • shadcn/ui <Switch>.

8. fn-registry (diseñado para ser usado por el LLM)

Biblioteca mínima de funciones GLSL reutilizables, guardadas en localStorage y consultables tanto por el usuario (REGISTRY sidebar) como por el LLM (vía tool use).

Modelo de datos

interface RegisteredFunction {
  id: string;                    // nanoid
  name: string;                  // nombre GLSL, e.g. "hash12"
  signature: string;             // "float hash12(vec2 p)"
  description: string;           // 1-2 frases: qué hace
  tags: string[];                // ["noise", "hash"]
  body: string;                  // cuerpo GLSL completo (función entera, firma incluida)
  dependencies: string[];        // nombres de otras funciones del registry que usa
  createdAt: number;
  updatedAt: number;
}

Operaciones

  • list()RegisteredFunction[].
  • search(query: string)RegisteredFunction[] (match en name, description, tags).
  • get(name: string)RegisteredFunction | null.
  • save(fn: RegisteredFunction) → upsert.
  • delete(id: string) → void.
  • resolveDependencies(names: string[]) → devuelve el conjunto cerrado transitivo ordenado topológicamente.

Inyección en el shader actual

Se usa el patrón de markers: el shader tiene un bloque marker, y el renderer — antes de compilar — reemplaza el contenido entre markers con el cuerpo de las funciones declaradas más sus dependencias transitivas.

// @registry_inject_begin
// hash12, perlin2d, rotate2d
// @registry_inject_end

void main() { ... }

Si el usuario edita dentro del bloque manualmente, se regenera al guardar (con confirmación si hay cambios).

Semilla inicial (seed)

Cargar ~15 funciones clásicas la primera vez que se abre la app. Mínimo:

  • hash11, hash12, hash22 (hashes deterministas sin sin()).
  • value_noise_2d, perlin_noise_2d, simplex_noise_2d.
  • fbm (fractal brownian motion).
  • rotate2d.
  • sdf_circle, sdf_box, sdf_line.
  • smoothmin.
  • palette (Inigo Quilez cosine palette).
  • hsv2rgb, rgb2hsv.

Cada una con tags y description relevantes para que el LLM pueda buscarlas semánticamente.

Panel Functions (ya descrito en §3 como REGISTRY sidebar)


9. Integración con Claude (LLM)

Configuración

  • Usuario pega su API key en un modal de Settings → se guarda en localStorage (warning claro: "se guarda en local, no la uses en ordenadores compartidos").
  • Selector de modelo: claude-opus-4-7 (default, mejor calidad), claude-sonnet-4-6 (más rápido), claude-haiku-4-5-20251001 (muy rápido, para iteración).

Cliente

  • @anthropic-ai/sdk con dangerouslyAllowBrowser: true.
  • Streaming siempre activo.
  • Historial de conversación persistido en localStorage (último N mensajes, truncable).

System prompt (template)

You are a creative shader programmer helping the user write WebGL2 fragment shaders for visual art and VJing.

The host environment provides these uniforms automatically — never redeclare them:
  uniform vec2  u_resolution;
  uniform float u_time;
  uniform vec2  u_mouse;

The target is WebGL2 / GLSL ES 3.00. Use `fragColor` as the output (it's predeclared as `out vec4 fragColor`). The `#version 300 es` directive is prepended automatically — don't include it.

When you declare uniforms the user should be able to tweak, annotate them with a magic comment so the UI generates a control automatically. Supported annotations:

  uniform float u_speed;     // @slider min=0 max=5 default=1
  uniform float u_freq;      // @slider min=0.1 max=100 default=10 log=true
  uniform vec3  u_color;     // @color default=0.1,0.2,0.5
  uniform vec4  u_tint;      // @color default=1,1,1,1
  uniform vec2  u_pos;       // @xy min=-1 max=1 default=0,0
  uniform float u_angle;     // @knob min=0 max=6.283 default=0
  uniform bool  u_debug;     // @toggle default=false

Guidelines:
- Prefer to REUSE functions from the registry when possible. You have tools to search and insert registry functions.
- Keep shaders self-contained and working on first compile.
- Use functional style: pure functions, no side effects inside helpers, compose via explicit parameters.
- When producing a complete shader, annotate uniforms the user is likely to want to tweak live.
- Prefer hash functions that don't rely on `sin()` (use hash12/hash22 from the registry).
- If the user asks for a modification, return the full updated shader via apply_shader, not a diff.
- Keep aspect-ratio correctness in mind: use `(gl_FragCoord.xy - 0.5*u_resolution.xy) / u_resolution.y` for centered, non-stretched coordinates unless a different framing is asked for.

Tools available:
- search_registry(query): find functions by name/description/tags.
- get_function(name): retrieve a function's full body.
- list_registry(): list all available function names and signatures.
- apply_shader(source): replace the user's current shader with this source. Use this when the user explicitly asks you to generate or modify their shader.
- save_function({name, signature, description, tags, body, dependencies}): add a function to the registry.

Tools (Anthropic tool use)

const tools = [
  {
    name: 'search_registry',
    description: 'Search for reusable GLSL functions in the local registry by name, description, or tags.',
    input_schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
  },
  {
    name: 'get_function',
    description: 'Retrieve the full body of a registered function by name.',
    input_schema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
  },
  {
    name: 'list_registry',
    description: 'List all functions in the registry with their signatures and tags.',
    input_schema: { type: 'object', properties: {} },
  },
  {
    name: 'apply_shader',
    description: 'Replace the user\'s current fragment shader with new source. Use when the user asks to generate or modify their shader.',
    input_schema: { type: 'object', properties: { source: { type: 'string' } }, required: ['source'] },
  },
  {
    name: 'save_function',
    description: 'Save a reusable GLSL function to the registry so it can be used in future shaders.',
    input_schema: {
      type: 'object',
      properties: {
        name: { type: 'string' },
        signature: { type: 'string' },
        description: { type: 'string' },
        tags: { type: 'array', items: { type: 'string' } },
        body: { type: 'string' },
        dependencies: { type: 'array', items: { type: 'string' } },
      },
      required: ['name', 'signature', 'body'],
    },
  },
];

Loop de tool use

Loop estándar de Anthropic: mandar mensaje con tools, si stop_reason === 'tool_use' ejecutar las tools, añadir resultados como tool_result, volver a llamar al API, repetir hasta stop_reason === 'end_turn'.

Las tools son locales y síncronas: todas tocan localStorage o el store Zustand. No hay red más allá de la llamada al API de Anthropic.

Confirmación antes de aplicar

  • apply_shader no reemplaza silenciosamente el código actual. Muestra un diff side-by-side y el usuario confirma. Crítico para que el LLM no borre trabajo del usuario.
  • save_function aplica directamente (es aditivo, no destructivo), pero muestra toast con undo.

Prompts de demostración (chips en el AgentSidebar)

  • "Make a lava lamp shader"
  • "Add audio-reactive colors using u_time"
  • "Refactor the current shader to use registry functions"
  • "Explain what this shader does line by line"
  • "Create a kaleidoscope with 8 segments"

10. Persistencia (localStorage)

Schema

interface StorageSchema {
  version: 1;
  currentShader: string;        // id del shader actualmente cargado
  shaders: Record<string, {
    id: string;
    name: string;
    source: string;
    uniformValues: Record<string, unknown>;
    thumbnail?: string;         // dataURL pequeño para mostrar en SHADERS sidebar
    updatedAt: number;
  }>;
  functions: Record<string, RegisteredFunction>;
  conversations: Array<{
    id: string;
    messages: Array<{ role: string; content: unknown }>;
    updatedAt: number;
  }>;
  settings: {
    apiKey: string | null;
    model: string;
    theme: 'dark' | 'light';
    codeMode: 'sidebar' | 'overlay';    // modo del editor: acoplado o flotante
  };
  layout: {
    sidebars: { left: string | null; right: string | null };
    widths: { left: number; right: number };
  };
}

Claves

  • Una única clave root: shader-playground:v1.
  • Migraciones: si se encuentra version < 1 o key vieja, crear backup con sufijo timestamp y regenerar.
  • Debounce 500 ms en todas las escrituras.
  • API key jamás incluida en exports/imports manuales del usuario.

11. Atajos de teclado (completos)

Sidebars

  • F1 — toggle CODE sidebar.
  • F2 — toggle CONTROLS sidebar.
  • F3 — toggle AGENT sidebar.
  • F4 — toggle REGISTRY sidebar.
  • F5 — toggle SHADERS sidebar.
  • Cmd/Ctrl + B — colapsar ambos sidebars (canvas máximo, no fullscreen).
  • Cmd/Ctrl + / — abrir CODE como modal overlay sobre canvas.

Render / modo VJ

  • F — toggle fullscreen VJ.
  • Esc — cerrar modal → cerrar sidebars → salir fullscreen (en cascada).
  • Space (fuera del editor) — play/pause.
  • Cmd/Ctrl + R — reset time.
  • 1..9 (en fullscreen) — cargar shader guardado N.

Trabajo

  • Cmd/Ctrl + S — guardar snapshot inmediato.
  • Cmd/Ctrl + Enter — forzar recompile (desde editor) o enviar mensaje (desde chat).
  • Cmd/Ctrl + K — focus en chat input (abre AGENT si está cerrado).

12. Criterios de aceptación

Fase A — Sábado (core + layout)

  • Editor GLSL funcional con CodeMirror y highlight.
  • WebGL2 renderer con fullscreen quad, hot-recompile con debounce.
  • Error de compilación con línea, canvas mantiene el último válido.
  • Parser de uniforms con todas las anotaciones funcionando.
  • 6 widgets de control conectados.
  • Icon rail visible con 6-7 iconos y toggles funcionales.
  • Los 4 sidebars principales (CODE, CONTROLS, SHADERS, settings modal) funcionan con toggle y ancho redimensionable.
  • Layout default (CODE izq + CONTROLS der) al primer arranque.
  • Setting codeMode (sidebar / overlay) funciona: al cambiarlo en Settings, el icono "Code" del rail abre el editor en el modo elegido.
  • Estado del layout persiste en localStorage.
  • Cambiar un slider actualiza el render sin lag.
  • Persistencia: recarga la página y vuelve todo igual, incluidos qué sidebars estaban abiertos.
  • 4 shaders de ejemplo cargan correctamente.
  • Fullscreen funciona con F y Esc, icon rail y sidebars desaparecen limpiamente.
  • Modo "code overlay" funciona cuando está activado en Settings: el icono "Code" abre un modal flotante sobre el canvas en vez del sidebar.
  • Tests del parser pasan.

Si la fase A está completa y funciona, ya hay algo usable. La fase B es aditiva.

Fase B — Domingo (LLM + registry)

  • REGISTRY sidebar con búsqueda, lista filtrable, acciones "insert / view / edit / delete".
  • Seed inicial de ~15 funciones cargadas al primer arranque.
  • Markers @registry_inject_begin/end funcionan, se reemplazan antes de compilar.
  • Modal de Settings para API key de Claude.
  • AGENT sidebar con chat funcional, streaming visible.
  • Tool use funcional (search_registry, get_function, list_registry, apply_shader, save_function).
  • apply_shader muestra diff y pide confirmación.
  • Chips de prompts de demostración.
  • Conversación persiste entre recargas.

Features opcionales (solo si sobra tiempo)

  • Thumbnails generados en el SHADERS sidebar.
  • Botón overlay flotante en fullscreen para salir (si no, queda Esc).
  • Export/import del registry como JSON.

Criterio global

  • Puedo usar la app para: (1) pedirle a Claude "haz un shader de nubes con double domain warping, guardado como función en el registry", (2) verlo aparecer en el REGISTRY sidebar, (3) abrir un shader nuevo en blanco, (4) pedirle "carga la función de nubes del registry y úsala con una paleta roja-naranja", (5) ajustar los sliders que aparezcan automáticamente, (6) guardarlo, (7) entrar en fullscreen, (8) recargar la página y seguir teniendo todo. Si este flujo end-to-end no funciona, el MVP no está hecho.

13. Orden de implementación

Sábado (Fase A)

  1. Scaffold: Bun init, Vite, React, Tailwind, shadcn CLI, Zustand. Layout con icon rail + stage central + sidebar containers vacíos.
  2. WebGL2 wrapper: fullscreen quad, fragment shader hardcoded sólido. Verifica render.
  3. CodeMirror con GLSL en CODE sidebar. Source en store. Recompile al cambiar (debounce).
  4. Error handling: parse del infoLog, display en editor.
  5. Layout store: toggle de sidebars, reglas de lado preferido, persistencia del layout.
  6. Parser de uniforms + tests.
  7. Store de uniformValues sincronizado con descriptors (diff al cambiar shader).
  8. CONTROLS sidebar renderizando widgets autogenerados.
  9. Widgets uno a uno: Slider → Toggle → Color → XY → Knob → Slider2D. Cada uno end-to-end.
  10. Persistencia localStorage con debounce (shaders + layout + uniformValues).
  11. SHADERS sidebar: lista, guardar/cargar/renombrar/duplicar.
  12. Shaders de ejemplo en seed.
  13. Fullscreen + atajos de teclado + setting codeMode con ambas variantes (sidebar y overlay).

Domingo (Fase B)

  1. fn-registry: modelo, CRUD, búsqueda, seed, tests.
  2. Markers @registry_inject_begin/end y preprocesado antes de compilar.
  3. REGISTRY sidebar con búsqueda y acciones.
  4. Settings modal con API key.
  5. Cliente Claude con streaming (sin tools).
  6. AGENT sidebar: chat, markdown rendering, persistencia.
  7. Tool definitions y loop de tool use.
  8. Diff + confirmación para apply_shader.
  9. Chips de prompts de demostración.
  10. Pulido visual, toasts, mensajes de error amigables.

Qué sacrificar si algo se alarga (en orden, de menos a más crítico)

  1. Thumbnails del SHADERS sidebar (fallback: icon/gradient generado desde el nombre).
  2. Botón flotante de salida de fullscreen (queda solo Esc).
  3. Slider2D y Knob (los vec2 usan XY, los float normal Slider).
  4. Setting codeMode — dejar solo modo sidebar; overlay va a v2.
  5. Modal de creación de función en registry (solo insert, edit a mano en JSON).
  6. Chips de prompts de demostración.
  7. Markers @registry_inject_* (el LLM pega código directo).

14. Calidad de código

  • TypeScript strict mode, noImplicitAny, strictNullChecks.
  • Módulos parser/, renderer/, registry/, llm/ son puros y testables sin DOM ni React.
  • Widgets reciben value/onChange y son ignorantes del store (el puente se hace en ControlsSidebar).
  • Render loop NO pasa por React. Subscribe al store de Zustand y lee valores directamente cada frame.
  • Cada sidebar es un componente React autocontenido que lee/escribe en su slice del store.
  • Vitest para tests del parser y del registry (resolver dependencias transitivas).
  • Prettier + ESLint básicos, sin fanatismo.
  • Commits en imperativo corto ("Add icon rail", "Wire AGENT sidebar to Claude client").

15. Lo que NO hay que hacer aunque apetezca

  • No añadir audio / MIDI en el MVP.
  • No añadir multi-pass "porque es solo un buffer más".
  • No refactorizar los widgets a una abstracción genérica antes de tener los 6 implementados.
  • No hacer los sidebars flotantes/arrastrables tipo Ableton/Resolume. Siempre acoplados a bordes.
  • No permitir más de un sidebar por lado en MVP. Si quieres ver REGISTRY y CONTROLS a la vez, abres uno en cada lado. No tabs apilados.
  • No meter un sistema de plugins.
  • No añadir LangChain, vector DBs, ni RAG. La tool search_registry es un string match simple y es suficiente.
  • No hacer backend en Go para persistir "cuando sea". Todo en localStorage hasta que duela de verdad.
  • No soportar vertex shaders custom.
  • No soportar múltiples shaders simultáneos con crossfade. Un shader activo, fullscreen, listo.

Cada una de estas es un día comido y medio MVP menos. Después del MVP y de un mes usándolo de verdad, vuelvo a mirar qué duele y decido.


16. Referencias útiles