前几天看到一个个人博客首页:https://tblog.mmzhiku.xyz/。
第一眼的感觉是:它很“活”。不是那种只在按钮 hover 时抖一下的活,而是整个页面从加载、入场、滚动、悬停到图片切换,都像有一套自己的节奏。
我一开始也很难准确说出它用了什么动画。后来拆了一圈才发现,它并不是某一个神秘效果,而是很多种动效组合在一起:加载动画、首屏入场、鼠标视差、滚动揭示、滚动钉住、图片快门、WebGL 着色器、hover 微交互、页面转场。
换句话说,它不是“一招鲜”,而是一整套动效语言。
这篇笔记就把它拆开讲一遍:这些动画叫什么,大概怎么实现,以及如果我们自己想学,应该从哪里开始。
先说结论
这个首页的技术组合大致是:
- Astro:静态站点框架。
- GSAP:负责复杂时间线动画。
- ScrollTrigger:负责滚动触发和滚动控制动画。
- WebGL shader:负责文字或图片的像素级视觉特效。
- CSS transition / transform / clip-path:负责大量轻量动效。
- Swup:负责页面切换转场。
它真正厉害的地方不是“用了很高级的库”,而是动效分工很清楚:
- 加载动画负责进入感。
- 首屏入场负责第一印象。
- 鼠标视差负责活物感。
- 滚动揭示负责阅读节奏。
- 滚动钉住负责记忆点。
- hover 微交互负责精致感。
- WebGL 特效负责技术个性。
如果只看效果,很容易被吓住。拆开之后会发现,大部分都是可以一步步学会的。
1. 加载动画:Page Loader
进入页面时,页面先显示一个加载层,里面有一张动图:
<div id="page-loader" class="page-loader page-loader--visible">
<div class="page-loader__animation">
<img src="/assets/images/loading/feibi-loading.webp" alt="">
</div>
</div>
这种叫 Page Loader,也可以叫 Preloader。
它的作用不是单纯“炫一下”,而是遮住页面资源加载时的空白和跳动。尤其是这种首页有很多图片、脚本和动画,如果直接裸加载,用户可能会看到元素一块一块闪出来。加载动画把这个过程包装成一个完整的开场。
实现上通常分三步:
- 默认显示加载层。
- 页面资源准备好后给它加退出动画。
- 动画结束后移除或隐藏加载层。
最简单可以这样写:
.page-loader {
position: fixed;
inset: 0;
z-index: 9999;
display: grid;
place-items: center;
background: #0f1115;
opacity: 1;
transition: opacity .45s ease, visibility .45s ease;
}
.page-loader.is-hidden {
opacity: 0;
visibility: hidden;
}
window.addEventListener("load", () => {
document.querySelector(".page-loader")?.classList.add("is-hidden");
});
这个效果很基础,但它决定了页面的“开门方式”。
2. 首屏入场:Hero Entrance Animation
这个博客首页的首屏不是所有东西一起出现,而是按顺序出现:边框、标题、头像、人物、文本、装饰线条,一层一层铺开。
这类动画通常叫 Hero Entrance Animation,也可以叫 Staggered Reveal,中文可以理解成“首屏错峰入场”。
它的核心不是让东西动,而是让东西“有顺序地出现”。
常见的入场属性有这些:
opacity: 0
y: 60
scale: 0.9
filter: "blur(12px)"
clipPath: "inset(0 0 100% 0)"
最后变成:
opacity: 1
y: 0
scale: 1
filter: "blur(0px)"
clipPath: "inset(0 0 0% 0)"
用 GSAP 大概是这样:
gsap.timeline({ defaults: { ease: "expo.out" } })
.from(".hero-frame", {
opacity: 0,
scaleX: 0,
duration: 1
})
.from(".hero-title", {
opacity: 0,
y: 70,
clipPath: "inset(0 0 100% 0)",
duration: 1.1
}, "-=0.4")
.from(".hero-avatar", {
opacity: 0,
scale: 0.8,
duration: 0.8
}, "-=0.5")
.from(".hero-card", {
opacity: 0,
y: 50,
filter: "blur(10px)",
stagger: 0.12,
duration: 0.9
}, "-=0.4");
这里最值得注意的是 stagger。
stagger 的意思是:一组元素不要同时动,而是间隔一点点依次动。高级感很多时候就来自这个“不要一起动”。
3. 裁切揭示:Clip-path Reveal
这个首页大量使用了类似“幕布揭开”的效果。标题、图片、卡片不是简单淡入,而是从某个方向被裁切出来。
这种叫 Clip-path Reveal。
CSS 里可以这样理解:
.reveal {
opacity: 0;
transform: translateY(30px);
clip-path: inset(0 0 100% 0);
transition:
opacity .8s ease,
transform .8s ease,
clip-path .8s ease;
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
clip-path: inset(0 0 0 0);
}
clip-path: inset(0 0 100% 0) 的意思是:从底部裁掉 100%,所以元素看不见。
clip-path: inset(0 0 0 0) 的意思是:四边都不裁,完整显示。
这个技巧非常实用。它比单纯的 opacity 更有设计感,又比 WebGL 简单很多。
我觉得这是最值得先学的动画之一。
4. 鼠标视差:Mouse Parallax
首屏中间的人物层会跟随鼠标轻微移动。这个叫 Mouse Parallax,中文一般叫“鼠标视差”。
实现原理很简单:
- 监听鼠标位置。
- 把鼠标相对屏幕中心的位置换算成一个
-1到1的比例。 - 根据这个比例移动元素。
- 用
requestAnimationFrame做平滑跟随。
核心代码大概是这样:
const layer = document.querySelector(".character-layer");
let targetX = 0;
let targetY = 0;
let currentX = 0;
let currentY = 0;
document.addEventListener("mousemove", event => {
targetX = (event.clientX - window.innerWidth / 2) / (window.innerWidth / 2) * 18;
targetY = (event.clientY - window.innerHeight / 2) / (window.innerHeight / 2) * 12;
});
function tick() {
currentX += (targetX - currentX) * 0.08;
currentY += (targetY - currentY) * 0.08;
layer.style.transform = `translate(${currentX}px, ${currentY}px)`;
requestAnimationFrame(tick);
}
tick();
最关键的是这一句:
currentX += (targetX - currentX) * 0.08;
这不是直接跳到目标位置,而是每一帧只追一点点。这个追赶过程就是“阻尼感”。
如果没有它,元素会硬邦邦地跟着鼠标跑。有了它,元素就像有重量。
5. 滚动揭示:Scroll Reveal
往下滚动时,标题、数据卡片、图片区域会依次出现。这类动画叫 Scroll Reveal,也就是“滚动到这里才出现”。
基础版可以用 IntersectionObserver:
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.2
});
document.querySelectorAll(".reveal").forEach(el => {
observer.observe(el);
});
如果要更复杂,就用 GSAP ScrollTrigger:
gsap.from(".data-card", {
opacity: 0,
y: 90,
rotateX: 10,
filter: "blur(10px)",
stagger: 0.18,
duration: 1.15,
ease: "expo.out",
scrollTrigger: {
trigger: ".data-section",
start: "top 82%",
once: true
}
});
这里的 start: "top 82%" 可以读成:当触发元素的顶部到达视口 82% 的位置时,开始动画。
6. 滚动视差:Scroll Parallax
首页中有些图片会在滚动时轻微移动,速度和页面不完全一致。这叫 Scroll Parallax。
它的本质是:滚动进度控制元素位移。
用 GSAP 可以这样写:
gsap.to(".visual-img", {
yPercent: -8,
ease: "none",
scrollTrigger: {
trigger: ".visual-card",
start: "top bottom",
end: "bottom top",
scrub: 1.2
}
});
scrub 的意思是:动画进度跟随滚动进度。
如果 scrub: true,就是严格绑定。scrub: 1.2 则会带一点延迟和平滑感。
滚动视差的诀窍是幅度要小。图片移动太多会晕,移动一点点就够有层次了。
7. 滚动钉住叙事:Pinned Scroll Storytelling
首页下方最复杂的是作品展示区域。它会把一个区域固定在屏幕中,然后用户继续滚动时,里面的图片和面板开始播放一段长动画。
这种叫 Pinned Scroll Animation 或 Scroll-driven Storytelling。
中文可以叫“滚动钉住动画”或“滚动叙事”。
基本结构是:
const timeline = gsap.timeline({
scrollTrigger: {
trigger: ".portfolio-section",
start: "top top",
end: "+=6000",
pin: ".portfolio-viewport",
scrub: true
}
});
timeline
.to(".panel-1", { yPercent: 0 })
.to(".panel-2", { yPercent: 0 })
.to(".panel-3", { yPercent: 0 })
.to(".final-mask", { scaleY: 1 })
.to(".final-image", { opacity: 1, scale: 1 });
这里的 pin 很关键。它让某个区域暂时固定住,不跟着页面滚走。
end: "+=6000" 表示这段动画需要 6000px 的滚动距离来播放完。
这种效果非常适合用在:
- 作品集展示。
- 产品功能介绍。
- 时间线故事。
- 年度总结。
- 视觉冲击型首页。
但它也容易过度。页面内容本来很简单时,硬做长滚动叙事会显得拖沓。
8. 图片快门:Shutter Transition
这个博客的作品展示模块里有 shutter 相关命名。它的效果像是多块面板从上下进入,再合并成最终画面。
这类效果可以叫:
- Shutter Transition
- Panel Reveal
- Image Shutter Effect
- 百叶窗式揭示
- 快门式转场
简化版可以只用 CSS 和 GSAP:
<section class="shutter">
<div class="panel panel-up"><img src="1.webp" alt=""></div>
<div class="panel panel-down"><img src="2.webp" alt=""></div>
<div class="panel panel-up"><img src="3.webp" alt=""></div>
</section>
gsap.fromTo(".panel-up", {
yPercent: 120
}, {
yPercent: 0,
stagger: 0.15,
ease: "power3.out",
scrollTrigger: {
trigger: ".shutter",
start: "top center"
}
});
gsap.fromTo(".panel-down", {
yPercent: -120
}, {
yPercent: 0,
stagger: 0.15,
ease: "power3.out",
scrollTrigger: {
trigger: ".shutter",
start: "top center"
}
});
它的设计关键是方向交错:有的从上来,有的从下来。这样画面会有节奏。
9. WebGL 着色器特效:Shader Effects
这个首页里最“看不懂”的部分,大概就是 WebGL 特效。源码里能看到很多 shader 名字:
glitchrgbShiftpixelatehalftonesinewaveshineduotonetritonewarpTransition
这些不是普通 DOM 动画,而是像素级处理。
可以粗略理解成:
普通 CSS 动画是在移动一个盒子。WebGL shader 是在处理盒子里的每一个像素。
比如 rgbShift 会把红、绿、蓝三个颜色通道稍微错开,于是画面出现故障感。
pixelate 会把坐标取整,于是画面变成像素块。
sinewave 会用正弦函数扭曲横向坐标,于是画面像水波。
如果你现在还没写过动画,不建议一开始就学这个。它很迷人,但学习曲线比较陡。
更合理的路线是:
- 先学 CSS transform 和 transition。
- 再学 GSAP 时间线。
- 再学 ScrollTrigger。
- 最后再学 Three.js / OGL / shader。
WebGL 是锦上添花,不是入门必需品。
10. Hover 微交互:Micro-interaction
首页的数据卡片 hover 时,会出现覆盖层、小圆形扩散、标签弹出。
这类叫 Micro-interaction,中文就是“微交互”。
它看起来不起眼,但很影响页面质感。
简化版:
const card = document.querySelector(".data-card");
const hoverTimeline = gsap.timeline({ paused: true })
.to(".card-overlay", {
opacity: 1,
duration: 0.25
})
.from(".card-pill", {
opacity: 0,
y: 20,
scale: 0.85,
stagger: 0.06,
ease: "back.out(1.7)"
}, "-=0.1");
card.addEventListener("pointerenter", () => hoverTimeline.play());
card.addEventListener("pointerleave", () => hoverTimeline.reverse());
这里有个小技巧:离开时不要重新写一套动画,直接 reverse()。
这会让进入和退出天然对应,手感更顺。
我会怎么复刻一个简化版
如果让我用较低成本复刻这个首页的感觉,我不会一上来做完整 WebGL,也不会先做超长滚动叙事。
我会先做这五件事:
- 首屏背景图 + 人物图。
- 标题、头像、说明文字错峰入场。
- 人物图跟随鼠标轻微视差。
- 下方内容区滚动揭示。
- 项目区做一个简单的快门式图片揭示。
这样已经能拿到原站 70% 的观感。
剩下 30% 再慢慢加:页面转场、WebGL 字体特效、复杂滚动钉住、音乐播放器、搜索框动效。
一个学习顺序
这是我觉得最稳的路线:
第一阶段:CSS 动效基础
先掌握:
transitiontransformopacityfilterclip-pathwill-change
目标:能做 hover、淡入、上浮、图片放大、裁切揭示。
第二阶段:滚动触发
学习:
IntersectionObserver- 给元素加
is-visible - 进入视口后只播放一次
目标:能做普通博客文章卡片的滚动出现。
第三阶段:GSAP
学习:
gsap.from()gsap.to()gsap.fromTo()gsap.timeline()staggerease
目标:能做首屏复杂入场。
第四阶段:ScrollTrigger
学习:
triggerstart / endscrubpinonce
目标:能做滚动揭示、滚动视差、滚动钉住。
第五阶段:Shader
学习:
- canvas / WebGL 基础
- fragment shader
- uv 坐标
- texture
- time uniform
- RGB shift / glitch / pixelate
目标:能给文字或图片加一个轻量视觉特效。
最后:高级感来自节奏,不来自堆效果
看这种首页时,很容易误以为“高级感 = 动画多”。
其实不是。
真正重要的是节奏:什么时候出现,出现多快,哪个先动,哪个后动,哪个跟随滚动,哪个只在 hover 时回应,哪个作为视觉高潮。
动画本身只是语法,节奏才是表达。
如果你想做自己的个人博客首页,不需要一开始就做到这个复杂度。先从一个首屏入场、一个鼠标视差、一个滚动揭示开始。只要这三个做好,页面就已经会从“静态网页”变成“有生命的界面”。
等这些都熟了,再加快门转场、滚动叙事和 WebGL 特效。
一步一步来,动画并不是魔法。它只是把很多小的位移、透明度、裁切、延迟和响应,编排成一个让人愿意继续看的瞬间。