本教程教你如何在 Bricks Builder 里,用特定 data 属性中的 JSON 对象运行基础 GSAP 动画。
不是所有人都喜欢动画。但如果你和我一样喜欢——GSAP 就是 JavaScript 生态里 #1 必学库。
GSAP 是高级前端开发者使用的复杂库。要发挥它的全部潜力,你需要扎实的 JavaScript 功底。但如果我们能简化流程,直接在 Bricks Builder 里管理这个库呢?
这就是本教程的目标:带你入门 GSAP 的基础,并提供一组属性,让你在构建器里轻松管理 tween 和 timeline。
这种做法的优势:
- JavaScript Helper 极其轻量,不会对网站性能造成负面影响。
- 兼容所有 GSAP 参数和大多数(甚至全部)GSAP 插件,包括流行的 ScrollTrigger 插件。
- 不需要为每个页面单独处理 init 文件或内联代码——所有选项都在构建器里管理。
- 熟悉这些属性之后,设置 tween 和 timeline 只是几分钟的事。
- 顺便还能学到一点 JavaScript/JSON 的逻辑。
加载 GSAP 库
在这个页面下载库:https://greensock.com/docs/v3/Installation。把压缩版 gsap.min.js 上传到子主题并加载。
如果打算用 ScrollTrigger 插件,记得把 ScrollTrigger.min.js 也上传并加载。
作者偏好的脚本加载方式见这篇文章。
JavaScript
确保下面的代码在 GSAP 库(以及所有插件)初始化之后运行:
// Register the GSAP Plugin
gsap.registerPlugin(ScrollTrigger);
// Run the script after the DOM content is loaded
window.addEventListener('DOMContentLoaded', () => {
// Tweens
const gsapTweens = document.querySelectorAll('[data-tween]');
const tweenFn = (arr) => {
// Loop into each tween
arr.forEach(tween => {
// Get the tween type
const type = tween.dataset.tween;
let selector = tween;
if (tween.dataset.tweenSelector) selector = tween.dataset.tweenSelector;
// Get the tween option parameters
const options = JSON.parse(tween.dataset.tweenOptions);
// Run the tween based on the type of the animation
switch (type) {
case "to":
gsap.to(selector, options);
break;
case "from":
gsap.from(selector, options);
break;
case "fromTo":
gsap.fromTo(selector, options.from, options.to);
break;
};
});
}
// Check if there are tweens on the page
if (gsapTweens.length > 0) tweenFn(gsapTweens);
// Timeline
const gsapContainers = document.querySelectorAll('[data-tl-container="true"]');
const timelineFn = (arr) => {
// Loop into each timeline container
arr.forEach(container => {
// Get the timeline name
const containerName = container.dataset.tlName;
// Get the timeline options if any
let containerOptions;
if (container.dataset.tlOptions) containerOptions = JSON.parse(container.dataset.tlOptions);
// Create an Array where we'll store all the tween options
let tweensOptions = [];
// Query the nested tweens that are part the timeline
const tweens = container.querySelectorAll('[data-tl-tween][data-tl-name=' + containerName + ']');
tweens.forEach(tween => {
// Get the Timeline name related to the tween
const tweenTl = tween.dataset.tlName;
// Get the tween type
let tweenType = tween.dataset.tlTween;
// Set the selector
let tweenSelector = tween;
if (tween.dataset.tlTweenSelector) tweenSelector = tween.dataset.tlTweenSelector;
// Get the order of the tween inside the timeline. Default is 999.
let tweenOrder = 999;
if (tween.dataset.tlTweenOrder) tweenOrder = parseInt(tween.dataset.tlTweenOrder);
// Get the position of the tween inside the timeline. Default is "-=0"
let tweenPosition = "+=0";
if (tween.dataset.tlTweenPosition) tweenPosition = tween.dataset.tlTweenPosition;
// Get the tween options
const tweenOptions = tween.dataset.tlTweenOptions;
const splitOptions = tweenOptions.split(';');
splitOptions.forEach(option => {
option = JSON.parse(option);
let options;
if (option.options) options = option.options;
// override selector
let selector = tweenSelector;
if (option.selector) selector = option.selector;
// override order
let order = tweenOrder;
if (option.order) order = parseInt(option.order);
// override position
let position = tweenPosition;
if (option.position) position = option.position;
// override type
let type = tweenType;
if (option.type) type = option.type;
// Push all our options inside an Array
tweensOptions.push({
selector: selector,
timeline: tweenTl,
type: type,
options: options,
order: order,
position: position,
})
})
})
// Sort the array based on the order parameter
tweensOptions = Object.values(tweensOptions).sort((a, b) => a.order - b.order);
// Initialize the timeline
const tl = gsap.timeline(containerOptions);
// Loop inside our tween options Array
tweensOptions.forEach(el => {
// Add the tween to the timeline
switch (el.type) {
case "to":
tl.to(el.selector, el.options, el.position);
break;
case "from":
tl.from(el.selector, el.options, el.position);
break;
case "fromTo":
tl.fromTo(el.selector, el.options.from, el.options.to, el.position);
break;
}
})
})
}
// Check if there are timelines set on the page
if (gsapContainers.length > 0) timelineFn(gsapContainers);
})
注意这段代码的第一行:我们注册了 ScrollTrigger 插件。如果你不打算用它,可以删掉这一行;用同样的方式也可以注册其他 GSAP 插件。
在 Bricks 里添加动画
这一部分我们来学 GSAP 更常用的功能:
- 设置简单的 tween(from / to / fromTo)
- 创建 timeline 并向其中添加嵌套 tween
- 用 ScrollTrigger 插件把这些动画绑定到滚动
Tweens(补间动画)
简单来说,tween 就是附着在目标(理解为网站上的一个元素)上的单个动画。单个 tween 可以同时动画多个参数:颜色、形状、尺寸、位置等。更多信息参考官方文档。
from
可以把
gsap.from()理解成反向 tween:你定义值从哪开始,然后它动画到当前状态。这非常适合把对象“动画进画面”——因为你可以先把元素摆成最终想要的样子,再从别处飞入。
用我们的 JavaScript Helper 设置 from tween 非常简单:在想要动画的元素上创建一个叫 data-tween 的 data 属性,值填 from:
下一步把所有参数作为一个 JSON 对象插入。推荐用这个免费的在线 JSON 格式化器来转换/校验 JSON。
下面是一个带动画参数的 JSON 对象示例:
{
"right":"0",
"rotation":"720",
"borderRadius":"50%",
"backgroundColor":"#2fff00",
"duration":"2"
}
新建一个叫 data-tween-options 的 data 属性,把 JSON 对象粘进去:
搞定!刷新前端页面,页面加载完成后动画就会立刻运行。
to
最常见的动画类型是 to() tween,因为它允许你定义目标值(大多数人习惯“动画到某个值”的思维方式)。
to 动画和上面说的 “from” 动画运行方式完全一样,唯一区别是在 data-tween 属性里填 to:
记住:from 设置动画的初始状态,to 设置最终状态——所以按需选择参数。
fromTo
gsap.fromTo()让你同时定义动画的起始值和结束值(而 from() 和 to() 分别用当前状态作为起点或终点)。这对完全掌控动画非常有帮助,尤其是当它与其他动画链式组合时。
要设置 fromTo 动画,把 data-tween 属性设为 fromTo:
然后在 JSON 里同时包含 from 和 to 两个参数块。JSON 结构如下:
{
"from":{
...
},
"to":{
...
}
}
一个例子:
{
"from":{
"borderRadius":"0%",
"backgroundColor":"#000000"
},
"to":{
"right":"0",
"rotation":"720",
"borderRadius":"50%",
"backgroundColor":"#2fff00",
"duration":"2"
}
}
把它粘进 data-tween-options 属性:
搞定!
全局选择器(可选)
默认情况下,动画作用在设置了属性的那个元素上。有些场景你想把动画应用到一个自定义选择器——比如某个 class——然后批量作用于所有匹配该选择器的元素。
为此我们引入了一个可选属性 data-tween-selector,值可以是任意自定义选择器:
有了这个可选属性,你就可以在页面上构建全局动画,不用给每个元素重复加同样的属性。它还解锁了更高级的功能,比如 staggers(错落动画)。
Timelines(时间线)
Timeline 是强大的排序工具,作为 tween 和其他 timeline 的容器,方便把它们作为一个整体控制并精确管理时序。没有 Timeline,构建复杂序列会非常笨拙,因为你得给每个动画单独加
delay。
这里的思路是创建彼此关联的 tween。Timeline 在网页开发中的典型用法:按预定义顺序动画不同元素——比如先淡入区块的页眉,再淡入描述文字,然后放大行动召唤(CTA)按钮,最后揭示对应的图片。
用我们的 JavaScript Helper 创建 timeline:需要设置一个 timeline container(时间线容器),把所有嵌套 tween 关联到它。
来看具体怎么做。
容器
时间线容器是所有嵌套 tween 的父元素。可以设在任意 Bricks 元素上。DOM 结构如下:
tween 不要求是直接子元素,但必须是子元素。
设置 timeline 只需要几个 data 属性。
data-tl-container
第一个属性是 data-tl-container,值为 true:
data-tl-name
第二个必填属性是 data-tl-name。随便取个你喜欢的名字,比如 tl1、timeline1 或 headerTimeline。
记住这个 timeline 的名字,后面要把相关 tween 关联到这个 timeline 时需要用到。
如果页面上要设置多个 timeline,务必给每个 timeline 取唯一的名字。
data-tl-options(可选)
还可以创建 data-tl-options 属性,用 JSON 对象给整个 timeline 设置全局参数:
这个属性是可选的。不加的话,timeline 使用 GSAP 默认值。
现在 timeline 设置好了,来给它加几个 tween!
嵌套 tween
在 timeline 里创建嵌套 tween,和上面单个 tween 的配置非常相似,只需插入对应的 timeline 名字来建立关联。
data-tl-name
在 tween 上填上之前设置在时间线容器上的 timeline 名字:
现在这个 tween 就是 timeline 的一部分了。同一 timeline 的所有 tween 都要加这个属性。
data-tl-tween-options
这和之前说的 data-tween-options 属性基本一样,但 JSON 结构略有修改:在 timeline 里,需要把参数包在一个 options 属性里:
JSON 结构如下:
{
"options":{
"...your parameters..."
}
}
示例:
{
"options":{
"right":"0",
"rotation":"720",
"borderRadius":"50%",
"backgroundColor":"#2fff00",
"duration":"2"
}
}
data-tl-tween
这和单个 tween 的 data-tween 属性完全一样:接受 to、from、fromTo 三个值。
当同一元素上要处理多个 tween 时,可以在 JSON 对象里通过 type 属性覆盖这个值:
{
"options":{
"right":"0",
"rotation":"720",
"borderRadius":"50%",
"backgroundColor":"#2fff00",
"duration":"2"
},
"type":"from"
}
data-tl-tween-selector(可选)
和之前 tween 的 data-tween-selector 属性一样,在 timeline 里可以用 data-tl-tween-selector 属性把动画应用到自定义选择器:
也可以在 JSON 对象里通过 selector 属性覆盖这个值:
{
"options":{
"right":"0",
"rotation":"720",
"borderRadius":"50%",
"backgroundColor":"#2fff00",
"duration":"2"
},
"selector":".box2"
}
data-tl-tween-order(可选)
你可能会想显式设置每个 tween 在 timeline 里的顺序——比如先动画容器里最后一个 DOM 元素,再动画其他元素。这种情况下,直接在 data-tl-tween-order 属性里设置自定义顺序即可:
这个属性可选。留空的话,默认顺序值是 999。
也可以在 JSON 对象里通过 order 属性覆盖这个值:
{
"options":{
"right":"0",
"rotation":"720",
"borderRadius":"50%",
"backgroundColor":"#2fff00",
"duration":"2"
},
"order":"1"
}
data-tl-tween-position(可选)
打造精美序列的秘诀是理解
position参数——GSAP 的很多方法都会用到它。这个超灵活的参数控制 tween、标签、回调、暂停甚至嵌套 timeline 的摆放位置,所以你几乎可以把任何东西放到任何序列的任何位置。
timeline 的默认行为是:tween B 在 tween A 播完后才开始。有些场景你想让 tween B 在 tween A 完成**之前(或之后)**开始。这时 data-tl-tween-position 属性就派上用场了。
比如我们想让某个 tween 在上一个 tween 播完 1 秒后再开始,就这样设置属性:
这个属性可选。默认值是 +=0。可以用的各种取值类型见官方文档。
也可以在 JSON 对象里通过 position 属性覆盖这个值:
{
"options":{
"right":"0",
"rotation":"720",
"borderRadius":"50%",
"backgroundColor":"#2fff00",
"duration":"2"
},
"position":"+=1"
}
同一元素上的多个 tween
有时需要在 timeline 的不同位置给同一个元素加多个动画。用我们的 JavaScript Helper 完全没问题:在 data-tl-tween-options 属性里用 “;” 分隔多个 JSON 对象即可。
示例:
{
"options":{
"autoAlpha":"1"
},
"order":"0"
};{
"options":{
"autoAlpha":"0"
},
"order":"2"
};{
"options":{
"autoAlpha":"1"
},
"order":"4"
}
注意这不是合法的 JSON,因为 “;” 不是合法的分隔符。但别担心,脚本会先把这 3 个对象拆开,再逐个解析 JSON。
ScrollTrigger
ScrollTrigger 用极少的代码就能创建令人惊叹的滚动驱动动画。或者触发任何与滚动相关的东西,哪怕和动画完全无关。
如果你按上面的步骤都做完了,你的 tween 和/或 timeline 已经设置好,页面加载后就能正常运行。
但如果你想等访客滚动到网站的特定区域再播放动画,而不是页面加载完就立刻播放呢?
用 GSAP 的 ScrollTrigger 插件 很容易做到。来看怎么做。
元素进入视口时触发动画
要在 tween 或 timeline 里激活 ScrollTrigger,只需在 JSON 选项文件里加几行。结构如下:
{
"scrollTrigger":{
"trigger":"#selector",
"scrub":false,
},
...your normal parameter...
}
用之前的例子演示一下:
{
"scrollTrigger":{
"trigger":"#container1",
"scrub":false,
},
"right":"0",
"rotation":"720",
"borderRadius":"50%",
"backgroundColor":"#2fff00",
"duration":"2"
}
注意这一行:“trigger”:“#container1”。#container1 是触发元素的 ID——当它进入视口可见时,就会触发我们的动画。
这里我们用了 “scrub”:false:触发器激活后完整播放动画。后面我们看看 scrub 设为 true 时会发生什么。
理解视口的触发位置
定义滚动位置极其灵活——比如「该元素中心碰到视口中心时开始,那个元素底部碰到视口底部时结束」,可以用关键字(top、center、bottom、left、right)、百分比、像素,甚至相对值如
"+=300px"。掌握语法后,用起来非常直观。
你可以根据元素在视口中的位置,轻松定义触发器何时激活。
比如想让触发元素的顶部碰到视口中心时激活动画,只需加一行代码:
{
"scrollTrigger":{
"trigger":"#selector",
"scrub":false,
"start":"top center"
},
...your normal parameter...
}
如果想在网站上开启可视化标记(生产站不推荐),直观看到触发起止位置,就在 JSON 对象里加 “markers”:true:
{
"scrollTrigger":{
"trigger":"#selector",
"scrub":false,
"start":"top center",
"markers":true,
},
...your normal parameter...
}
把动画绑定到鼠标滚动
ScrollTrigger 可以在进入/离开定义区域时对动画执行操作(play、pause、resume、restart、reverse、complete、reset),也可以直接把动画绑到滚动条上,让它像 scrubber 一样工作(
scrub: true)。
接下来是见证奇迹的时刻。你可能不想在触发器进入视口时一次性播完整个动画,而是想把动画和访客的鼠标滚动绑定起来。很简单:把 scrub 设为 true:
{
"scrollTrigger":{
"trigger":"#selector",
"scrub":true,
"start":"top center"
},
...your normal parameter...
}
你还可以软化动画和滚动条之间的绑定,让它花一定时间“追上”滚动——比如 scrub: 1 就是花 1 秒追上:
{
"scrollTrigger":{
"trigger":"#selector",
"scrub":1,
"start":"top center"
},
...your normal parameter...
}
滚动时就会产生丝滑的联动效果。
最后的小贴士
不提到这个我没法收尾:Greensock 官方为所有 GSAP 相关动画制作了超好用的速查表:https://greensock.com/cheatsheet/。相信我,这是金子!