Manchmal müssen wir ein wenig weiter gehen als HTML, CSS und JavaScript, um die benötigte Benutzeroberfläche zu erstellen, und stattdessen andere Ressourcen wie SVG, WebGL, Canvas und andere verwenden.
Zum Beispiel können mit WebGL die erstaunlichsten Effekte erzielt werden, da es sich um eine JavaScript-API handelt, die für das Rendern interaktiver 2D- und 3D-Grafiken in jedem kompatiblen Webbrowser entwickelt wurde und GPU-beschleunigte Bildverarbeitung ermöglicht.
Das gesagt, kann die Arbeit mit WebGL sehr komplex sein. Daher gibt es eine Vielzahl von Bibliotheken, die es relativ einfach machen, wie z. B. PixiJS, Three.js und Babylon.js, unter anderem. Wir werden mit einem davon arbeiten, PixiJS, um eine Galerie zufälliger Bilder zu erstellen, die von diesem Fragment eines Dribbble-Shots von Zhenya Rynzhuk inspiriert ist.

Das sieht schwierig aus, aber man braucht keine fortgeschrittenen Kenntnisse von WebGL oder sogar PixiJS, um folgen zu können, obwohl grundlegende Kenntnisse von JavaScript (ES6) nützlich sein werden. Vielleicht möchten Sie sogar zuerst mit dem grundlegenden Konzept von Fragment-Shadern, die in WebGL verwendet werden, vertraut werden, wobei The Book of Shaders ein guter Ausgangspunkt ist.
Damit wollen wir uns nun der Verwendung von PixiJS widmen, um diesen WebGL-Effekt zu erzielen!
Ersteinrichtung
Hier ist, was wir für den Anfang brauchen
- Fügen Sie die PixiJS-Bibliothek als Skript in die HTML-Datei ein.
- Verfügen Sie über ein
<canvas>-Element (oder fügen Sie es dynamisch aus JavaScript hinzu), um die Anwendung zu rendern. - Initialisieren Sie die Anwendung mit
new PIXI.Application(options).
Sehen Sie, noch nichts allzu Verrücktes. Hier ist das JavaScript, das wir als Boilerplate verwenden können
// 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()
Beim Ausführen dieses Codes sehen wir nur einen schwarzen Bildschirm und eine Meldung wie diese, wenn wir die Konsole öffnenPixiJS 5.0.2 - WebGL 2 - http://www.pixijs.com/.
Wir sind bereit, mit PixiJS und WebGL auf dem Canvas zu zeichnen!
Erstellung des Gitterhintergrunds mit einem WebGL-Shader
Als Nächstes erstellen wir einen Hintergrund mit einem Gitter, der es uns ermöglicht, den Verzerrungseffekt, den wir erzielen möchten, klar zu visualisieren. Aber zuerst müssen wir wissen, was ein Shader ist und wie er funktioniert. Ich habe Ihnen zuvor The Book of Shaders als Ausgangspunkt für das Erlernen von Shadern empfohlen, und hier werden diese Konzepte zum Tragen kommen. Wenn Sie dies noch nicht getan haben, empfehle ich Ihnen dringend, dieses Material noch einmal durchzugehen und erst dann hier fortzufahren.
Wir werden einen Fragment-Shader erstellen, der einen Gitterhintergrund auf dem Bildschirm ausgibt
// 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);
}
Dieser Code stammt aus einer Demo auf Shadertoy, die eine großartige Quelle für Inspiration und Ressourcen für Shader ist.
Um diesen Shader verwenden zu können, müssen wir zuerst den Code aus der Datei laden, in der er sich befindet, und erst nachdem er korrekt geladen wurde, werden wir die App initialisieren.
// 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)
Damit unser Shader funktioniert und wir das Ergebnis sehen können, fügen wir ein neues Element (einen leeren Sprite) zur Bühne hinzu, das wir zur Definition eines Filters verwenden. So können wir benutzerdefinierte Shader wie den gerade erstellten in PixiJS ausführen.
// 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)
}
Und jetzt sehen wir den gegitterten Hintergrund mit blauen Linien. Schauen Sie genau hin, denn die Linien sind auf dem dunklen Hintergrund leicht erkennbar.
Der Verzerrungseffekt
Unser Hintergrund ist nun fertig, also sehen wir, wie wir den gewünschten Effekt (kubische Linsenverzerrung) auf die gesamte Bühne anwenden können, einschließlich des Hintergrunds und jedes anderen Elements, das wir später hinzufügen, wie z. B. Bilder. Dazu müssen wir einen neuen Filter erstellen und ihn zur Bühne hinzufügen. Ja, wir können auch Filter definieren, die die gesamte Bühne von PixiJS beeinflussen!
Dieses Mal haben wir den Code unseres Shaders auf dieser großartigen Shadertoy-Demo basiert, die den Verzerrungseffekt mit verschiedenen konfigurierbaren Parametern implementiert.
#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);
}
Wir verwenden dieses Mal zwei Uniforms. Uniforms sind Variablen, die wir über JavaScript an den Shader übergeben
uResolution: Dies ist ein JavaScript-Objekt, das{x: width, y: height}enthält. Dieses Uniform ermöglicht es uns, die Koordinaten jedes Pixels im Bereich[0, 1]zu normalisieren.uPointerDown: Dies ist eine Gleitkommazahl im Bereich[0, 1], die es uns ermöglicht, den Verzerrungseffekt zu animieren und seine Intensität proportional zu erhöhen.
Sehen wir uns den Code an, den wir zu unserem JavaScript hinzufügen müssen, um den Verzerrungseffekt unseres neuen Shaders zu sehen
// 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]
Wir können uns bereits unseres Verzerrungseffekts erfreuen!
Dieser Effekt ist im Moment statisch, also noch nicht besonders unterhaltsam. Als Nächstes sehen wir, wie wir den Effekt dynamisch auf Zeigerereignisse reagieren lassen können.
Zuhören auf Zeigerereignisse
PixiJS macht es überraschend einfach, auf Ereignisse zu hören, auch auf mehrere Ereignisse, die gleichermaßen auf Maus- und Touch-Interaktionen reagieren. In diesem Fall möchten wir, dass unsere Animation sowohl auf dem Desktop als auch auf einem Mobilgerät funktioniert, daher müssen wir auf die entsprechenden Ereignisse beider Plattformen hören.
PixiJs bietet ein Attribut interactive, mit dem wir genau das tun können. Wir wenden es auf ein Element an und beginnen, mit einer API zu hören, die der von jQuery ähnelt
// 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)
}
Von hier aus beginnen wir mit einem dritten Uniform (uPointerDiff) zu arbeiten, der es uns ermöglicht, die Bildergalerie per Drag & Drop zu durchsuchen. Sein Wert entspricht der Übersetzung der Szene, während wir die Galerie erkunden. Unten sehen Sie den Code für jede der ereignisbehandelnden Funktionen
// 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)
}
}
Wir werden noch keine Animation sehen, wenn wir uns unsere Arbeit ansehen, aber wir können beginnen zu sehen, wie die Meldungen, die wir in jeder ereignisbehandelnden Funktion definiert haben, korrekt in der Konsole ausgegeben werden.
Wenden wir uns nun der Implementierung unserer Animationen zu!
Animieren des Verzerrungseffekts und der Drag-and-Drop-Funktionalität
Das Erste, was wir für eine Animation mit PixiJS (oder jeder Canvas-basierten Animation) benötigen, ist eine Animationsschleife. Sie besteht normalerweise aus einer Funktion, die kontinuierlich aufgerufen wird, indem requestAnimationFrame verwendet wird, das bei jedem Aufruf die Grafiken auf dem Canvas-Element rendert und so die gewünschte Animation erzeugt.
Wir können unsere eigene Animationsschleife in PixiJS implementieren oder die in der Bibliothek enthaltenen Dienstprogramme verwenden. In diesem Fall verwenden wir die add-Methode von app.ticker, mit der wir eine Funktion übergeben können, die in jedem Frame ausgeführt wird. Am Ende der init-Funktion fügen wir dies hinzu
// 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
})
In der Zwischenzeit übergeben wir im Konstruktor des Filters für den Hintergrund die Uniforms im stage-Filter. Dies ermöglicht es uns, den Translations-Effekt des Hintergrunds mit dieser winzigen Änderung im entsprechenden Shader zu simulieren
uniform vec2 uPointerDiff;
void main () {
// Coordinates minus the `uPointerDiff` value
vec2 coord = gl_FragCoord.xy - uPointerDiff;
// ... more code here ...
}
Und jetzt können wir den Verzerrungseffekt in Aktion sehen, einschließlich der Drag-and-Drop-Funktionalität für den gegitterten Hintergrund. Spielen Sie damit!
Zufällige Generierung eines Masonry-Rasterlayouts
Um unsere Benutzeroberfläche interessanter zu gestalten, können wir die Größe und Abmessungen der Rasterzellen zufällig generieren. Das heißt, jedes Bild kann unterschiedliche Abmessungen haben, wodurch eine Art Masonry-Layout entsteht.
Lassen Sie uns Unsplash Source verwenden, das es uns ermöglicht, zufällige Bilder von Unsplash zu erhalten und die gewünschten Abmessungen zu definieren. Dies erleichtert die Erstellung eines zufälligen Masonry-Layouts, da die Bilder beliebige Abmessungen haben können und somit das Layout im Voraus generiert werden kann.
Um dies zu erreichen, werden wir einen Algorithmus verwenden, der die folgenden Schritte ausführt
- Wir beginnen mit einer Liste von Rechtecken.
- Wir wählen das erste Rechteck in der Liste aus, teilen es in zwei Rechtecke mit zufälligen Abmessungen, solange beide Rechtecke Abmessungen haben, die größer oder gleich dem festgelegten Mindestlimit sind. Wir fügen eine Prüfung hinzu, um sicherzustellen, dass dies möglich ist, und fügen, wenn ja, beide resultierenden Rechtecke zur Liste hinzu.
- Wenn die Liste leer ist, beenden wir die Ausführung. Wenn nicht, gehen wir zurück zu Schritt zwei.
Ich denke, Sie werden ein viel besseres Verständnis dafür bekommen, wie der Algorithmus in der folgenden Demo funktioniert. Verwenden Sie die Schaltflächen, um zu sehen, wie er ausgeführt wird: Weiter führt Schritt zwei aus, Alle führt den gesamten Algorithmus aus und Zurücksetzen setzt auf Schritt eins zurück.
Zeichnen von soliden Rechtecken
Jetzt, da wir unser zufälliges Gitterlayout richtig generieren können, werden wir die vom Algorithmus generierte Liste von Rechtecken verwenden, um solide Rechtecke in unserer PixiJS-Anwendung zu zeichnen. Auf diese Weise können wir sehen, ob es funktioniert und Anpassungen vornehmen, bevor wir die Bilder über die Unsplash Source API hinzufügen.
Um diese Rechtecke zu zeichnen, werden wir ein zufälliges Gitterlayout generieren, das fünfmal größer als der Viewport ist, und es in der Mitte der Bühne positionieren. Das ermöglicht uns, uns mit einiger Freiheit in jede Richtung in der Galerie zu bewegen.
// 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()
}
Bisher haben wir die Liste der Rechtecke generiert. Um sie zur Bühne hinzuzufügen, ist es praktisch, einen Container zu erstellen, da wir dann die Bilder zum selben Container hinzufügen und die Bewegung beim Ziehen der Galerie erleichtern können.
Einen Container in PixiJS zu erstellen, geht so
let container
// Initialize a Container element for solid rectangles and images
function initContainer () {
container = new PIXI.Container()
app.stage.addChild(container)
}
Nun können wir die Rechtecke zum Container hinzufügen, damit sie auf dem Bildschirm angezeigt werden.
// 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)
}
Beachten Sie, dass wir den Berechnungen einen Abstand (imagePadding) für jedes Rechteck hinzugefügt haben. Auf diese Weise haben die Bilder etwas Platz zwischen sich.
Schließlich müssen wir in der Animationsschleife den folgenden Code hinzufügen, um die Position für den Container richtig zu definieren
// Set position for the container
container.x = uniforms.uPointerDiff.x - centerX
container.y = uniforms.uPointerDiff.y - centerY
Und jetzt erhalten wir das folgende Ergebnis
Aber es gibt noch ein paar Details zu beheben, wie z. B. die Festlegung von Grenzen für die Drag-and-Drop-Funktion. Fügen wir dies zum onPointerMove-Ereignishandler hinzu, wo wir die Grenzen entsprechend der Größe des berechneten Gitters überprüfen
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))
Ein weiteres kleines Detail, das die Dinge verfeinert, ist das Hinzufügen eines Offsets zum Gitterhintergrund. Dadurch bleiben die blauen Gitterlinien erhalten. Wir müssen nur den gewünschten Offset (in unserem Fall imagePadding / 2) auf diese Weise zum Hintergrund-Shader hinzufügen
// Coordinates minus the `uPointerDiff` value, and plus an offset
vec2 coord = gl_FragCoord.xy - uPointerDiff + vec2(10.0);
Und wir erhalten das endgültige Design für unser zufälliges Gitterlayout
Hinzufügen von Bildern von Unsplash Source
Wir haben unser Layout bereit, also sind wir bereit, Bilder hinzuzufügen. Um ein Bild in PixiJS hinzuzufügen, benötigen wir einen Sprite, der das Bild als Texture davon definiert. Es gibt mehrere Möglichkeiten, dies zu tun. In unserem Fall erstellen wir zuerst einen leeren Sprite für jedes Bild und erst wenn der Sprite im Viewport ist, laden wir das Bild, erstellen die Texture und fügen sie zum Sprite hinzu. Klingt nach viel? Wir gehen es Schritt für Schritt durch.
Um die leeren Sprites zu erstellen, modifizieren wir die Funktion initRectsAndImages. Achten Sie bitte auf die Kommentare für ein besseres Verständnis
// 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)
})
}
Bisher haben wir nur leere Sprites. Als Nächstes erstellen wir eine Funktion, die für das Herunterladen eines Bildes und dessen Zuweisung als Texture zum entsprechenden Sprite zuständig ist. Diese Funktion wird nur aufgerufen, wenn der Sprite im Viewport ist, damit das Bild nur bei Bedarf heruntergeladen wird.
Andererseits, wenn die Galerie gezogen wird und ein Sprite während des Downloads nicht mehr im Viewport ist, kann diese Anfrage abgebrochen werden, da wir einen AbortController verwenden werden ( mehr dazu auf MDN). Auf diese Weise werden wir unnötige Anfragen abbrechen, während wir die Galerie ziehen, und den Anfragen Vorrang geben, die zu jedem Zeitpunkt den im Viewport befindlichen Sprites entsprechen.
Sehen wir uns den Code an, um die Ideen besser zu veranschaulichen
// 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.
})
}
Nun müssen wir die Funktion loadTextureForImage für jedes Bild aufrufen, dessen entsprechender Sprite mit dem Viewport überschneidet. Außerdem werden wir die nicht mehr benötigten fetch-Anfragen abbrechen und einen alpha-Übergang hinzufügen, wenn die Rechtecke in den Viewport ein- oder austreten.
// 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
}
}
})
}
Und die Funktion, die prüft, ob ein Rechteck mit dem Viewport überschneidet, ist die folgende
// 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
)
}
Zuletzt müssen wir die Funktion checkRectsAndImages zur Animationsschleife hinzufügen
// Animation loop
app.ticker.add(() => {
// ... more code here ...
// Check rects and load/cancel images as needded
checkRectsAndImages()
})
Unsere Animation ist fast fertig!
Behandlung von Änderungen der Viewport-Größe
Bei der Initialisierung der Anwendung haben wir den Renderer so skaliert, dass er den gesamten Viewport einnimmt, aber wenn sich die Größe des Viewports aus irgendeinem Grund ändert (z. B. der Benutzer dreht sein Mobilgerät), sollten wir die Abmessungen neu anpassen und die Anwendung neu starten.
// 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)
Die Funktion clean löscht alle Rückstände der Animation, die wir ausgeführt haben, bevor sich die Abmessungen des Viewports geändert haben
// 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()
}
})
}
Auf diese Weise reagiert unsere Anwendung ordnungsgemäß auf die Abmessungen des Viewports, unabhängig davon, wie sie sich ändern. Dies gibt uns das vollständige und endgültige Ergebnis unserer Arbeit!
Einige abschließende Gedanken
Danke, dass Sie diese Reise mit mir unternommen haben! Wir haben viel durchgemacht, aber unterwegs viele Konzepte gelernt und ein ziemlich gutes UI-Element mit nach Hause genommen. Sie können den Code auf GitHub einsehen oder mit den Demos auf CodePen spielen.
Wenn Sie bereits mit WebGL gearbeitet haben (mit oder ohne andere Bibliotheken), hoffe ich, dass Sie gesehen haben, wie angenehm die Arbeit mit PixiJS ist. Es abstrahiert die Komplexität der WebGL-Welt auf großartige Weise und ermöglicht es uns, uns auf das zu konzentrieren, was wir tun wollen, anstatt auf die technischen Details, um es zum Laufen zu bringen.
Fazit ist, dass PixiJS die Welt von WebGL näher an die Reichweite von Front-End-Entwicklern bringt und viele Möglichkeiten über HTML, CSS und JavaScript hinaus eröffnet.
Das ist erstaunlich. Ich würde gerne wissen, wie man das auf die nächste Stufe hebt und anstatt nur Bilder zu durchsuchen, könnte man es in DOM-Elemente wie einen Link mit mehreren DIVs darin umwandeln? Dann wäre jedes Bild ein Link?
Danke, Joe!
Es ist nicht möglich, echte DOM-Elemente mit WebGL zu zeichnen.
Was Sie tun können, ist, die Elemente zu rasterisieren (mit etwas wie html2canvas) und dann diese mit WebGL zu zeichnen. Sie verlieren die gesamte Interaktivität, die DOM-Elemente haben könnten, aber dann können Sie Klickereignisse verfolgen und versuchen, mit mathematischen Berechnungen herauszufinden, welches Element angeklickt wurde.
In diesem Beispiel haben wir Bilder, also müssen wir nichts rasterisieren. Aber wir müssen trotzdem Berechnungen durchführen und herausfinden, welches Bild angeklickt wurde.
Es ist also nicht ganz einfach, aber Sie könnten etwas Interaktivität erzielen und großartige Ergebnisse erzielen.
Hallo. Wie lade ich lokale Bilder aus einem Projektordner hoch?
Hallo Sergey!
Um die Bilder zu zeichnen, verwenden wir ein zufällig generiertes Layout und erhalten dann zufällige Bilder von Unsplash unter Verwendung der generierten Abmessungen.
Wir kennen also die Abmessungen der Bilder nicht im Voraus.
Um lokale Bilder zu verwenden, müssen Sie möglicherweise einen anderen Algorithmus verwenden oder die Datenstrukturen manuell erstellen, die wir im Code verwenden.
Auf jeden Fall müssen Sie den Code ändern, um das gewünschte Ergebnis zu erzielen, da dies nur ein Proof of Concept ist, keine Bibliothek oder eine vollständige Lösung.
Ich hoffe, das hilft.
Viel Glück!
Hallo, das ist etwas, wonach ich schon lange gesucht habe, also danke! Mein Ziel ist es, eine Website wie https://www.powerhouse-company.com/ zu erstellen und ich sehe, dass diese Website das Flickity-Plugin verwendet. Glauben Sie, dass ich dasselbe mit Ihrer Demo machen könnte?