Nedávno jsem si udělal výlet do daleké země driblování a viděl jsem něco magického. Všude se vznášely rozmazané koule a krásná, sklu podobná rozhraní. Klidný!
Tohle mě přivedlo k zamyšlení. Nebylo by skvělé vytvořit generativní vstupní stránka v tomto stylu?
Konečný výsledek
Nejprve je zde jakýsi vizuální TL;DR.
Zde se také můžete podívat na celostránkový příklad.
Paleta barev je náhodná v rámci omezení. Barevné koule se pohybují s vlastní myslí. Díky těmto prvkům náhodnosti je naše vstupní stránka generativní.
Pokud je pro vás generativní umění/design novinkou, zde je vynikající základ od Ali Spittel &James Reichard.
Líbí se ti co vidíš? Pojďme stavět!
Předpoklady
Abyste z tohoto tutoriálu vytěžili maximum, budete muset umět psát HTML, CSS a JavaScript.
Pokud jste četli „WebGL“ a upadli do stavu paniky vyvolané shadery, nebojte se. Budeme používat PixiJS, abychom odstranili děsivé věci. Tento tutoriál vám poslouží jako pěkný úvod do Pixi, pokud jste jej ještě nepoužili.
Vytvoření animace na pozadí
První věc, kterou postavíme, jsou koule. K jejich vytvoření budeme potřebovat nějaké knihovny/balíčky. Pojďme nejprve odstranit nudné věci a přidat je do projektu.
Přehled balíčku
Zde je rychlý souhrn knihoven/balíčků, které budeme používat.
- PixiJS – Výkonná grafická knihovna postavená na WebGL, kterou použijeme k vykreslení našich koulí.
- KawaseBlurFilter – Plugin filtru PixiJS pro mimořádně hladké rozmazání.
- SimplexNoise – Používá se ke generování proudu sobě podobných náhodných čísel. Více o tom již brzy.
- hsl-to-hex – malý JS nástroj pro převod barev HSL na HEX.
- debounce – JavaScriptová funkce debounce.
Instalace balíčku
Pokud sledujete CodePen, přidejte do svého souboru JavaScript následující importy a můžete začít:
import * as PIXI from "https://cdn.skypack.dev/pixi.js";
import { KawaseBlurFilter } from "https://cdn.skypack.dev/@pixi/filter-kawase-blur";
import SimplexNoise from "https://cdn.skypack.dev/simplex-noise";
import hsl from "https://cdn.skypack.dev/hsl-to-hex";
import debounce from "https://cdn.skypack.dev/debounce";
Pokud se nacházíte ve svém vlastním prostředí, můžete nainstalovat požadované balíčky pomocí:
npm i pixi.js @pixi/filter-kawase-blur simplex-noise hsl-to-hex debounce
Poté je můžete importovat takto:
import * as PIXI from "pixi.js";
import { KawaseBlurFilter } from "@pixi/filter-kawase-blur";
import SimplexNoise from "simplex-noise";
import hsl from "hsl-to-hex";
import debounce from "debounce";
Poznámka:Mimo CodePen budete ke zpracování těchto importů potřebovat nástroj pro sestavení, jako je Webpack nebo Parcel.
Prázdné (Pixi) plátno
Skvělé, nyní máme vše, co potřebujeme, abychom mohli začít. Začněme tím, že přidáme <canvas>
prvek do našeho HTML:
<canvas class="orb-canvas"></canvas>
Dále můžeme vytvořit novou instanci Pixi s prvkem canvas, jak je „zobrazit“ (kde se Pixi vykreslí) . Naši instanci nazveme app
:
// Create PixiJS app
const app = new PIXI.Application({
// render to <canvas class="orb-canvas"></canvas>
view: document.querySelector(".orb-canvas"),
// auto adjust size to fit the current window
resizeTo: window,
// transparent background, we will be creating a gradient background later using CSS
transparent: true
});
Pokud si prohlédnete DOM a změníte velikost prohlížeče, měli byste vidět změnu velikosti prvku canvas, aby se vešel do okna. Kouzlo!
Některé užitečné nástroje
Než půjdeme dále, měli bychom do našeho JavaScriptu přidat některé užitečné funkce.
// return a random number within a range
function random(min, max) {
return Math.random() * (max - min) + min;
}
// map a number from 1 range to another
function map(n, start1, end1, start2, end2) {
return ((n - start1) / (end1 - start1)) * (end2 - start2) + start2;
}
Pokud jste již dříve sledovali některý z mých návodů, možná je již znáte. Jsem trochu posedlý...
random
vrátí náhodné číslo v omezeném rozsahu. Například „Dejte mi náhodné číslo mezi 5 a 10“ .
map
vezme číslo z jednoho rozsahu a mapuje ho do jiného. Pokud například číslo (0,5) obvykle existuje v rozsahu mezi 0 – 1 a namapujeme ho na rozsah 0 – 100, číslo bude 50.
Doporučuji trochu experimentovat s těmito dvěma nástroji, pokud jsou pro vás nové. Budou užitečnými společníky na vaší generativní cestě! Vložením do konzole a experimentováním s výstupem je skvělý začátek.
Vytvoření třídy Orb
Nyní bychom měli mít vše, co potřebujeme k vytvoření naší orb animace. Pro začátek vytvořte Orb
třída:
// Orb class
class Orb {
// Pixi takes hex colors as hexidecimal literals (0x rather than a string with '#')
constructor(fill = 0x000000) {
// bounds = the area an orb is "allowed" to move within
this.bounds = this.setBounds();
// initialise the orb's { x, y } values to a random point within it's bounds
this.x = random(this.bounds["x"].min, this.bounds["x"].max);
this.y = random(this.bounds["y"].min, this.bounds["y"].max);
// how large the orb is vs it's original radius (this will modulate over time)
this.scale = 1;
// what color is the orb?
this.fill = fill;
// the original radius of the orb, set relative to window height
this.radius = random(window.innerHeight / 6, window.innerHeight / 3);
// starting points in "time" for the noise/self similar random values
this.xOff = random(0, 1000);
this.yOff = random(0, 1000);
// how quickly the noise/self similar random values step through time
this.inc = 0.002;
// PIXI.Graphics is used to draw 2d primitives (in this case a circle) to the canvas
this.graphics = new PIXI.Graphics();
this.graphics.alpha = 0.825;
// 250ms after the last window resize event, recalculate orb positions.
window.addEventListener(
"resize",
debounce(() => {
this.bounds = this.setBounds();
}, 250)
);
}
}
Naše Orb
je jednoduchý kruh, který existuje ve 2D prostoru.
Má hodnotu x a y, poloměr, barvu výplně, hodnotu měřítka (jak je velká oproti původnímu poloměru) a soubor hranic. Jeho hranice definují oblast, ve které se může pohybovat, jako soubor virtuálních zdí. To zabrání tomu, aby se koule příliš přiblížily k našemu textu.
Můžete si všimnout použití neexistujícího setBounds
funkce ve úryvku výše. Tato funkce bude definovat virtuální omezení, ve kterých naše koule existují. Pojďme to přidat do Orb
třída:
setBounds() {
// how far from the { x, y } origin can each orb move
const maxDist =
window.innerWidth < 1000 ? window.innerWidth / 3 : window.innerWidth / 5;
// the { x, y } origin for each orb (the bottom right of the screen)
const originX = window.innerWidth / 1.25;
const originY =
window.innerWidth < 1000
? window.innerHeight
: window.innerHeight / 1.375;
// allow each orb to move x distance away from it's { x, y }origin
return {
x: {
min: originX - maxDist,
max: originX + maxDist
},
y: {
min: originY - maxDist,
max: originY + maxDist
}
};
}
OK skvěle. Tohle jde dohromady! Dále bychom měli přidat update
a render
funkce na naše Orb
třída. Obě tyto funkce poběží na každém snímku animace. Více o tom za chvíli.
Aktualizační funkce bude definovat, jak by se měla měnit pozice a velikost koule v průběhu času. Funkce render bude definovat, jak se má koule zobrazovat na obrazovce.
Za prvé, zde je update
funkce:
update() {
// self similar "psuedo-random" or noise values at a given point in "time"
const xNoise = simplex.noise2D(this.xOff, this.xOff);
const yNoise = simplex.noise2D(this.yOff, this.yOff);
const scaleNoise = simplex.noise2D(this.xOff, this.yOff);
// map the xNoise/yNoise values (between -1 and 1) to a point within the orb's bounds
this.x = map(xNoise, -1, 1, this.bounds["x"].min, this.bounds["x"].max);
this.y = map(yNoise, -1, 1, this.bounds["y"].min, this.bounds["y"].max);
// map scaleNoise (between -1 and 1) to a scale value somewhere between half of the orb's original size, and 100% of it's original size
this.scale = map(scaleNoise, -1, 1, 0.5, 1);
// step through "time"
this.xOff += this.inc;
this.yOff += this.inc;
}
Aby tato funkce běžela, musíme také definovat simplex
. Chcete-li tak učinit, přidejte následující úryvek kamkoli před Orb
definice třídy:
// Create a new simplex noise instance
const simplex = new SimplexNoise();
Hodně se tu mluví o „hluku“. Uvědomuji si, že pro některé lidi to bude neznámý pojem.
V tomto tutoriálu se nebudu podrobně zabývat hlukem, ale jako základ bych doporučil toto video od Daniela Shiffmana. Pokud jste novým konceptem hluku – pozastavte tento článek, podívejte se na video a vraťte se zpět!
Stručně řečeno, šum je skvělý způsob generování _ sobě podobných_ náhodných čísel. Tato čísla jsou úžasná pro animaci, protože vytvářejí plynulý, ale nepředvídatelný pohyb.
Zde je obrázek z The Nature of Code, který ukazuje rozdíl mezi tradičním náhodným výběrem (např. Math.random()
a náhodné náhodné hodnoty:
update
Funkce zde používá šum k modulaci x
koule , y
a scale
vlastnosti v průběhu času. Hodnoty šumu vybíráme na základě našeho xOff
a yOff
pozice. Potom použijeme map
škálovat hodnoty (vždy mezi -1 a 1) do nových rozsahů.
Výsledek tohoto? Orb se bude vždy pohybovat ve svých mezích. Jeho velikost je náhodná v rámci omezení. Chování koule je nepředvídatelné. Nejsou zde žádné klíčové snímky ani pevné hodnoty.
To je všechno v pořádku, ale stále nic nevidíme! Pojďme to opravit přidáním render
funkce na naše Orb
třída:
render() {
// update the PIXI.Graphics position and scale values
this.graphics.x = this.x;
this.graphics.y = this.y;
this.graphics.scale.set(this.scale);
// clear anything currently drawn to graphics
this.graphics.clear();
// tell graphics to fill any shapes drawn after this with the orb's fill color
this.graphics.beginFill(this.fill);
// draw a circle at { 0, 0 } with it's size set by this.radius
this.graphics.drawCircle(0, 0, this.radius);
// let graphics know we won't be filling in any more shapes
this.graphics.endFill();
}
render
nakreslí na naše plátno každý snímek nový kruh.
Můžete si všimnout, že kruh je x
a y
obě hodnoty jsou 0. Je to proto, že přesouváme graphics
prvek samotný, spíše než kruh v něm.
Proč je to?
Představte si, že byste chtěli tento projekt rozšířit a vykreslit složitější kouli. Vaše nová koule se nyní skládá z> 100 kruhů. Je jednodušší přesunout celou instanci grafiky než přesunout každý prvek v ní. To může vám také poskytne určité zvýšení výkonu.
Vytváříme nějaké koule!
Je čas vložit naše Orb
třídy k dobrému využití. Vytvořme 10 zbrusu nových instancí orbu a vložíme je do orbs
pole:
// Create orbs
const orbs = [];
for (let i = 0; i < 10; i++) {
// each orb will be black, just for now
const orb = new Orb(0x000000);
app.stage.addChild(orb.graphics);
orbs.push(orb);
}
Voláme app.stage.addChild
přidat každou instanci grafiky na naše plátno. Je to podobné volání document.appendChild()
na prvku DOM.
Animace! Nebo žádná animace?
Nyní, když máme 10 nových koulí, můžeme je začít animovat. Nepředpokládejme však, že každý chce pohyblivé pozadí.
Když vytváříte tento druh stránky, je důležité respektovat preference uživatele. V našem případě, pokud má uživatel prefers-reduced-motion
set, vykreslíme statické pozadí.
Zde je návod, jak můžeme nastavit smyčku animace Pixi, která bude respektovat preference uživatele:
// Animate!
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
app.ticker.add(() => {
// update and render each orb, each frame. app.ticker attempts to run at 60fps
orbs.forEach((orb) => {
orb.update();
orb.render();
});
});
} else {
// perform one update and render per orb, do not animate
orbs.forEach((orb) => {
orb.update();
orb.render();
});
}
Když zavoláme app.ticker.add(function)
, řekneme Pixi, aby tuto funkci opakovalo s frekvencí přibližně 60 snímků za sekundu. V našem případě, pokud uživatel preferuje omezený pohyb, spustíme pouze update
a jednou vykreslit naše koule.
Po přidání výše uvedeného úryvku by se v prohlížeči mělo zobrazit něco takového:
Hurá! Hnutí! Věřte nebo ne, už jsme skoro tam.
Přidání rozostření
Naše koule teď vypadají trochu... drsně. Pojďme to napravit přidáním filtru rozostření na naše plátno Pixi. To je ve skutečnosti velmi jednoduché a bude to mít velký vliv na náš vizuální výstup.
Umístěte tento řádek pod váš app
definice:
app.stage.filters = [new KawaseBlurFilter(30, 10, true)];
Nyní, když se podíváte do prohlížeče, měli byste vidět mnohem měkčí koule!
Vypadá skvěle. Přidáme trochu barvy.
Generativní paleta barev využívající HSL
Abychom do našeho projektu vnesli trochu barvy, vytvoříme ColorPalette
třída. Tato třída bude definovat sadu barev, které můžeme použít k vyplnění našich koulí, ale také k úpravě širší stránky.
Při práci s barvou vždy používám HSL. Je intuitivnější než hex a docela dobře se hodí pro generativní práci. Zde je postup:
class ColorPalette {
constructor() {
this.setColors();
this.setCustomProperties();
}
setColors() {
// pick a random hue somewhere between 220 and 360
this.hue = ~~random(220, 360);
this.complimentaryHue1 = this.hue + 30;
this.complimentaryHue2 = this.hue + 60;
// define a fixed saturation and lightness
this.saturation = 95;
this.lightness = 50;
// define a base color
this.baseColor = hsl(this.hue, this.saturation, this.lightness);
// define a complimentary color, 30 degress away from the base
this.complimentaryColor1 = hsl(
this.complimentaryHue1,
this.saturation,
this.lightness
);
// define a second complimentary color, 60 degrees away from the base
this.complimentaryColor2 = hsl(
this.complimentaryHue2,
this.saturation,
this.lightness
);
// store the color choices in an array so that a random one can be picked later
this.colorChoices = [
this.baseColor,
this.complimentaryColor1,
this.complimentaryColor2
];
}
randomColor() {
// pick a random color
return this.colorChoices[~~random(0, this.colorChoices.length)].replace(
"#",
"0x"
);
}
setCustomProperties() {
// set CSS custom properties so that the colors defined here can be used throughout the UI
document.documentElement.style.setProperty("--hue", this.hue);
document.documentElement.style.setProperty(
"--hue-complimentary1",
this.complimentaryHue1
);
document.documentElement.style.setProperty(
"--hue-complimentary2",
this.complimentaryHue2
);
}
}
Vybíráme 3 hlavní barvy. Náhodná základní barva a dvě doplňkové. Doplňkové barvy vybíráme otočením odstínu o 30 a 60 stupňů od základny.
Poté nastavíme 3 odstíny jako vlastní vlastnosti v DOM a definujeme randomColor
funkce. randomColor
vrátí náhodnou barvu HSL kompatibilní s Pixi při každém spuštění. Použijeme to pro naše koule.
Pojďme definovat ColorPalette
instance předtím, než vytvoříme naše koule:
const colorPalette = new ColorPalette();
Potom můžeme dát každé kouli náhodnou výplň při stvoření:
const orb = new Orb(colorPalette.randomColor());
Pokud zkontrolujete prohlížeč, měli byste nyní vidět nějakou barvu!
Pokud zkontrolujete kořenový adresář html
prvek v DOM, měli byste také vidět, že byly nastaveny některé uživatelské vlastnosti. Nyní jsme připraveni přidat nějaké označení a styly pro stránku.
Vytvoření zbytku stránky
Úžasný! Takže naše animace je kompletní. Vypadá skvěle a běží opravdu rychle díky Pixi. Nyní musíme vytvořit zbytek vstupní stránky.
Přidání označení
Nejprve do našeho souboru HTML přidejte nějaké označení:
<!-- Overlay -->
<div class="overlay">
<!-- Overlay inner wrapper -->
<div class="overlay__inner">
<!-- Title -->
<h1 class="overlay__title">
Hey, would you like to learn how to create a
<span class="text-gradient">generative</span> UI just like this?
</h1>
<!-- Description -->
<p class="overlay__description">
In this tutorial we will be creating a generative “orb” animation using pixi.js, picking some lovely random colors, and pulling it all together in a nice frosty UI.
<strong>We're gonna talk accessibility, too.</strong>
</p>
<!-- Buttons -->
<div class="overlay__btns">
<button class="overlay__btn overlay__btn--transparent">
Tutorial out Feb 2, 2021
</button>
<button class="overlay__btn overlay__btn--colors">
<span>Randomise Colors</span>
<span class="overlay__btn-emoji">🎨</span>
</button>
</div>
</div>
</div>
Tady se nic moc šíleného neděje, takže se v tom nebudu moc rýpat. Pojďme k našemu CSS:
Přidání CSS
:root {
--dark-color: hsl(var(--hue), 100%, 9%);
--light-color: hsl(var(--hue), 95%, 98%);
--base: hsl(var(--hue), 95%, 50%);
--complimentary1: hsl(var(--hue-complimentary1), 95%, 50%);
--complimentary2: hsl(var(--hue-complimentary2), 95%, 50%);
--font-family: "Poppins", system-ui;
--bg-gradient: linear-gradient(
to bottom,
hsl(var(--hue), 95%, 99%),
hsl(var(--hue), 95%, 84%)
);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
max-width: 1920px;
min-height: 100vh;
display: grid;
place-items: center;
padding: 2rem;
font-family: var(--font-family);
color: var(--dark-color);
background: var(--bg-gradient);
}
.orb-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
}
strong {
font-weight: 600;
}
.overlay {
width: 100%;
max-width: 1140px;
max-height: 640px;
padding: 8rem 6rem;
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.375);
box-shadow: 0 0.75rem 2rem 0 rgba(0, 0, 0, 0.1);
border-radius: 2rem;
border: 1px solid rgba(255, 255, 255, 0.125);
}
.overlay__inner {
max-width: 36rem;
}
.overlay__title {
font-size: 1.875rem;
line-height: 2.75rem;
font-weight: 700;
letter-spacing: -0.025em;
margin-bottom: 2rem;
}
.text-gradient {
background-image: linear-gradient(
45deg,
var(--base) 25%,
var(--complimentary2)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
-moz-background-clip: text;
-moz-text-fill-color: transparent;
}
.overlay__description {
font-size: 1rem;
line-height: 1.75rem;
margin-bottom: 3rem;
}
.overlay__btns {
width: 100%;
max-width: 30rem;
display: flex;
}
.overlay__btn {
width: 50%;
height: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.875rem;
font-weight: 600;
color: var(--light-color);
background: var(--dark-color);
border: none;
border-radius: 0.5rem;
cursor: not-allowed;
transition: transform 150ms ease;
outline-color: hsl(var(--hue), 95%, 50%);
}
.overlay__btn--colors:hover {
transform: scale(1.05);
cursor: pointer;
}
.overlay__btn--transparent {
background: transparent;
color: var(--dark-color);
border: 2px solid var(--dark-color);
border-width: 2px;
margin-right: 0.75rem;
outline: none;
}
.overlay__btn-emoji {
margin-left: 0.375rem;
}
@media only screen and (max-width: 1140px) {
.overlay {
padding: 8rem 4rem;
}
}
@media only screen and (max-width: 840px) {
body {
padding: 1.5rem;
}
.overlay {
padding: 4rem;
height: auto;
}
.overlay__title {
font-size: 1.25rem;
line-height: 2rem;
margin-bottom: 1.5rem;
}
.overlay__description {
font-size: 0.875rem;
line-height: 1.5rem;
margin-bottom: 2.5rem;
}
}
@media only screen and (max-width: 600px) {
.overlay {
padding: 1.5rem;
}
.overlay__btns {
flex-wrap: wrap;
}
.overlay__btn {
width: 100%;
font-size: 0.75rem;
margin-right: 0;
}
.overlay__btn:first-child {
margin-bottom: 1rem;
}
}
Klíčovou částí této šablony stylů je definování uživatelských vlastností v :root
. Tyto vlastní vlastnosti využívají hodnoty, které jsme nastavili pomocí našeho ColorPalette
třída.
Pomocí již definovaných vlastních vlastností 3 odstínů vytvoříme následující:
-
--dark-color
– Chcete-li použít pro všechny naše styly textu a primárních tlačítek, je to téměř černá s nádechem našeho základního odstínu. To pomáhá, aby naše barevná paleta působila soudržně. -
--light-color
- K použití místo čistě bílé. To je v podstatě stejné jako tmavá barva, téměř bílá s nádechem našeho základního odstínu. -
--complimentary1
– Naše první doplňková barva naformátovaná podle HSL přátelského ke CSS. -
--complimentary2
- Naše druhá doplňková barva, naformátovaná na HSL přátelské ke CSS. -
--bg-gradient
- Jemný lineární gradient založený na našem základním odstínu. Toto používáme pro pozadí stránky.
Tyto hodnoty pak aplikujeme v celém našem uživatelském rozhraní. Pro styly tlačítek, barvy obrysu, dokonce i efekt přechodu textu.
Poznámka k usnadnění
V tomto tutoriálu jsme téměř nastavit naše barvy a nechat je volně běžet. V tomto případě bychom měli být v pořádku s ohledem na výběr designu, který jsme učinili. Při výrobě se však vždy ujistěte, že splňujete alespoň pokyny pro barevný kontrast WCAG 2.0 .
Náhodné změny barev v reálném čase
Naše uživatelské rozhraní a animace na pozadí jsou nyní dokončeny. Vypadá to skvěle a pokaždé, když stránku obnovíte, uvidíte novou barevnou paletu/animaci orb.
Bylo by dobré, kdybychom mohli náhodně barvy bez osvěžení. Naštěstí je to díky našim uživatelským vlastnostem/nastavení palety barev jednoduché.
Přidejte tento malý úryvek do svého JavaScriptu:
document
.querySelector(".overlay__btn--colors")
.addEventListener("click", () => {
colorPalette.setColors();
colorPalette.setCustomProperties();
orbs.forEach((orb) => {
orb.fill = colorPalette.randomColor();
});
});
S tímto úryvkem nasloucháme události kliknutí na našem primárním tlačítku. Po kliknutí vygenerujeme novou sadu barev, aktualizujeme vlastní vlastnosti CSS a nastavíme výplň každé koule na novou hodnotu.
Protože jsou vlastní vlastnosti CSS reaktivní, celé naše uživatelské rozhraní se aktualizuje v reálném čase. Mocná věc.
To je vše, přátelé
Hurá, zvládli jsme to! Doufám, že jste se bavili a něco se z tohoto návodu naučili.
Náhodné barevné palety mohou být pro většinu aplikací trochu experimentální, ale je zde toho hodně, co lze ubrat. Zavedení prvku náhody může být skvělým doplňkem vašeho procesu navrhování.
Ani s generativní animací nikdy neuděláte chybu.
Sledujte na Twitteru @georgedoescode pro více kreativního kódování / front-end vývojového obsahu.
Vytvoření tohoto článku a ukázky trvalo přibližně 12 hodin. Pokud byste chtěli podpořit moji tvorbu, můžete mi koupit ☕ ❤️