---
title: "Game feel en la web: squash, shake y el arte del juice"
description: "Un botón que solo funciona está muerto. Pasé una noche enseñándole a un proyecto paralelo a devolver el golpe, y estas son las seis técnicas que lo lograron. Cada una jugable, sin librerías y respetando tus preferencias."
date: 2026-06-03
url: https://valdemird.com/blog/es/game-feel-on-the-web/
lang: es
tags: ["game-feel", "animation", "web-audio", "frontend", "interaction-design"]
---

# Game feel en la web: squash, shake y el arte del juice

> Un botón que solo funciona está muerto. Pasé una noche enseñándole a un proyecto paralelo a devolver el golpe, y estas son las seis técnicas que lo lograron. Cada una jugable, sin librerías y respetando tus preferencias.

El mes pasado estaba construyendo un pequeño juego multijugador en `games.valdemird.com`. Uno de los modos es una caza de números: una cuadrícula de casillas y tú marcas las correctas antes que tu rival. Funcionó a la primera. Tocabas una casilla, se encendía, el puntaje subía, la ronda terminaba. Correcto, y completamente muerto.

Me quedé ahí tocando la misma casilla una y otra vez, y se sentía como enviar un formulario. Así que dediqué una noche entera a una sola cosa: hacer que ese toque se sintiera bien. Ninguna función nueva. Ahora la casilla se aplasta cuando la golpeas, lanza unas partículas, el tono del clic sube a medida que crece tu racha, y al llegar a una racha de cincuenta el tablero entero tiembla. La lógica de abajo es la misma. La diferencia es que por fin la interfaz reconoce que hiciste algo.

Esa diferencia tiene nombre.

La trampa es usar un solo `scale()`. Eso es un globo inflándose, no un objeto con peso. El truco está en animar `scaleX` y `scaleY` en oposición, de modo que el elemento conserve su área. En el juego, el tablero usa esto al aparecer: se planta en vez de caer desde arriba.

Ahí está lo sorprendente. Nada del movimiento cambió. La velocidad del golpe, el retroceso, las partículas son idénticos. La única diferencia es un `setTimeout` entre el contacto y la reacción, y tu cerebro lee esa pausa más larga como un golpe más pesado. Es el peso más barato que vas a añadir a una interfaz.

Las variantes mediana y grande añaden una fracción de grado de rotación. Esa rotación es justo lo que la mayoría de las implementaciones web se saltan. La traslación pura se lee como un glitch. Unas décimas de grado de rotación se leen como fuerza. Este es el código que está en producción, incluida la línea que todo el mundo olvida:

El detalle que importa aquí es dónde viven las partículas. En una app de React, crear cuarenta elementos por clic a través del estado dispararía cuarenta re-renders y hundiría los fotogramas en cuanto arrancara una racha. Por eso la función de burst esquiva a React y toca el DOM directamente:

Un sonido no es más que una frecuencia, una forma de onda y una envolvente de volumen en el tiempo. Una onda triangular corta que sube rápido y decae es un clic. Una onda senoidal que desciende es un golpe sordo. Este es el núcleo, unas quince líneas:

La escalada no es magia, es una función escalonada. La racha se mapea a un tier, y cada efecto se lee de ese tier: más partículas, más dispersión, un temblor mayor, un tinte más rojo, un tono más agudo.

```ts
function comboTier(streak: number): 0 | 1 | 2 | 3 | 4 {
  if (streak >= 50) return 4;
  if (streak >= 30) return 3;
  if (streak >= 20) return 2;
  if (streak >= 10) return 1;
  return 0;
}

// en cada marca exitosa:
sfx.pop(streak);                                              // el tono sigue a la racha
juice.burst(x, y, { count: 8 + tier * 5, spread: 80 + tier * 35 }); // más escombros por tier
juice.flash(flashColor, 0.07 + tier * 0.025);                // flash más brillante por tier
juice.shake(tier >= 3 ? 'md' : 'sm');                        // el tablero solo tiembla cuando se lo gana
```

Dos decisiones hacen que esto se sienta bien y no agotador. El tablero solo tiembla de tier tres en adelante, así que el efecto más fuerte se mantiene raro y sigue significando algo. Y la racha decae tras 1.6 segundos de inactividad, lo que convierte un contador en tensión. No estás solo acumulando puntos, estás manteniendo algo con vida. Ese decaimiento es lo que hace que la celebración se sienta ganada, porque se puede perder.

```ts
function onMark(cell: Cell) {
  setMarked((m) => [...m, cell]);
  setScore((s) => s + 1);
}
```

