VIBE CODING CON CLAUDE CODE · UN PROYECTO REAL

De una idea
a una partitura
que cobra vida.

Cómo desarrollé Partitura Viva, una aplicación donde la música se ve y se escucha: una partitura en 3D, personajes que saltan de nota en nota y 28 canciones sintetizadas en el navegador. Todo el código lo escribió Claude Code; yo dirigí el proyecto con prompts.

Por Diego Moisset / Aprender construyendo

PROYECTO / 002PARTITURA VIVA
Una partitura que avanza. Una esfera que toca cada nota.
12puntos del recorrido
09etapas de desarrollo
28canciones originales
HECHO CON

HTML · CSS · JavaScript
Canvas 2D · Web Audio

EL RECORRIDO

Del primer prompt
a la aplicación publicada.

Los tres primeros puntos preparan el trabajo: el objetivo, el prompt inicial y la forma de trabajar con Claude Code. A partir del cuarto, cada tema cuenta una etapa con los prompts que usé, lo que hizo la IA, lo que verifiqué, los ajustes que pedí y un fragmento del código real.

Cada etapa se abre con el botón «Explorar la etapa».

01

EL PUNTO DE PARTIDA

El objetivo que me planteé

“Quiero una aplicación donde la música se vea y se escuche: una partitura que avanza en perspectiva y un personaje que salta de nota en nota, justo cuando suena. Sin librerías ni archivos de audio: todo debe nacer en el navegador.”

La idea nació de una pregunta simple: ¿qué pasaría si una partitura pudiera verse como un escenario? En lugar de leer los pentagramas de izquierda a derecha, la cámara los recorre en 3D mientras una esfera luminosa (más tarde, un dragón, un colibrí, un astronauta o un gato) va tocando cada nota en el instante exacto en que suena.

El proyecto tiene dos caras. Por un lado, una aplicación que se puede abrir en una sala, en una notebook o en un televisor y dejarla funcionando. Por otro, un experimento de vibe coding: construir un programa grande conversando con una IA, sin escribir yo mismo el código, pero manteniendo el criterio de qué se ve bien, qué suena bien y qué falta.

PARA QUIEN LA MIRA

Una experiencia visual y sonora: 28 canciones originales, cada una con su personaje, su estilo musical y su letra.

PARA EL DESARROLLO

Un proyecto de gran tamaño construido por etapas con Claude Code, donde yo decido, escucho, pruebo y corrijo.

Conceptos del temaIdea y alcance · objetivo del experimento · criterios de aceptación «a ojo y a oído» · papel del autor y de la IA.

EL RESULTADO DEL PROYECTOProbá Partitura Vivatutorialesprogramacionya.online/partituraviva/Abre en otra pestaña
02

DE LA IDEA A LAS INSTRUCCIONES

Escribir el primer prompt

El primer mensaje le da contexto a la IA: qué quiero construir, con qué tecnologías, cómo debe verse, cómo debe sonar y cuáles son las reglas que no se negocian. Termina con una petición clave: proponer etapas y esperar a que se pidan de a una. Así evito recibir un programa gigante imposible de revisar.

Fijé tres decisiones de arquitectura desde el principio, porque todo lo demás depende de ellas:

  1. MundoLas notas viven en coordenadas propias (u, v) sobre un papel plano.
  2. CámaraUna función P(u, v, z) convierte el mundo en píxeles, con perspectiva.
  3. RelojEl tiempo lo manda el reloj del audio, no los temporizadores.

Conceptos del temaContexto · especificaciones visuales y sonoras · restricciones técnicas · reloj de audio como fuente de verdad · pedir un plan antes de generar.

Leer el prompt inicial completo
MENSAJE INICIAL · RECONSTRUCCIÓN
Quiero desarrollar una aplicación web llamada Partitura Viva.
Paso las especificaciones:
- JavaScript vanilla, HTML y CSS. Sin librerías ni frameworks.
  Todo se dibuja en un canvas 2D a pantalla completa.
- Se muestra una partitura impresa sobre una hoja de papel gris, vista en
  perspectiva 3D, como si estuviera acostada sobre una mesa. La cámara avanza
  hacia la derecha a la velocidad de la música.
- Hay varios pentagramas, uno por instrumento (melodía, violines, viola,
  violonchelo, contrabajo y clave), con clave de sol o de fa según corresponda.
- Una esfera luminosa salta de nota en nota y cae exactamente en el instante en
  que la nota suena. Al caer, la nota se ilumina con su color. Un color por nota:
  Do rosa, Re naranja, Mi amarillo, Fa blanco, Sol celeste, La violeta, Si verde.
- El sonido se genera en el navegador con Web Audio. Nada de archivos de audio:
  cada instrumento es un timbre sintetizado.
- La música se genera sola y no se termina nunca, usando la progresión de acordes
  del canon: Do, Sol, La menor, Mi menor, Fa, Do, Fa, Sol.
- Fondo negro con estrellas. Arriba, unos controles discretos: reproducir/detener,
  tempo y volumen.
- La esfera y el sonido deben estar sincronizados con el reloj del audio
  (audioContext.currentTime). No uses setTimeout ni setInterval para el ritmo.
Quiero que me indiques en cuántas etapas podés desarrollar esta aplicación y luego
las voy a ir pidiendo de a una, verificando los resultados.
03

PLANIFICAR ANTES DE GENERAR

Claude Code y el método de trabajo

Claude Code es un asistente de programación que trabaja en la terminal, dentro de la carpeta del proyecto. Lee los archivos, propone cambios, los escribe y explica qué hizo. Yo le escribo en lenguaje natural, abro la aplicación en el navegador y le cuento qué veo y qué escucho. No hay que copiar y pegar código entre una ventana de chat y el editor: la IA modifica los archivos directamente.

Respondió a mi primer prompt proponiendo nueve etapas, cada una con algo que se puede abrir y comprobar. Esas etapas son los temas 4 a 12.

Los textos de los prompts de este tutorial son una reconstrucción didáctica: conservan la intención, el orden y el tipo de instrucciones que di, pero no son una transcripción literal de la conversación. El código mostrado es real y pertenece a la versión publicada.

  1. 01Preparar el proyecto y los controlesTema 4 ↗
  2. 02Una cámara 3D hecha a manoTema 5 ↗
  3. 03El sonido con Web AudioTema 6 ↗
  4. 04Escribir las cancionesTema 7 ↗
  5. 05La esfera que salta y las lucesTema 8 ↗
  6. 06La letra y los controlesTema 9 ↗
  7. 07Personajes que saltanTema 10 ↗
  8. 0828 canciones y 40 instrumentosTema 11 ↗
  9. 09Reproducción automática, pulido y publicaciónTema 12 ↗

