Creación de una galería de imágenes con PixiJS y WebGL | Programar Plus

A veces, tenemos que ir un poco más allá de HTML, CSS y JavaScript para crear la interfaz de usuario que necesitamos y, en su lugar, usar otros recursos, como SVG, WebGL, canvas y otros.

Por ejemplo, los efectos más sorprendentes se pueden crear con WebGL, porque es una API de JavaScript diseñada para generar gráficos 2D y 3D interactivos dentro de cualquier navegador web compatible, lo que permite el procesamiento de imágenes acelerado por GPU.

Dicho esto, trabajar con WebGL puede ser muy complejo. Como tal, hay una variedad de bibliotecas que lo hacen relativamente más fácil, como PixiJS, Three.js y Babylon.js, entre otras. Vamos a trabajar con uno específico de esos, PixiJS, para crear una galería de imágenes aleatorias inspiradas en este fragmento de una toma de Dribbble de Zhenya Rynzhuk.

Esto parece difícil, pero en realidad no necesita tener un conocimiento avanzado de WebGL o incluso PixiJS para seguirlo, aunque algunos conocimientos básicos de Javascript (ES6) serán útiles. Es posible que incluso desee comenzar por familiarizarse con el concepto básico de sombreadores de fragmentos que se utilizan en WebGL, con The Book of Shaders como un buen punto de partida.

Con eso, profundicemos en el uso de PixiJS para crear este efecto WebGL.

Configuración inicial

Esto es lo que necesitaremos para empezar:

  1. Agregue la biblioteca PixiJS como un script en el HTML.
  2. Tener un <canvas> (o agréguelo dinámicamente desde Javascript), para representar la aplicación.
  3. Inicializar la aplicación con new PIXI.Application(options).

Mira, nada demasiado loco todavía. Aquí está el JavaScript que podemos usar como repetitivo:

// Get canvas view
const view = document.querySelector('.view')
let width, height, app

// Set dimensions
function initDimensions () {
  width = window.innerWidth
  height = window.innerHeight
}

// Init the PixiJS Application
function initApp () {
  // Create a PixiJS Application, using the view (canvas) provided
  app = new PIXI.Application({ view })
  // Resizes renderer view in CSS pixels to allow for resolutions other than 1
  app.renderer.autoDensity = true
  // Resize the view to match viewport dimensions
  app.renderer.resize(width, height)
}

// Init everything
function init () {
  initDimensions()
  initApp()
}

// Initial call
init()

Al ejecutar este código lo único que veremos es una pantalla negra y un mensaje como este en el si abrimos la consola:
PixiJS 5.0.2 - WebGL 2 - http://www.pixijs.com/.

¡Estamos listos para comenzar a dibujar en el lienzo usando PixiJS y WebGL!

Crear el fondo de la cuadrícula con un Shader WebGL

A continuación, crearemos un fondo que contenga una cuadrícula, lo que nos permitirá visualizar claramente el efecto de distorsión que buscamos. Pero primero, debemos saber qué es un sombreador y cómo funciona. Recomendé The Book of Shaders anteriormente como punto de partida para aprender sobre ellos y estos conceptos entrarán en juego. Si aún no lo ha hecho, le recomiendo encarecidamente que revise ese material, y solo entonces continúe aquí.

Vamos a crear un sombreador de fragmentos que imprima un fondo de cuadrícula en la pantalla:

// It is required to set the float precision for fragment shaders in OpenGL ES
// More info here: https://stackoverflow.com/a/28540641/4908989
#ifdef GL_ES
precision mediump float;
#endif

// This function returns 1 if `coord` correspond to a grid line, 0 otherwise
float isGridLine (vec2 coord) {
  vec2 pixelsPerGrid = vec2(50.0, 50.0);
  vec2 gridCoords = fract(coord / pixelsPerGrid);
  vec2 gridPixelCoords = gridCoords * pixelsPerGrid;
  vec2 gridLine = step(gridPixelCoords, vec2(1.0));
  float isGridLine = max(gridLine.x, gridLine.y);
  return isGridLine;
}

