创建生成式着陆页和基于 WebGL 的背景动画
最近我去了趟遥远的Dribbble世界,看到了一些神奇的东西。到处都是毛茸茸的光球和美丽如玻璃般的界面。真是太美了!
这让我想到,如果用这种风格 创建一个生成式落地页,岂不是很酷?
最终结果
首先,这里有一个视觉化的TL;DR(太长不看版)。
色彩搭配在一定限制条件下是随机的。彩色光球仿佛拥有自己的意识,自由移动。正是这些随机元素,使我们的落地页具有生成性。
如果你对生成艺术/设计还不熟悉,这里有一篇Ali Spittel和James Reichard撰写的优秀入门指南。
喜欢你所看到的吗?让我们一起建造吧!
先决条件
要充分利用本教程,您需要熟练掌握 HTML、CSS 和 JavaScript 的编写。
如果你读过“WebGL”相关内容后,被着色器搞得晕头转向,别担心。我们将使用PixiJS来抽象化那些令人望而生畏的部分。如果你之前没用过 Pixi,本教程也是一个很好的入门指南。
创建背景动画
我们首先要构建的是球体。要创建它们,我们需要一些库/包。让我们先把这些枯燥的部分处理完,然后把它们添加到项目中。
套餐概述
以下是我们将要使用的库/软件包的简要概述。
- PixiJS是一个基于 WebGL 构建的强大图形库,我们将使用它来渲染我们的球体。
- KawaseBlurFilter - 一款用于实现超平滑模糊效果的 PixiJS 滤镜插件。
- SimplexNoise——用于生成自相似的随机数流。稍后会详细介绍。
- hsl-to-hex - 一个用于将 HSL 颜色转换为 HEX 的小型 JS 实用程序。
- debounce - 一个 JavaScript防抖函数。
软件包安装
如果您正在使用 CodePen 进行演示,请将以下导入语句添加到您的 JavaScript 文件中,即可开始使用:
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";
如果你在自己的环境中操作,可以使用以下命令安装所需的软件包:
npm i pixi.js @pixi/filter-kawase-blur simplex-noise hsl-to-hex debounce
然后您可以像这样导入它们:
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";
注意:在 CodePen 之外,您需要像 Webpack 或 Parcel 这样的构建工具来处理这些导入。
一张空白的(Pixi)画布
太棒了,我们现在拥有了开始所需的一切。让我们从<canvas>在 HTML 中添加一个元素开始吧:
<canvas class="orb-canvas"></canvas>
接下来,我们可以创建一个新的 Pixi 实例,并将 canvas 元素作为其“视图” (Pixi 将在此渲染)。我们将我们的实例命名为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
});
如果你检查 DOM 并调整浏览器窗口大小,你应该会看到 canvas 元素调整大小以适应窗口。神奇吧!
一些有用的工具
在继续之前,我们应该在 JavaScript 中添加一些实用函数。
// 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;
}
如果你之前看过我的教程,可能已经对这些很熟悉了。我有点着迷……
random将返回一个限定范围内的随机数。例如,“给我一个介于 5 和 10 之间的随机数”。
map将一个数字从一个范围映射到另一个范围。例如,如果数字 (0.5) 通常存在于 0 到 1 的范围内,而我们将其映射到 0 到 100 的范围,则该数字变为 50。
如果你是第一次使用这两个工具,我鼓励你稍微尝试一下。它们将是你生成式编程之旅的得力助手!将它们粘贴到控制台并尝试不同的输出结果是一个很好的起点。
创建 Orb 类
现在,我们应该拥有创建球体动画所需的一切。首先,让我们创建一个Orb类:
// 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)
);
}
}
我们的Orb图形是一个简单的圆,存在于二维空间中。
它有 x 轴和 ay 轴坐标、半径、填充颜色、缩放值(相对于原始半径的大小)以及一组边界。它的边界定义了它可以移动的区域,就像一组虚拟墙。这将防止球体过于靠近我们的文本。
您可能会注意到上面的代码片段中使用了一个不存在的setBounds函数。这个函数将定义我们的球体存在的虚拟约束。让我们把它添加到Orb类中:
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
}
};
}
好的,太棒了!一切都在顺利进行!接下来,我们应该在类中添加一个update`and`函数。这两个函数都会在每个动画帧上运行。稍后会详细介绍。renderOrb
更新函数将定义球体的位置和大小如何随时间变化。渲染函数将定义球体在屏幕上的显示方式。
首先,以下是该update函数:
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;
}
为了使该函数运行,我们还必须定义它simplex。为此,请在Orb类定义之前添加以下代码片段:
// Create a new simplex noise instance
const simplex = new SimplexNoise();
这里有很多关于“噪音”的讨论。我知道对某些人来说,这可能是一个陌生的概念。
本教程不会深入探讨噪声,但我推荐您先观看Daniel Shiffman 的这段视频作为入门。如果您对噪声概念还不熟悉,请暂停本文,观看视频,然后再回来!
简而言之,噪声是生成自相似随机数的绝佳方法。这些随机数非常适合动画制作,因为它们可以创造出流畅而又不可预测的运动。
下图来自《代码的本质》Math.random() ,展示了传统随机数(例如)和带噪声的随机数 之间的区别:
此处的函数update利用噪声来随时间调制光球的x、y和scale属性。我们根据xOff和yOff位置选择噪声值。然后,我们使用map缩放函数将这些值(始终介于 -1 和 1 之间)缩放到新的范围。
结果如何?球体始终会在其边界内漂移。它的大小在一定范围内随机变化。球体的行为不可预测。这里没有关键帧或固定值。
这一切都很好,但我们仍然什么也看不到!让我们通过将render函数添加到Orb类中来解决这个问题:
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 每一帧都会在画布上绘制一个新的圆。
您可能会注意到圆的 xx和yy 值均为 0。这是因为我们移动的是graphics元素本身,而不是元素内的圆。
这是为什么呢?
假设你想扩展这个项目,渲染一个更复杂的球体。你的新球体现在由超过 100 个圆组成。移动整个图形实例比移动其中的每个元素要简单得多。这样做也可能带来一些性能提升。
创造一些光球!
是时候让我们的Orb类派上用场了。让我们创建 10 个全新的 orb 实例,并将它们放入一个orbs数组中:
// 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);
}
我们正在调用函数app.stage.addChild将每个图形实例添加到画布中。这类似于document.appendChild()对 DOM 元素进行操作。
动画!或者,没有动画?
现在我们有了10个新的光球,可以开始制作它们的动画了。不过,我们不能想当然地认为每个人都想要动态背景。
构建这类页面时,尊重用户偏好至关重要。在本例中,如果用户已prefers-reduced-motion设置,我们将渲染静态背景。
以下是如何设置一个能够遵循用户偏好的 Pixi 动画循环:
// 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();
});
}
当我们调用该函数时app.ticker.add(function),我们告诉 Pixi 以大约每秒 60 帧的速度重复执行该函数。在我们的例子中,如果用户希望减少动态效果,我们只会运行update并渲染一次光球。
添加上述代码片段后,您应该会在浏览器中看到类似这样的内容:
太棒了!开始行动了!信不信由你,我们快要成功了。
添加模糊效果
现在我们的光球看起来有点……刺眼。让我们通过给 Pixi 画布添加模糊滤镜来解决这个问题。这其实很简单,但却能极大地改善视觉效果。
app在你的定义下方添加这行代码:
app.stage.filters = [new KawaseBlurFilter(30, 10, true)];
现在,如果你查看浏览器,你应该会看到一些柔和得多的光球!
看起来很棒。我们来加点颜色吧。
使用 HSL 的生成式调色板
为了给我们的项目增添一些色彩,我们将创建一个ColorPalette类。这个类将定义一组颜色,我们不仅可以用这些颜色填充球体,还可以设置整个页面的样式。
我在处理颜色时总是使用 HSL 色值。它比十六进制色值更直观,而且非常适合生成式创作。方法如下:
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
);
}
}
我们选取三种主色:一种随机的底色和两种互补色。互补色是通过将底色的色调分别旋转30度和60度得到的。
然后,我们将这三种色调设置为 DOM 中的自定义属性,并定义一个randomColor函数。randomColor该函数每次运行时都会返回一个随机的、与 Pixi 兼容的 HSL 颜色。我们将用它来制作光球。
ColorPalette在创建球体之前,让我们先定义一个实例:
const colorPalette = new ColorPalette();
然后我们可以为每个球体在创建时随机填充:
const orb = new Orb(colorPalette.randomColor());
如果您查看浏览器,现在应该可以看到一些颜色了!
如果您检查 DOM 中的根html元素,应该也会看到一些自定义属性已被设置。现在我们可以为页面添加一些标记和样式了。
构建页面的其余部分
太棒了!我们的动画完成了。效果很棒,而且多亏了 Pixi,运行速度非常快。现在我们需要继续构建着陆页的其余部分。
添加标记
首先,让我们在 HTML 文件中添加一些标记:
<!-- 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>
这里没什么太复杂的情况,所以我就不深入探讨了。我们继续来看CSS:
添加 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;
}
}
此样式表的关键部分是定义自定义属性:root。这些自定义属性使用我们通过ColorPalette类设置的值。
利用已定义的 3 个色调自定义属性,我们创建以下内容:
--dark-color- 我们将所有文本和主要按钮样式都使用这种近乎黑色、略带基础色调的颜色。这有助于使我们的调色板看起来更加协调一致。--light-color- 可代替纯白色使用。这种颜色与深色非常相似,几乎是白色,带有一点我们基础色调的痕迹。--complimentary1- 我们的第一个互补色,格式为 CSS 友好的 HSL。--complimentary2- 我们的第二个互补色,格式为 CSS 友好的 HSL。--bg-gradient- 基于我们基础色调的微妙线性渐变。我们将其用作页面背景。
然后,我们将这些值应用到整个用户界面中,包括按钮样式、轮廓颜色,甚至是渐变文本效果。
关于无障碍功能的说明
在本教程中,我们几乎已经设定好了颜色,并让它们自由发挥。鉴于我们所做的设计选择,这种情况应该没问题。但在生产环境中,务必确保至少符合WCAG 2.0 颜色对比度指南。
实时随机化颜色
我们的用户界面和背景动画已经完成。效果很棒,每次刷新页面时,您都会看到新的调色板/光球动画。
如果能在不刷新页面的情况下随机显示颜色就更好了。幸运的是,得益于我们自定义的属性/调色板设置,这很容易实现。
将以下代码片段添加到您的 JavaScript 代码中:
document
.querySelector(".overlay__btn--colors")
.addEventListener("click", () => {
colorPalette.setColors();
colorPalette.setCustomProperties();
orbs.forEach((orb) => {
orb.fill = colorPalette.randomColor();
});
});
这段代码监听主按钮的点击事件。点击后,我们会生成一组新的颜色,更新 CSS 自定义属性,并将每个圆环的填充值设置为新值。
由于 CSS 自定义属性是响应式的,我们的整个用户界面将实时更新。非常强大。
就这些了,各位。
太棒了,我们成功了!希望你们从这个教程中学到东西,并且玩得开心。
随机配色方案对大多数应用来说可能有点实验性,但其中蕴含着很多值得借鉴之处。引入一些随机元素或许能为你的设计流程增添不少亮点。
生成式动画也永远不会出错。
关注推特账号@georgedoescode,获取更多创意编程/前端开发内容。
这篇文章和演示大概花了12个小时制作完成。如果你想支持我的工作,可以请我喝杯咖啡☕❤️
文章来源:https://dev.to/georgedoescode/create-a-generative-landing-page-webgl-powered-background-animation-3nl0