EL MÉTODO QUE REPETIREMOS

Pedir → Generar → Mirar y escuchar → Describir lo que falla → Ajustar

Esta aplicación se juzga con los sentidos: no alcanza con que no haya errores en la consola. Hay que verla moverse y oírla sonar. Por eso, cuando algo no me gustaba, escribía lo que veía o escuchaba con la mayor precisión posible: «el dragón tapa a los demás», «la voz queda tapada por los timbales», «el temblor del mouse al hacer clic reabre los controles». Cuanto más concreta la descripción, mejor la corrección.

Reglas que mantuve durante todo el proyecto:

  • Una etapa por vez. No pasaba a la siguiente hasta que la anterior se veía y sonaba bien.
  • Un solo archivo de lógica. Todo vive en app.js, ordenado por secciones con comentarios. Con un archivo, Claude Code siempre encuentra todo y los cambios no se desparraman.
  • Describir síntomas, no soluciones. Contaba qué pasaba y dejaba que la IA propusiera la causa.
  • Pedir comentarios en el código. Cada función lleva una línea que explica su idea; así yo mismo puedo volver a entenderla meses después.

Para ver el proyecto mientras se construye alcanza con abrir index.html en el navegador; no necesita servidor ni instalación. Después de cada cambio, recargo la página y pulso «Reproducir».

Conceptos del temaDescomposición del problema · entregas incrementales · revisión sensorial (vista y oído) · describir síntomas · un archivo, secciones comentadas.

DE LA PLANIFICACIÓN A LA PRÁCTICA

Nueve etapas.
Una aplicación completa.

Abrí cada etapa para ver los prompts que usé, qué hizo Claude Code, qué comprobé, los ajustes que pedí después y un fragmento del código real de la versión publicada.

04

Etapa 1 / 9 · Estructura

Preparar el proyecto y los controles

Pasar de una idea a una base que se pueda abrir, entender y revisar.

Conceptos del temaHTML mínimo · canvas a pantalla completa · unidades relativas · controles que no estorban · primer archivo de lógica.

El avance que buscamosUna página negra a pantalla completa con un canvas, una barra de controles discreta y tres archivos listos para crecer.

Explorar la etapa

Qué pedirle a la IA

Prompt 1 · estructura
Desarrollá solamente la etapa 1. Creá index.html, style.css y app.js. En el HTML poné una barra superior con un selector de canción, un botón «Reproducir», un deslizador de tempo y uno de volumen; un canvas que ocupe toda la ventana y un contenedor para la letra de la canción. Fondo casi negro. No agregues librerías. Al final, explicame cómo abrirlo.
Prompt 2 · un televisor en mente
Quiero poder usarlo en un televisor 4k desde el sillón. Hacé que los controles y los textos crezcan con el tamaño de la pantalla, y que la barra se aclare solo cuando paso el mouse por encima.
Qué hizo Claude CodeCreó los tres archivos y dejó el canvas con position: fixed; inset: 0, de modo que siempre cubre la ventana. Para el escalado, definió el tamaño de letra de toda la página con clamp() y usó rem en los controles: 1 rem equivale a 16 px en una pantalla de 1920 y llega a 32 px en una 4k. La barra queda al 35 % de opacidad y pasa al 100 % con :hover.

Qué verificar antes de continuar

Abrir index.html, comprobar que el canvas negro llena la ventana, que no aparecen barras de desplazamiento, que la consola no muestra errores y que los controles se ven y se aclaran al pasar el mouse. Achicar y agrandar la ventana para ver que todo se acomoda.

Archivos que se estudiarán

  • index.html
  • style.css
  • app.js
index.html · fragmento real
<header>
  <div class="controls">
    <select id="piece" title="Tema"></select>
    <label title="Al terminar una canción, pasa a la siguiente de la lista"><input id="auto" type="checkbox" checked> Reproducción automática</label>
    <button id="play">▶ Reproducir</button>
    <label>Tempo <input id="tempo" type="range" min="50" max="200" value="100"></label>
    <label>Volumen <input id="vol" type="range" min="0" max="100" value="60"></label>
  </div>
</header>
<canvas id="stage"></canvas>
<div id="lyrics" aria-live="polite"></div>
<script src="app.js"></script>

Es toda la interfaz. Lo demás (papel, notas, personajes, estrellas, partículas) se dibuja dentro del canvas. Los controles ya tienen identificadores, así que app.js los puede conectar sin reconstruir la página. Los dos controles que hoy parecen sobrar, el selector y la casilla de reproducción automática, se activan en etapas posteriores.

El fragmento pertenece a la versión final de la aplicación; en esta primera etapa la barra solo tenía el botón y los deslizadores.

05

Etapa 2 / 9 · Imagen

Una cámara 3D hecha a mano

Dibujar el papel, los pentagramas y las notas en perspectiva sin usar WebGL.

Conceptos del temaCoordenadas del mundo · proyección en perspectiva · rotación e inclinación · transformaciones afines del canvas · escala según pantalla · dibujar solo lo visible.

El avance que buscamosUna hoja gris acostada, con cinco líneas por pentagrama, compases, claves de sol y de fa y notas con su cabeza, plica y corchete.

Explorar la etapa

Qué pedirle a la IA

Prompt 1 · la cámara
Etapa 2. No uses WebGL ni Three.js: quiero una cámara 3D propia sobre el canvas 2D. Definí un mundo con coordenadas (u, v) sobre el papel y una función P(u, v, z) que devuelva la posición en pantalla y la escala. La cámara debe mirar el papel inclinado y con un leve giro lateral, y seguir una posición cam.u que podré mover. Dibujá un papel gris y dos pentagramas de prueba.
Prompt 2 · la notación
Ahora dibujá notas como en una partitura real: cabeza ovalada ligeramente inclinada, vacía para las blancas y rellena para las negras; plica hacia arriba o hacia abajo según la altura; corchete para las corcheas; líneas adicionales cuando la nota sale del pentagrama; sostenidos y bemoles. Agregá las líneas de compás y las claves de sol y de fa.
Prompt 3 · todo impreso sobre el papel
Los textos (las claves, el nombre de cada instrumento, el título) se ven planos, como pegados a la pantalla. Quiero que parezcan impresos en el papel: que se inclinen y se achiquen con la perspectiva igual que las notas.
Qué hizo Claude CodeEscribió P(u, v, z): resta la posición de la cámara, gira el plano con el ángulo YAW, lo inclina con TILT y divide por la profundidad para obtener la escala s. Esa escala sirve para el grosor de las líneas y el tamaño de cada objeto. Para el Prompt 3 creó worldText, que en lugar de escribir en la pantalla aplica al canvas una transformación que lleva los ejes del papel a la pantalla: el texto queda deformado exactamente igual que el papel. Para las notas, worldPoly y ellipsePts proyectan los puntos de cada figura.
Ajustes posteriores
  • «Dibujá solo los pentagramas que se ven en pantalla»: cada pentagrama calcula el tramo visible (de cam.u - VIS a cam.u + VIS) y no el papel entero.
  • «En una pantalla ancha se corta el pentagrama»: apareció VF, que ensancha el campo visible según la relación de aspecto.
  • «En el televisor todo se ve chiquito»: apareció K, un factor de escala que toma como referencia 1920×1080.

