返回文章列表
2026年7月5日 13:1745 分钟阅读lemon

一个会动的博客首页,是怎么做出来的

#前端

前几天看到一个个人博客首页: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

它的作用不是单纯“炫一下”,而是遮住页面资源加载时的空白和跳动。尤其是这种首页有很多图片、脚本和动画,如果直接裸加载,用户可能会看到元素一块一块闪出来。加载动画把这个过程包装成一个完整的开场。

实现上通常分三步:

  1. 默认显示加载层。
  2. 页面资源准备好后给它加退出动画。
  3. 动画结束后移除或隐藏加载层。

最简单可以这样写:

.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. 监听鼠标位置。
  2. 把鼠标相对屏幕中心的位置换算成一个 -11 的比例。
  3. 根据这个比例移动元素。
  4. 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 AnimationScroll-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 名字:

  • glitch
  • rgbShift
  • pixelate
  • halftone
  • sinewave
  • shine
  • duotone
  • tritone
  • warpTransition

这些不是普通 DOM 动画,而是像素级处理。

可以粗略理解成:

普通 CSS 动画是在移动一个盒子。WebGL shader 是在处理盒子里的每一个像素。

比如 rgbShift 会把红、绿、蓝三个颜色通道稍微错开,于是画面出现故障感。

pixelate 会把坐标取整,于是画面变成像素块。

sinewave 会用正弦函数扭曲横向坐标,于是画面像水波。

如果你现在还没写过动画,不建议一开始就学这个。它很迷人,但学习曲线比较陡。

更合理的路线是:

  1. 先学 CSS transform 和 transition。
  2. 再学 GSAP 时间线。
  3. 再学 ScrollTrigger。
  4. 最后再学 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,也不会先做超长滚动叙事。

我会先做这五件事:

  1. 首屏背景图 + 人物图。
  2. 标题、头像、说明文字错峰入场。
  3. 人物图跟随鼠标轻微视差。
  4. 下方内容区滚动揭示。
  5. 项目区做一个简单的快门式图片揭示。

这样已经能拿到原站 70% 的观感。

剩下 30% 再慢慢加:页面转场、WebGL 字体特效、复杂滚动钉住、音乐播放器、搜索框动效。

一个学习顺序

这是我觉得最稳的路线:

第一阶段:CSS 动效基础

先掌握:

  • transition
  • transform
  • opacity
  • filter
  • clip-path
  • will-change

目标:能做 hover、淡入、上浮、图片放大、裁切揭示。

第二阶段:滚动触发

学习:

  • IntersectionObserver
  • 给元素加 is-visible
  • 进入视口后只播放一次

目标:能做普通博客文章卡片的滚动出现。

第三阶段:GSAP

学习:

  • gsap.from()
  • gsap.to()
  • gsap.fromTo()
  • gsap.timeline()
  • stagger
  • ease

目标:能做首屏复杂入场。

第四阶段:ScrollTrigger

学习:

  • trigger
  • start / end
  • scrub
  • pin
  • once

目标:能做滚动揭示、滚动视差、滚动钉住。

第五阶段:Shader

学习:

  • canvas / WebGL 基础
  • fragment shader
  • uv 坐标
  • texture
  • time uniform
  • RGB shift / glitch / pixelate

目标:能给文字或图片加一个轻量视觉特效。

最后:高级感来自节奏,不来自堆效果

看这种首页时,很容易误以为“高级感 = 动画多”。

其实不是。

真正重要的是节奏:什么时候出现,出现多快,哪个先动,哪个后动,哪个跟随滚动,哪个只在 hover 时回应,哪个作为视觉高潮。

动画本身只是语法,节奏才是表达。

如果你想做自己的个人博客首页,不需要一开始就做到这个复杂度。先从一个首屏入场、一个鼠标视差、一个滚动揭示开始。只要这三个做好,页面就已经会从“静态网页”变成“有生命的界面”。

等这些都熟了,再加快门转场、滚动叙事和 WebGL 特效。

一步一步来,动画并不是魔法。它只是把很多小的位移、透明度、裁切、延迟和响应,编排成一个让人愿意继续看的瞬间。

Thanks for reading.

评论

登录后即可评论

去登录
加载中...