发布于 2026-01-05 1 阅读
0

创建生成式着陆页和基于 WebGL 的背景动画

创建生成式着陆页和基于 WebGL 的背景动画

最近我去了趟遥远的Dribbble世界,看到了一些神奇的东西。到处都是毛茸茸的光球和美丽如玻璃般的界面。真是太美了! 

这让我想到,如果用这种风格 创建一个生成式落地页,岂不是很酷?


最终结果 

首先,这里有一个视觉化的TL;DR(太长不看版)。

您也可以在这里查看完整的页面示例。

色彩搭配在一定限制条件下是随机的。彩色光球仿佛拥有自己的意识,自由移动。正是这些随机元素,使我们的落地页具有生成性。

如果你对生成艺术/设计还不熟悉,这里有一篇Ali SpittelJames 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";


Enter fullscreen mode Exit fullscreen mode

如果你在自己的环境中操作,可以使用以下命令安装所需的软件包:



npm i pixi.js @pixi/filter-kawase-blur simplex-noise hsl-to-hex debounce


Enter fullscreen mode Exit fullscreen mode

然后您可以像这样导入它们:



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";


Enter fullscreen mode Exit fullscreen mode

注意:在 CodePen 之外,您需要像 Webpack 或 Parcel 这样的构建工具来处理这些导入。

一张空白的(Pixi)画布 

太棒了,我们现在拥有了开始所需的一切。让我们从<canvas>在 HTML 中添加一个元素开始吧:



<canvas class="orb-canvas"></canvas>


Enter fullscreen mode Exit fullscreen mode

接下来,我们可以创建一个新的 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
});


Enter fullscreen mode Exit fullscreen mode

如果你检查 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;
}


Enter fullscreen mode Exit fullscreen mode

如果你之前看过我的教程,可能已经对这些很熟悉了。我有点着迷……

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)
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

我们的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
      }
  };
}


Enter fullscreen mode Exit fullscreen mode

好的,太棒了!一切都在顺利进行!接下来,我们应该在类中添加一个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;
}


Enter fullscreen mode Exit fullscreen mode

为了使该函数运行,我们还必须定义它simplex。为此,请在Orb类定义之前添加以下代码片段:



// Create a new simplex noise instance
const simplex = new SimplexNoise();


Enter fullscreen mode Exit fullscreen mode

这里有很多关于“噪音”的讨论。我知道对某些人来说,这可能是一个陌生的概念。 

本教程不会深入探讨噪声,但我推荐您先观看Daniel Shiffman 的这段视频作为入门。如果您对噪声概念还不熟悉,请暂停本文,观看视频,然后再回来!

简而言之,噪声是生成自相似随机数的绝佳方法。这些随机数非常适合动画制作,因为它们可以创造出流畅而又不可预测的运动。 

下图来自《代码的本质》Math.random() ,展示了传统随机数(例如)和带噪声的随机数 之间的区别:

与平滑的噪声分布相比,参差不齐的随机分布更为明显。

此处的函数update利用噪声来随时间调制光球的xyscale属性。我们根据xOffyOff位置选择噪声值。然后,我们使用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();
}


Enter fullscreen mode Exit fullscreen mode

render  每一帧都会在画布上绘制一个新的圆。 

您可能会注意到圆的 xxyy 值均为 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);
}


Enter fullscreen mode Exit fullscreen mode

我们正在调用函数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();
  });
}


Enter fullscreen mode Exit fullscreen mode

当我们调用该函数时app.ticker.add(function),我们告诉 Pixi 以大约每秒 60 帧的速度重复执行该函数。在我们的例子中,如果用户希望减少动态效果,我们只会运行update并渲染一次光球。  

添加上述代码片段后,您应该会在浏览器中看到类似这样的内容: 

一组黑色圆圈

太棒了!开始行动了!信不信由你,我们快要成功了。 

添加模糊效果 

现在我们的光球看起来有点……刺眼。让我们通过给 Pixi 画布添加模糊滤镜来解决这个问题。这其实很简单,但却能极大地改善视觉效果。

app在你的定义下方添加这行代码:



app.stage.filters = [new KawaseBlurFilter(30, 10, true)];


Enter fullscreen mode Exit fullscreen mode

现在,如果你查看浏览器,你应该会看到一些柔和得多的光球! 

一组模糊的黑色圆圈

看起来很棒。我们来加点颜色吧。 


使用 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
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

我们选取三种主色:一种随机的底色和两种互补色。互补色是通过将底色的色调分别旋转30度和60度得到的。

然后,我们将这三种色调设置为 DOM 中的自定义属性,并定义一个randomColor函数。randomColor该函数每次运行时都会返回一个随机的、与 Pixi 兼容的 HSL 颜色。我们将用它来制作光球。 

ColorPalette在创建球体之前,让我们先定义一个实例:



const colorPalette = new ColorPalette();


Enter fullscreen mode Exit fullscreen mode

然后我们可以为每个球体在创建时随机填充:



const orb = new Orb(colorPalette.randomColor());


Enter fullscreen mode Exit fullscreen mode

如果您查看浏览器,现在应该可以看到一些颜色了! 

一组色彩斑斓、模糊不清的圆圈

如果您检查 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>


Enter fullscreen mode Exit fullscreen mode

这里没什么太复杂的情况,所以我就不深入探讨了。我们继续来看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;
  }
}


Enter fullscreen mode Exit fullscreen mode

此样式表的关键部分是定义自定义属性: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();
    });
  });


Enter fullscreen mode Exit fullscreen mode

这段代码监听主按钮的点击事件。点击后,我们会生成一组新的颜色,更新 CSS 自定义属性,并将每个圆环的填充值设置为新值。

由于 CSS 自定义属性是响应式的,我们的整个用户界面将实时更新。非常强大。


就这些了,各位。

太棒了,我们成功了!希望你们从这个教程中学到东西,并且玩得开心。 

随机配色方案对大多数应用来说可能有点实验性,但其中蕴含着很多值得借鉴之处。引入一些随机元素或许能为你的设计流程增添不少亮点。

生成式动画也永远不会出错。 


关注推特账号@georgedoescode,获取更多创意编程/前端开发内容。  

这篇文章和演示大概花了12个小时制作完成。如果你想支持我的工作,可以请我喝杯咖啡☕❤️

文章来源:https://dev.to/georgedoescode/create-a-generative-landing-page-webgl-powered-background-animation-3nl0