Reconocedor de gestos táctiles: toques, deslizamientos, pellizcos y giros
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
- 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 primergesto:Línea N: fuera de un gesto. - Un dedo que se mueve menos de 10 px es un
toque en (x, y), o unapulsació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 esdeslizar hacia la derecha|izquierdaohacia arriba|abajosegú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. - 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. - 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. - 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
| Constante | Valor | Para qué |
|---|---|---|
| MOVIMIENTO_MIN | 10 px | menos es un toque; también el desplazamiento mínimo con dos dedos |
| PULSACION_LARGA | 500 ms | un toque que dura más |
| DOBLE_TOQUE_MS / _PX | 300 ms / 30 px | entre dos toques |
| VELOCIDAD_DESLIZAR | 0,5 px/ms | deslizar frente a arrastrar |
| GIRO_MIN | 15° | girar |
| ESCALA_AMPLIAR / REDUCIR | 1,2 / 0,8 | pellizcar |
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.
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.
Ejemplo
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
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
Solución explicada
Ver la solución completa
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.