// Main function
void main () {
  // Coordinates for the current pixel
  vec2 coord = gl_FragCoord.xy;
  // Set `color` to black
  vec3 color = vec3(0.0);
  // If it is a grid line, change blue channel to 0.3
  color.b = isGridLine(coord) * 0.3;
  // Assing the final rgba color to `gl_FragColor`
  gl_FragColor = vec4(color, 1.0);
}

Este código se extrae de una demostración en Shadertoy, que es una gran fuente de inspiración y recursos para sombreadores.

Para usar este sombreador, primero debemos cargar el código del archivo en el que se encuentra y, solo después de que se haya cargado correctamente, inicializaremos la aplicación.

// Loaded resources will be here
const resources = PIXI.Loader.shared.resources

// Load resources, then init the app
PIXI.Loader.shared.add([
  'shaders/backgroundFragment.glsl'
]).load(init)

Ahora, para que nuestro sombreador funcione donde podemos ver el resultado, agregaremos un nuevo elemento (un Sprite) al escenario, que usaremos para definir un filtro. Esta es la forma en que PixiJS nos permite ejecutar sombreadores personalizados como el que acabamos de crear.

// Init the gridded background
function initBackground () {
  // Create a new empty Sprite and define its size
  background = new PIXI.Sprite()
  background.width = width
  background.height = height
  // Get the code for the fragment shader from the loaded resources
  const backgroundFragmentShader = resources['shaders/backgroundFragment.glsl'].data
  // Create a new Filter using the fragment shader
  // We don't need a custom vertex shader, so we set it as `undefined`
  const backgroundFilter = new PIXI.Filter(undefined, backgroundFragmentShader)
  // Assign the filter to the background Sprite
  background.filters = [backgroundFilter]
  // Add the background to the stage
  app.stage.addChild(background)
}

Y ahora vemos el fondo cuadriculado con líneas azules. Mire de cerca porque las líneas son un poco débiles contra el color de fondo oscuro.

El efecto de distorsión

Nuestro fondo ya está listo, así que veamos cómo podemos agregar el efecto deseado (distorsión de lente cúbica) a todo el escenario, incluido el fondo y cualquier otro elemento que agreguemos más adelante, como imágenes. Para esto, necesitamos crear un nuevo filtro y agregarlo al escenario. ¡Sí, también podemos definir filtros que afectan a todo el escenario de PixiJS!

Esta vez, hemos basado el código de nuestro sombreador en esta increíble demostración de Shadertoy que implementa el efecto de distorsión usando diferentes parámetros configurables.

#ifdef GL_ES
precision mediump float;
#endif

// Uniforms from Javascript
uniform vec2 uResolution;
uniform float uPointerDown;

// The texture is defined by PixiJS
varying vec2 vTextureCoord;
uniform sampler2D uSampler;

// Function used to get the distortion effect
vec2 computeUV (vec2 uv, float k, float kcube) {
  vec2 t = uv - 0.5;
  float r2 = t.x * t.x + t.y * t.y;
  float f = 0.0;
  if (kcube == 0.0) {
    f = 1.0 + r2 * k;
  } else {
    f = 1.0 + r2 * (k + kcube * sqrt(r2));
  }
  vec2 nUv = f * t + 0.5;
  nUv.y = 1.0 - nUv.y;
  return nUv;
}

void main () {
  // Normalized coordinates
  vec2 uv = gl_FragCoord.xy / uResolution.xy;

  // Settings for the effect
  // Multiplied by `uPointerDown`, a value between 0 and 1
  float k = -1.0 * uPointerDown;
  float kcube = 0.5 * uPointerDown;
  float offset = 0.02 * uPointerDown;
  
  // Get each channel's color using the texture provided by PixiJS
  // and the `computeUV` function
  float red = texture2D(uSampler, computeUV(uv, k + offset, kcube)).r;
  float green = texture2D(uSampler, computeUV(uv, k, kcube)).g;
  float blue = texture2D(uSampler, computeUV(uv, k - offset, kcube)).b;
  
  // Assing the final rgba color to `gl_FragColor`
  gl_FragColor = vec4(red, green, blue, 1.0);
}