Qué verificar antes de continuar

Mover cam.u a mano y ver cómo el papel se desliza en perspectiva sin deformar los símbolos. Comprobar que las claves y los nombres siguen el plano del papel, que las líneas adicionales aparecen en las notas muy agudas o graves y que la imagen se mantiene nítida al redimensionar la ventana.

Archivos que se estudiarán

  • app.js · secciones «cámara 3D» y «dibujo»
app.js · fragmento real
const YAW = -0.42, TILT = 1.08, CAMD = 1500, FOC = 1250;
const cy = Math.cos(YAW), sy = Math.sin(YAW), cT = Math.cos(TILT), sT = Math.sin(TILT);
const cam = { u: LEFT, v: rowV(2.5) };
let W, H, dpr;
let K = 1, VF = 1;       // K: escala según el tamaño de pantalla (1 = 1920×1080; en un televisor 4k, 2); VF: ensanche del campo visible en pantallas muy anchas
function P(u, v, z = 0) {
  const x = u - cam.u, y = v - cam.v;
  const a = x * cy - y * sy, b = x * sy + y * cy;
  const dep = CAMD - b * sT - z * cT;
  const s = FOC * K / Math.max(dep, 60);
  return { x: W / 2 + a * s, y: H * .5 + (b * cT - z * sT) * s, s };
}

Toda la «tercera dimensión» de la aplicación cabe en estas diez líneas. u y v son los ejes del papel, z es la altura sobre él (la que usan los saltos) y s es la escala que devuelve la perspectiva. Cualquier objeto nuevo, desde una nota hasta un dragón, se coloca llamando a P.

El fragmento pertenece a la versión final de la aplicación; los factores K y VF se agregaron al preparar la aplicación para pantallas grandes.

06

Etapa 3 / 9 · Sonido

El sonido con Web Audio

Sintetizar instrumentos sin un solo archivo de audio y programar cada nota en su instante exacto.

Conceptos del temaAudioContext · osciladores y filtros · envolventes de volumen · compresor y reverberación · reloj de audio · programar con anticipación.

El avance que buscamosNotas que suenan con varios timbres, con un tempo ajustable y sin cortes ni clics, programadas con el reloj del audio.

Explorar la etapa

Qué pedirle a la IA

Prompt 1 · el motor de audio
Etapa 3. Usá solamente Web Audio, sin archivos de audio. Creá el AudioContext recién cuando el usuario pulsa «Reproducir», con un volumen general, un compresor para que no sature y una reverberación de sala. Hacé una función voice(kind, freq, t, dur, vol, pan) que sintetice cada timbre con osciladores, filtros y envolventes. Empezá con voz, instrumentos de arco, pulsado, bajo, órgano, trompeta, flauta, bombo y caja.
Prompt 2 · notas en el tiempo exacto
Cada nota tiene un beat de inicio y una duración en tiempos. Convertí los beats a segundos con el tempo y programá cada nota en el reloj del audio con dos segundos de anticipación, de modo que no dependa del rendimiento del dibujo. El deslizador de tempo debe funcionar.
Qué hizo Claude CodeArmó la cadena voces → compresor → salida y una reverberación con ConvolverNode cuyo impulso es ruido que decae. Cada timbre es una receta distinta: la voz es una onda triangular con vibrato y ataque suave; el pulsado, una sierra cuyo filtro se cierra rápido; el bajo, una triangular grave. Como el AudioContext solo puede iniciarse con un gesto del usuario, lo crea en ensureAudio(), llamado desde el botón. La conversión de beats a segundos quedó en una sola función, beatDur().
Ajustes posteriores
  • «Se escuchan clics al terminar las notas»: se agregaron rampas de volumen al final de cada envolvente, en lugar de cortar el oscilador de golpe.
  • «Al cambiar el tempo, lo que ya estaba programado sigue sonando al ritmo viejo»: el deslizador detiene y reinicia la reproducción con el tempo nuevo.
  • «Al detener, el sonido queda colgado»: al detener se cierra el AudioContext y se vuelve a crear en la próxima reproducción.

Qué verificar antes de continuar

Escuchar cada timbre por separado, subir y bajar el volumen, mover el tempo en medio de una canción y detenerla. Si hay clics o saturación, describirlos en un prompt: «se escucha un clic al final de cada nota de la voz».

Archivos que se estudiarán

  • app.js · sección «audio»
app.js · fragmento real
function ensureAudio() {
  if (audio) return;
  audio = new (window.AudioContext || window.webkitAudioContext)();
  master = audio.createGain();
  master.gain.value = volEl.value / 100;
  const comp = audio.createDynamicsCompressor();
  master.connect(comp); comp.connect(audio.destination);
  // reverberación de sala: impulso de ruido que decae
  wet = audio.createGain(); wet.gain.value = .45;
  const conv = audio.createConvolver(), len = Math.floor(audio.sampleRate * 2.4), ir = audio.createBuffer(2, len, audio.sampleRate);
  for (let ch = 0; ch < 2; ch++) { const d = ir.getChannelData(ch); for (let i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 3); }
  conv.buffer = ir; wet.connect(conv); conv.connect(master);
}

La reverberación no es un archivo: es ruido aleatorio que se apaga gradualmente durante 2,4 segundos. Ese «eco de sala» se mezcla con el sonido seco de los instrumentos que lo piden.

app.js · fragmento real: programar con anticipación
      p.notes.forEach(n => {                               // sonidos con 2 s de anticipación
        if (!n.sched && hitTime(n) < now + 2) {
          n.sched = true;
          if (autoEl.checked && n.beat >= songLen + 2) return;     // con «Reproducción automática» no suena la repetición: se pasa a la siguiente
          voice(p.kind, n.freq, Math.max(hitTime(n), now), n.dur * bd, p.vol, p.pan);
        }
      });

