关于 motion-background
motion-background 是用于制作内容背后动态背景的技能,提供 CSS、GLSL 和 Canvas 实现方案。它强调循环衔接、文字可读性、减少动态效果偏好及性能控制。
主要功能
- 01CSS 渐变与关键帧动画
- 02Three.js GLSL 流动着色器
- 03Canvas 粒子与连线
- 04无缝循环与静帧截图
- 05减少动态效果及暂停控制
适用场景
- 01为落地页制作柔和动态背景
- 02为登录页或空状态添加氛围
- 03制作极光、网格或粒子背景
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ iart-ai/ motion-design-skills --skill motion-background安装后在 Claude Code 中输入/motion-background确认已加载。
附带文件README.mdreferences/background-recipes.md
详细介绍
动态背景
置于内容背后、不会喧宾夺主的氛围动态背景。目标是实现细腻、无缝循环的动态效果,同时保持内容清晰易读且性能良好。本技能完全自包含:以下每种技术都附带可运行的代码——CSS、GLSL 和 canvas;使用着色器时,除 Three.js 外不依赖其他外部项。
适用场景
- 带有细微动态效果的首屏/落地页背景;营造区块氛围。
- 登录页/启动页/空状态的动态背景。
- 文本或 UI 后方的渐变、网格、极光、着色器或粒子/星座效果。
选择方案
| 效果 | 技术 | 开销 |
|---|---|---|
| 柔和的动态渐变/网格 | CSS 渐变 + 关键帧 | 最低,无需 JS |
| 流动的极光/有机噪声 | GLSL 片段着色器(Three.js 全屏四边形) | GPU,适中 |
| 星座/漂移的点 | Canvas 2D 粒子 | CPU,随数量增长 |
| 深度/视差 | 使用鼠标驱动偏移的同款着色器/粒子 | 适中 |
如果渐变就能满足需求,优先使用 CSS——它开销最低,也最可靠。需要有机流动的色彩效果时使用着色器;需要星座/网络效果时使用 canvas 粒子。
设计规则
- 细腻:与内容保持较低对比度,缓慢运动(周期较长,循环时长为 8–30 秒),避免与文本争抢注意力。
- 清晰易读:确保前景文本具有足够的对比度;必要时添加遮罩层(
linear-gradient覆盖层或backdrop)。 - 无缝循环:使用周期函数驱动动态效果,使时间回绕时不会出现明显跳变(见下文)。
- 尊重动态效果偏好并节省电量:遵循
prefers-reduced-motion,并在内容滚出视口或标签页隐藏时暂停。
核心技术(内联,可运行)
1. CSS 动态网格渐变(无需 JS)
叠加多个径向渐变,让它们的位置缓慢漂移。在大于视口的渐变上为 background-position 添加动画,即可实现无缝循环。
.bg {
position: fixed; inset: 0; z-index: -1;
background:
radial-gradient(40% 50% at 20% 30%, #5b8cff55, transparent 60%),
radial-gradient(45% 55% at 80% 20%, #b05bff55, transparent 60%),
radial-gradient(50% 60% at 50% 80%, #2de1c255, transparent 60%),
#0b0e1a;
background-size: 200% 200%;
animation: meshmove 24s ease-in-out infinite;
}
@keyframes meshmove {
0%, 100% { background-position: 0% 0%, 100% 0%, 50% 100%; }
50% { background-position: 30% 20%, 70% 30%, 60% 70%; }
}
@media (prefers-reduced-motion: reduce) {
.bg { animation: none; }
}
0% 和 100% 关键帧完全相同,因此循环不会出现接缝。
2. 全屏 GLSL 渐变 + 噪声着色器(Three.js)
在全屏平面上的片段着色器中使用值噪声,实现流动的极光/渐变效果。每帧递增 uTime;要实现循环,请输入经过回绕处理的时间(第 4 节)。
import * as THREE from 'three';
const canvas = document.querySelector('#bg');
const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // cap for perf
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const uniforms = {
uTime: {value: 0},
uRes: {value: new THREE.Vector2()},
uColorA: {value: new THREE.Color('#5b8cff')},
uColorB: {value: new THREE.Color('#b05bff')},
};
const material = new THREE.ShaderMaterial({
uniforms,
vertexShader: `void main(){ gl_Position = vec4(position, 1.0); }`,
fragmentShader: `
precision highp float;
uniform float uTime; uniform vec2 uRes;
uniform vec3 uColorA, uColorB;
// hash + value noise
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }
float noise(vec2 p){
vec2 i = floor(p), f = fract(p);
float a = hash(i), b = hash(i+vec2(1,0));
float c = hash(i+vec2(0,1)), d = hash(i+vec2(1,1));
vec2 u = f*f*(3.0-2.0*f);
return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);
}
float fbm(vec2 p){
float v=0.0, a=0.5;
for(int i=0;i<5;i++){ v += a*noise(p); p*=2.0; a*=0.5; }
return v;
}
void main(){
vec2 uv = gl_FragCoord.xy / uRes.xy;
float n = fbm(uv*3.0 + vec2(uTime*0.05, uTime*0.03));
vec3 col = mix(uColorA, uColorB, smoothstep(0.2, 0.8, n + uv.y*0.3));
gl_FragColor = vec4(col, 1.0);
}`,
});
const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
scene.add(quad);
function resize(){
renderer.setSize(window.innerWidth, window.innerHeight);
uniforms.uRes.value.set(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', resize); resize();
const clock = new THREE.Clock();
let running = true;
function loop(){
if (running){
uniforms.uTime.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
requestAnimationFrame(loop);
}
loop();
3. Canvas 星座粒子
漂移的点在彼此接近时用线连接——经典的“网络”背景。纯 Canvas 2D,无需依赖项。
const canvas = document.querySelector('#stars');
const ctx = canvas.getContext('2d');
let W, H, pts;
const COUNT = 80, LINK = 120;
function init(){
W = canvas.width = innerWidth; H = canvas.height = innerHeight;
pts = Array.from({length: COUNT}, () => ({
x: Math.random()*W, y: Math.random()*H,
vx: (Math.random()-0.5)*0.3, vy: (Math.random()-0.5)*0.3,
}));
}
addEventListener('resize', init); init();
function frame(){
ctx.clearRect(0, 0, W, H);
for (const p of pts){
p.x += p.vx; p.y += p.vy;
if (p.x<0||p.x>W) p.vx*=-1;
if (p.y<0||p.y>H) p.vy*=-1;
ctx.fillStyle = '#9db4ff'; ctx.fillRect(p.x, p.y, 2, 2);
}
for (let i=0;i<COUNT;i++) for (let j=i+1;j<COUNT;j++){
const dx=pts[i].x-pts[j].x, dy=pts[i].y-pts[j].y;
const d=Math.hypot(dx, dy);
if (d<LINK){
ctx.strokeStyle = `rgba(157,180,255,${1-d/LINK})`;
ctx.beginPath(); ctx.moveTo(pts[i].x,pts[i].y); ctx.lineTo(pts[j].x,pts[j].y); ctx.stroke();
}
}
requestAnimationFrame(frame);
}
frame();
对于约 120 个点,O(n²) 连线循环完全够用;数量更多时,应将点按空间哈希划分到网格中,只检测相邻网格单元。
4. 无缝循环技术
对于 canvas/JS,将时间回绕到固定周期,使动态效果精确重复。将循环相位(0→2π)作为周期函数的参数:
const PERIOD = 12; // seconds
const phase = (t % PERIOD) / PERIOD * Math.PI * 2;
const offset = Math.sin(phase) * amp; // returns to start at t = PERIOD
任何仅由 sin/cos 的 phase(或其整数倍)构成的动态效果都能无缝循环。在着色器中,将 uTime = phase,并且只对它使用 sin/cos;对于滚动噪声的渐变,每个周期滚动整数个噪声单元,使图案能够平铺。
5. 减少动态效果 + 滚出视口/隐藏时暂停
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) {
running = false; // render one static frame, then stop
renderer.render(scene, camera);
}
// Pause when the tab is hidden
document.addEventListener('visibilitychange', () => {
running = !document.hidden && !reduce;
});
// Pause when the canvas scrolls offscreen
new IntersectionObserver(([e]) => {
running = e.isIntersecting && !document.hidden && !reduce;
}).observe(canvas);
当 running 为 false 时,在 rAF 循环中跳过渲染(如第 2 节所示)——循环仍会运行,以便轻松恢复,但不会执行任何 GPU/CPU 工作。
交付与验证(独立 HTML)
打包的辅助脚本(
scripts/):scripts/seek-shot.sh anim.html 0 1.5 3会冻结?t=N测试框架并截取每个时刻的画面;scripts/contact-sheet.sh sheet.png frame-*.png会将它们拼成一张图,方便快速查看。参见scripts/README.md。
动态背景以一个可直接在浏览器中打开的 .html 文件交付——所有标记、背景以及(对于着色器/canvas)来自 CDN 的 Three.js 或 canvas JS 都内嵌其中。一个文件就是合适的交付规格。
输出要求:
- 一个
.html文件:背景层 + 一段叠加在背景上的前景示例文字,用于检查可读性/对比度。 - 一个动画驱动器——CSS
@keyframes、着色器的 rAF 循环或 canvas 的 rAF 循环;只能有一个。 - 根据所用技术加入下面的冻结测试框架,以便确定性地截取任意时刻的画面。
冻结测试框架——固定画面以供截图。 根据所用技术匹配实现机制:
<script>
const t = new URLSearchParams(location.search).get("t");
if (t !== null) {
const T = parseFloat(t);
// CSS mesh gradient:
document.querySelectorAll(".bg").forEach(el => {
el.style.animationDelay = (-T) + "s";
el.style.animationPlayState = "paused";
});
// GLSL shader instead? → render exactly one frame at fixed time:
// uniforms.uTime.value = T; renderer.render(scene, camera); running = false;
// Canvas particles? → seed deterministically, step the sim to T, draw once, stop the loop.
}
window.__ready = true; // ready signal for headless wait
</script>
验证流程——渲染 → 冻结 → 截图 → 检查:
- 在一个循环周期内,分别打开文件并冻结在开始/中间/结束时刻:
…/bg.html?t=0、?t=<period/2>、?t=<period>。 - 为每个冻结的画面截图。
- 检查视觉效果(细腻、符合品牌风格、无缝)和瑕疵——每一帧中的文字对比度都足够,渐变/着色器没有色带,循环接缝(
t=0与t=period)一致,且没有 GPU/控制台错误。
npx playwright screenshot --wait-for-timeout=600 "file://$PWD/bg.html?t=12" frame-mid.png
完成前:
- 可在浏览器中独立打开——没有控制台/WebGL 错误,也没有缺失的 CDN 资源。
- 只有一个驱动器;
?t=N可冻结到精确帧(着色器仅渲染一帧,canvas 仿真以确定性方式步进)。 - 已在开始/中间/结束时刻截图——前景文字始终清晰可读,没有色带,循环接缝不可见。
- 遵循
prefers-reduced-motion(渲染一帧静态画面并停止循环),并在元素离屏/页面隐藏时暂停。 - 将
devicePixelRatio限制为 2;动态效果应缓慢且细腻,绝不能比内容更抢眼。
快速参考
| 需求 | 做法 |
|---|---|
| 最省资源的渐变 | CSS 分层 radial-gradient + background-position 关键帧 |
| 有机流动效果 | GLSL fbm 噪声,滚动 uTime |
| 网络/点状效果 | canvas 粒子 + 按距离连接的线条 |
| 无接缝 | 用 sin/cos(phase) 驱动动画,首尾关键帧完全一致 |
| 性能上限 | setPixelRatio(min(dpr, 2)),节流,在移动设备上减少粒子数量 |
| 无障碍 | prefers-reduced-motion 静态回退方案 |
| 节省电量 | 通过 visibilitychange + IntersectionObserver 暂停 |
常见问题
- 绝不能让背景比前景文字更抢眼——如果出现这种情况,就加一层遮罩。
- 在 Retina/4K 屏幕上不限制
devicePixelRatio会严重拖垮 GPU;上限设为 2。 - 只有首尾关键帧完全一致时,
background-position循环才不会出现接缝。 - 只有当滚动距离是周期内的整数个单元格时,噪声滚动着色器才能无缝循环;否则循环时会明显跳变。
- 忘记在元素离屏/页面隐藏时暂停,会导致移动设备持续耗电,即使用户看不到动画也是如此。
参考文件
references/background-recipes.md— 更完整的可直接使用实现:带模糊效果的多色块 CSS 网格、带鼠标视差和真正无缝循环时间回绕的完整 Three.js 极光着色器、针对网格优化且带鼠标排斥效果的星群场、可完美循环的噪声流动技术,以及一个将上述效果封装起来的完整动态效果减少设置 + 离屏暂停管理器。