Estamos usando dos uniformes esta vez. Los uniformes son variables que pasamos al sombreador a través de JavaScript:

  • uResolution: Este es un objeto JavaScript que incluye {x: width, y: height}. Este uniforme nos permite normalizar las coordenadas de cada píxel en el rango [0, 1].
  • uPointerDown: Este es un flotador en el rango [0, 1], que nos permite animar el efecto de distorsión, aumentando su intensidad proporcionalmente.

Veamos el código que tenemos que añadir a nuestro JavaScript para ver el efecto de distorsión que provoca nuestro nuevo shader:

// Target for pointer. If down, value is 1, else value is 0
// Here we set it to 1 to see the effect, but initially it will be 0
let pointerDownTarget = 1
let uniforms

// Set initial values for uniforms
function initUniforms () {
  uniforms = {
    uResolution: new PIXI.Point(width, height),
    uPointerDown: pointerDownTarget
  }
}

// Set the distortion filter for the entire stage
const stageFragmentShader = resources['shaders/stageFragment.glsl'].data
const stageFilter = new PIXI.Filter(undefined, stageFragmentShader, uniforms)
app.stage.filters = [stageFilter]

¡Ya podemos disfrutar de nuestro efecto de distorsión!

Este efecto es estático en este momento, por lo que todavía no es muy divertido. A continuación, veremos cómo podemos hacer que el efecto responda dinámicamente a eventos de puntero.

Escuchar eventos de puntero

PixiJS hace que sea sorprendentemente simple escuchar eventos, incluso múltiples eventos que responden por igual a las interacciones táctiles y del mouse. En este caso, queremos que nuestra animación funcione igual de bien en un escritorio que en un dispositivo móvil, por lo que debemos escuchar los eventos correspondientes a ambas plataformas.

PixiJs proporciona una interactive atributo que nos permite hacer precisamente eso. Lo aplicamos a un elemento y comenzamos a escuchar eventos con una API similar a jQuery:

// Start listening events
function initEvents () {
  // Make stage interactive, so it can listen to events
  app.stage.interactive = true

  // Pointer & touch events are normalized into
  // the `pointer*` events for handling different events
  app.stage
    .on('pointerdown', onPointerDown)
    .on('pointerup', onPointerUp)
    .on('pointerupoutside', onPointerUp)
    .on('pointermove', onPointerMove)
}

A partir de aquí, comenzaremos a usar un tercer uniforme (uPointerDiff), que nos permitirá explorar la galería de imágenes mediante arrastrar y soltar. Su valor será igual a la traslación de la escena a medida que exploramos la galería. A continuación se muestra el código correspondiente a cada una de las funciones de manejo de eventos:

// On pointer down, save coordinates and set pointerDownTarget
function onPointerDown (e) {
  console.log('down')
  const { x, y } = e.data.global
  pointerDownTarget = 1
  pointerStart.set(x, y)
  pointerDiffStart = uniforms.uPointerDiff.clone()
}

// On pointer up, set pointerDownTarget
function onPointerUp () {
  console.log('up')
  pointerDownTarget = 0
}

// On pointer move, calculate coordinates diff
function onPointerMove (e) {
  const { x, y } = e.data.global
  if (pointerDownTarget) {
    console.log('dragging')
    diffX = pointerDiffStart.x + (x - pointerStart.x)
    diffY = pointerDiffStart.y + (y - pointerStart.y)
  }
}

Todavía no veremos ninguna animación si miramos nuestro trabajo, pero podemos empezar a ver cómo los mensajes que hemos definido en cada función de controlador de eventos se imprimen correctamente en la consola.

¡Pasemos ahora a implementar nuestras animaciones!

