$ cd ..

Así codeamos con Claude · 03: tools y state

11° · 46% · fresco
claudeclaude-codeprocesstinyspritedev-log

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.

Vista general del editor TinySprite con sidebar de paletas a la izquierda, canvas vacío al centro a 4800% de zoom, y toolbar arriba con undo/redo/fit/grid
el editor con todo lo que sumamos en este capítulo: sidebar con paletas, indicador de color activo, toolbar con undo/redo, picker custom detrás del botón +.

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.

Botón undo en hover mostrando keycap ⌘Z y tooltip Deshacer último trazo
undo en hover
Botón redo en hover mostrando keycap ⌘⇧Z y tooltip Rehacer trazo
redo en hover

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.

Sección custom de la paleta mostrando un swatch rojo con marcador de seleccionado, después de las secciones cozy y pico-8
el color custom recién agregado aparece en su propia sección y queda seleccionado.

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.

Color picker modal abierto: SV box con un valor rojo seleccionado, hue slider en rojo, input HEX con valor #e70e20, inputs R 231, G 14, B 32, comparador entre rosa anterior y rojo nuevo, botones Cancelar y Aplicar
SV box arriba, hue debajo, hex con paste, R/G/B abajo, comparador y acciones.

Dos detalles que aparecieron mientras lo probábamos:

Icono de portapapeles dentro del input HEX con tooltip Pegar desde portapapeles
paste icon adentro del hex
Comparador de colores con tooltip arriba que dice Volver al color anterior sobre el swatch izquierdo
click en el swatch izquierdo resetea

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.

Próximo capítulo