Cada cuadro de animación revisa qué notas caerán en los próximos dos segundos y se las entrega al motor de audio con su hora exacta (hitTime). El audio nunca depende de si el dibujo se atrasó un instante.

El fragmento pertenece a la versión final de la aplicación; la condición de la reproducción automática se sumó en la última etapa.

07

Etapa 4 / 9 · Música

Escribir las canciones

Pasar de una música generada al azar a una notación de texto para componer canciones de verdad.

Conceptos del temaMúsica generativa · semillas · progresión de acordes · notación de texto · analizador (parser) · partes e instrumentos · componer con la IA.

El avance que buscamosUna forma de escribir canciones completas como texto, con melodía, acompañamiento, bajo y percusión, que la aplicación convierte en notas.

Explorar la etapa

Qué pedirle a la IA

Prompt 1 · música infinita
Etapa 4. Generá la música de forma infinita con seis partes: melodía, violín II, viola, violonchelo, contrabajo y clave, cada una en su pentagrama. Cada 2 tiempos entra un acorde nuevo de la progresión Do, Sol, La menor, Mi menor, Fa, Do, Fa, Sol. La melodía debe moverse por las notas del acorde con pasos cortos y cambiar de ritmo cada ocho acordes. Que use un generador pseudoaleatorio con semilla para que sea repetible.
Qué hizo Claude CodeEscribió rng(seed) y genChord, que para cada acorde y cada parte decide qué notas agregar. ensure(upto) genera música por delante de la cámara y addNote coloca cada nota en el mundo (u según el beat, v según la altura) y le asigna su color y su frecuencia.
Mi escucha

Funcionaba, pero sonaba a «música de ascensor»: correcta, genérica y sin personalidad. Ahí cambió el rumbo del proyecto. Dejé de pedir música y empecé a pedir canciones.

Prompt 2 · una notación de texto
La música generada no tiene gracia. Quiero componer canciones reales. Inventá una notación de texto compacta para escribir partituras: Letra, alteración opcional, octava y duración en tiempos separadas por dos puntos, por ejemplo «E5:.5» o «F#4:2». «R:2» es un silencio de 2 tiempos. Quiero poder agregar una sílaba de letra a una nota con «^». Armá una función que convierta ese texto en notas.
Prompt 3 · la primera canción
Componé una canción original, una balada lenta en Sol mayor, 4/4. Escribí la melodía con estrofa, estribillo, segunda estrofa, estribillo y cierre. Acompañala con violín (acordes largos), piano (arpegio de corcheas) y violonchelo. Escribilo usando la notación nueva, y las partes de acompañamiento calculalas a partir de la lista de acordes de cada compás.
Prompt 4 · repetir la receta
Ahora una segunda canción, un rock en Mi menor a 144 pulsos por minuto, con guitarra eléctrica distorsionada, bajo, bombo y caja. Y que cada canción sea una función aparte, así puedo sumar muchas más.
Qué hizo Claude CodeEscribió parseScore con una expresión regular que reconoce letra, alteración, octava, duración y sílaba. Cada canción pasó a ser una función buildNombre() que devuelve una lista de partes (name, clef, kind, vol, src). Para el acompañamiento no escribió cada nota a mano: definió los acordes por compás y generó los arpegios, los bajos y los ritmos con funciones pequeñas. Una clase Line permite armar la melodía pegando frases.
Ajustes posteriores
  • «La segunda estrofa es idéntica a la primera»: pedí variaciones de la última frase para que cierre distinto.
  • «El estribillo no resuelve»: le pedí ajustar las últimas notas hacia la tónica.
  • «Que la canción vuelva a empezar al terminar»: las partes se repiten con un desplazamiento (cycle) mientras no se active la reproducción automática.

Qué verificar antes de continuar

Escuchar la canción completa. Una melodía mal escrita se nota enseguida: una nota fuera de tono, una frase que no cierra, un compás con un tiempo de más. Cuando algo falla, la descripción útil es musical: «en el compás 5 del estribillo, la melodía suena disonante contra el Em».

Archivos que se estudiarán

  • app.js · secciones «partituras escritas» y «generación»