Animación del efecto de distorsión y la funcionalidad de arrastrar y soltar

Lo primero que necesitamos para iniciar una animación con PixiJS (o cualquier animación basada en lienzo) es un bucle de animación. Por lo general, consiste en una función que se llama continuamente, usando requestAnimationFrame, que en cada llamada representa los gráficos en el elemento de lienzo, produciendo así la animación deseada.

Podemos implementar nuestro propio bucle de animación en PixiJS, o podemos usar las utilidades incluidas en la biblioteca. En este caso, usaremos el método add de app.ticker, que nos permite pasar una función que se ejecutará en cada fotograma. Al final de la función init agregaremos esto:

// Animation loop
// Code here will be executed on every animation frame
app.ticker.add(() => {
  // Multiply the values by a coefficient to get a smooth animation
  uniforms.uPointerDown += (pointerDownTarget - uniforms.uPointerDown) * 0.075
  uniforms.uPointerDiff.x += (diffX - uniforms.uPointerDiff.x) * 0.2
  uniforms.uPointerDiff.y += (diffY - uniforms.uPointerDiff.y) * 0.2
})

Mientras tanto, en el constructor de filtros para el fondo, pasaremos los uniformes en el filtro de escenario. Esto nos permite simular el efecto de traducción del fondo con esta pequeña modificación en el shader correspondiente:

uniform vec2 uPointerDiff;

void main () {
  // Coordinates minus the `uPointerDiff` value
  vec2 coord = gl_FragCoord.xy - uPointerDiff;

  // ... more code here ...
}

Y ahora podemos ver el efecto de distorsión en acción, incluida la funcionalidad de arrastrar y soltar para el fondo de cuadrícula. ¡Juega con ello!

Genera aleatoriamente un diseño de cuadrícula de mampostería

Para hacer que nuestra interfaz de usuario sea más interesante, podemos generar aleatoriamente el tamaño y las dimensiones de las celdas de la cuadrícula. Es decir, cada imagen puede tener diferentes dimensiones, creando una especie de diseño de mampostería.

Usemos Unsplash Source, que nos permitirá obtener imágenes aleatorias de Unsplash y definir las dimensiones que queremos. Esto facilitará la tarea de crear un diseño de mampostería aleatorio, ya que las imágenes pueden tener la dimensión que queramos y, por lo tanto, generar el diseño de antemano.

Para lograr esto, utilizaremos un algoritmo que ejecuta los siguientes pasos:

  1. Comenzaremos con una lista de rectángulos.
  2. Seleccionaremos el primer rectángulo de la lista y lo dividiremos en dos rectángulos de dimensiones aleatorias, siempre que ambos rectángulos tengan dimensiones iguales o superiores al límite mínimo establecido. Agregaremos una verificación para asegurarnos de que sea posible y, si lo es, agregaremos los dos rectángulos resultantes a la lista.
  3. Si la lista está vacía, terminaremos de ejecutar. Si no, volveremos al paso dos.

Creo que comprenderá mucho mejor cómo funciona el algoritmo en esta próxima demostración. Usa los botones para ver cómo funciona: próximo ejecutará el paso dos, Todo ejecutará todo el algoritmo, y Reiniciar se restablecerá al paso uno.

Dibujar rectángulos sólidos

Ahora que podemos generar correctamente nuestro diseño de cuadrícula aleatoria, usaremos la lista de rectángulos generados por el algoritmo para dibujar rectángulos sólidos en nuestra aplicación PixiJS. De esa manera, podemos ver si funciona y hacer ajustes antes de agregar las imágenes usando la API de Unsplash Source.

Para dibujar esos rectángulos, generaremos un diseño de cuadrícula aleatorio que sea cinco veces más grande que la ventana gráfica y lo colocaremos en el centro del escenario. Eso nos permite movernos con cierta libertad en cualquier dirección de la galería.

