Apuntes DAM
Volver al inicio

Reconocedor de gestos táctiles: toques, deslizamientos, pellizcos y giros

Ejercicio de JavaScriptMedioUnos 60 minutos

Programa lo que hace una pantalla táctil con los puntos que lee: decidir si el dedo hizo un toque, un doble toque, una pulsación larga, un deslizamiento o un arrastre, y con dos dedos, si pellizcó, giró o desplazó, con los umbrales de distancia, tiempo y velocidad de los sistemas reales.

  • Interfaces naturales
  • Eventos táctiles y punteros
  • Umbrales de distancia y tiempo
  • Velocidad y dirección
  • Geometría: distancia y ángulo (Math.hypot, Math.atan2)
  • Máquinas de estado sencillas

Enunciado

Una pantalla táctil no sabe nada de gestos: solo informa de dónde está cada dedo y cuándo (los eventos pointerdown, pointermove y pointerup en la web, o MotionEvent en Android). Son las bibliotecas de gestos (GestureDetector en Android, Hammer.js en la web) las que convierten esa serie de puntos en «deslizó a la izquierda» o «pellizcó para ampliar», comparando distancias, tiempos y velocidades con unos umbrales.

Cada gesto empieza con una línea gesto y sigue con sus puntos, uno por línea: t x y con un dedo o t x1 y1 x2 y2 con dos, con el tiempo en milisegundos y las coordenadas en píxeles (la y crece hacia abajo, como en cualquier pantalla). El código de partida ya los lee y los agrupa: tú los reconoces.

Qué tiene que hacer el programa

  1. Solo cuentan el primer punto y el último de cada gesto. Un gesto con números que no lo son, con una cantidad de valores distinta de 3 o 5 o que cambia entre puntos, o con un tiempo que no crece: Gesto N: datos no válidos. Con menos de 2 puntos: Gesto N: hacen falta al menos 2 puntos. Una línea antes del primer gesto: Línea N: fuera de un gesto.
  2. Un dedo que se mueve menos de 10 px es un toque en (x, y), o una pulsación larga en (x, y) (D ms) si dura 500 ms o más. Si se mueve más: a 0,5 px/ms o más es deslizar hacia la derecha|izquierda o hacia arriba|abajo según el eje con más movimiento, con (D px en T ms, V px/ms); más despacio, arrastrar de (x0, y0) a (x1, y1) (D px en T ms). D se redondea al entero y V lleva dos decimales y coma.
  3. Dos toques seguidos (gestos consecutivos) en los que el segundo empieza 300 ms o menos después de que acabe el primero y a 30 px o menos de él son Gestos N y M: doble toque en (x, y), con la posición del primero; un tercer toque no se une a un doble toque.
  4. Dos dedos, comparando el segmento que los une al principio y al final: si gira 15° o más, girar G° en sentido horario|antihorario (el ángulo crece en sentido horario en pantalla; G redondeado, de −180 a 180); si no, si su longitud se multiplica por 1,2 o más, pellizcar para ampliar (×E), y por 0,8 o menos, pellizcar para reducir (×E); si no, si su centro se mueve 10 px o más, desplazar con dos dedos hacia ...; y si no, dos dedos sin gesto reconocido. Si los dos dedos están en el mismo punto: datos no válidos.
  5. Al final: Gestos leídos: N.

Entrada

Cada gesto: una línea gesto y sus puntos, t x y (un dedo) o t x1 y1 x2 y2 (dos dedos).

Datos de referencia

Umbrales
ConstanteValorPara qué
MOVIMIENTO_MIN10 pxmenos es un toque; también el desplazamiento mínimo con dos dedos
PULSACION_LARGA500 msun toque que dura más
DOBLE_TOQUE_MS / _PX300 ms / 30 pxentre dos toques
VELOCIDAD_DESLIZAR0,5 px/msdeslizar frente a arrastrar
GIRO_MIN15°girar
ESCALA_AMPLIAR / REDUCIR1,2 / 0,8pellizcar