app.js · fragmento real: la notación
// Notación: Letra[#|b]Octava[:duración en tiempos], R:duración = silencio. Do central = C4.
function parseScore(str) {
  let beat = 0;
  const notes = [];
  str.trim().split(/\s+/).forEach(tok => {
    // opcional: ^sílaba de la letra ('-' = la palabra sigue, '/' = fin de verso)
    const m = tok.match(/^([A-GR])([#b]?)(\d)?(?::([\d.]+))?(?:\^(.+))?$/);
    if (!m) throw new Error('nota inválida: ' + tok);
    const dur = m[4] ? +m[4] : 1;
    if (m[1] !== 'R') notes.push({ beat, dur, step: LET.indexOf(m[1]) + 7 * (+m[3] - 4), acc: m[2] === '#' ? 1 : m[2] === 'b' ? -1 : 0, lyr: m[5] });
    beat += dur;
  });
  return { notes, len: beat };
}

Una canción entera cabe en texto plano: 'B4:1 D5:1 D5:.5 E5:1.5 D5:.5 B4:.5' son seis notas. Esa decisión permitió componer 28 canciones: cada una es una lista de frases de texto más una receta de acompañamiento.

app.js · fragmento real: las partes de una canción
  const mk = (name, clef, kind, vol, str) => ({ name, clef, kind, vol, src: parseScore(str).notes });
  return {
    parts: [
      mk('Voz', 'treble', 'voice', .13, MEL),
      mk('Guitarra', 'treble', 'pluck', .1, bars.map(arp).join(' ')),
      mk('Teclado', 'treble', 'organ', .04, bars.map(organ).join(' ')),
      mk('Bajo', 'bass', 'bass', .15, bars.map(bass).join(' ')),
      mk('Bombo', 'bass', 'kick', .35, bars.map(kick).join(' ')),
      mk('Güiro', 'bass', 'snare', .1, bars.map(guiro).join(' ')),
    ],
    len: 144,
  };

Cada parte tiene un nombre (el que se imprime junto al pentagrama), una clave, un timbre, un volumen y las notas. Para sumar un instrumento a una canción alcanza con agregar una línea.

Los fragmentos pertenecen a la versión final de la aplicación (el segundo es de una de las primeras canciones originales). La partitura se escribe en texto y la IA compone con mi guía: el criterio musical sigue siendo humano.

08

Etapa 5 / 9 · Movimiento

La esfera que salta y las luces

Hacer que cada nota se vea en el mismo instante en que se escucha.

Conceptos del temaInterpolación · trayectoria parabólica · anticipación · mezcla aditiva de luces · gradientes radiales · ondas · liberar notas viejas.

El avance que buscamosUna esfera por pentagrama que salta de nota en nota y cae en el instante exacto del sonido, dejando la nota iluminada con su color.

Explorar la etapa

Qué pedirle a la IA

Prompt 1 · el salto
Etapa 5. Cada parte tiene su esfera. Espera sobre la nota anterior y salta hacia la siguiente, de modo que aterriza exactamente cuando esa nota suena. Cuanto más lejos esté la próxima nota, más alto debe saltar. Si hay un silencio largo, que espere y salte solo en los últimos 2 tiempos. Usá la altura z de la cámara para que el salto se vea en 3D.
Prompt 2 · la luz
Cuando la esfera cae, la nota se enciende con su color: un resplandor sobre el papel, un núcleo blanco y una onda que se expande y se desvanece. El brillo debe apagarse con el tiempo, pero la nota debe quedar teñida. Usá mezcla aditiva para que las luces se sumen entre sí.
Qué hizo Claude CodeEscribió ballState, que calcula para cada parte dónde está la esfera: interpola la posición entre la nota anterior y la siguiente según el tiempo del reloj de audio y suma una altura parabólica h·4·q·(1−q). Después escribió drawLights, que activa globalCompositeOperation = 'lighter' y dibuja varios resplandores radiales con glowAt, además de las ondas (ripples). Como la posición depende de hitTime(n), la misma función que usa el audio, el salto y el sonido no pueden desfasarse.
Ajustes posteriores
  • «Las esferas se acumulan y la aplicación se pone lenta a los minutos»: se descartan las notas ya pasadas con un shift() cuando quedan 22 tiempos atrás.
  • «La esfera flota, no se ve dónde cae»: se agregó una luz plana sobre el papel que crece con la altura del salto, para dar sensación de sombra.
  • «Antes de saltar debería agacharse un momento»: el estado devuelve pre, un valor que indica que está a punto de saltar, y luego lo usarán los personajes.

Qué verificar antes de continuar

Mirar un tramo largo y comprobar que el instante en que se enciende cada nota coincide con el sonido. Probar con tempos extremos (50 % y 200 %) y con una melodía que mezcle notas rápidas y silencios largos.

Archivos que se estudiarán

  • app.js · funciones ballState, drawLights y glowAt
app.js · fragmento real
function ballState(p, now) {
  if (piece.runners) return runnerState(p, now);
  const next = p.notes[p.cur], prev = p.cur > 0 ? p.notes[p.cur - 1] : null;
  const tn = hitTime(next);
  const pu = prev ? prev.u : next.u - 2 * BEATW, pv = prev ? prev.v : next.v;
  // la esfera espera sobre la nota anterior y solo salta durante los últimos 2 tiempos (silencios largos)
  const tp = Math.max(prev ? hitTime(prev) : t0, tn - 2 * beatDur());
  const q = Math.max(0, Math.min(1, (now - tp) / (tn - tp)));
  const dist = Math.hypot(next.u - pu, next.v - pv);
  const h = Math.min(190, 16 + dist * .6);           // más lejos la próxima nota, más alto salta
  return { u: pu + (next.u - pu) * q, v: pv + (next.v - pv) * q, z: h * 4 * q * (1 - q), color: next.color, q,
    pre: now < tp ? Math.max(0, 1 - (tp - now) / .22) : 0, since: prev ? now - hitTime(prev) : -1 };   // pre: a punto de saltar; since: desde que aterrizó
}

La variable q vale 0 cuando la esfera deja la nota anterior y 1 cuando aterriza en la siguiente. Con ella se interpola la posición y se calcula la parábola 4·q·(1−q). Todo se obtiene de un único dato: el reloj del audio.

El fragmento pertenece a la versión final de la aplicación: ya incluye el caso de los corredores, que se agregó cuando se sumaron personajes.

09

Etapa 6 / 9 · Letra y controles

La letra y los controles

Hacer que la canción se cante en pantalla y que los controles dejen de estorbar.

Conceptos del temaSílabas sincronizadas · subtítulos tipo karaoke · estado de la interfaz · temporizadores · eventos del mouse · detalles de usabilidad.

El avance que buscamosLa letra aparece sílaba a sílaba bajo las notas y en un subtítulo, y la barra de controles se oculta mientras suena la música.

Explorar la etapa

Qué pedirle a la IA

Prompt 1 · la letra bajo las notas
Etapa 6. Quiero que las canciones tengan letra. Cuando la esfera cae en una nota que lleva sílaba, la sílaba aparece debajo del pentagrama, en cursiva y con un resplandor del color de la nota. Si la palabra continúa, la notación lleva un guion al final de la sílaba, y si termina el verso, una barra.
Prompt 2 · el subtítulo
Además, abajo de la pantalla mostrá el verso que se está cantando, como en un karaoke: la sílaba actual resaltada en rosa, lo ya cantado atenuado, y cuando termina el verso se limpia para el siguiente.
Prompt 3 · controles que desaparecen
Al reproducir, la barra de controles debe ocultarse sola. Que reaparezca al mover el mouse y se vuelva a ocultar a los pocos segundos si no hay movimiento.
Qué hizo Claude CodeAgregó a parseScore el reconocimiento de ^sílaba y escribió drawLyric (letra sobre el papel, con worldText) y sing (subtítulo en el elemento HTML #lyrics). Para los controles creó hideControls y showControls con un temporizador de 2,5 segundos.
Ajustes posteriores
  • «Al hacer clic en Reproducir, el pequeño temblor del mouse vuelve a mostrar la barra»: se ignoran los movimientos durante los primeros 600 ms tras ocultarla.
  • «Si tengo el desplegable de canciones abierto, la barra se esconde igual»: no se oculta mientras el selector tiene el foco.
  • «La letra tapa las notas en pantallas chicas»: el tamaño del subtítulo usa clamp() y se ubica al 7 % del borde inferior.

Qué verificar antes de continuar

Cantar mentalmente con la pantalla. Cada sílaba debe aparecer cuando suena, las palabras largas deben quedar unidas y los versos deben limpiarse en el momento correcto. Probar también que la barra reaparece con el mouse y que no se esconde con el selector abierto.

Archivos que se estudiarán

  • app.js
  • style.css · bloque #lyrics
app.js · fragmento real: el subtítulo
// subtítulo con el verso que se está cantando; la sílaba actual se resalta
const cap = { old: '', brk: true };
function clearCaption() { cap.old = ''; cap.brk = true; if (lyricsEl) { lyricsEl.textContent = ''; lyricsEl.classList.remove('on'); } }
function sing(l) {
  if (!lyricsEl) return;
  const end = l.endsWith('/'), cont = /-\/?$/.test(l), s = l.replace(/[-/]+$/, '');
  if (cap.brk) { cap.old = ''; cap.brk = false; }
  lyricsEl.textContent = '';
  const o = document.createElement('span'), c = document.createElement('span');
  o.className = 'old'; o.textContent = cap.old; c.className = 'cur'; c.textContent = s;
  lyricsEl.append(o, c); lyricsEl.classList.add('on');
  cap.old += s + (cont ? '' : ' ');
  if (end) cap.brk = true;
}

La sílaba trae su propia marca: un guion final significa «la palabra sigue» (no se agrega espacio) y una barra significa «fin del verso» (la próxima sílaba empieza un renglón nuevo).

app.js · fragmento real: los controles
document.addEventListener('mousemove', () => { if (performance.now() - hiddenAt > 600) showControls(); });   // el temblor del mouse al hacer clic no los reabre
playBtn.onclick = () => { if (playing) { stop(); showControls(); } else { start(); hideControls(); } };
tempoEl.oninput = () => { tempoScale = tempoEl.value / 100; if (playing) { stop(); start(); showControls(); } };
volEl.oninput = () => { if (master) master.gain.value = volEl.value / 100; };

El volumen cambia en vivo porque solo toca la ganancia general. El tempo, en cambio, reinicia la canción: todas las notas ya programadas dependen de él.

Los fragmentos pertenecen a la versión final de la aplicación.

10

Etapa 7 / 9 · Personajes

Personajes que saltan

Reemplazar la esfera por criaturas dibujadas con matemática, cada una con su manera de moverse.

Conceptos del temaDibujo con curvas · sistemas de coordenadas locales · poses clave e interpolación · cinemática · partículas · una función por personaje · banderas por canción.

El avance que buscamosPersonajes animados que saltan, vuelan, caminan o flotan de nota en nota y reaccionan con efectos propios al llegar.

Explorar la etapa

Qué pedirle a la IA

Prompt 1 · el primer personaje
Etapa 7. La esfera es linda pero quiero personajes. Empecemos por un sapito de perfil que salta de nota en nota: en la nota tiene las patas plegadas contra el cuerpo y en el aire las estira, las traseras hacia atrás y las delanteras hacia delante. Dibujalo solo con curvas del canvas, sin imágenes. Que al caer largue unas partículas del color de la nota.
Prompt 2 · personajes con cuerpo
Ahora unos hombrecitos hechos con dos dedos. Cuando hay tiempo entre notas, se agachan, se impulsan, hacen una zancada en el aire con las rodillas al pecho y caen estirando las piernas. Definí poses clave (de pie, agachado, impulso, apogeo, caída) y mezclalas con una función según el avance del salto.
Prompt 3 y siguientes · uno por canción
Para la canción «El dragón» quiero un dragón de cuerpo gris azulado con detalles dorados, que flote sobre el pentagrama batiendo las alas. Justo antes de cada nota abre la boca, gira la cabeza hacia ella y sopla fuego; la nota queda chamuscada. Tiene que ser más chico que el pentagrama para que los de las distintas partes no se tapen entre sí.
Qué hizo Claude CodeSiguió siempre el mismo patrón, que después repitió para cada nuevo personaje: drawXBody (el cuerpo, en un sistema de coordenadas propio, con el centro en el origen), drawX (lo coloca con P(), lo escala y lo anima según ballState), xBurst (la explosión de partículas al llegar) y, a veces, una marca que queda en la nota. En PIECES cada canción lleva una bandera (dragon: true, colibri: true…) y el dibujo elige el personaje según esa bandera.
Ajustes posteriores (ejemplos reales)
  • Hombrecitos que corren: «los pies patinan sobre el papel». La solución fue que el ciclo de paso dependa de la distancia recorrida y no del tiempo.
  • Gato negro: «no tiene sentido que salte; caminá con calma». Sale cinco tiempos antes de la nota, con arranque y frenada suaves.
  • Astronauta: «que tenga más tiempo en el aire, como en la Luna». Un arco altísimo con la cumbre muy plana, y al aterrizar queda una huella de bota con polvo.
  • Delfín: «que deje un charco donde cae». El charco se aquieta y se evapora despacio.
  • Cupido: «que la flecha quede clavada en la nota, con un corazón que late».

Qué verificar antes de continuar

Mirar a cada personaje varias veces seguidas: cuando salta, cuando espera, cuando la próxima nota está muy lejos o muy cerca, y con el tempo en 50 % y en 200 %. Revisar que no se superpongan entre partes y que el efecto de llegada no tape la partitura.

Archivos que se estudiarán

  • app.js · una sección por personaje (// ---------- dragón ----------, // ---------- colibrí ----------…)
app.js · fragmento real: poses de los hombrecitos
const MAN_STAND = [4, 0, 0, 0, 0, 5, 20, 5, 20];
const MAN_CROUCH = [38, 62, 112, 62, 112, -45, 10, -45, 10];
const MAN_FLIGHT = [                                // impulso, zancada, apogeo (rodillas al pecho), caída, alcanzar el suelo
  [0, [10, 4, 6, 2, 4, 150, 10, 140, 10]],
  [.25, [16, 72, 85, -35, 20, -30, 15, 125, 20]],
  [.5, [10, 82, 112, 42, 100, 95, 30, 90, 30]],
  [.75, [8, 52, 55, 26, 42, 100, 30, 80, 35]],
  [1, [16, 34, 30, 20, 32, 45, 40, 40, 40]],
];
const smooth01 = x => { x = Math.max(0, Math.min(1, x)); return x * x * (3 - 2 * x); };
const mixPose = (A, B, t) => A.map((v, i) => v + (B[i] - v) * t);
function flightPose(q) {
  let k = 0;
  while (k < MAN_FLIGHT.length - 2 && q > MAN_FLIGHT[k + 1][0]) k++;
  const [qa, A] = MAN_FLIGHT[k], [qb, B] = MAN_FLIGHT[k + 1];
  return mixPose(A, B, Math.max(0, Math.min(1, (q - qa) / (qb - qa))));
}

Cada pose es una lista de ángulos de las articulaciones. Para animar, el programa elige las dos poses entre las que está el salto (q va de 0 a 1) y las mezcla. Es la misma idea de los fotogramas clave de la animación, escrita en diez líneas.

app.js · fragmento real: un efecto por personaje
function burst(p, n) {
  if (piece.hammer) return hammerBurst(p, n);
  if (piece.dragon) return dragonBurst(p, n);
  if (piece.colibri) return hummingBurst(p, n);
  if (piece.cupid) return cupidBurst(p, n);
  if (piece.phoenix) return phoenixBurst(p, n);
  if (piece.soccer) return soccerBurst(p, n);
  if (piece.flea) return fleaBurst(p, n);
  if (piece.dolphin) return dolphinBurst(p, n);
  if (piece.bubbles) return bubbleBurst(p, n);

Al llegar a una nota, la aplicación pregunta qué canción es y llama al efecto de ese personaje: aro de luz del color de la nota, chispas, gotas, pétalos, tornillos… Si la canción no tiene personaje, se usa el efecto general de chispas y corazones.

Los fragmentos pertenecen a la versión final de la aplicación. En total hay más de veinte personajes y escenas, cada uno dibujado enteramente con funciones del canvas.

11

Etapa 8 / 9 · Repertorio

28 canciones y 40 instrumentos

Una canción original, con su estilo, su letra y su arreglo, para cada personaje.

Conceptos del temaEstilos musicales · tempo y compás por canción · síntesis de instrumentos y percusión · paneo estéreo · mezcla de volúmenes · escribir prompts musicales precisos.

El avance que buscamosUn repertorio variado en estilo (vals, rock, salsa, tango, metal, bolero, huayno, reggaetón…) con timbres sintetizados que suenan distinto en cada canción.

Explorar la etapa

Qué pedirle a la IA

Prompt tipo · una canción completa
Componé la canción número 16: «Colibrí de mi alma», un huayno épico andino en Do# menor a 112 pulsos por minuto. Con intro, dos estrofas, estribillo, puente y cierre. Quiero letra en español. Instrumentos: voz, quena, zampoña, charango, bajo y bombo. El personaje es un colibrí que pica cada nota. Si algún instrumento no existe todavía en el sintetizador, agregalo.
Qué hizo Claude CodeCada canción nueva trajo la misma estructura: una función buildX() con las frases de la melodía, la letra marcada con ^, las listas de acordes por sección y las funciones del acompañamiento. En PIECES sumó una entrada con nombre, tempo, compás (bar), título, subtítulo, reverberación y bandera del personaje. Cuando una canción pedía un instrumento que no existía, agregó una receta nueva a voice() o a perc().

Instrumentos que nacieron de los prompts

Hoy el sintetizador reconoce más de cuarenta timbres, todos hechos de osciladores, ruido y filtros: voces (voz, coro, coro de guerreros, grito rasgado), cuerdas (violonchelo, violín celta, arpa, charango, requinto), vientos (flauta, quena, zampoña, gaita, trompeta, trombón, cuerno francés, cuerno de guerra), teclas (piano, órgano, órgano de tubos, celesta, campanas tubulares, bandoneón), sintetizadores (supersaw), guitarra distorsionada y toda una batería de percusión: bombo, caja, conga, bongó, maracas, cencerro, clave, timbal, tambor de guerra, bodhrán, yunque, gong, platillo y hasta el rugido de un dragón.

Ejemplo de prompt para un instrumento
Agregá un gong: sonido muy grave y largo, hecho con varios parciales inarmónicos que se mezclen entre sí, con un ataque casi instantáneo y una caída de tres segundos y medio. Usalo en la canción del dragón para marcar el fin de cada sección.
Ajustes de mezcla (los más frecuentes)
  • «La voz queda tapada por los timbales»: se bajó el volumen de la percusión y se subió el de la melodía.
  • «Todo suena en el centro»: se sumó el campo pan a cada parte, y el sonido se reparte en el estéreo: los cuernos a la izquierda, el arpa a la derecha, el cello apenas corrido.
  • «Las canciones sin reverberación suenan secas y las otras mojadas»: la reverberación pasó a ser una opción por canción (reverb: true).
  • «Los tambores graves deberían hacer temblar la pantalla»: el bombo, el timbal y el tambor de guerra sacuden el dibujo al sonar.

Qué verificar antes de continuar

Escuchar cada canción entera, de principio a fin, con auriculares y con parlantes. Anotar lo que falla («en el segundo estribillo la quena desafina», «el puente es demasiado largo») y pedirlo en prompts separados, uno por problema, para poder comprobar cada corrección.

Archivos que se estudiarán

  • app.js · lista PIECES y funciones build…
app.js · fragmento real: el repertorio
const PIECES = [
  { name: 'El dragón', bpm: 108, bar: 3, build: buildDragon, dragon: true, reverb: true, title: 'El dragón que te ama', subtitle: 'vals épico' },
  { name: 'Fuego en mi sangre', bpm: 144, build: buildRock, title: 'Fuego en mi sangre', subtitle: 'rock' },
  { name: 'El colibrí', bpm: 112, bar: 2, build: buildColibri, colibri: true, reverb: true, title: 'Colibrí de mi alma', subtitle: 'huayno épico' },
  { name: 'Corredores en zig zag', bpm: 126, build: buildCorro, men: true, runners: true, title: 'Corro hacia vos', subtitle: 'pop folk' },

Cada entrada define cómo se llama la canción en el selector, a cuántos pulsos por minuto va, cuántos tiempos tiene cada compás, qué función la construye y qué personaje la acompaña. Sumar una canción es sumar una línea.

app.js · fragmento real: la orquesta del dragón
  const mk = (name, clef, kind, vol, str, pan = 0) => ({ name, clef, kind, vol, pan, src: parseScore(str).notes });
  return {
    parts: [
      mk('Voz', 'treble', 'voice', .13, MEL, 0),
      mk('Celesta', 'treble', 'celesta', .07, bars.map(celesta).join(' '), -.4),
      mk('Arpa', 'treble', 'harp', .09, bars.map(arpa).join(' '), .4),
      mk('Cuernos de guerra', 'treble', 'warhorn', .08, bars.map(cuernos).join(' '), -.25),
      mk('Violonchelo', 'bass', 'cello', .1, bars.map(chelo).join(' '), .2),
      mk('Timbales', 'bass', 'timpani', .28, bars.map(timp).join(' ')),
      mk('Rugido', 'bass', 'roar', .3, bars.map(rugido).join(' ')),
    ],
    len: 216,
  };

Siete partes, siete pentagramas y siete timbres. El número final de cada línea es el paneo (−1 a la izquierda, 1 a la derecha) y el volumen está ajustado parte por parte: los timbales y el rugido necesitan mucho más volumen que la voz para oírse.

Los fragmentos pertenecen a la versión final de la aplicación. Las canciones del repertorio se pueden escuchar en la aplicación online.

12

Etapa 9 / 9 · Entrega

Reproducción automática, pulido y publicación

Cerrar el ciclo con una experiencia continua, fluida en cualquier pantalla y lista para compartir.

Conceptos del temaTransiciones sin cortes · estado de varias canciones a la vez · rendimiento · resolución según pantalla · límites de partículas · pruebas de recorrido · publicación estática.

El avance que buscamosUna aplicación que se puede dejar sola en un televisor: encadena las canciones sin cortes, se ve fluida en 4k y está publicada para que cualquiera la abra.

Explorar la etapa

Qué pedirle a la IA

Prompt 1 · reproducción automática
Etapa 9. Agregá una casilla «Reproducción automática», activada por defecto. Cuando una canción se acerca al final, la siguiente de la lista debe asomar por la derecha sobre el mismo papel, y la cámara debe seguir avanzando sin cortes, con la anterior saliendo por la izquierda. Después de la última canción, vuelve la primera. La repetición de la canción actual no debe sonar.
Prompt 2 · rendimiento
Lo probé en un televisor 4k y se entrecorta. Revisá el rendimiento: limitá la resolución del canvas, no dibujes lo que está fuera de la pantalla, poné un tope a las partículas y liberá las notas que ya pasaron.
Prompt 3 · el recorrido completo
Revisá la aplicación de punta a punta. Recorré las 28 canciones en orden, una a una, y para cada una decime si hay algo que pueda fallar al encadenarla con la siguiente: personajes que cambian de tamaño, partes que no existen en la otra, bordes del papel, letra que queda colgada. Corregí lo que encuentres y listá lo que cambiaste.
Qué hizo Claude CodePara encadenar canciones, mantuvo tres «canciones activas» a la vez (la anterior, la actual y la siguiente) y escribió withSong, que cambia temporalmente el contexto global para dibujar o calcular una canción ajena sin tocar las demás. queueNext prepara la siguiente cuando falta poco, ya posicionada a la derecha, y activateNext la convierte en la actual. Para el rendimiento limitó la resolución real del canvas a unos 8,4 millones de píxeles, recortó el campo visible, fijó un tope de 900 partículas y eliminó las notas pasadas. El borde derecho del papel se funde con el fondo negro para que no se vea el final de la hoja.
Ajustes posteriores
  • «Al pasar a la siguiente canción la cámara pega un salto»: la siguiente se coloca exactamente donde termina la actual, así que el cambio de contexto es invisible.
  • «Hay un tramo de papel en blanco antes de que aparezca la siguiente»: el papel termina justo donde terminan pentagramas y notas.
  • «La letra de la canción anterior queda colgada»: al activar la siguiente se limpia el subtítulo.
  • «En una pantalla muy ancha se corta el papel a la derecha»: se amplió el campo visible según la proporción de la pantalla.

Publicar la aplicación

La aplicación son tres archivos estáticos (index.html, style.css y app.js). No necesita servidor, base de datos ni compilación: basta con subirlos a cualquier hosting web. Una vez publicada, está disponible en tutorialesprogramacionya.online/partituraviva. Como los navegadores no permiten reproducir audio sin una acción del usuario, la música arranca recién cuando se pulsa «Reproducir».

Qué verificar antes de dar por terminado

  • Abrir la URL publicada en Chrome, Edge y Firefox y reproducir varias canciones seguidas.
  • Dejarla veinte minutos con la reproducción automática: sin cortes, sin que se ponga lenta, sin fugas de memoria.
  • Probar en un televisor o monitor grande y en un teléfono.
  • Revisar la consola del navegador: sin errores en rojo.
  • Confirmar que el volumen, el tempo y la casilla de reproducción automática responden en cualquier momento.

Archivos que se estudiarán

  • app.js · funciones queueNext, activateNext, withSong y resize
app.js · fragmento real: cambiar de canción
// canciones del modo «Reproducción automática»: la actual, la que acaba de terminar (sigue a la vista, saliendo por la izquierda) y la siguiente (entra por la derecha)
let curSong = null, prevSong = null, nextSong = null, ENDU = Infinity;
function withSong(s, fn) {
  const sv = [piece, PARTS, songLen, LEFT];
  [piece, PARTS, songLen, LEFT] = [s.piece, s.PARTS, s.songLen, s.left];
  try { return fn(); } finally { [piece, PARTS, songLen, LEFT] = sv; }
}

La aplicación fue pensada con una sola canción en variables globales. Para poder mostrar dos al mismo tiempo, en lugar de reescribir todo, Claude Code intercambia esas variables durante un instante (withSong) y las restaura al terminar. Es una solución práctica, aunque no la más elegante: un buen ejemplo de decisiones que conviene revisar con criterio humano.

app.js · fragmento real: ajustarse a la pantalla
function resize() {
  W = canvas.clientWidth; H = canvas.clientHeight;
  dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, Math.sqrt(8.4e6 / (W * H))));   // tope de ~4k de píxeles para que fluya bien
  K = Math.max(.35, Math.min(3, W / 1920, H / 1080));
  VF = Math.max(1, W / H / (16 / 9));
  VIS = 1150 * VF;
  canvas.width = W * dpr; canvas.height = H * dpr;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}

El tamaño del dibujo se recalcula cada vez que cambia la ventana. La densidad de píxeles tiene un tope para que una pantalla enorme no obligue a dibujar millones de puntos de más; K adapta todo al tamaño de la pantalla y VF amplía el campo visible en pantallas muy anchas.

Los fragmentos pertenecen a la versión final de la aplicación.

LO QUE NOS LLEVAMOS

La IA escribe el código.
Nosotros dirigimos el proyecto.

Una aplicación de más de seis mil líneas, con cámara 3D, síntesis de sonido, 28 canciones y más de veinte personajes animados, se construyó conversando. El aprendizaje no está en el código sino en el proceso: fijar reglas desde el principio, avanzar por etapas, juzgar con los ojos y con los oídos, describir con precisión lo que falla y volver a probar. La partitura es el proyecto; el método es lo que podremos volver a usar.

Ver la aplicación funcionando ↗ (abre en otra pestaña)