// Variables and settings for grid
const gridSize = 50
const gridMin = 3
let gridColumnsCount, gridRowsCount, gridColumns, gridRows, grid
let widthRest, heightRest, centerX, centerY, rects

// Initialize the random grid layout
function initGrid () {
  // Getting columns
  gridColumnsCount = Math.ceil(width / gridSize)
  // Getting rows
  gridRowsCount = Math.ceil(height / gridSize)
  // Make the grid 5 times bigger than viewport
  gridColumns = gridColumnsCount * 5
  gridRows = gridRowsCount * 5
  // Create a new Grid instance with our settings
  grid = new Grid(gridSize, gridColumns, gridRows, gridMin)
  // Calculate the center position for the grid in the viewport
  widthRest = Math.ceil(gridColumnsCount * gridSize - width)
  heightRest = Math.ceil(gridRowsCount * gridSize - height)
  centerX = (gridColumns * gridSize / 2) - (gridColumnsCount * gridSize / 2)
  centerY = (gridRows * gridSize / 2) - (gridRowsCount * gridSize / 2)
  // Generate the list of rects
  rects = grid.generateRects()
}

Hasta ahora, hemos generado la lista de rectángulos. Para añadirlas al escenario conviene crear un contenedor, ya que así podemos añadir las imágenes al mismo contenedor y facilitar el movimiento cuando arrastramos la galería.

Crear un contenedor en PixiJS es así:

let container

// Initialize a Container element for solid rectangles and images
function initContainer () {
  container = new PIXI.Container()
  app.stage.addChild(container)
}

Ahora podemos agregar los rectángulos al contenedor para que puedan mostrarse en la pantalla.

// Padding for rects and images
const imagePadding = 20

// Add solid rectangles and images
// So far, we will only add rectangles
function initRectsAndImages () {
  // Create a new Graphics element to draw solid rectangles
  const graphics = new PIXI.Graphics()
  // Select the color for rectangles
  graphics.beginFill(0xAA22CC)
  // Loop over each rect in the list
  rects.forEach(rect => {
    // Draw the rectangle
    graphics.drawRect(
      rect.x * gridSize,
      rect.y * gridSize,
      rect.w * gridSize - imagePadding,
      rect.h * gridSize - imagePadding
    )
  })
  // Ends the fill action
  graphics.endFill()
  // Add the graphics (with all drawn rects) to the container
  container.addChild(graphics)
}

Tenga en cuenta que hemos agregado a los cálculos un relleno (imagePadding) para cada rectángulo. De esta forma las imágenes tendrán algo de espacio entre ellas.

Finalmente, en el bucle de animación, debemos agregar el siguiente código para definir correctamente la posición del contenedor:

// Set position for the container
container.x = uniforms.uPointerDiff.x - centerX
container.y = uniforms.uPointerDiff.y - centerY

Y ahora obtenemos el siguiente resultado:

Pero todavía hay algunos detalles que corregir, como definir límites para la función de arrastrar y soltar. Agreguemos esto a la onPointerMove manejador de eventos, donde comprobamos efectivamente los límites según el tamaño de la grilla que hemos calculado:

diffX = diffX > 0 ? Math.min(diffX, centerX + imagePadding) : Math.max(diffX, -(centerX + widthRest))
diffY = diffY > 0 ? Math.min(diffY, centerY + imagePadding) : Math.max(diffY, -(centerY + heightRest))

Otro pequeño detalle que hace que las cosas sean más refinadas es agregar un desplazamiento al fondo de la cuadrícula. Eso mantiene intactas las líneas azules de la cuadrícula. Solo tenemos que agregar el desplazamiento deseado (imagePadding / 2 en nuestro caso) al sombreador de fondo de esta manera:

// Coordinates minus the `uPointerDiff` value, and plus an offset
vec2 coord = gl_FragCoord.xy - uPointerDiff + vec2(10.0);

Y obtendremos el diseño final para nuestro diseño de cuadrícula aleatoria:

Agregar imágenes desde Unsplash Source