Ejemplos de ejecución

Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.

Un dedo: toques, deslizar y arrastrar

Entrada

gesto
0 200 300
80 203 302
gesto
250 205 298
320 205 299
gesto
1000 50 50
1700 52 51
gesto
2000 300 400
2060 200 405
2180 80 410
gesto
3000 100 100
3400 100 160
3900 100 300

Salida por consola

Gestos 1 y 2: doble toque en (200, 300)
Gesto 3: pulsación larga en (50, 50) (700 ms)
Gesto 4: deslizar hacia la izquierda (220 px en 180 ms, 1,22 px/ms)
Gesto 5: arrastrar de (100, 100) a (100, 300) (200 px en 900 ms)
Gestos leídos: 5

Dos dedos y errores

Entrada

0 1 1
gesto
0 100 200 200 200
300 50 200 250 200
gesto
0 100 100 200 100
300 100 100 186 150
gesto
0 100 300 300 300
400 160 300 240 300
gesto
0 100 100 200 100
200 140 160 240 160
gesto
0 100 100 200 100
150 102 101 201 99
gesto
0 10 10
gesto
0 10 10
5 10 x
gesto
0 10 10
10 12 12 14 14
gesto
0 50 50 50 50
100 60 60 90 90

Salida por consola

Línea 1: fuera de un gesto
Gesto 1: pellizcar para ampliar (×2,00)
Gesto 2: girar 30° en sentido horario
Gesto 3: pellizcar para reducir (×0,40)
Gesto 4: desplazar con dos dedos hacia abajo
Gesto 5: dos dedos sin gesto reconocido
Gesto 6: hacen falta al menos 2 puntos
Gesto 7: datos no válidos
Gesto 8: datos no válidos
Gesto 9: datos no válidos
Gestos leídos: 9

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. Distancia, duración y velocidad

Con el primer y el último punto: Math.hypot(dx, dy) es la distancia y la velocidad, distancia entre duración. Compara siempre con las constantes, nunca con números sueltos.

javascript
const [t0, x0, y0] = puntos[0], [t1, x1, y1] = puntos[puntos.length - 1];
const d = Math.hypot(x1 - x0, y1 - y0);
const v = d / (t1 - t0);
2. El ángulo de dos dedos

Math.atan2(dy, dx) da el ángulo del segmento entre los dedos. La diferencia entre el final y el principio es el giro; pásala a grados y llévala a (−180, 180].

3. El doble toque espera

Un toque no se puede escribir en cuanto se reconoce: el siguiente gesto podría convertirlo en doble toque. Guárdalo como pendiente y escríbelo cuando llegue otro gesto que no lo complete, o al final.

4. El orden importa

Un pellizco casi nunca es perfecto: los dedos también giran un poco y el centro se mueve. Por eso se prueba primero el giro (con un umbral alto), después la escala y, por último, el desplazamiento.

Resuélvelo aquí

El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.

🟨JavaScriptReconocedor de gestos táctiles: toques, deslizamientos, pellizcos y girosMedio

Ejemplo

