Letztes Jahr wurde ich von Shawn Wang (swyx) kontaktiert, um einige Arbeiten für Temporal zu übernehmen. Die Idee war, mein kreatives Auge über das zu werfen, was auf der Website zu sehen war, und mir einige Ideen einfallen zu lassen, die der Website das gewisse Etwas verleihen würden. Dies war eine ziemlich nette Herausforderung, da ich mich mehr als Entwickler denn als Designer betrachte. Aber ich liebe es zu lernen und die Designseite meines Könnens zu verbessern.
Eine der Ideen, die mir einfielen, war dieser interaktive Sternenhintergrund. Sie können ihn in dieser geteilten Demo in Aktion sehen
Blockquote-Konzept mit etwas Perspektive und CSS-Custom-Properties 😎
— Jhey 🔨🐻✨ (@jh3yy) 2. Juli 2021
Ich genieße die kreative Freiheit, mir Dinge für @temporalio auszudenken 🤓
Ein Hauch von Verspieltheit für Materialien 🎉
⚒️ @reactjs && @tailwindcss (Website ist NextJS)
👉 https://#/GHtUEcl674 via @CodePen pic.twitter.com/s9xP2tRrOx
Das Schöne an diesem Design ist, dass es als Drop-in React-Komponente erstellt wurde. Und es ist super konfigurierbar in dem Sinne, dass Sie es, sobald Sie die Grundlagen dafür gelegt haben, komplett zu Ihrem eigenen machen können. Keine Sterne gewünscht? Setzen Sie etwas anderes an seine Stelle. Keine zufällig positionierten Partikel gewünscht? Platzieren Sie sie auf konstruierte Weise. Sie haben die volle Kontrolle darüber, was Sie nach Ihrem Willen formen.
Lassen Sie uns also sehen, wie wir diese Drop-in-Komponente für Ihre Website erstellen können! Die Waffen der Wahl heute? React, GreenSock und HTML <canvas>. Der React-Teil ist natürlich völlig optional, aber diese interaktive Hintergrundgrafik als Drop-in-Komponente macht sie zu etwas, das Sie in anderen Projekten verwenden können.
Lassen Sie uns mit dem Aufbau einer einfachen App beginnen
import React from 'https://cdn.skypack.dev/react'
import ReactDOM from 'https://cdn.skypack.dev/react-dom'
import gsap from 'https://cdn.skypack.dev/gsap'
const ROOT_NODE = document.querySelector('#app')
const Starscape = () => <h1>Cool Thingzzz!</h1>
const App = () => <Starscape/>
ReactDOM.render(<App/>, ROOT_NODE)
Als Erstes müssen wir ein <canvas>-Element rendern und eine Referenz darauf abrufen, die wir innerhalb von Reacts useEffect verwenden können. Für diejenigen, die kein React verwenden, speichern Sie stattdessen eine Referenz auf das <canvas> in einer Variablen.
const Starscape = () => {
const canvasRef = React.useRef(null)
return <canvas ref={canvasRef} />
}
Unser <canvas> benötigt auch einige Stile. Zum Anfang können wir dafür sorgen, dass die Leinwand die volle Ansichtsfenstergröße einnimmt und hinter dem Inhalt liegt
canvas {
position: fixed;
inset: 0;
background: #262626;
z-index: -1;
height: 100vh;
width: 100vw;
}
Cool! Aber noch ist nicht viel zu sehen.
Wir brauchen Sterne in unserem Himmel
Wir werden hier ein wenig "schummeln". Wir werden nicht die "klassische" spitze Sternform zeichnen. Wir werden Kreise mit unterschiedlicher Deckkraft und Größe verwenden.
Einen Kreis auf einem <canvas> zu zeichnen, bedeutet, einen Kontext vom <canvas> zu holen und die Funktion arc zu verwenden. Lassen Sie uns einen Kreis, äh, einen Stern, in der Mitte rendern. Das können wir innerhalb eines React useEffect tun
const Starscape = () => {
const canvasRef = React.useRef(null)
const contextRef = React.useRef(null)
React.useEffect(() => {
canvasRef.current.width = window.innerWidth
canvasRef.current.height = window.innerHeight
contextRef.current = canvasRef.current.getContext('2d')
contextRef.current.fillStyle = 'yellow'
contextRef.current.beginPath()
contextRef.current.arc(
window.innerWidth / 2, // X
window.innerHeight / 2, // Y
100, // Radius
0, // Start Angle (Radians)
Math.PI * 2 // End Angle (Radians)
)
contextRef.current.fill()
}, [])
return <canvas ref={canvasRef} />
}
Wir haben also einen großen gelben Kreis
Das ist ein guter Anfang! Der Rest unseres Codes findet innerhalb dieser useEffect-Funktion statt. Deshalb ist der React-Teil irgendwie optional. Sie können diesen Code extrahieren und in jeder gewünschten Form verwenden.
Wir müssen überlegen, wie wir eine Reihe von "Sternen" generieren und rendern werden. Lassen Sie uns eine LOAD-Funktion erstellen. Diese Funktion kümmert sich um die Generierung unserer Sterne sowie um die allgemeine <canvas>-Einrichtung. Wir können auch die Logik zur Größenanpassung der <canvas>-Größenanpassung in diese Funktion verschieben
const LOAD = () => {
const VMIN = Math.min(window.innerHeight, window.innerWidth)
const STAR_COUNT = Math.floor(VMIN * densityRatio)
canvasRef.current.width = window.innerWidth
canvasRef.current.height = window.innerHeight
starsRef.current = new Array(STAR_COUNT).fill().map(() => ({
x: gsap.utils.random(0, window.innerWidth, 1),
y: gsap.utils.random(0, window.innerHeight, 1),
size: gsap.utils.random(1, sizeLimit, 1),
scale: 1,
alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),
}))
}
Unsere Sterne sind jetzt ein Array von Objekten. Und jeder Stern hat Eigenschaften, die seine Charakteristika definieren, darunter
x: Die Position des Sterns auf der x-Achsey: Die Position des Sterns auf der y-Achsesize: Die Größe des Sterns in Pixelnscale: Die Skalierung des Sterns, die bei der Interaktion mit der Komponente zum Tragen kommtalpha: Der Alpha-Wert des Sterns, oderOpazität, der ebenfalls bei Interaktionen zum Tragen kommt
Wir können die random()-Methode von GreenSock verwenden, um einige dieser Werte zu generieren. Möglicherweise fragen Sie sich auch, woher sizeLimit, defaultAlpha und densityRatio stammen. Dies sind nun props, die wir der Starscape-Komponente übergeben können. Wir haben einige Standardwerte dafür bereitgestellt
const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5 }) => {
Ein zufällig generiertes SternenObjekt könnte so aussehen
{
"x": 1252,
"y": 29,
"size": 4,
"scale": 1,
"alpha": 0.5
}
Aber wir müssen diese Sterne sehen, und das tun wir, indem wir sie rendern. Lassen Sie uns eine RENDER-Funktion erstellen. Diese Funktion durchläuft unsere Sterne und rendert jeden einzelnen auf der <canvas> mit der Funktion arc
const RENDER = () => {
contextRef.current.clearRect(
0,
0,
canvasRef.current.width,
canvasRef.current.height
)
starsRef.current.forEach(star => {
contextRef.current.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})`
contextRef.current.beginPath()
contextRef.current.arc(star.x, star.y, star.size / 2, 0, Math.PI * 2)
contextRef.current.fill()
})
}
Nun, wir brauchen die Funktion clearRect für unsere aktuelle Implementierung nicht, da wir nur einmal auf einer leeren <canvas> rendern. Aber das Löschen der <canvas>, bevor etwas gerendert wird, ist keine schlechte Angewohnheit, und eine, die wir brauchen werden, wenn wir unsere canvas interaktiv machen.
Betrachten Sie diese Demo, die die Auswirkung des Nicht-Löschens zwischen den Frames zeigt.
Unsere Starscape-Komponente nimmt langsam Gestalt an.
Siehe den Code
const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5 }) => {
const canvasRef = React.useRef(null)
const contextRef = React.useRef(null)
const starsRef = React.useRef(null)
React.useEffect(() => {
contextRef.current = canvasRef.current.getContext('2d')
const LOAD = () => {
const VMIN = Math.min(window.innerHeight, window.innerWidth)
const STAR_COUNT = Math.floor(VMIN * densityRatio)
canvasRef.current.width = window.innerWidth
canvasRef.current.height = window.innerHeight
starsRef.current = new Array(STAR_COUNT).fill().map(() => ({
x: gsap.utils.random(0, window.innerWidth, 1),
y: gsap.utils.random(0, window.innerHeight, 1),
size: gsap.utils.random(1, sizeLimit, 1),
scale: 1,
alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),
}))
}
const RENDER = () => {
contextRef.current.clearRect(
0,
0,
canvasRef.current.width,
canvasRef.current.height
)
starsRef.current.forEach(star => {
contextRef.current.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})`
contextRef.current.beginPath()
contextRef.current.arc(star.x, star.y, star.size / 2, 0, Math.PI * 2)
contextRef.current.fill()
})
}
LOAD()
RENDER()
}, [])
return <canvas ref={canvasRef} />
}
Spielen Sie mit den props in dieser Demo herum, um zu sehen, wie sie die Art und Weise beeinflussen, wie Sterne gerendert werden.
Bevor wir weitermachen, haben Sie vielleicht ein Eigenart in der Demo bemerkt, bei der das Ändern der Größe des Ansichtsfensters die <canvas> verzerrt. Als schneller Gewinn können wir unsere LOAD- und RENDER-Funktionen bei resize erneut ausführen. In den meisten Fällen werden wir dies auch debouncen wollen. Wir können den folgenden Code zu unserem useEffect-Aufruf hinzufügen. Beachten Sie, wie wir den Event-Listener auch beim Aufräumen entfernen.
// Naming things is hard...
const RUN = () => {
LOAD()
RENDER()
}
RUN()
// Set up event handling
window.addEventListener('resize', RUN)
return () => {
window.removeEventListener('resize', RUN)
}
Cool. Jetzt bekommen wir, wenn wir das Ansichtsfenster ändern, einen neu generierten Sternenhimmel.
Interaktion mit dem Sternenhintergrund
Nun zum spaßigen Teil! Machen wir dieses Ding interaktiv.
Die Idee ist, dass wir, während wir unseren Zeiger über den Bildschirm bewegen, die Nähe der Sterne zum Mauszeiger erkennen. Abhängig von dieser Nähe werden die Sterne heller und skalieren hoch.
Wir müssen einen weiteren Event-Listener hinzufügen, um dies zu erreichen. Nennen wir ihn UPDATE. Dieser berechnet den Abstand zwischen dem Zeiger und jedem Stern und animiert dann die scale- und alpha-Werte jedes Sterns. Um sicherzustellen, dass diese animierten Werte korrekt sind, können wir das mapRange()-Dienstprogramm von GreenSock verwenden. Tatsächlich können wir in unserer LOAD-Funktion Referenzen auf einige Mapping-Funktionen sowie eine Größeneinheit erstellen und diese zwischen den Funktionen teilen, falls wir sie benötigen.
Hier ist unsere neue LOAD-Funktion. Beachten Sie die neuen props für scaleLimit und proximityRatio. Sie werden verwendet, um den Bereich zu begrenzen, wie groß oder klein ein Stern werden kann, sowie die Nähe, auf der dies basiert.
const Starscape = ({
densityRatio = 0.5,
sizeLimit = 5,
defaultAlpha = 0.5,
scaleLimit = 2,
proximityRatio = 0.1
}) => {
const canvasRef = React.useRef(null)
const contextRef = React.useRef(null)
const starsRef = React.useRef(null)
const vminRef = React.useRef(null)
const scaleMapperRef = React.useRef(null)
const alphaMapperRef = React.useRef(null)
React.useEffect(() => {
contextRef.current = canvasRef.current.getContext('2d')
const LOAD = () => {
vminRef.current = Math.min(window.innerHeight, window.innerWidth)
const STAR_COUNT = Math.floor(vminRef.current * densityRatio)
scaleMapperRef.current = gsap.utils.mapRange(
0,
vminRef.current * proximityRatio,
scaleLimit,
1
);
alphaMapperRef.current = gsap.utils.mapRange(
0,
vminRef.current * proximityRatio,
1,
defaultAlpha
);
canvasRef.current.width = window.innerWidth
canvasRef.current.height = window.innerHeight
starsRef.current = new Array(STAR_COUNT).fill().map(() => ({
x: gsap.utils.random(0, window.innerWidth, 1),
y: gsap.utils.random(0, window.innerHeight, 1),
size: gsap.utils.random(1, sizeLimit, 1),
scale: 1,
alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),
}))
}
}
Und hier ist unsere UPDATE-Funktion. Sie berechnet den Abstand und generiert eine entsprechende scale und alpha für einen Stern
const UPDATE = ({ x, y }) => {
starsRef.current.forEach(STAR => {
const DISTANCE = Math.sqrt(Math.pow(STAR.x - x, 2) + Math.pow(STAR.y - y, 2));
gsap.to(STAR, {
scale: scaleMapperRef.current(
Math.min(DISTANCE, vminRef.current * proximityRatio)
),
alpha: alphaMapperRef.current(
Math.min(DISTANCE, vminRef.current * proximityRatio)
)
});
})
};
Aber halt... es tut nichts?
Nun, es tut etwas. Aber wir haben unsere Komponente nicht darauf vorbereitet, Updates anzuzeigen. Wir müssen neue Frames rendern, während wir interagieren. Wir können oft zu requestAnimationFrame greifen. Da wir jedoch GreenSock verwenden, können wir gsap.ticker nutzen. Dies wird oft als "Herzschlag der GSAP-Engine" bezeichnet und ist ein guter Ersatz für requestAnimationFrame.
Um es zu verwenden, fügen wir die RENDER-Funktion zum ticker hinzu und stellen sicher, dass wir sie beim Aufräumen entfernen. Eines der netten Dinge bei der Verwendung des ticker ist, dass wir die Anzahl der Frames pro Sekunde (fps) festlegen können. Ich bevorzuge "kinematische" 24fps
// Remove RUN
LOAD()
gsap.ticker.add(RENDER)
gsap.ticker.fps(24)
window.addEventListener('resize', LOAD)
document.addEventListener('pointermove', UPDATE)
return () => {
window.removeEventListener('resize', LOAD)
document.removeEventListener('pointermove', UPDATE)
gsap.ticker.remove(RENDER)
}
Beachten Sie, wie wir LOAD jetzt auch bei resize ausführen. Wir müssen auch sicherstellen, dass unsere scale in dieser RENDER-Funktion berücksichtigt wird, wenn wir arc verwenden.
const RENDER = () => {
contextRef.current.clearRect(
0,
0,
canvasRef.current.width,
canvasRef.current.height
)
starsRef.current.forEach(star => {
contextRef.current.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})`
contextRef.current.beginPath()
contextRef.current.arc(
star.x,
star.y,
(star.size / 2) * star.scale,
0,
Math.PI * 2
)
contextRef.current.fill()
})
}
Es funktioniert! 🙌
Es ist ein sehr subtiler Effekt. Aber das ist beabsichtigt, denn obwohl es sehr nett ist, wollen wir nicht, dass so etwas von den eigentlichen Inhalten ablenkt. Ich empfehle, mit den props für die Komponente zu spielen, um verschiedene Effekte zu sehen. Es ist auch sinnvoll, allen Sternen standardmäßig eine niedrige alpha zu geben.
Die folgende Demo ermöglicht es Ihnen, mit den verschiedenen props zu spielen. Ich habe hier einige ziemlich herausragende Standardwerte für die Demonstration gewählt! Aber denken Sie daran, dass es in diesem Artikel mehr darum geht, Ihnen die Techniken zu zeigen, damit Sie Ihre eigenen coolen Hintergründe erstellen können - während Sie darauf achten, wie sie mit den Inhalten interagieren.
Verfeinerungen
Es gibt ein Problem mit unserem interaktiven Sternenhintergrund. Wenn der Mauszeiger die <canvas> verlässt, bleiben die Sterne hell und hochskaliert, aber wir möchten, dass sie zu ihrem ursprünglichen Zustand zurückkehren. Um dies zu beheben, können wir einen zusätzlichen Handler für pointerleave hinzufügen. Wenn der Zeiger die Szene verlässt, werden alle Sterne auf die Skalierung 1 und den ursprünglichen Alpha-Wert, der durch defaultAlpha festgelegt wurde, animiert.
const EXIT = () => {
gsap.to(starsRef.current, {
scale: 1,
alpha: defaultAlpha,
})
}
// Set up event handling
window.addEventListener('resize', LOAD)
document.addEventListener('pointermove', UPDATE)
document.addEventListener('pointerleave', EXIT)
return () => {
window.removeEventListener('resize', LOAD)
document.removeEventListener('pointermove', UPDATE)
document.removeEventListener('pointerleave', EXIT)
gsap.ticker.remove(RENDER)
}
Schön! Jetzt skalieren unsere Sterne wieder herunter und kehren zu ihrem vorherigen Alpha-Wert zurück, wenn der Mauszeiger die Szene verlässt.
Bonus: Ein Osterei hinzufügen
Bevor wir zum Ende kommen, fügen wir eine kleine Osterei-Überraschung zu unserem interaktiven Sternenhintergrund hinzu. Schon mal vom Konami Code gehört? Das ist ein berühmter Cheat-Code und eine coole Möglichkeit, ein Osterei in unsere Komponente einzubauen.
Wir können mit dem Hintergrund praktisch alles machen, sobald der Code läuft. Zum Beispiel könnten wir alle Sterne zufällig pulsieren lassen. Oder sie könnten mit zusätzlichen Farben lebendig werden? Es ist eine Gelegenheit, kreativ zu werden!
Wir werden auf Tastaturereignisse hören und erkennen, ob der Code eingegeben wird. Beginnen wir mit der Erstellung einer Variablen für den Code
const KONAMI_CODE =
'arrowup,arrowup,arrowdown,arrowdown,arrowleft,arrowright,arrowleft,arrowright,keyb,keya';
Dann erstellen wir einen zweiten Effekt innerhalb unseres Sternenhintergrunds. Dies ist eine gute Möglichkeit, die Trennung der Zuständigkeiten aufrechtzuerhalten, da ein Effekt für das gesamte Rendering zuständig ist und der andere für das Osterei. Speziell hören wir auf keyup-Ereignisse und prüfen, ob unsere Eingabe mit dem Code übereinstimmt.
const codeRef = React.useRef([])
React.useEffect(() => {
const handleCode = e => {
codeRef.current = [...codeRef.current, e.code]
.slice(
codeRef.current.length > 9 ? codeRef.current.length - 9 : 0
)
if (codeRef.current.join(',').toLowerCase() === KONAMI_CODE) {
// Party in here!!!
}
}
window.addEventListener('keyup', handleCode)
return () => {
window.removeEventListener('keyup', handleCode)
}
}, [])
Wir speichern die Benutzereingabe in einem Array, das wir in einem ref speichern. Sobald wir den Party-Code treffen, können wir das Array löschen und tun, was wir wollen. Zum Beispiel können wir ein gsap.timeline erstellen, das eine Zeit lang etwas mit unseren Sternen macht. In diesem Fall möchten wir die Eingabe des Konami-Codes nicht zulassen, während die Timeline aktiv ist. Stattdessen können wir die timeline in einem ref speichern und eine weitere Prüfung durchführen, bevor wir den Party-Code ausführen.
const partyRef = React.useRef(null)
const isPartying = () =>
partyRef.current &&
partyRef.current.progress() !== 0 &&
partyRef.current.progress() !== 1;
Für dieses Beispiel habe ich eine kleine Timeline erstellt, die jeden Stern einfärbt und ihn an eine neue Position bewegt. Dies erfordert die Aktualisierung unserer LOAD- und RENDER-Funktionen.
Erstens muss jeder Stern jetzt seine eigene hue, saturation und lightness haben
// Generating stars! ⭐️
starsRef.current = new Array(STAR_COUNT).fill().map(() => ({
hue: 0,
saturation: 0,
lightness: 100,
x: gsap.utils.random(0, window.innerWidth, 1),
y: gsap.utils.random(0, window.innerHeight, 1),
size: gsap.utils.random(1, sizeLimit, 1),
scale: 1,
alpha: defaultAlpha
}));
Zweitens müssen wir diese neuen Werte bei der Darstellung berücksichtigen
starsRef.current.forEach((star) => {
contextRef.current.fillStyle = `hsla(
${star.hue},
${star.saturation}%,
${star.lightness}%,
${star.alpha}
)`;
contextRef.current.beginPath();
contextRef.current.arc(
star.x,
star.y,
(star.size / 2) * star.scale,
0,
Math.PI * 2
);
contextRef.current.fill();
});
Und hier ist der spaßige Teil des Codes, der alle Sterne bewegt
partyRef.current = gsap.timeline().to(starsRef.current, {
scale: 1,
alpha: defaultAlpha
});
const STAGGER = 0.01;
for (let s = 0; s < starsRef.current.length; s++) {
partyRef.current
.to(
starsRef.current[s],
{
onStart: () => {
gsap.set(starsRef.current[s], {
hue: gsap.utils.random(0, 360),
saturation: 80,
lightness: 60,
alpha: 1,
})
},
onComplete: () => {
gsap.set(starsRef.current[s], {
saturation: 0,
lightness: 100,
alpha: defaultAlpha,
})
},
x: gsap.utils.random(0, window.innerWidth),
y: gsap.utils.random(0, window.innerHeight),
duration: 0.3
},
s * STAGGER
);
}
Von dort erstellen wir eine neue Timeline und animieren die Werte jedes Sterns. Diese neuen Werte werden von RENDER übernommen. Wir fügen einen Stagger hinzu, indem wir jeden Tween in der Timeline mit GSAPs Position Parameter positionieren.
Das ist alles!
Das ist eine Möglichkeit, einen interaktiven Sternenhintergrund für Ihre Website zu erstellen. Wir haben GSAP und eine HTML <canvas> kombiniert und sogar etwas React eingestreut, was ihn konfigurierbarer und wiederverwendbarer macht. Wir haben sogar ein Osterei hineingepackt!
Wo können Sie diese Komponente von hier aus weiterentwickeln? Wie könnten Sie sie auf einer Website verwenden? Die Kombination aus GreenSock und <canvas> macht viel Spaß und ich freue mich darauf zu sehen, was Sie erstellen werden! Hier sind ein paar weitere Ideen, um Ihre Kreativität anzuregen...
Das ist so erstaunlich, gut gemacht
Jeden Tag eine Lektion! Das ist so cool in meinem Browser, gut gemacht