Tenemos nuestro diseño listo, por lo que estamos listos para agregarle imágenes. Para agregar una imagen en PixiJS, necesitamos un Sprite, que define la imagen como un Texture de eso Hay múltiples formas de hacer esto. En nuestro caso, primero crearemos un vacío Sprite para cada imagen y, sólo cuando el Sprite está dentro de la ventana gráfica, cargaremos la imagen, crearemos la Texture y añádelo al Sprite. ¿Suena como mucho? Lo revisaremos paso a paso.

Para crear los sprites vacíos, modificaremos el initRectsAndImages función. Por favor, preste atención a los comentarios para una mejor comprensión:

// For the list of images
let images = []

// Add solid rectangles and images
function initRectsAndImages () {
  // Create a new Graphics element to draw solid rectangles
  const graphics = new PIXI.Graphics()
  // Select the color for rectangles
  graphics.beginFill(0x000000)
  // Loop over each rect in the list
  rects.forEach(rect => {
    // Create a new Sprite element for each image
    const image = new PIXI.Sprite()
    // Set image's position and size
    image.x = rect.x * gridSize
    image.y = rect.y * gridSize
    image.width = rect.w * gridSize - imagePadding
    image.height = rect.h * gridSize - imagePadding
    // Set it's alpha to 0, so it is not visible initially
    image.alpha = 0
    // Add image to the list
    images.push(image)
    // Draw the rectangle
    graphics.drawRect(image.x, image.y, image.width, image.height)
  })
  // Ends the fill action
  graphics.endFill()
  // Add the graphics (with all drawn rects) to the container
  container.addChild(graphics)
  // Add all image's Sprites to the container
  images.forEach(image => {
    container.addChild(image)
  })
}

Hasta ahora, solo tenemos sprites vacíos. A continuación, crearemos una función que se encargue de descargar una imagen y asignarla como Texture a la correspondiente Sprite. Esta función solo se llamará si el Sprite está dentro de la ventana gráfica para que la imagen solo se descargue cuando sea necesario.

Por otro lado, si se arrastra la galería y se Sprite ya no está dentro de la ventana gráfica durante el curso de la descarga, esa solicitud puede cancelarse, ya que vamos a utilizar un AbortController (más sobre esto en MDN). De esta forma, cancelaremos las solicitudes innecesarias a medida que arrastramos la galería, dando prioridad a las solicitudes correspondientes a los sprites que se encuentran dentro del viewport en cada momento.

Veamos el código para aterrizar un poco mejor las ideas:

// To store image's URL and avoid duplicates
let imagesUrls = {}

// Load texture for an image, giving its index
function loadTextureForImage (index) {
  // Get image Sprite
  const image = images[index]
  // Set the url to get a random image from Unsplash Source, given image dimensions
  const url = `https://source.unsplash.com/random/${image.width}x${image.height}`
  // Get the corresponding rect, to store more data needed (it is a normal Object)
  const rect = rects[index]
  // Create a new AbortController, to abort fetch if needed
  const { signal } = rect.controller = new AbortController()
  // Fetch the image
  fetch(url, { signal }).then(response => {
    // Get image URL, and if it was downloaded before, load another image
    // Otherwise, save image URL and set the texture
    const id = response.url.split('?')[0]
    if (imagesUrls[id]) {
      loadTextureForImage(index)
    } else {
      imagesUrls[id] = true
      image.texture = PIXI.Texture.from(response.url)
      rect.loaded = true
    }
  }).catch(() => {
    // Catch errors silently, for not showing the following error message if it is aborted:
    // AbortError: The operation was aborted.
  })
}

Ahora tenemos que llamar al loadTextureForImage función para cada imagen cuya correspondiente Sprite se cruza con la ventana gráfica. Además, cancelaremos las solicitudes de búsqueda que ya no sean necesarias y agregaremos un alpha transición cuando los rectángulos entran o salen de la ventana gráfica.