Entrada (lo que se escribe por teclado)
gesto
0 200 300
80 203 302
gesto
250 205 298
320 205 299
gesto
1000 50 50
1700 52 51
gesto
2000 300 400
2060 200 405
2180 80 410
gesto
3000 100 100
3400 100 160
3900 100 300
Salida esperada
Gestos 1 y 2: doble toque en (200, 300)
Gesto 3: pulsación larga en (50, 50) (700 ms)
Gesto 4: deslizar hacia la izquierda (220 px en 180 ms, 1,22 px/ms)
Gesto 5: arrastrar de (100, 100) a (100, 300) (200 px en 900 ms)
Gestos leídos: 5
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
javascript
1const lineas = require("fs").readFileSync(0, "utf8").split("\n");
2
3const MOVIMIENTO_MIN = 10;          // px: con menos, el dedo no se ha movido (es un toque)
4const PULSACION_LARGA = 500;        // ms
5const DOBLE_TOQUE_MS = 300;         // ms entre el final del primer toque y el principio del segundo
6const DOBLE_TOQUE_PX = 30;
7const VELOCIDAD_DESLIZAR = 0.5;     // px/ms: más rápido es deslizar; más lento, arrastrar
8const GIRO_MIN = 15;                // grados
9const ESCALA_AMPLIAR = 1.2, ESCALA_REDUCIR = 0.8;
10
11const coma = (x) => x.toFixed(2).replace(".", ",");
12const DIRECCIONES = { derecha: "hacia la derecha", izquierda: "hacia la izquierda", arriba: "hacia arriba", abajo: "hacia abajo" };
13
14/** El eje que domina el movimiento; en pantalla, la y crece hacia abajo. */
15const direccion = (dx, dy) => DIRECCIONES[Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? "derecha" : "izquierda") : dy > 0 ? "abajo" : "arriba"];
16
17/** Agrupa las líneas en gestos: cada uno, una lista de puntos [t, x, y] o [t, x1, y1, x2, y2]. */
18function leerGestos() {
19  const gestos = [];
20  lineas.forEach((bruta, i) => {
21    const linea = bruta.trim();
22    if (!linea) return;
23    if (linea === "gesto") {
24      gestos.push({ puntos: [], valido: true });
25      return;
26    }
27    const g = gestos[gestos.length - 1];
28    if (!g) {
29      console.log(`Línea ${i + 1}: fuera de un gesto`);
30      return;
31    }
32    const nums = linea.split(/\s+/).map(Number);
33    const anterior = g.puntos[g.puntos.length - 1];
34    if ((nums.length !== 3 && nums.length !== 5) || nums.some((n) => !Number.isFinite(n)) || (anterior && (anterior.length !== nums.length || nums[0] <= anterior[0]))) {
35      g.valido = false;
36    }
37    g.puntos.push(nums);
38  });
39  return gestos;
40}
41
42/** Un dedo: toque, pulsación larga, deslizar o arrastrar. */
43function unDedo(puntos) {
44  const [t0, x0, y0] = puntos[0], [t1, x1, y1] = puntos[puntos.length - 1];
45  const dur = t1 - t0, dx = x1 - x0, dy = y1 - y0, d = Math.hypot(dx, dy);
46  if (d < MOVIMIENTO_MIN) {
47    if (dur >= PULSACION_LARGA) return { texto: `pulsación larga en (${x0}, ${y0}) (${dur} ms)` };
48    return { toque: true, x: x0, y: y0, t0, t1, texto: `toque en (${x0}, ${y0})` };
49  }
50  const v = d / dur;
51  if (v >= VELOCIDAD_DESLIZAR) return { texto: `deslizar ${direccion(dx, dy)} (${Math.round(d)} px en ${dur} ms, ${coma(v)} px/ms)` };
52  return { texto: `arrastrar de (${x0}, ${y0}) a (${x1}, ${y1}) (${Math.round(d)} px en ${dur} ms)` };
53}
54
55/** Dos dedos: girar, pellizcar o desplazar, según cómo cambia el segmento que los une. */
56function dosDedos(puntos) {
57  const [, a1, b1, a2, b2] = puntos[0], [, c1, d1, c2, d2] = puntos[puntos.length - 1];
58  const antes = [a2 - a1, b2 - b1], despues = [c2 - c1, d2 - d1];
59  const largoAntes = Math.hypot(...antes), largoDespues = Math.hypot(...despues);
60  if (largoAntes === 0 || largoDespues === 0) return null;
61  let giro = ((Math.atan2(despues[1], despues[0]) - Math.atan2(antes[1], antes[0])) * 180) / Math.PI;
62  if (giro > 180) giro -= 360;
63  if (giro <= -180) giro += 360;
64  const escala = largoDespues / largoAntes;
65  // el centro de los dos dedos: si se mueve sin girar ni pellizcar, es un desplazamiento
66  const mx = (c1 + c2) / 2 - (a1 + a2) / 2, my = (d1 + d2) / 2 - (b1 + b2) / 2;
67  if (Math.abs(giro) >= GIRO_MIN) return { texto: `girar ${Math.round(Math.abs(giro))}° en sentido ${giro > 0 ? "horario" : "antihorario"}` };
68  if (escala >= ESCALA_AMPLIAR) return { texto: `pellizcar para ampliar (×${coma(escala)})` };
69  if (escala <= ESCALA_REDUCIR) return { texto: `pellizcar para reducir (×${coma(escala)})` };
70  if (Math.hypot(mx, my) >= MOVIMIENTO_MIN) return { texto: `desplazar con dos dedos ${direccion(mx, my)}` };
71  return { texto: "dos dedos sin gesto reconocido" };
72}
73
74function main() {
75  const gestos = leerGestos();
76  let previo = null;              // el último toque suelto, por si llega el segundo de un doble toque
77  let pendiente = null;           // su línea, que aún no se ha escrito
78  const escribir = (l) => {
79    if (pendiente) console.log(pendiente);
80    pendiente = null;
81    previo = null;
82    console.log(l);
83  };
84  gestos.forEach((g, i) => {
85    const n = i + 1;
86    if (!g.valido) return escribir(`Gesto ${n}: datos no válidos`);
87    if (g.puntos.length < 2) return escribir(`Gesto ${n}: hacen falta al menos 2 puntos`);
88    const r = g.puntos[0].length === 3 ? unDedo(g.puntos) : dosDedos(g.puntos);
89    if (!r) return escribir(`Gesto ${n}: datos no válidos`);
90    if (r.toque && previo && r.t0 - previo.t1 <= DOBLE_TOQUE_MS && Math.hypot(r.x - previo.x, r.y - previo.y) <= DOBLE_TOQUE_PX) {
91      pendiente = null;
92      escribir(`Gestos ${previo.n} y ${n}: doble toque en (${previo.x}, ${previo.y})`);
93      return;
94    }
95    if (r.toque) {
96      if (pendiente) console.log(pendiente);
97      previo = { ...r, n };
98      pendiente = `Gesto ${n}: ${r.texto}`;
99      return;
100    }
101    escribir(`Gesto ${n}: ${r.texto}`);
102  });
103  if (pendiente) console.log(pendiente);
104  console.log(`Gestos leídos: ${gestos.length}`);
105}
106
107main();

Los umbrales separan la intención del temblor: un dedo nunca está quieto del todo, y sin el margen de 10 px cada toque sería un arrastre minúsculo. Android lo llama «touch slop» y lo ajusta a la densidad de la pantalla.

La espera del doble toque es la razón de que, en muchas aplicaciones, un toque simple tarde un poco en responder: hasta que pasa el tiempo máximo no se sabe si llegará el segundo. Los navegadores móviles quitaban ese retraso cuando la página no permite hacer zoom con doble toque.

Velocidad y no distancia separa deslizar de arrastrar: el mismo recorrido rápido pasa de foto y lento mueve un objeto. Es la base de los gestos «fling» de Android.

En una interfaz natural el gesto debe dar respuesta inmediata (el contenido sigue al dedo mientras se arrastra) y ser predecible: por eso los umbrales son iguales en todo el sistema.

Para ir más allá

  • Usa todos los puntos del gesto y no solo los extremos: detecta un círculo o una «L».
  • Añade el gesto de tres dedos hacia arriba, como el de cambiar de aplicación.
  • Convierte el reconocedor en una máquina de estados que reciba los puntos de uno en uno, como lo hace un GestureDetector.

Dónde se explica