Correcto. Se publica. No te cuenta nada más allá de un número que cambió. Esto es lo que tenía el día uno, y por eso me gasté la noche.

```ts
function onMark(cell: Cell, e: PointerEvent) {
  const next = streak + 1;
  setStreak(next);
  setMarked((m) => [...m, cell]);
  sfx.pop(next);
  juice.burst(e.clientX, e.clientY, { count: 6, spread: 64, size: 7 });
  clearTimeout(decay.current);
  decay.current = setTimeout(() => setStreak(0), 1600);
}
```

Ocho líneas más. La misma corrección, más peso, sonido y una razón para seguir.

La parte que no es opcional

Todo lo de aquí está diseñado para captar atención, lo que significa que todo lo de aquí puede volverse un ataque contra alguien que no lo pidió. Los trastornos vestibulares hacen que el screen shake dé náuseas. Hay gente que trabaja en silencio a propósito. La web tiene una señal real para lo primero, la media query [`prefers-reduced-motion`](https://developer.mozilla.org/es/docs/Web/CSS/@media/prefers-reduced-motion), y una cortesía básica para lo segundo, y ambas son una línea.

Mira de nuevo cada función de arriba. Todas empiezan igual:

```ts
function reduced(): boolean {
  return matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// shake, flash y burst empiezan con: if (reduced()) return;
```

Si el sistema operativo pide movimiento reducido, el temblor, el flash y las partículas sencillamente no ocurren. El juego sigue siendo del todo jugable. Por eso mismo las demos del blog respetan la misma preferencia. Si tu sistema tiene el movimiento reducido activado, las demos de movimiento de esta página también se silencian, y estás leyendo un artículo que sobre todo demuestra su propia ausencia. El sonido recibe el mismo trato mediante un interruptor de silencio persistente, audible por defecto pero a un clic del silencio y recordado entre sesiones.

Cada vez que me dije que puliría la sensación cuando la lógica estuviera lista, la lógica se endureció alrededor de no tener ninguna sensación. Los manejadores de clic quedaron escritos asumiendo una respuesta síncrona e instantánea, y meterles después una ventana de reacción de 400ms implicaba desenredarlos. La noche que describí al principio funcionó porque el juego era pequeño. En algo más grande, la capa de feedback quiere ser hermana de la lógica desde el inicio: un `juice.burst()` junto al `setState`, no atornillado en una pasada posterior.

Casi todo lo de aquí se apoya en un puñado de charlas conocidas y un libro. Si quieres los originales:

- **Steve Swink, *Game Feel: A Game Designer's Guide to Virtual Sensation*** (2008) — el libro que le puso nombre al campo. [game-feel.com](http://www.game-feel.com/), o el ensayo más largo [*Game Feel: The Secret Ingredient*](https://www.gamedeveloper.com/game-platforms/feature-game-feel---the-secret-ingredient-).
- **Los doce principios de animación de Disney** — squash & stretch es el principio uno, de *The Illusion of Life* de Frank Thomas y Ollie Johnston. [Resumen](https://en.wikipedia.org/wiki/Twelve_basic_principles_of_animation).
- **Martin Jonasson y Petri Purho, *Juice it or lose it*** (2012) — [la charla](https://www.youtube.com/watch?v=Fy0aCDmgnxg), y el [Breakout jugable](http://grapefrukt.com/f/games/juicy-breakout/) que llenan de juice en vivo.
- **Jan Willem Nijman / Vlambeer, *The art of screenshake*** (2013) — [la charla](https://www.youtube.com/watch?v=AJdEqssNZ-U). Unos 30 trucos en 25 minutos.
- **Mark Brown, *Secrets of Game Feel and Juice*** — [el video-ensayo de Game Maker's Toolkit](https://www.youtube.com/watch?v=216_5nu4aVQ), si prefieres ver a leer.
- **Hitstop / hitlag** — definido para los juegos de pelea en el [glosario de Infil](https://glossary.infil.net/?t=Hitstop).

La caza de números se juega idéntica con todos los efectos quitados. Nadie pierde la partida por haberla silenciado o por apagar el movimiento. Esa es la línea. El juice es algo que pones encima de algo que ya funciona, nunca una pieza de la que dependa si funciona o no.

Vuelve arriba y machaca la moneda una vez más. Ese tirón que sientes, esa pequeña resistencia a parar, es la razón entera por la que vale la pena escribir todo esto. Son ocho líneas de código entre enviar un formulario y algo que no quieres soltar.
