Eloquent JavaScript Libro PDF
Document Details
2024
Marijn Haverbeke
Tags
Summary
Eloquent JavaScript, 4th edition, is a comprehensive textbook for learning JavaScript programming. It covers fundamental concepts like values, data types, operators, functions, data structures, and more, making it an ideal resource for those starting or advancing their JavaScript skills.
Full Transcript
Eloquent JavaScript 4th edition Marijn Haverbeke Copyright © 2024 by Marijn Haverbeke This work is licensed under a Creative Commons attribution-noncommercial license (http://creativecommons.org/licenses/by-nc/3.0/). All code in the book may also be considered licensed under an MIT l...
Eloquent JavaScript 4th edition Marijn Haverbeke Copyright © 2024 by Marijn Haverbeke This work is licensed under a Creative Commons attribution-noncommercial license (http://creativecommons.org/licenses/by-nc/3.0/). All code in the book may also be considered licensed under an MIT license (https://eloquentjavascript. net/code/LICENSE). The illustrations are contributed by various artists: Cover by Péchane Sumi- e. Chapter illustrations by Madalina Tantareanu. Pixel art in Chapters 7 and 16 by Antonio Perdomo Pastor. Regular expression diagrams in Chapter 9 generated with regexper.com by Jeff Avallone. Village photograph in Chapter 11 by Fabrice Creuzot. Game concept for Chapter 16 by Thomas Palef. You can buy a print version of this book, with an extra bonus chapter included, printed by No Starch Press at http://a-fwd.com/com=marijhaver-20&asin- com=1593279507. i Contents Introducción 1 Sobre la programación........................... 2 Por qué importa el lenguaje........................ 3 ¿Qué es JavaScript?............................. 6 Código y qué hacer con él......................... 7 Visión general de este libro......................... 8 Convenciones tipográficas......................... 9 1 Valores, Tipos y Operadores 10 Valores.................................... 10 Números................................... 11 Cadenas.................................... 13 Operadores unarios............................. 15 Valores booleanos.............................. 16 Valores vacíos................................ 18 Conversión automática de tipos...................... 18 Resumen................................... 21 2 Estructura del Programa 22 Expresiones y declaraciones........................ 22 Bindings................................... 23 Nombres de enlaces............................. 25 El entorno.................................. 25 Funciones................................... 26 La función console.log............................ 26 Valores de retorno.............................. 27 Control de flujo............................... 27 Ejecución condicional............................ 28 Bucles while y do.............................. 30 Sangrado de Código............................. 31 bucles for................................... 32 Saliendo de un bucle............................ 33 ii Actualización concisa de enlaces...................... 34 Despachar un valor con switch....................... 34 Capitalización................................ 35 Comentarios................................. 36 Resumen................................... 36 Ejercicios................................... 37 3 Funciones 39 Definir una función............................. 39 Ligaduras y ámbitos............................. 40 Ámbito anidado............................... 41 Funciones como valores........................... 42 Notación de declaración.......................... 43 Funciones de flecha............................. 43 La pila de llamadas............................. 44 Argumentos Opcionales........................... 45 Clausura................................... 47 Recursión................................... 48 Crecimiento de funciones.......................... 51 Funciones y efectos secundarios...................... 53 Resumen................................... 54 Ejercicios................................... 55 4 Estructuras de datos: Objetos y Arrays 56 El hombreardilla............................... 56 Conjuntos de datos............................. 57 Propiedades................................. 58 Métodos................................... 59 Objetos.................................... 60 Mutabilidad................................. 62 El diario del licántropo........................... 63 Calculando la correlación.......................... 65 Bucles de Array............................... 67 El análisis final................................ 67 Más arreología................................ 69 Strings y sus propiedades.......................... 71 Parámetros restantes............................ 72 El objeto Math................................ 73 Desestructuración.............................. 75 Acceso opcional a propiedades....................... 76 iii JSON..................................... 76 Resumen................................... 77 Ejercicios................................... 78 5 Funciones de Orden Superior 81 Abstracción.................................. 82 Conjunto de datos de script........................ 85 Filtrado de arrays.............................. 86 Transformación con map.......................... 87 Resumen con reduce............................ 87 Composabilidad............................... 89 Cadenas y códigos de caracteres...................... 90 Reconociendo texto............................. 92 Resumen................................... 93 Ejercicios................................... 94 6 La Vida Secreta de los Objetos 95 Tipos de Datos Abstractos......................... 95 Métodos................................... 96 Prototipos.................................. 97 Clases..................................... 99 Propiedades privadas............................ 101 Sobrescribiendo propiedades derivadas.................. 102 Mapas..................................... 103 Polimorfismo................................. 105 Getters, setters y estáticos......................... 106 Símbolos................................... 107 La interfaz del iterador........................... 109 Herencia................................... 111 El operador instanceof........................... 112 Resumen................................... 113 Ejercicios................................... 114 7 Proyecto: Un Robot 116 Meadowfield................................. 116 La tarea.................................... 118 Datos persistentes.............................. 120 Simulación.................................. 121 Ruta del camión de correo......................... 122 Búsqueda de caminos............................ 123 iv Ejercicios................................... 125 8 Bugs y Errores 127 Lenguaje................................... 127 Modo estricto................................ 128 Tipos..................................... 129 Pruebas.................................... 130 Depuración.................................. 131 Propagación de errores........................... 133 Excepciones................................. 134 Limpiando después de excepciones.................... 135 Captura selectiva.............................. 137 Afirmaciones................................. 140 Resumen................................... 140 Ejercicios................................... 141 9 Expresiones regulares 142 Creando una expresión regular....................... 142 Pruebas de coincidencias.......................... 143 Conjuntos de caracteres.......................... 143 Caracteres internacionales......................... 145 Repetir partes de un patrón........................ 146 Agrupación de subexpresiones....................... 147 Coincidencias y grupos........................... 147 La clase Date................................. 149 Límites y anticipación............................ 150 Patrones de elección............................. 151 La mecánica de la coincidencia...................... 151 Retroceso................................... 152 El método replace.............................. 154 Avaricia.................................... 155 Creación dinámica de objetos RegExp.................. 156 El método search.............................. 157 La propiedad lastIndex........................... 158 Analizando un archivo INI......................... 159 Unidades de código y caracteres...................... 162 Resumen................................... 162 Ejercicios................................... 164 v 10 Módulos 166 Programas modulares............................ 166 Módulos ES................................. 167 Paquetes................................... 169 Módulos CommonJS............................ 170 Compilación y empaquetado........................ 173 Diseño de módulos............................. 174 Resumen................................... 176 Ejercicios................................... 177 11 Programación Asíncrona 179 Asincronía.................................. 179 Retrollamadas................................ 181 Promesas................................... 182 Falla...................................... 184 Carla..................................... 186 Infiltración.................................. 187 Funciones asíncronas............................ 189 Generadores................................. 190 Un Proyecto de Arte de Corvidos..................... 191 El bucle de eventos............................. 195 Errores asincrónicos............................. 196 Resumen................................... 198 Ejercicios................................... 198 12 Proyecto: Un Lenguaje de Programación 200 Análisis Sintáctico.............................. 200 El evaluador................................. 205 Formas especiales.............................. 206 El entorno.................................. 208 Funciones................................... 209 Compilación................................. 210 Haciendo trampa.............................. 211 Ejercicios................................... 212 13 JavaScript y el Navegador 214 Redes y el Internet............................. 214 La Web.................................... 216 HTML.................................... 217 HTML y JavaScript............................. 219 vi En el entorno controlado.......................... 220 Compatibilidad y las guerras de navegadores.............. 221 14 El Modelo de Objetos del Documento 222 Estructura del documento......................... 222 Árboles.................................... 223 El estándar.................................. 224 Movimiento a través del árbol....................... 225 Encontrando elementos........................... 226 Cambiando el documento.......................... 227 Creación de nodos.............................. 228 Atributos................................... 230 Diseño..................................... 231 Estilos..................................... 233 Estilos en cascada.............................. 234 Selectores de consulta............................ 235 Posicionamiento y animación....................... 236 Resumen................................... 239 Ejercicios................................... 239 15 Manejo de Eventos 242 Controladores de Eventos.......................... 242 Eventos y nodos DOM........................... 243 Objetos de eventos............................. 244 Propagación................................. 244 Acciones predeterminadas......................... 246 Eventos de teclado............................. 247 Eventos de puntero............................. 248 Eventos de desplazamiento......................... 252 Eventos de enfoque............................. 253 Evento de carga............................... 254 Eventos y el bucle de eventos....................... 255 Temporizadores............................... 256 Debouncing.................................. 257 Resumen................................... 258 Ejercicios................................... 258 16 Proyecto: Un juego de plataformas 261 El juego.................................... 261 La tecnología................................. 262 vii Niveles.................................... 263 Leyendo un nivel.............................. 263 Actores.................................... 265 Dibujo..................................... 269 Movimiento y colisión............................ 274 Actualizaciones de actores......................... 277 Seguimiento de teclas............................ 279 Ejecutando el juego............................. 280 Ejercicios................................... 282 17 Dibujando en Canvas 284 SVG...................................... 284 El elemento canvas............................. 285 Líneas y superficies............................. 286 Caminos................................... 287 Curvas.................................... 289 Dibujo de un diagrama de sectores.................... 291 Texto..................................... 292 Imágenes................................... 293 Transformación............................... 295 Almacenando y eliminando transformaciones.............. 297 De vuelta al juego.............................. 299 Elección de una interfaz gráfica...................... 304 Resumen................................... 305 Ejercicios................................... 306 18 HTTP y Formularios 308 El protocolo................................. 308 Navegadores y HTTP............................ 310 Fetch..................................... 312 Aislamiento HTTP............................. 313 Apreciando HTTP.............................. 314 Seguridad y HTTPS............................ 314 Campos de formulario............................ 315 Enfoque.................................... 317 Campos deshabilitados........................... 318 El formulario en su totalidad........................ 319 Campos de texto............................... 320 Casillas de verificación y botones de radio................ 321 Campos de selección............................ 323 viii Campos de archivo............................. 324 Almacenando datos del lado del cliente.................. 326 Resumen................................... 328 Ejercicios................................... 329 19 Proyecto: Editor de Arte Pixelado 331 Componentes................................. 331 El estado................................... 333 Construcción del DOM........................... 335 El lienzo................................... 335 La aplicación................................. 338 Herramientas de dibujo........................... 340 Historial de deshacer............................ 346 Vamos a dibujar............................... 347 ¿Por qué es tan difícil?........................... 348 Ejercicios................................... 349 20 Node.js 352 Antecedentes................................. 352 El comando node.............................. 353 Módulos................................... 354 Instalando con NPM............................ 355 El módulo del sistema de archivos..................... 357 El módulo HTTP.............................. 359 Flujos..................................... 360 Un servidor de archivos........................... 361 Resumen................................... 367 Ejercicios................................... 368 21 Proyecto: Sitio web de intercambio de habilidades 370 Diseño..................................... 370 Long polling................................. 371 Interfaz HTTP................................ 372 El servidor.................................. 374 El cliente................................... 381 Ejercicios................................... 388 Exercise Hints 390 Estructura del Programa.......................... 390 Funciones................................... 391 ix Estructuras de datos: Objetos y Arrays................. 392 Funciones de Orden Superior....................... 394 La Vida Secreta de los Objetos...................... 395 Proyecto: Un Robot............................. 396 Bugs y Errores................................ 397 Expresiones regulares............................ 397 Módulos................................... 398 Programación Asíncrona.......................... 400 x “Creemos que estamos creando el sistema para nuestros propios propósitos. Creemos que lo estamos haciendo a nuestra propia imagen... Pero la computadora en realidad no es como nosotros. Es una proyección de una parte muy pequeña de nosotros mismos: esa parte dedicada a la lógica, el orden, la regla y la claridad.” —Ellen Ullman, Cerca de la máquina: Tecnofilia y sus Descontentos Introducción Este es un libro sobre cómo instruir a computadoras. Las computadoras son tan comunes como los destornilladores hoy en día, pero son bastante más complejas, y hacer que hagan lo que quieres no siempre es fácil. Si la tarea que tienes para tu computadora es común, bien entendida, como mostrarte tu correo electrónico o actuar como una calculadora, puedes abrir la aplicación correspondiente y ponerte a trabajar. Pero para tareas únicas o abiertas, a menudo no hay una aplicación adecuada. Ahí es donde entra en juego la programación. Programar es el acto de con- struir un programa—un conjunto de instrucciones precisas que le dicen a una computadora qué hacer. Debido a que las computadoras son bestias tontas y pedantes, programar es fundamentalmente tedioso y frustrante. Por suerte, si puedes superar ese hecho—e incluso disfrutar del rigor de pensar en términos que las máquinas tontas pueden manejar—programar puede ser gratificante. Te permite hacer cosas en segundos que te tomarían una eternidad a mano. Es una forma de hacer que tu herramienta informática haga cosas que antes no podía hacer. Además, se convierte en un maravilloso juego de resolución de acertijos y pensamiento abstracto. La mayoría de la programación se realiza con lenguajes de programación. Un lenguaje de programación es un lenguaje artificialmente construido utilizado para instruir a las computadoras. Es interesante que la forma más efectiva que hemos encontrado para comunicarnos con una computadora se base tanto en la forma en que nos comunicamos entre nosotros. Al igual que los idiomas humanos, los lenguajes informáticos permiten combinar palabras y frases de nuevas formas, lo que permite expresar conceptos cada vez más nuevos. En un momento dado, las interfaces basadas en lenguaje, como los prompts de BASIC y DOS de los años 1980 y 1990, eran el principal método de in- teractuar con las computadoras. Para el uso informático rutinario, estas se han reemplazado en gran medida por interfaces visuales, que son más fáciles de aprender pero ofrecen menos libertad. Pero si sabes dónde buscar, los lenguajes todavía están ahí. Uno de ellos, JavaScript, está integrado en cada navegador web moderno—y por lo tanto está disponible en casi todos los dispositivos. 1 Este libro intentará que te familiarices lo suficiente con este lenguaje para hacer cosas útiles y entretenidas con él. Sobre la programación Además de explicar JavaScript, presentaré los principios básicos de la progra- mación. Resulta que programar es difícil. Las reglas fundamentales son simples y claras, pero los programas construidos sobre estas reglas tienden a volverse lo suficientemente complejos como para introducir sus propias reglas y compleji- dades. Estás construyendo tu propio laberinto, de alguna manera, y fácilmente puedes perderte en él. Habrá momentos en los que leer este libro resulte terriblemente frustrante. Si eres nuevo en la programación, habrá mucho material nuevo que asimilar. Gran parte de este material luego se combinará de maneras que requieren que hagas conexiones adicionales. Depende de ti hacer el esfuerzo necesario. Cuando te cueste seguir el libro, no saques conclusiones precipitadas sobre tus propias capacidades. Estás bien, simplemente necesitas seguir adelante. Tómate un descanso, vuelve a leer algo de material y asegúrate de leer y comprender los programas de ejemplo y los ejercicios. Aprender es un trabajo duro, pero todo lo que aprendas será tuyo y facilitará aún más el aprendizaje futuro. Cuando la acción se vuelve poco rentable, recopila información; cuando la información se vuelve poco rentable, duerme. Un programa es muchas cosas. Es un trozo de texto escrito por un progra- mador, es la fuerza directiva que hace que la computadora haga lo que hace, es información en la memoria de la computadora, y al mismo tiempo controla las acciones realizadas en esta memoria. Las analogías que intentan comparar los programas con objetos familiares tienden a quedarse cortas. Una comparación vagamente adecuada es comparar un programa con una máquina: suelen estar implicadas muchas partes separadas y, para hacer que todo funcione, debemos considerar las formas en que estas partes se interconectan y contribuyen a la operación del conjunto. Una computadora es una máquina física que actúa como anfitriona de estas máquinas inmateriales. Las computadoras mismas solo pueden hacer cosas in- creíblemente sencillas. La razón por la que son tan útiles es que hacen estas cosas a una velocidad increíblemente alta. Un programa puede combinar inge- niosamente un número enorme de estas acciones simples para hacer cosas muy complicadas. 2 Un programa es una construcción del pensamiento. Es gratuito de construir, es liviano y crece fácilmente bajo nuestras manos al teclear. Pero a medida que un programa crece, también lo hace su complejidad. La habilidad de programar es la habilidad de construir programas que no te confundan a ti mismo. Los mejores programas son aquellos que logran hacer algo interesante mientras siguen siendo fáciles de entender. Algunos programadores creen que esta complejidad se gestiona mejor uti- lizando solo un conjunto pequeño de técnicas bien comprendidas en sus progra- mas. Han compuesto reglas estrictas (“mejores prácticas”) que prescriben la forma que deberían tener los programas y se mantienen cuidadosamente dentro de su pequeña zona segura. Esto no solo es aburrido, es inefectivo. A menudo, nuevos problemas re- quieren soluciones nuevas. El campo de la programación es joven y aún se está desarrollando rápidamente, y es lo suficientemente variado como para tener es- pacio para enfoques radicalmente diferentes. Hay muchos errores terribles que cometer en el diseño de programas, y deberías ir y cometerlos al menos una vez para entenderlos. Una noción de cómo es un buen programa se desarrolla con la práctica, no se aprende de una lista de reglas. Por qué importa el lenguaje Al principio, en los inicios de la informática, no existían los lenguajes de pro- gramación. Los programas lucían algo así: 00110001 00000000 00000000 00110001 00000001 00000001 00110011 00000001 00000010 01010001 00001011 00000010 00100010 00000010 00001000 01000011 00000001 00000000 01000001 00000001 00000001 00010000 00000010 00000000 01100010 00000000 00000000 Este es un programa para sumar los números del 1 al 10 y mostrar el resultado: 1 + 2 +... + 10 = 55. Podría ejecutarse en una máquina hipotética simple. Para programar los primeros ordenadores, era necesario configurar grandes con- juntos de interruptores en la posición correcta o perforar agujeros en tiras de cartón y alimentarlos al ordenador. Puedes imaginar lo tedioso y propenso a errores que era este procedimiento. Incluso escribir programas simples requería mucha astucia y disciplina. Los complejos eran casi inconcebibles. 3 Por supuesto, introducir manualmente estos patrones arcanos de bits (los unos y ceros) hacía que el programador se sintiera como un mago poderoso. Y eso debe valer algo en términos de satisfacción laboral. Cada línea del programa anterior contiene una única instrucción. Podría escribirse en inglés de la siguiente manera: 1. Almacena el número 0 en la ubicación de memoria 0. 2. Almacena el número 1 en la ubicación de memoria 1. 3. Almacena el valor de la ubicación de memoria 1 en la ubicación de memo- ria 2. 4. Resta el número 11 al valor en la ubicación de memoria 2. 5. Si el valor en la ubicación de memoria 2 es el número 0, continúa con la instrucción 9. 6. Suma el valor de la ubicación de memoria 1 a la ubicación de memoria 0. 7. Añade el número 1 al valor de la ubicación de memoria 1. 8. Continúa con la instrucción 3. 9. Muestra el valor de la ubicación de memoria 0. Aunque eso ya es más legible que la sopa de bits, sigue siendo bastante con- fusa. Usar nombres en lugar de números para las instrucciones y las ubicaciones de memoria ayuda: Establecer “total” en 0. Establecer “count” en 1. [bucle] Establecer “compare” en “count”. Restar 11 de “compare”. Si “compare” es cero, continuar en [fin]. Sumar “count” a “total”. Añadir 1 a “count”. Continuar en [bucle]. [fin] Mostrar “total”. ¿Puedes ver cómo funciona el programa en este punto? Las dos primeras líneas asignan los valores iniciales a dos ubicaciones de memoria: total se utilizará para construir el resultado de la computación, y count llevará la cuenta del 4 número que estamos observando en ese momento. Las líneas que utilizan compare probablemente sean las más confusas. El programa quiere ver si count es igual a 11 para decidir si puede dejar de ejecutarse. Debido a que nuestra máquina hipotética es bastante primitiva, solo puede comprobar si un número es cero y tomar una decisión en función de ese valor. Por lo tanto, utiliza la ubicación de memoria etiquetada como compare para calcular el valor de count - 11 y tomar una decisión basada en ese valor. Las siguientes dos líneas suman el valor de count al resultado e incrementan count en 1 cada vez que el programa decide que count aún no es 11. Aquí está el mismo programa en JavaScript: let total = 0, count = 1; while (count.animal")); // Hijo directo de // → 1 A diferencia de métodos como getElementsByTagName, el objeto devuelto por querySelectorAll no es dinámico. No cambiará cuando cambies el documento. Aun así, no es un array real, por lo que necesitas llamar a Array.from si deseas tratarlo como tal. El método querySelector (sin la parte All) funciona de manera similar. Este es útil si deseas un elemento específico y único. Solo devolverá el primer elemento coincidente o null cuando no haya ningún elemento coincidente. Posicionamiento y animación La propiedad de estilo position influye en el diseño de una manera poderosa. De forma predeterminada, tiene un valor de static, lo que significa que el elemento se sitúa en su lugar normal en el documento. Cuando se establece en 236 relative, el elemento sigue ocupando espacio en el documento, pero ahora las propiedades de estilo top y left se pueden usar para moverlo con respecto a ese lugar normal. Cuando position se establece en absolute, el elemento se elimina del flujo normal del documento, es decir, ya no ocupa espacio y puede superponerse con otros elementos. Además, sus propiedades de top y left se pueden usar para posicionarlo absolutamente con respecto a la esquina superior izquierda del elemento contenedor más cercano cuya propiedad de position no sea static, o con respecto al documento si no existe tal elemento contenedor. Podemos usar esto para crear una animación. El siguiente documento mues- tra una imagen de un gato que se mueve en una elipse: let cat = document.querySelector("img"); let angle = Math.PI / 2; function animate(time, lastTime) { if (lastTime != null) { angle += (time - lastTime) * 0.001; } cat.style.top = (Math.sin(angle) * 20) + "px"; cat.style.left = (Math.cos(angle) * 200) + "px"; requestAnimationFrame(newTime => animate(newTime, time)); } requestAnimationFrame(animate); La flecha gris muestra la trayectoria a lo largo de la cual se mueve la imagen. Nuestra imagen está centrada en la página y tiene una posición de relative. Actualizaremos repetidamente los estilos top e left de esa imagen para moverla. El script utiliza requestAnimationFrame para programar la ejecución de la función animar siempre que el navegador esté listo para repintar la pantalla. La función animar a su vez vuelve a llamar a requestAnimationFrame para progra- mar la siguiente actualización. Cuando la ventana del navegador (o pestaña) está activa, esto provocará que las actualizaciones ocurran a una velocidad de 237 aproximadamente 60 por segundo, lo que suele producir una animación atrac- tiva. Si simplemente actualizáramos el DOM en un bucle, la página se congelaría y nada aparecería en la pantalla. Los navegadores no actualizan su pantalla mientras se ejecuta un programa JavaScript, ni permiten ninguna interacción con la página. Por eso necesitamos requestAnimationFrame — le indica al navegador que hemos terminado por ahora, y puede continuar haciendo las cosas que hacen los navegadores, como actualizar la pantalla y responder a las acciones del usuario. La función de animación recibe el tiempo actual como argumento. Para ase- gurar que el movimiento del gato por milisegundo sea estable, basa la velocidad a la que cambia el ángulo en la diferencia entre el tiempo actual y el último tiempo en que se ejecutó la función. Si simplemente moviera el ángulo por una cantidad fija por paso, el movimiento se interrumpiría si, por ejemplo, otra tarea pesada que se está ejecutando en la misma computadora impidiera que la función se ejecutara durante una fracción de segundo. Moverse en círculos se hace utilizando las funciones trigonométricas Math.cos y Math.sin. Para aquellos que no estén familiarizados con ellas, las presentaré brevemente ya que ocasionalmente las utilizaremos en este libro. Math.cos y Math.sin son útiles para encontrar puntos que se encuentran en un círculo alrededor del punto (0,0) con un radio de uno. Ambas funciones interpretan su argumento como la posición en este círculo, con cero denotando el punto en el extremo derecho del círculo, avanzando en el sentido de las agujas del reloj hasta que 2π (aproximadamente 6,28) nos ha llevado alrededor de todo el círculo. Math.cos te indica la coordenada x del punto que corresponde a la posición dada, y Math.sin devuelve la coordenada y. Las posiciones (o ángulos) mayores que 2π o menores que 0 son válidos, la rotación se repite de manera que a+2π se refiere al mismo ángulo que a. Esta unidad para medir ángulos se llama radianes — un círculo completo son 2π radianes, similar a cómo son 360 grados al medir en grados. La constante π está disponible como Math.PI en JavaScript. cos(-⅔π) sin(-⅔π) sin(¼π) cos(¼π) 238 El código de animación del gato mantiene un contador, angle, para el ángulo actual de la animación e incrementa el mismo cada vez que se llama la función animate. Luego puede usar este ángulo para calcular la posición actual del elemento de imagen. El estilo top es calculado con Math.sin y multiplicado por 20, que es el radio vertical de nuestra elipse. El estilo left se basa en Math.cos y multiplicado por 200 para que la elipse sea mucho más ancha que alta. Ten en cuenta que los estilos usualmente necesitan unidades. En este caso, tenemos que añadir "px" al número para indicarle al navegador que estamos contando en píxeles (en lugar de centímetros, “ems” u otras unidades). Esto es fácil de olvidar. Usar números sin unidades resultará en que tu estilo sea ignorado — a menos que el número sea 0, lo cual siempre significa lo mismo, independientemente de su unidad. Resumen Los programas de JavaScript pueden inspeccionar e interferir con el documento que el navegador está mostrando a través de una estructura de datos llamada el DOM. Esta estructura de datos representa el modelo del documento del navegador, y un programa de JavaScript puede modificarlo para cambiar el documento visible. El DOM está organizado como un árbol, en el cual los elementos están dis- puestos jerárquicamente de acuerdo a la estructura del documento. Los objetos que representan elementos tienen propiedades como parentNode y childNodes, las cuales pueden ser usadas para navegar a través de este árbol. La forma en que un documento es mostrado puede ser influenciada por el estilo, tanto adjuntando estilos directamente a nodos como definiendo reglas que coincidan con ciertos nodos. Hay muchas propiedades de estilo diferentes, como color o display. El código de JavaScript puede manipular el estilo de un elemento directamente a través de su propiedad style. Ejercicios Construir una tabla Una tabla HTML se construye con la siguiente estructura de etiquetas: nombre 239 altura lugar Kilimanjaro 5895 Tanzania Dado un conjunto de datos de montañas, un array de objetos con propiedades name, height, y place, genera la estructura DOM para una tabla que enumera los objetos. Debería haber una columna por clave y una fila por objeto, además de una fila de encabezado con elementos en la parte superior, enumerando los nombres de las columnas. Escribe esto de manera que las columnas se deriven automáticamente de los objetos, tomando los nombres de las propiedades del primer objeto en los datos. Muestra la tabla resultante en el documento agregándola al elemento que tenga un atributo id de "mountains". Una vez que tengas esto funcionando, alinea a la derecha las celdas que contienen valores numéricos estableciendo su propiedad style.textAlign en "right". Elementos por nombre de etiqueta El método document.getElementsByTagName devuelve todos los elementos hijos con un nombre de etiqueta dado. Implementa tu propia versión de esto como una función que tome un nodo y un string (el nombre de la etiqueta) como argumentos y devuelva un array que contenga todos los nodos de elementos descendientes con el nombre de etiqueta dado. Tu función debe recorrer el documento en sí. No puede usar un método como querySelectorAll para hacer el trabajo. Para encontrar el nombre de etiqueta de un elemento, usa su propiedad nodeName. Pero ten en cuenta que esto devolverá el nombre de la etiqueta en mayúsculas. Usa los métodos de string toLowerCase o toUpperCase para compensar esto. El sombrero del gato Extiende la animación del gato definida anteriormente para que tanto el gato como su sombrero () orbiten en lados opuestos de la elipse. 240 O haz que el sombrero circule alrededor del gato. O altera la animación de alguna otra manera interesante. Para facilitar el posicionamiento de varios objetos, es probablemente una buena idea cambiar a posicionamiento absoluto. Esto significa que top y left se cuentan en relación al extremo superior izquierdo del documento. Para evitar usar coordenadas negativas, que harían que la imagen se salga de la página visible, puedes agregar un número fijo de píxeles a los valores de posición. 241 “Tienes poder sobre tu mente, no sobre los eventos externos. Date cuenta de esto y encontrarás fuerza.” —Marco Aurelio, Meditaciones Chapter 15 Manejo de Eventos Algunos programas trabajan con la entrada directa del usuario, como acciones del ratón y del teclado. Ese tipo de entrada no está disponible de antemano, como una estructura de datos bien organizada, llega pieza por pieza, en tiempo real, y el programa debe responder a medida que sucede. Controladores de Eventos Imagina una interfaz donde la única forma de saber si una tecla en el teclado está siendo presionada es leyendo el estado actual de esa tecla. Para poder reaccionar a las pulsaciones de teclas, tendrías que leer constantemente el estado de la tecla para capturarla antes de que se libere nuevamente. Sería peligroso realizar otras computaciones intensivas en tiempo, ya que podrías perder una pulsación de tecla. Algunas máquinas primitivas manejan la entrada de esa manera. Un paso adelante sería que el hardware o el sistema operativo noten la pulsación de tecla y la pongan en una cola. Un programa puede luego verificar periódicamente la cola en busca de nuevos eventos y reaccionar a lo que encuentre allí. Por supuesto, tiene que recordar mirar la cola y hacerlo a menudo, porque cualquier tiempo transcurrido entre la presión de la tecla y la notificación del evento por parte del programa hará que el software se sienta sin respuesta. Este enfoque se llama sondeo. La mayoría de los programadores prefieren evitarlo. Un mecanismo mejor es que el sistema notifique activamente a nuestro código cuando ocurre un evento. Los navegadores hacen esto al permitirnos registrar funciones como manejadores para eventos específicos. Haz clic en este documento para activar el manejador. window.addEventListener("click", () => { console.log("¿Llamaste?"); }); 242 La asignación window se refiere a un objeto integrado proporcionado por el navegador. Representa la ventana del navegador que contiene el documento. Llamar a su método addEventListener registra el segundo argumento para que se llame cada vez que ocurra el evento descrito por su primer argumento. Eventos y nodos DOM Cada controlador de eventos del navegador se registra en un contexto. En el ejemplo anterior llamamos a addEventListener en el objeto window para registrar un controlador para toda la ventana. Un método similar también se encuentra en elementos del DOM y algunos otros tipos de objetos. Los escuchas de eventos solo se llaman cuando el evento ocurre en el contexto del objeto en el que están registrados. Haz clic No hay manejador aquí. let button = document.querySelector("button"); button.addEventListener("click", () => { console.log("Botón clickeado."); }); Ese ejemplo adjunta un manejador al nodo del botón. Los clics en el botón hacen que se ejecute ese manejador, pero los clics en el resto del documento no lo hacen. Darle a un nodo un atributo onclick tiene un efecto similar. Esto funciona para la mayoría de tipos de eventos: puedes adjuntar un manejador a través del atributo cuyo nombre es el nombre del evento con on al inicio. Pero un nodo solo puede tener un atributo onclick, por lo que solo puedes registrar un manejador por nodo de esa manera. El método addEventListener te permite agregar cualquier cantidad de manejadores, por lo que es seguro agregar manejadores incluso si ya hay otro manejador en el elemento. El método removeEventListener, llamado con argumentos similares a addEventListener , remueve un manejador. Botón de acción única let button = document.querySelector("button"); function unaVez() { console.log("¡Hecho!"); button.removeEventListener("click", unaVez); 243 } button.addEventListener("click", unaVez); La función proporcionada a removeEventListener debe ser el mismo valor de función que se proporcionó a addEventListener. Por lo tanto, para anular el registro de un manejador, querrás darle un nombre a la función (unaVez, en el ejemplo) para poder pasar el mismo valor de función a ambos métodos. Objetos de eventos Aunque lo hemos ignorado hasta ahora, las funciones de manejadores de eventos reciben un argumento: el objeto de evento. Este objeto contiene información adicional sobre el evento. Por ejemplo, si queremos saber cuál botón del mouse se presionó, podemos mirar la propiedad button del objeto de evento. Haz clic como quieras let button = document.querySelector("button"); button.addEventListener("mousedown", event => { if (event.button == 0) { console.log("Botón izquierdo"); } else if (event.button == 1) { console.log("Botón del medio"); } else if (event.button == 2) { console.log("Botón derecho"); } }); La información almacenada en un objeto de evento difiere según el tipo de evento. Discutiremos diferentes tipos más adelante en el capítulo. La propiedad type del objeto siempre contiene una cadena que identifica el evento (como " click" o "mousedown"). Propagación Para la mayoría de tipos de evento, los manejadores registrados en nodos con hijos también recibirán eventos que ocurran en los hijos. Si se hace clic en un botón dentro de un párrafo, los manejadores de eventos en el párrafo también verán el evento de clic. Pero si tanto el párrafo como el botón tienen un controlador, el controlador 244 más específico —el del botón— tiene prioridad para ejecutarse primero. Se dice que el evento se propaga hacia afuera, desde el nodo donde ocurrió hacia el nodo padre de ese nodo y hasta la raíz del documento. Finalmente, después de que todos los controladores registrados en un nodo específico hayan tenido su turno, los controladores registrados en toda la ventana tienen la oportunidad de responder al evento. En cualquier momento, un controlador de eventos puede llamar al método stopPropagation en el objeto de evento para evitar que los controladores su- periores reciban el evento. Esto puede ser útil cuando, por ejemplo, tienes un botón dentro de otro elemento clickeable y no quieres que los clics en el botón activen el comportamiento de click del elemento externo. El siguiente ejemplo registra controladores de "mousedown" tanto en un botón como en el párrafo que lo rodea. Cuando se hace clic con el botón derecho del ratón, el controlador del botón llama a stopPropagation, lo que evitará que se ejecute el controlador en el párrafo. Cuando el botón se hace clic con otro botón del ratón, ambos controladores se ejecutarán. Un párrafo con un botón. let para = document.querySelector("p"); let button = document.querySelector("button"); para.addEventListener("mousedown", () => { console.log("Controlador para el párrafo."); }); button.addEventListener("mousedown", event => { console.log("Controlador para el botón."); if (event.button == 2) event.stopPropagation(); }); La mayoría de los objetos de eventos tienen una propiedad target que se refiere al nodo donde se originaron. Puedes usar esta propiedad para asegurarte de que no estás manejando accidentalmente algo que se propagó desde un nodo que no deseas manejar. También es posible usar la propiedad target para abarcar un amplio rango para un tipo específico de evento. Por ejemplo, si tienes un nodo que con- tiene una larga lista de botones, puede ser más conveniente registrar un único controlador de clic en el nodo externo y hacer que utilice la propiedad target para averiguar si se hizo clic en un botón, en lugar de registrar controladores individuales en todos los botones. A B 245 C document.body.addEventListener("click", event => { if (event.target.nodeName == "BUTTON") { console.log("Clic en", event.target.textContent); } }); Acciones predeterminadas Muchos eventos tienen una acción predeterminada asociada a ellos. Si haces clic en un enlace, serás llevado al destino del enlace. Si presionas la flecha hacia abajo, el navegador desplazará la página hacia abajo. Si haces clic derecho, obtendrás un menú contextual. Y así sucesivamente. Para la mayoría de los tipos de eventos, los controladores de eventos de JavaScript se ejecutan antes de que ocurra el comportamiento predeterminado. Si el controlador no desea que este comportamiento normal ocurra, típica- mente porque ya se encargó de manejar el evento, puede llamar al método preventDefault en el objeto de evento. Esto se puede utilizar para implementar tus propios atajos de teclado o menús contextuales. También se puede usar para interferir de manera molesta con el comportamiento que los usuarios esperan. Por ejemplo, aquí hay un enlace que no se puede seguir: MDN let link = document.querySelector("a"); link.addEventListener("click", event => { console.log("¡Incorrecto!"); event.preventDefault(); }); Trata de no hacer este tipo de cosas a menos que tengas una razón realmente válida. Será desagradable para las personas que utilicen tu página cuando se rompa el comportamiento esperado. Dependiendo del navegador, algunos eventos no se pueden interceptar en absoluto. En Chrome, por ejemplo, el atajo de teclado para cerrar la pestaña actual (control-W o command-W) no se puede manejar con JavaScript. 246 Eventos de teclado Cuando se presiona una tecla en el teclado, tu navegador dispara un evento "keydown". Cuando se suelta, obtienes un evento "keyup". Esta página se vuelve violeta cuando mantienes presionada la tecla V. window.addEventListener("keydown", event => { if (event.key == "v") { document.body.style.background = "violet"; } }); window.addEventListener("keyup", event => { if (event.key == "v") { document.body.style.background = ""; } }); A pesar de su nombre, "keydown" se dispara no solo cuando la tecla se presiona físicamente hacia abajo. Cuando se presiona y se mantiene una tecla, el evento se vuelve a disparar cada vez que la tecla se repite. A veces tienes que tener cuidado con esto. Por ejemplo, si agregas un botón al DOM cuando se presiona una tecla y lo eliminas de nuevo cuando se suelta la tecla, podrías agregar accidentalmente cientos de botones cuando se mantiene presionada la tecla durante más tiempo. El ejemplo observó la propiedad key del objeto evento para ver sobre qué tecla es el evento. Esta propiedad contiene una cadena que, para la mayoría de las teclas, corresponde a lo que escribirías al presionar esa tecla. Para teclas especiales como enter, contiene una cadena que nombra la tecla ("Enter", en este caso). Si mantienes presionado shift mientras presionas una tecla, eso también puede influir en el nombre de la tecla: "v" se convierte en "V", y "1" puede convertirse en "!", si eso es lo que produce al presionar shift-1 en tu teclado. Las teclas modificadoras como shift, control, alt y meta (command en Mac) generan eventos de tecla igual que las teclas normales. Pero al buscar combinaciones de teclas, también puedes averiguar si estas teclas se mantienen presionadas mirando las propiedades shiftKey, ctrlKey, altKey y metaKey de los eventos de teclado y ratón. Pulsa Control-Espacio para continuar. 247 window.addEventListener("keydown", event => { if (event.key == " " && event.ctrlKey) { console.log("¡Continuando!"); } }); El nodo del DOM donde se origina un evento de teclado depende del elemento que tiene foco cuando se presiona la tecla. La mayoría de los nodos no pueden tener foco a menos que les des un atributo tabindex, pero cosas como los enlaces, botones y campos de formulario pueden. Volveremos a los campos de formulario en el Capítulo 18. Cuando nada en particular tiene foco, document.body actúa como el nodo objetivo de los eventos de teclado. Cuando el usuario está escribiendo texto, utilizar eventos de teclado para averiguar qué se está escribiendo es problemático. Algunas plataformas, es- pecialmente el teclado virtual en teléfonos Android, no disparan eventos de teclado. Pero incluso cuando se tiene un teclado tradicional, algunos tipos de entrada de texto no coinciden con las pulsaciones de teclas de manera directa, como el software de editor de método de entrada (IME) utilizado por personas cuyos guiones no caben en un teclado, donde múltiples pulsaciones de teclas se combinan para crear caracteres. Para detectar cuando se ha escrito algo, los elementos en los que se puede escribir, como las etiquetas y , activan eventos "input" cada vez que el usuario cambia su contenido. Para obtener el contenido real que se ha escrito, lo mejor es leerlo directamente del campo enfocado. Capítulo 18 mostrará cómo hacerlo. Eventos de puntero Actualmente existen dos formas ampliamente utilizadas de señalar cosas en una pantalla: los ratones (incluyendo dispositivos que actúan como ratones, como touchpads y trackballs) y las pantallas táctiles. Estas producen diferentes tipos de eventos. Clics de ratón Presionar un botón de ratón provoca que se disparen varios eventos. Los eventos "mousedown" y "mouseup" son similares a "keydown" y "keyup" y se activan cuando se presiona y se suelta el botón. Estos eventos ocurren en los nodos del DOM que están inmediatamente debajo del puntero del ratón cuando se produce el evento. 248 Después del evento "mouseup", se dispara un evento "click" en el nodo más específico que contenía tanto la pulsación como la liberación del botón. Por ejemplo, si presiono el botón del ratón en un párrafo y luego muevo el puntero a otro párrafo y suelto el botón, el evento "click" ocurrirá en el elemento que contiene ambos párrafos. Si dos clics ocurren cerca uno del otro, también se dispara un evento " dblclick" (doble clic), después del segundo evento de clic. Para obtener información precisa sobre el lugar donde ocurrió un evento de ratón, puedes mirar sus propiedades clientX y clientY, que contienen las coordenadas del evento (en píxeles) relativas a la esquina superior izquierda de la ventana, o pageX y pageY, que son relativas a la esquina superior izquierda de todo el documento (lo cual puede ser diferente cuando la ventana ha sido desplazada). El siguiente programa implementa una aplicación de dibujo primitiva. Cada vez que haces clic en el documento, agrega un punto bajo el puntero de tu ratón. Ver Capítulo 19 para una aplicación de dibujo menos primitiva. body { height: 200px; background: beige; }.dot { height: 8px; width: 8px; border-radius: 4px; background: teal; position: absolute; } window.addEventListener("click", event => { let dot = document.createElement("div"); dot.className = "dot"; dot.style.left = (event.pageX - 4) + "px"; dot.style.top = (event.pageY - 4) + "px"; document.body.appendChild(dot); }); Movimiento del ratón Cada vez que el puntero del ratón se mueve, se dispara un evento "mousemove". Este evento se puede usar para rastrear la posición del ratón. Una situación 249 común en la que esto es útil es al implementar algún tipo de funcionalidad de arrastrar y soltar con el ratón. Como ejemplo, el siguiente programa muestra una barra y configura contro- ladores de eventos para que al arrastrar hacia la izquierda o hacia la derecha en esta barra, se haga más estrecha o más ancha: Arrastra la barra para cambiar su anchura: let lastX; // Rastrea la última posición X del ratón observada let bar = document.querySelector("div"); bar.addEventListener("mousedown", event => { if (event.button == 0) { lastX = event.clientX; window.addEventListener("mousemove", moved); event.preventDefault(); // Prevenir selección } }); function moved(event) { if (event.buttons == 0) { window.removeEventListener("mousemove", moved); } else { let dist = event.clientX - lastX; let newWidth = Math.max(10, bar.offsetWidth + dist); bar.style.width = newWidth + "px"; lastX = event.clientX; } } La página resultante se ve así: Ten en cuenta que el controlador "mousemove" está registrado en toda la win- dow. Incluso si el ratón sale de la barra durante el cambio de tamaño, mientras el botón se mantenga presionado todavía queremos actualizar su tamaño. Debemos detener el cambio de tamaño de la barra cuando se libere el botón del ratón. Para eso, podemos usar la propiedad buttons (notar el plural), que nos indica qué botones están actualmente presionados. Cuando este valor es cero, ningún botón está presionado. Cuando se mantienen presionados botones, 250 su valor es la suma de los códigos de esos botones—el botón izquierdo tiene el código 1, el derecho 2 y el central 4. Con el botón izquierdo y el derecho presionados, por ejemplo, el valor de buttons será 3. Es importante destacar que el orden de estos códigos es diferente al uti- lizado por button, donde el botón central venía antes que el derecho. Como se mencionó, la consistencia no es realmente un punto fuerte de la interfaz de programación del navegador. Eventos táctiles El estilo de navegador gráfico que usamos fue diseñado pensando en interfaces de ratón, en una época donde las pantallas táctiles eran raras. Para hacer que la web “funcione” en los primeros teléfonos con pantalla táctil, los navegadores de esos dispositivos fingían, hasta cierto punto, que los eventos táctiles eran eventos de ratón. Si tocas la pantalla, recibirás eventos de "mousedown", " mouseup" y "click". Pero esta ilusión no es muy robusta. Una pantalla táctil funciona de manera diferente a un ratón: no tiene múltiples botones, no se puede rastrear el dedo cuando no está en la pantalla (para simular "mousemove"), y permite que varios dedos estén en la pantalla al mismo tiempo. Los eventos de ratón solo cubren la interacción táctil en casos sencillos: si agregas un controlador de "click" a un botón, los usuarios táctiles aún po- drán usarlo. Pero algo como la barra redimensionable del ejemplo anterior no funciona en una pantalla táctil. Existen tipos específicos de eventos disparados por la interacción táctil. Cuando un dedo comienza a tocar la pantalla, se genera un evento "touchstart". Cuando se mueve mientras toca, se generan eventos "touchmove". Finalmente, cuando deja de tocar la pantalla, verás un evento "touchend". Debido a que muchas pantallas táctiles pueden detectar varios dedos al mismo tiempo, estos eventos no tienen un único conjunto de coordenadas asociadas. Más bien, sus objetos de eventos tienen una propiedad touches, que contiene un objeto similar a un array de puntos, cada uno con sus propias propiedades clientX, clientY, pageX y pageY. Podrías hacer algo como esto para mostrar círculos rojos alrededor de cada dedo que toca: dot { position: absolute; display: block; border: 2px solid red; border-radius: 50px; height: 100px; width: 100px; } 251 Toca esta página function update(event) { for (let dot; dot = document.querySelector("dot");) { dot.remove(); } for (let i = 0; i < event.touches.length; i++) { let {pageX, pageY} = event.touches[i]; let dot = document.createElement("dot"); dot.style.left = (pageX - 50) + "px"; dot.style.top = (pageY - 50) + "px"; document.body.appendChild(dot); } } window.addEventListener("touchstart", update); window.addEventListener("touchmove", update); window.addEventListener("touchend", update); A menudo querrás llamar a preventDefault en los controladores de eventos tác- tiles para anular el comportamiento predeterminado del navegador (que puede incluir desplazar la página al deslizar) y evitar que se generen eventos de ratón, para los cuales también puedes tener un controlador. Eventos de desplazamiento Cada vez que un elemento se desplaza, se dispara un evento "scroll". Esto tiene varios usos, como saber qué está viendo actualmente el usuario (para desactivar animaciones fuera de la pantalla o enviar informes de vigilancia a tu malvada sede) o mostrar alguna indicación de progreso (resaltando parte de una tabla de contenidos o mostrando un número de página).El siguiente ejemplo dibuja una barra de progreso sobre el documento y la actualiza para llenarla a medida que se desplaza hacia abajo: #progress { border-bottom: 2px solid blue; width: 0; position: fixed; top: 0; left: 0; } 252 // Create some content document.body.appendChild(document.createTextNode( "supercalifragilisticexpialidocious ".repeat(1000))); let bar = document.querySelector("#progress"); window.addEventListener("scroll", () => { let max = document.body.scrollHeight - innerHeight; bar.style.width = `${(pageYOffset / max) * 100}%`; }); Darle a un elemento una position de fixed actúa de manera similar a una posición absolute, pero también evita que se desplace junto con el resto del documento. El efecto es hacer que nuestra barra de progreso permanezca en la parte superior. Su ancho se cambia para indicar el progreso actual. Usamos %, en lugar de px, como unidad al establecer el ancho para que el elemento tenga un tamaño relativo al ancho de la página. El enlace global innerHeight nos da la altura de la ventana, que debemos restar de la altura total desplazable, ya que no se puede seguir desplazando cuando se llega al final del documento. También existe un innerWidth para el ancho de la ventana. Al dividir pageYOffset, la posición actual de desplaza- miento, por la posición máxima de desplazamiento y multiplicar por 100, obten- emos el porcentaje para la barra de progreso. Llamar a preventDefault en un evento de desplazamiento no impide que ocurra el desplazamiento. De hecho, el controlador de eventos se llama solo después de que ocurre el desplazamiento. Eventos de enfoque Cuando un elemento recibe el enfoque, el navegador dispara un evento "focus" en él. Cuando pierde el enfoque, el elemento recibe un evento "blur". A diferencia de los eventos discutidos anteriormente, estos dos eventos no se propagan. Un controlador en un elemento padre no recibe notificaciones cuando un elemento hijo recibe o pierde el enfoque. El siguiente ejemplo muestra texto de ayuda para el campo de texto que actualmente tiene el foco: Nombre: Edad: 253 let help = document.querySelector("#help"); let fields = document.querySelectorAll("input"); for (let field of Array.from(fields)) { field.addEventListener("focus", event => { let text = event.target.getAttribute("data-help"); help.textContent = text; }); field.addEventListener("blur", event => { help.textContent = ""; }); } Esta captura de pantalla muestra el texto de ayuda para el campo de edad. El objeto ((window)) recibirá eventos "focus" y "blur" cuando el usuario se mueva desde o hacia la pestaña o ventana del navegador en la que se muestra el documento. Evento de carga Cuando una página termina de cargarse, se dispara el evento "load" en los objetos ventana y cuerpo del documento. Esto se usa a menudo para progra- mar acciones de inicialización que requieren que todo el documento haya sido construido. Recuerda que el contenido de las etiquetas se ejecuta inmediatamente cuando se encuentra la etiqueta. Esto puede ser demasiado pronto, por ejemplo, cuando el script necesita hacer algo con partes del docu- mento que aparecen después de la etiqueta. Elementos como imágenes y etiquetas de script que cargan un archivo externo también tienen un evento "load" que indica que se cargaron los archivos a los que hacen referencia. Al igual que los eventos relacionados con el enfoque, los eventos de carga no se propagan. Cuando se cierra una página o se navega lejos de ella (por ejemplo, al seguir un enlace), se dispara un evento "beforeunload". El uso principal de este evento es evitar que el usuario pierda accidentalmente su trabajo al cerrar un documento. Si previenes el comportamiento predeterminado en este evento y estableces la propiedad returnValue en el objeto de evento a una cadena, el 254 navegador mostrará al usuario un cuadro de diálogo preguntando si realmente desea abandonar la página. Ese cuadro de diálogo podría incluir tu cadena, pero debido a que algunos sitios maliciosos intentan usar estos cuadros de diálogo para confundir a las personas y hacer que se queden en su página para ver anuncios de pérdida de peso dudosos, la mayoría de los navegadores ya no los muestran. Eventos y el bucle de eventos En el contexto del bucle de eventos, como se discutió en el Capítulo 11, los controladores de eventos del navegador se comportan como otras notificaciones asíncronas. Se programan cuando ocurre el evento pero deben esperar a que otros scripts que se estén ejecutando terminen antes de tener la oportunidad de ejecutarse. El hecho de que los eventos solo se puedan procesar cuando no hay nada más en ejecución significa que, si el bucle de eventos está ocupado con otro trabajo, cualquier interacción con la página (que ocurre a través de eventos) se retrasará hasta que haya tiempo para procesarla. Entonces, si programas demasiado trabajo, ya sea con controladores de eventos de larga duración o con muchos que se ejecutan rápidamente, la página se volverá lenta y pesada de usar. Para casos en los que realmente quieres hacer algo que consume mucho tiempo en segundo plano sin congelar la página, los navegadores proporcionan algo llamado web workers. Un worker es un proceso de JavaScript que se ejecuta junto al script principal, en su propia línea de tiempo. Imagina que elevar al cuadrado un número es una computación pesada y de larga duración que queremos realizar en un hilo separado. Podríamos escribir un archivo llamado code/squareworker.js que responda a mensajes calculando un cuadrado y enviando un mensaje de vuelta. addEventListener("message", event => { postMessage(event.data * event.data); }); Para evitar los problemas de tener múltiples hilos tocando los mismos datos, los workers no comparten su alcance global ni ningún otro dato con el entorno del script principal. En cambio, debes comunicarte con ellos enviando mensajes de ida y vuelta. Este código genera un worker que ejecuta ese script, le envía algunos mensajes y muestra las respuestas. 255 let squareWorker = new Worker("code/squareworker.js"); squareWorker.addEventListener("message", event => { console.log("El worker respondió:", event.data); }); squareWorker.postMessage(10); squareWorker.postMessage(24); La función postMessage envía un mensaje, lo que causará que se dispare un evento "message" en el receptor. El script que creó el worker envía y recibe mensajes a través del objeto Worker, mientras que el worker se comunica con el script que lo creó enviando y escuchando directamente en su alcance global. Solo se pueden enviar como mensajes valores que puedan representarse como JSON; el otro lado recibirá una copia de ellos en lugar del valor en sí mismo. Temporizadores Vimos la función setTimeout en el Capítulo 11. Programa otra función para que se llame más tarde, después de un cierto número de milisegundos. A veces necesitas cancelar una función que has programado. Esto se hace almacenando el valor devuelto por setTimeout y llamando a clearTimeout sobre él. let bombTimer = setTimeout(() => { console.log("¡BOOM!"); }, 500); if (Math.random() < 0.5) { // 50% de probabilidad console.log("Desactivado."); clearTimeout(bombTimer); } La función cancelAnimationFrame funciona de la misma manera que clearTimeout ; llamarla en un valor devuelto por requestAnimationFrame cancelará ese fo- tograma (si no se ha llamado ya). Un conjunto similar de funciones, setInterval y clearInterval, se utilizan para programar temporizadores que deben repetirse cada X milisegundos. let ticks = 0; let reloj = setInterval(() => { console.log("tic", ticks++); if (ticks == 10) { clearInterval(reloj); console.log("¡Detener!"); } 256 }, 200); Debouncing Algunos tipos de eventos pueden activarse rápidamente, muchas veces seguidas (como los eventos "mousemove" y "scroll", por ejemplo). Al manejar tales eventos, debes tener cuidado de no hacer nada que consuma demasiado tiempo, ya que tu controlador tomará tanto tiempo que la interacción con el documento comenzará a sentirse lenta. Si necesitas hacer algo importante en un controlador de este tipo, puedes usar setTimeout para asegurarte de que no lo estás haciendo con demasiada frecuencia. Esto suele llamarse debouncing el evento. Hay varios enfoques ligeramente diferentes para esto. En el primer ejemplo, queremos reaccionar cuando el usuario ha escrito algo, pero no queremos hacerlo inmediatamente para cada evento de entrada. Cuando están escribiendo rápidamente, solo queremos esperar hasta que ocurra una pausa. En lugar de realizar inmediatamente una acción en el controlador de eventos, establecemos un tiempo de espera. También limpiamos el tiempo de espera anterior (si existe) para que cuando los eventos ocurran cerca uno del otro (más cerca de nuestro retraso de tiempo de espera), el tiempo de espera del evento anterior se cancele. Escribe algo aquí... let textarea = document.querySelector("textarea"); let timeout; textarea.addEventListener("input", () => { clearTimeout(timeout); timeout = setTimeout(() => console.log("¡Escrito!"), 500); }); Dar un valor no definido a clearTimeout o llamarlo en un tiempo de espera que ya ha pasado no tiene efecto. Por lo tanto, no tenemos que tener cuidado de cuándo llamarlo, y simplemente lo hacemos para cada evento. Podemos usar un patrón ligeramente diferente si queremos espaciar las re- spuestas para que estén separadas por al menos una cierta longitud de tiempo, pero queremos activarlas durante una serie de eventos, no solo después. Por ejemplo, podríamos querer responder a eventos "mousemove" mostrando las co- ordenadas actuales del mouse pero solo cada 250 milisegundos. 257 let programado = null; window.addEventListener("mousemove", event => { if (!programado) { setTimeout(() => { document.body.textContent = `Ratón en ${programado.pageX}, ${programado.pageY}`; programado = null; }, 250); } programado = event; }); Resumen Los controladores de eventos hacen posible detectar y reaccionar a eventos que ocurren en nuestra página web. El método addEventListener se utiliza para registrar dicho controlador. Cada evento tiene un tipo ("keydown", "focus", y así sucesivamente) que lo identifica. La mayoría de los eventos se activan en un elemento DOM específico y luego se propagan a los ancestros de ese elemento, lo que permite que los controladores asociados a esos elementos los manejen. Cuando se llama a un controlador de eventos, se le pasa un objeto de evento con información adicional sobre el evento. Este objeto también tiene métodos que nos permiten detener una mayor propagación (stopPropagation) y evitar el manejo predeterminado del evento por parte del navegador (preventDefault). Presionar una tecla dispara eventos "keydown" y "keyup". Presionar un botón del mouse dispara eventos "mousedown", "mouseup" y "click". Mover el mouse dispara eventos "mousemove". La interacción con pantallas táctiles dará lugar a eventos "touchstart", "touchmove" y "touchend". El desplazamiento se puede detectar con el evento "scroll", y los cambios de enfoque se pueden detectar con los eventos "focus" y "blur". Cuando el documento ha terminado de cargarse, se activa un evento "load" en la ventana. Ejercicios Globo Escribe una página que muestre un globo (usando el emoji de globo, 🎈). Cuando presiones la flecha hacia arriba, debería inflarse (crecer) un 10 por 258 ciento, y cuando presiones la flecha hacia abajo, debería desinflarse (encoger) un 10 por ciento. Puedes controlar el tamaño del texto (los emoji son texto) estableciendo la propiedad CSS font-size (style.fontSize) en su elemento padre. Recuerda incluir una unidad en el valor, por ejemplo, píxeles (10px). Los nombres de las teclas de flecha son "ArrowUp" y "ArrowDown". Asegúrate de que las teclas cambien solo el globo, sin hacer scroll en la página. Cuando eso funcione, añade una característica en la que, si inflas el globo más allá de un cierto tamaño, explote. En este caso, explotar significa que se reemplace con un emoji de 💥, y el manejador de eventos se elimine (para que no se pueda inflar o desinflar la explosión). Estela del ratón En los primeros días de JavaScript, que fue la época dorada de las páginas de inicio estridentes con un montón de imágenes animadas, la gente ideó formas verdaderamente inspiradoras de usar el lenguaje. Una de estas era la estela del ratón —una serie de elementos que seguirían al puntero del ratón mientras lo movías por la página. En este ejercicio, quiero que implementes una estela del ratón. Utiliza ele- mentos con posición absoluta y un tamaño fijo y color de fondo (consulta el código en la sección de “Clics de ratón” para un ejemplo). Crea un montón de estos elementos y, al mover el ratón, muéstralos en la estela del puntero del ratón. Hay varias aproximaciones posibles aquí. Puedes hacer tu solución tan simple o tan compleja como desees. Una solución simple para empezar es mantener un número fijo de elementos de estela y recorrerlos, moviendo el siguiente a la posición actual del ratón cada vez que ocurra un evento "mousemove". Pestañas Los paneles con pestañas son ampliamente utilizados en interfaces de usuario. Te permiten seleccionar un panel de interfaz eligiendo entre varias pestañas que sobresalen por encima de un elemento. En este ejercicio debes implementar una interfaz de pestañas simple. Escribe una función, asTabs, que tome un nodo DOM y cree una interfaz de pestañas que muestre los elementos secundarios de ese nodo. Debería insertar una lista de elementos en la parte superior del nodo, uno por cada elemento secundario, conteniendo el texto recuperado del atributo data-tabname del hijo. Todos los hijos originales excepto uno deben estar ocultos (con un estilo display 259 de none). El nodo actualmente visible se puede seleccionar haciendo clic en los botones. Cuando funcione, extiéndelo para dar estilo al botón de la pestaña actual- mente seleccionada de manera diferente para que sea obvio cuál pestaña está seleccionada. 260 “Toda la realidad es un juego.” —Iain Banks, The Player of Games Chapter 16 Proyecto: Un juego de plataformas Gran parte de mi fascinación inicial con las computadoras, al igual que la de muchos niños nerds, tenía que ver con los juegos de computadora. Me sentía atraído por los diminutos mundos simulados que podía manipular y en los que se desarrollaban historias (más o menos), supongo, debido a la forma en que proyectaba mi imaginación en ellos más que por las posibilidades que realmente ofrecían. No le desearía a nadie una carrera en programación de juegos. Al igual que la industria de la música, la discrepancia entre la cantidad de jóvenes entusiastas que desean trabajar en ella y la demanda real de tales personas crea un entorno bastante insalubre. Pero escribir juegos por diversión resulta entretenido. Este capítulo guiará a través de la implementación de un pequeño juego de plataformas. Los juegos de plataformas (o juegos de “saltos y carreras”) son juegos que esperan que el jugador mueva una figura a través de un mundo, que generalmente es bidimensional y se ve desde el lado, mientras salta sobre y sobre cosas. El juego Nuestro juego estará basado aproximadamente en Dark Blue (www.lessmilk.com/ games/10) de Thomas Palef. Elegí ese juego porque es entretenido, minimalista y se puede construir sin mucho código. Se ve así: 261 La caja oscura representa al jugador, cuya tarea es recolectar las cajas amar- illas (monedas) evitando las cosas rojas (lava). Un nivel se completa cuando se han recolectado todas las monedas. El jugador puede moverse con las teclas de flecha izquierda y derecha y puede saltar con la tecla de flecha hacia arriba. Saltar es una especialidad de este personaje del juego. Puede alcanzar varias veces su altura y puede cambiar de dirección en el aire. Esto puede no ser del todo realista, pero ayuda a darle al jugador la sensación de tener un control directo sobre el avatar en pantalla. El juego consiste en un fondo estático, dispuesto como una rejilla, con los elementos móviles superpuestos en ese fondo. Cada campo en la rejilla está vacío, sólido o es lava. Los elementos móviles son el jugador, las monedas y ciertas piezas de lava. Las posiciones de estos elementos no están restringidas a la rejilla: sus coordenadas pueden ser fraccionarias, permitiendo un movimiento suave. La tecnología Usaremos el DOM del navegador para mostrar el juego y leeremos la entrada del usuario manejando eventos de teclado. El código relacionado con la pantalla y el teclado es solo una pequeña parte del trabajo que necesitamos hacer para construir este juego. Dado que todo se ve como cajas de colores, dibujar es sencillo: creamos elementos del DOM y usamos estilos para darles un color de fondo, tamaño y posición. Podemos representar el fondo como una tabla ya que es una cuadrícula in- mutable de cuadrados. Los elementos de movimiento libre se pueden super- poner utilizando elementos posicionados absolutamente. En juegos y otros programas que deben animar gráficos y responder a la en- trada del usuario sin retrasos notables, la eficiencia es importante. Aunque el DOM no fue diseñado originalmente para gráficos de alto rendimiento, en real- idad es mejor en esto de lo que podrías esperar. Viste algunas animaciones en el Capítulo 14. En una máquina moderna, un juego simple como este funciona bien, incluso si no nos preocupamos mucho por la optimización. En el próximo capítulo, exploraremos otra tecnología del navegador, la eti- queta , que proporciona una forma más tradicional de dibujar gráficos, trabajando en términos de formas y píxeles en lugar de elementos del DOM. 262 Niveles Queremos una forma legible y editable por humanos para especificar niveles. Dado que está bien que todo comience en una cuadrícula, podríamos usar cadenas grandes en las que cada carácter represente un elemento, ya sea una parte de la cuadrícula de fondo o un elemento móvil. El plan para un nivel pequeño podría verse así: let simpleLevelPlan = `........................#................#....#..............=.#....#.........o.o....#....#.@......#####...#....#####............#........#++++++++++++#........##############........................`; Los puntos representan un espacio vacío, los caracteres de almohadilla (#) son paredes y los signos más son lava. La posición inicial del jugador es el signo de arroba (@). Cada carácter O es una moneda, y el signo igual (=) en la parte superior es un bloque de lava que se mueve de un lado a otro horizontalmente. Además de las dos formas adicionales de lava en movimiento, el carácter de tubería (|) crea blobs que se mueven verticalmente, y v indica lava goteante: lava que se mueve verticalmente y no rebota de un lado a otro, solo se mueve hacia abajo, volviendo a su posición de inicio cuando golpea el suelo. Un juego completo consta de varios niveles que el jugador debe completar. Un nivel se completa cuando se han recolectado todas las monedas. Si el jugador toca la lava, el nivel actual se restablece a su posición inicial y el jugador puede intentarlo de nuevo. Leyendo un nivel La siguiente clase almacena un objeto nivel. Su argumento debe ser la cadena que define el nivel. class Level { constructor(plan) { let rows = plan.trim().split("\n").map(l => [...l]); this.height = rows.length; this.width = rows.length; this.startActors = []; 263 this.rows = rows.map((row, y) => { return row.map((ch, x) => { let type = levelChars[ch]; if (typeof type != "string") { let pos = new Vec(x, y); this.startActors.push(type.create(pos, ch)); type = "empty"; } return type; }); }); } } El método trim se utiliza para eliminar los espacios en blanco al principio y al final de la cadena de plan. Esto permite que nuestro plan de ejemplo comience con una nueva línea para que todas las líneas estén directamente debajo unas de otras. La cadena restante se divide en líneas en caracteres de nueva línea, y cada línea se convierte en un array, produciendo arrays de caracteres. Entonces, rows contiene un array de arrays de caracteres, las filas del plan. Podemos derivar el ancho y alto del nivel a partir de estos. Pero aún debemos separar los elementos móviles de la cuadrícula de fondo. Llamaremos a los elementos móviles actores. Se almacenarán en un array de objetos. El fondo será un array de arrays de cadenas, que contienen tipos de campo como "empty", "wall", o "lava". Para crear estos arrays, mapeamos sobre las filas y luego sobre su contenido. Recuerda que map pasa el índice del array como segundo argumento a la función de mapeo, lo que nos indica las coordenadas x e y de un carácter dado. Las posiciones en el juego se almacenarán como pares de coordenadas, siendo la esquina superior izquierda 0,0 y cada cuadro de fondo siendo de 1 unidad de alto y ancho. Para interpretar los caracteres en el plan, el constructor de Level utiliza el objeto levelChars, que, para cada carácter utilizado en las descripciones de niveles, contiene una cadena si es un tipo de fondo, y una clase si produce un actor. Cuando type es una clase de actor, se utiliza su método estático create para crear un objeto, que se agrega a startActors, y la función de mapeo devuelve "empty" para este cuadro de fondo. La posición del actor se almacena como un objeto Vec. Este es un vector bidimensional, un objeto con propiedades x e y, como se ve en los ejercicios del Capítulo 6. A medida que el juego avanza, los actores terminarán en lugares diferentes o 264 incluso desaparecerán por completo (como hacen las monedas cuando se reco- gen). Utilizaremos una clase State para seguir el estado de un juego en ejecu- ción. class State { constructor(level, actors, status) { this.level = level; this.actors = actors; this.status = status; } static start(level) { return new State(level, level.startActors, "playing"); } get player() { return this.actors.find(a => a.type == "player"); } } La propiedad status cambiará a "lost" o "won" cuando el juego haya termi- nado. Este es nuevamente una estructura de datos persistente: actualizar el estado del juego crea un nuevo estado y deja intacto el anterior. Actores Los objetos de actores representan la posición actual y el estado de un elemento móvil dado en nuestro juego. Todos los objetos de actores se ajustan a la misma interfaz. Tienen las propiedades size y pos que contienen el tamaño y las coordenadas de la esquina superior izquierda del rectángulo que representa a este actor. Luego tienen un método update, que se utiliza para calcular su nuevo estado y posición después de un paso de tiempo dado. Simula la acción que realiza el actor: moverse en respuesta a las teclas de flecha para el jugador y rebotar de un lado a otro para la lava, y devuelve un nuevo objeto de actor actualizado. Una propiedad type contiene una cadena que identifica el tipo de actor: " player", "coin" o "lava". Esto es útil al dibujar el juego: la apariencia del rectángulo dibujado para un actor se basa en su tipo. Las clases de actores tienen un método estático create que es utilizado por el constructor Level para crear un actor a partir de un carácter en el plan de nivel. Recibe las coordenadas del carácter y el carácter en sí, que es necesario 265 porque la clase Lava maneja varios caracteres diferentes. Esta es la clase Vec que usaremos para nuestros valores bidimensionales, como la posición y tamaño de los actores. class Vec { constructor(x, y) { this.x = x; this.y = y; } plus(other) { return new Vec(this.x + other.x, this.y + other.y); } times(factor) { return new Vec(this.x * factor, this.y * factor); } } El método times escala un vector por un número dado. Será útil cuando necesitemos multiplicar un vector de velocidad por un intervalo de tiempo para obtener la distancia recorrida durante ese tiempo. Los diferentes tipos de actores tienen sus propias clases debido a que su comportamiento es muy diferente. Definamos estas clases. Llegaremos a sus métodos update más adelante. La clase Player tiene una propiedad speed que almacena su velocidad actual para simular el impulso y la gravedad. class Player { constructor(pos, speed) { this.pos = pos; this.speed = speed; } get type() { return "player"; } static create(pos) { return new Player(pos.plus(new Vec(0, -0.5)), new Vec(0, 0)); } } Player.prototype.size = new Vec(0.8, 1.5); Dado que un jugador tiene una altura de un cuadro y medio, su posición inicial se establece medio cuadro por encima de la posición donde apareció el carácter @. De esta manera, su parte inferior se alinea con la parte inferior del cuadro en el que apareció. 266 La propiedad size es la misma para todas las instancias de Player, por lo que la almacenamos en el prototipo en lugar de en las propias instancias. Podríamos haber utilizado un getter como type, pero eso crearía y devolvería un nuevo objeto Vec cada vez que se lee la propiedad, lo cual sería derrochador. (Las cadenas, al ser inmutables, no tienen que ser recreadas cada vez que se evalúan). Al construir un actor Lava, necesitamos inicializar el objeto de manera difer- ente dependiendo del personaje en el que se base. La lava dinámica se mueve a lo largo de su velocidad actual hasta que choca con un obstáculo. En ese momento, si tiene una propiedad de reset, saltará de nuevo a su posición de inicio (goteando). Si no la tiene, invertirá su velocidad y continuará en la otra dirección (rebotando). El método create mira el carácter que pasa el constructor de Level y crea el actor de lava apropiado. class Lava { constructor(pos, speed, reset) { this.pos = pos; this.speed = speed; this.reset = reset; } get type() { return "lava"; } static create(pos, ch) { if (ch == "=") { return new Lava(pos, new Vec(2, 0)); } else if (ch == "|") { return new Lava(pos, new Vec(0, 2)); } else if (ch == "v") { return new Lava(pos, new Vec(0, 3), pos); } } } Lava.prototype.size = new Vec(1, 1); Los actores Coin son relativamente simples. Mayoritariamente solo se quedan en su lugar. Pero para animar un poco el juego, se les da un “balanceo”, un ligero movimiento vertical de ida y vuelta. Para hacer un seguimiento de esto, un objeto moneda almacena una posición base y también una propiedad de wobble que sigue la fase del movimiento de balanceo. Juntos, estos determinan la posición real de la moneda (almacenada en la propiedad pos). 267 class Coin { constructor(pos, basePos, wobble) { this.pos = pos; this.basePos = basePos; this.wobble = wobble; } get type() { return "coin"; } static create(pos) { let basePos = pos.plus(new Vec(0.2, 0.1)); return new Coin(basePos, basePos, Math.random() * Math.PI * 2); } } Coin.prototype.size = new Vec(0.6, 0.6); En Capítulo 14, vimos que Math.sin nos da la coordenada y de un punto en un círculo. Esa coordenada va de ida y vuelta en una forma de onda suave a medida que nos movemos a lo largo del círculo, lo que hace que la función seno sea útil para modelar un movimiento ondulado. Para evitar una situación en la que todas las monedas se mueven hacia arriba y hacia abajo sincrónicamente, la fase inicial de cada moneda se aleatoriza. El periodo de la onda de Math.sin, el ancho de una onda que produce, es 2π. Multiplicamos el valor devuelto por Math.random por ese número para darle a la moneda una posición inicial aleatoria en la onda. Ahora podemos definir el objeto levelChars que mapea caracteres del plano a tipos de cuadrícula de fondo o clases de actor. const levelChars = { ".": "empty", "#": "wall", "+": "lava", "@": Player, "o": Coin, "=": Lava, "|": Lava, "v": Lava }; Esto nos brinda todas las partes necesarias para crear una instancia de Level. let simpleLevel = new Level(simpleLevelPlan); console.log(`${simpleLevel.width} by ${simpleLevel.height}`); // → 22 by 9 La tarea por delante es mostrar esos niveles en pantalla y modelar el tiempo y movimiento dentro de ellos. 268 Dibujo En el próximo capítulo, mostraremos el mismo juego de una manera diferente. Para hacerlo posible, colocamos la lógica de dibujo detrás de una interfaz y la pasamos al juego como argumento. De esta manera, podemos usar el mismo programa de juego con diferentes nuevos módulos de visualización. Un objeto de visualización de juego dibuja un nivel y estado dados. Pasamos su constructor al juego para permitir que sea reemplazado. La clase de vi- sualización que definimos en este capítulo se llama DOMDisplay porque utiliza elementos del DOM para mostrar el nivel. Utilizaremos una hoja de estilo para establecer los colores reales y otras propiedades fijas de los elementos que conforman el juego. También sería posi- ble asignar directamente a la propiedad style de los elementos al crearlos, pero eso produciría programas más verbosos. La siguiente función auxiliar proporciona una forma concisa de crear un elemento y darle algunos atributos y nodos secundarios: function elt(nombre, attrs,...children) { let dom = document.createElement(nombre); for (let attr of Object.keys(attrs)) { dom.setAttribute(attr, attrs[attr]); } for (let child of children) { dom.appendChild(child); } return dom; } Una visualización se crea dándole un elemento padre al que debe adjuntarse y un objeto de nivel. class DOMDisplay { constructor(padre, nivel) { this.dom = elt("div", {class: "game"}, dibujarGrid(nivel)); this.actorLayer = null; padre.appendChild(this.dom); } clear() { this.dom.remove(); } } La cuadrícula de fondo del nivel, que nunca cambia, se dibuja una vez. Los actores se vuelven a dibujar cada vez que se actualiza la visualización con un estado dado. La propiedad actorLayer se utilizará para realizar un seguimiento 269 del elemento que contiene a los actores para que puedan ser fácilmente elimi- nados y reemplazados. Nuestras coordenadas y tamaños se rastrean en unidades de cuadrícula, donde un tamaño o distancia de 1 significa un bloque de cuadrícula. Al es- tablecer tamaños de píxeles, tendremos que escalar estas coordenadas: todo en el juego sería ridículamente pequeño con un solo píxel por cuadrado. La con- stante scale indica el número de píxeles que una unidad ocupa en la pantalla. const escala = 20; function dibujarGrid(nivel) { return elt("table", { class: "background", style: `width: ${nivel.width * escala}px` },...nivel.rows.map(fila => elt("tr", {style: `height: ${escala}px`},...fila.map(tipo => elt("td", {class: tipo}))) )); } El elemento se corresponde bien con la estructura de la propiedad rows del nivel: cada fila de la cuadrícula se convierte en una fila de tabla (). Las cadenas en la cuadrícula se usan como nombres de clase para los elementos de celda de tabla (). El código utiliza el operador de propagación (triple punto) para pasar matrices de nodos secundarios a elt como argumentos separados.El siguiente CSS hace que la tabla se vea como el fondo que queremos:.background { background: rgb(52, 166, 251); table-layout: fixed; border-spacing: 0; }.background td { padding: 0; }.lava { background: rgb(255, 100, 100); }.wall { background: white; } Algunos de estos (table-layout, border-spacing y padding) se utilizan para suprimir comportamientos predeterminados no deseados. No queremos que el diseño de la tabla dependa del contenido de sus celdas, ni queremos espacio entre las celdas de la tabla o relleno dentro de ellas. La regla background establece el color de fondo. CSS permite que los colores se especifiquen tanto como palabras (white) como con un formato como rgb(R , G, B), donde los componentes rojo, verde y azul del color se separan en tres números de 0 a 255. Por lo tanto, en rgb(52, 166, 251), el componente rojo es 52, el verde es 166 y el azul es 251. Dado que el componente azul es el más 270 grande, el color resultante será azulado. En la regla.lava, el primer número (rojo) es el más grande. Dibujamos cada actor creando un elemento DOM para él y estableciendo la posición y el tamaño de ese elemento en función de las propiedades del actor. Los valores tienen que ser multiplicados por scale para pasar de unidades de juego a píxeles. function drawActors(actors) { return elt("div", {},...actors.map(actor => { let rect = elt("div", {class: `actor ${actor.type}`}); rect.style.width = `${actor.size.x * scale}px`; rect.style.height = `${actor.size.y * scale}px`; rect.style.left = `${actor.pos.x * scale}px`; rect.style.top = `${actor.pos.y * scale}px`; return rect; })); } Para agregar más de una clase a un elemento, separamos los nombres de las clases por espacios. En el siguiente código CSS mostrado a continuación, la clase actor da a los actores su posición absoluta. El nombre de su tipo se utiliza como una clase adicional para darles un color. No tenemos que definir la clase lava de nuevo porque estamos reutilizando la clase para las casillas de lava de la cuadrícula que definimos anteriormente..actor { position: absolute; }.coin { background: rgb(241, 229, 89); }.player { background: rgb(64, 64, 64); } El método syncState se utiliza para que la pantalla muestre un estado dado. Primero elimina los gráficos de actores antiguos, si los hay, y luego vuelve a dibujar los actores en sus nuevas posiciones. Puede ser tentador intentar reutilizar los elementos DOM para actores, pero para que eso funcione, nece- sitaríamos mucho más trabajo adicional para asociar actores con elementos DOM y asegurarnos de que eliminamos elementos cuando sus actores desa- parecen. Dado que típicamente habrá solo un puñado de actores en el juego, volver a dibujar todos ellos no es costoso. DOMDisplay.prototype.syncState = function(state) { if (this.actorLayer) this.actorLayer.remove(); this.actorLayer = drawActors(state.actors); this.dom.appendChild(this.actorLayer); this.dom.className = `game ${state.status}`; this.scrollPlayerIntoView(state); 271 }; Al agregar el estado actual del nivel como nombre de clase al contenedor, podemos estilizar ligeramente al actor del jugador cuando el juego se gana o se pierde, añadiendo una regla CSS que tenga efecto solo cuando el jugador tiene un elemento ancestro con una clase específica..lost.player { background: rgb(160, 64, 64); }.won.player { box-shadow: -4px -7px 8px white, 4px -7px 8px white; } Después de tocar la lava, el color del jugador se vuelve rojo oscuro, sugiriendo quemaduras. Cuando se ha recolectado la última moneda, agregamos dos som- bras blancas difuminadas, una en la parte superior izquierda y otra en la parte superior derecha, para crear un efecto de halo blanco. No podemos asumir que el nivel siempre encaja en el viewport – el el- emento en el que dibujamos el juego. Por eso es necesaria la llamada a scrollPlayerIntoView. Se asegura de que si el nivel sobresale del viewport, desplacemos ese viewport para asegurar que el jugador esté cerca de su centro. El siguiente CSS le da al elemento DOM contenedor del juego un tamaño máx- imo y asegura que cualquier cosa que sobresalga de la caja del elemento no sea visible. También le damos una posición relativa para que los actores dentro de él estén posicionados de manera relativa a la esquina superior izquierda del nivel..game { overflow: hidden; max-width: 600px; max-height: 450px; position: relative; } En el método scrollPlayerIntoView, encontramos la posición del jugador y actualizamos la posición de desplazamiento del elemento contenedor. Cambi- amos la posición de desplazamiento manipulando las propiedades scrollLeft y scrollTop de ese elemento cuando el jugador está demasiado cerca del borde. DOMDisplay.prototype.scrollPlayerIntoView = function(state) { let width = this.dom.clientWidth; let height = this.dom.clientHeight; let margin = width / 3; 272 // El viewport let left = this.dom.scrollLeft, right = left + width; let top = this.dom.scrollTop, bottom = top + height; let player = state.player; let center = player.pos.plus(player.size.times(0.5)).times(scale); if (center.x < left + margin) { this.dom.scrollLeft = center.x - margin; } else if (center.x > right - margin) { this.dom.scrollLeft = center.x + margin - width; } if (center.y < top + margin) { this.dom.scrollTop = center.y - margin; } else if (center.y > bottom - margin) { this.dom.scrollTop = center.y + margin - height; } }; La forma en que se encuentra el centro del jugador muestra cómo los métodos en nuestro tipo Vec permiten que los cálculos con objetos se escriban de una manera relativamente legible. Para encontrar el centro del actor, sumamos su posición (esquina superior izquierda) y la mitad de su tamaño. Ese es el centro en coordenadas de nivel, pero lo necesitamos en coordenadas de píxeles, así que luego multiplicamos el vector resultante por nuestra escala de visualización. A continuación, una serie de comprobaciones verifica que la posición del jugador no esté fuera del rango permitido. Ten en cuenta que a veces esto establecerá coordenadas de desplazamiento sin sentido que están por debajo de cero o más allá del área de