Así codeamos con Claude · 03: tools y state
Tercera entrega. Capítulos anteriores: setup andando y canvas que se siente nativo. Acá el editor empieza a parecer un editor: pinta píxeles, tiene historial, paleta y un picker custom para crear colores sin salir de la app.
El brush con bresenham
Click y drag pinta píxeles con el color activo. Suena trivial hasta que movés el mouse rápido: el cursor recorre 8 píxeles entre un evento de pointermove y el siguiente, y quedan huecos. La solución estándar es bresenham, el algoritmo clásico para discretizar líneas en grilla. Cada pointermove dispara una línea bresenham entre la posición anterior del mouse y la nueva. Trazos continuos hasta a 200 px/seg sin perder un solo píxel.
El store
Necesitaba estado compartido entre canvas, paleta y toolbar: color activo, píxeles del sprite, history. Tres caminos posibles: prop drilling, Context, o un store dedicado.
Prop drilling se ensucia rápido cuando tres componentes lejanos comparten el mismo state. Context dispara re-renders en todo el árbol con cada cambio. Un store con suscripciones selectivas es la opción profesional para este tamaño de app.
Elegimos Zustand. Una dependencia, ~100 líneas para el store completo del editor, suscripción por selector así un componente solo re-renderea cuando cambia lo que le importa.
Undo y redo
Snapshots por stroke. Cada pointer down marca strokeActive = true. Cada píxel pintado setea strokeDirty = true. Al soltar, si hubo cambios reales, pusheamos una copia del buffer al history.
Sprite máximo 128×128 = 64 KB por snapshot. Límite de history = 100. Memoria pico de 6.4 MB. Sobra en cualquier dispositivo moderno.
⌘Z deshace el último trazo. ⌘⇧Z lo rehace.
Las paletas
Idea original: columna vertical de swatches grandes. Le tiré una foto de Aseprite y migramos a grid compacto estilo editor pro. Cabe más en menos espacio, mejor jerarquía, las dos paletas iniciales entran sin scroll.
Vienen dos por defecto: cozy (las 8 nuestras pastel) y PICO-8 (los 16 clásicos de la consola fantasma). El swatch transparente arriba sirve de goma. Cuando creás tu primer color custom aparece una sección nueva al final.
El picker custom
Modal con todo lo esperable de un picker pro: SV box (saturación × valor), hue slider, input hex con icono de pegar desde el portapapeles, R/G/B numéricos, comparador antes → ahora, y dos botones de acción.
Dos detalles que aparecieron mientras lo probábamos:
Lo que aparece probando
Tres descubrimientos que no estaban en el plan, todos visibles solo al usar la app unos minutos.
Tooltips nativos no. El atributo HTML title se ve barato, tarda casi un segundo en aparecer y rompe el tono. Migramos todo a un patrón data-tooltip CSS-only con ::after, animación, y soporte de side y align para los casos de borde de pantalla.
Keycap overflow. "Ctrl+Shift+Z" no entraba en un botón de 64px. Lo arreglamos cambiando el botón a display: inline-grid: el label y la keycap viven en la misma celda. El botón se dimensiona al más ancho de los dos. Sin layout shift, sin overflow.
Hidratación en Mac. El server no sabe si sos Mac. Asume Windows y renderiza "Ctrl+Z". El cliente sí sabe, dice "⌘Z". React grita. La solución limpia: useSyncExternalStore con server snapshot estable y client snapshot real. Server y cliente coinciden en el primer render. Después de hidratar, React re-renderea con el valor correcto. Cero warnings.