// Check if rects intersects with the viewport
// and loads corresponding image
function checkRectsAndImages () {
  // Loop over rects
  rects.forEach((rect, index) => {
    // Get corresponding image
    const image = images[index]
    // Check if the rect intersects with the viewport
    if (rectIntersectsWithViewport(rect)) {
      // If rect just has been discovered
      // start loading image
      if (!rect.discovered) {
        rect.discovered = true
        loadTextureForImage(index)
      }
      // If image is loaded, increase alpha if possible
      if (rect.loaded && image.alpha < 1) {
        image.alpha += 0.01
      }
    } else { // The rect is not intersecting
      // If the rect was discovered before, but the
      // image is not loaded yet, abort the fetch
      if (rect.discovered && !rect.loaded) {
        rect.discovered = false
        rect.controller.abort()
      }
      // Decrease alpha if possible
      if (image.alpha > 0) {
        image.alpha -= 0.01
      }
    }
  })
}

Y la función que verifica si un rectángulo se cruza con la ventana gráfica es la siguiente:

// Check if a rect intersects the viewport
function rectIntersectsWithViewport (rect) {
  return (
    rect.x * gridSize + container.x <= width &&
    0 <= (rect.x + rect.w) * gridSize + container.x &&
    rect.y * gridSize + container.y <= height &&
    0 <= (rect.y + rect.h) * gridSize + container.y
  )
}

Por último, tenemos que agregar el checkRectsAndImages Función al bucle de animación:

// Animation loop
app.ticker.add(() => {
  // ... more code here ...

  // Check rects and load/cancel images as needded
  checkRectsAndImages()
})

¡Nuestra animación está casi lista!

Manejo de cambios en el tamaño de la ventana gráfica

Al inicializar la aplicación, cambiamos el tamaño del renderizador para que ocupe toda la ventana gráfica, pero si la ventana gráfica cambia de tamaño por algún motivo (por ejemplo, el usuario rota su dispositivo móvil), debemos reajustar las dimensiones y reiniciar la aplicación. .

// On resize, reinit the app (clean and init)
// But first debounce the calls, so we don't call init too often
let resizeTimer
function onResize () {
  if (resizeTimer) clearTimeout(resizeTimer)
  resizeTimer = setTimeout(() => {
    clean()
    init()
  }, 200)
}
// Listen to resize event
window.addEventListener('resize', onResize)

El clean La función limpiará cualquier residuo de la animación que estábamos ejecutando antes de que la ventana gráfica cambiara sus dimensiones:

// Clean the current Application
function clean () {
  // Stop the current animation
  app.ticker.stop()
  // Remove event listeners
  app.stage
    .off('pointerdown', onPointerDown)
    .off('pointerup', onPointerUp)
    .off('pointerupoutside', onPointerUp)
    .off('pointermove', onPointerMove)
  // Abort all fetch calls in progress
  rects.forEach(rect => {
    if (rect.discovered && !rect.loaded) {
      rect.controller.abort()
    }
  })
}

De esta forma, nuestra aplicación responderá adecuadamente a las dimensiones de la ventana gráfica, sin importar cómo cambie. ¡Esto nos da el resultado completo y final de nuestro trabajo!

Algunas reflexiones finales

¡Gracias por hacer este viaje conmigo! Caminamos mucho, pero aprendimos muchos conceptos en el camino y salimos con una interfaz de usuario bastante ordenada. Puede verificar el código en GitHub o jugar con demostraciones en CodePen.

Si ha trabajado con WebGL antes (con o sin usar otras bibliotecas), espero que haya visto lo bueno que es trabajar con PixiJS. Abstrae la complejidad asociada con el mundo WebGL de gran manera, lo que nos permite centrarnos en lo que queremos hacer en lugar de los detalles técnicos para que funcione.

La conclusión es que PixiJS acerca el mundo de WebGL para que los desarrolladores front-end lo comprendan, abriendo muchas posibilidades más allá de HTML, CSS y JavaScript.

(Visited 13 times, 1 visits today)