motion-background
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMotion Background
动态背景
Ambient, living backgrounds that sit behind content without stealing focus. The goal is subtle, seamlessly looping motion that stays readable and performant. This skill is self-contained: every technique below ships its own runnable code — CSS, GLSL, and canvas — with no external dependencies beyond Three.js where a shader is involved.
作为内容背后的氛围型动态背景,不会抢夺视觉焦点。目标是实现微妙、无缝循环的动态效果,同时保持内容可读性与性能。此技能完全独立:以下每种技术都附带可直接运行的代码——CSS、GLSL和Canvas代码,除了使用着色器时依赖Three.js外,无其他外部依赖。
When to use
适用场景
- Hero/landing background that subtly moves; section ambience.
- Login/splash/empty-state living backdrops.
- A gradient, mesh, aurora, shader, or particle/constellation field behind text or UI.
- 微妙动态的Hero/着陆页背景;页面区块氛围营造。
- 登录页/启动页/空状态的动态背景。
- 文本或UI背后的渐变、网格、极光、着色器或粒子/星座效果。
Pick an approach
选择实现方案
| Look | Technique | Cost |
|---|---|---|
| Soft animated gradient/mesh | CSS gradients + keyframes | Cheapest, no JS |
| Flowing aurora / organic noise | GLSL fragment shader (Three.js full-screen quad) | GPU, moderate |
| Constellation / drifting dots | Canvas 2D particles | CPU, scales with count |
| Depth / parallax | Same shader/particles with mouse-driven offset | Moderate |
Default to CSS if a gradient suffices — it is the cheapest and most reliable. Use a shader for organic flowing color; use canvas particles for a constellation/network look.
| 视觉效果 | 实现技术 | 性能成本 |
|---|---|---|
| 柔和动画渐变/网格 | CSS渐变 + keyframes | 最低,无需JS |
| 流动极光/有机噪波 | GLSL片段着色器(Three.js全屏四边形) | 占用GPU,中等 |
| 星座/漂移点 | Canvas 2D粒子 | 占用CPU,随粒子数量增加而上升 |
| 深度/视差 | 结合鼠标驱动偏移的着色器/粒子效果 | 中等 |
如果渐变效果足够,优先选择CSS方案——它成本最低且最可靠。需要有机流动色彩时使用着色器;需要星座/网络视觉效果时使用Canvas粒子。
Design rules
设计规则
- Subtle: low contrast versus content, slow motion (long periods, 8–30s loops), nothing that competes with text.
- Readable: keep contrast for foreground text; add a scrim (overlay or
linear-gradient) if needed.backdrop - Seamless loop: drive motion with periodic functions so time wraps with no visible jump (see below).
- Respect motion preferences and battery: honor , and pause when offscreen or the tab is hidden.
prefers-reduced-motion
- 微妙低干扰:与内容对比度低,动画速度缓慢(循环周期8–30秒),不与文本争夺注意力。
- 可读性保障:确保前景文本的对比度;必要时添加遮罩层(叠加层或
linear-gradient)。backdrop - 无缝循环:使用周期函数驱动动画,使时间轴循环时无明显跳变(详见下文)。
- 尊重动效偏好与电量:遵循设置,在背景移出视口或标签页隐藏时暂停动画。
prefers-reduced-motion
Core techniques (inlined, runnable)
核心技术(内嵌可运行代码)
1. Animated CSS mesh gradient (no JS)
1. 动画CSS网格渐变(无需JS)
Layered radial gradients whose positions drift. Animating on a larger-than-viewport gradient loops seamlessly.
background-positioncss
.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; }
}The and keyframes are identical, so the loop has no seam.
0%100%通过分层径向渐变的位置偏移实现动画。在大于视口尺寸的渐变上动画,实现无缝循环。
background-positioncss
.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. Full-screen GLSL gradient + noise shader (Three.js)
2. 全屏GLSL渐变+噪波着色器(Three.js)
A flowing aurora/gradient using value noise in a fragment shader on a full-screen plane. advances each frame; to loop, feed it a wrapped time (section 4).
uTimejs
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();在全屏平面上使用片段着色器中的值噪波实现流动极光/渐变效果。逐帧递增;如需循环,传入包装后的时间值(见第4节)。
uTimejs
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 constellation particles
3. Canvas星座粒子
Drifting points connected by lines when near — the classic "network" background. Pure canvas 2D, no dependencies.
js
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();The O(n²) link loop is fine to ~120 points; above that, spatial-hash into a grid and only test neighboring cells.
漂移的点在靠近时会被线条连接——经典的“网络”背景。纯Canvas 2D实现,无依赖。
js
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();O(n²)的连接循环在粒子数约120以内表现良好;超过该数量时,可将粒子按空间哈希划分到网格中,仅测试相邻单元格内的粒子。
4. Seamless loop technique
4. 无缝循环技术
For canvas/JS, wrap time into a fixed period so motion repeats exactly. Use the loop phase (0→2π) as the argument to periodic functions:
js
const PERIOD = 12; // seconds
const phase = (t % PERIOD) / PERIOD * Math.PI * 2;
const offset = Math.sin(phase) * amp; // returns to start at t = PERIODAny motion built only from / of (or integer multiples) loops seamlessly. In the shader, feed and use only / of it; for noise-scrolled gradients, scroll by an integer number of noise cells per period so the field tiles.
sincosphaseuTime = phasesincos对于Canvas/JS,将时间包装到固定周期内,使动画完全重复。使用循环相位(0→2π)作为周期函数的参数:
js
const PERIOD = 12; // seconds
const phase = (t % PERIOD) / PERIOD * Math.PI * 2;
const offset = Math.sin(phase) * amp; // returns to start at t = PERIOD任何仅基于/(或其整数倍)构建的动画都能实现无缝循环。在着色器中,传入并仅使用其/值;对于噪波滚动渐变,每个周期滚动整数个噪波单元,使纹理可平铺。
sincos(phase)uTime = phasesincos5. Reduced motion + offscreen/hidden pause
5. 减少动效+视口外/隐藏时暂停
js
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);When is false, skip the render inside the rAF loop (as in section 2) — the loop stays alive to resume cheaply, but does no GPU/CPU work.
runningjs
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);当为false时,跳过rAF循环内的渲染(如第2节所示)——循环保持运行以便快速恢复,但不占用GPU/CPU资源。
runningDeliver & verify (standalone HTML)
交付与验证(独立HTML)
Packaged helper ():scripts/freezes thescripts/seek-shot.sh anim.html 0 1.5 3harness and screenshots each moment;?t=Ntiles them for one-glance review. Seescripts/contact-sheet.sh sheet.png frame-*.png.scripts/README.md
A motion background ships as one file that opens directly in a browser — markup, the background, and (for shader/canvas) Three.js or canvas JS from CDN, all inline. One file is the right tier.
.htmlOutput contract:
- One file: the background layer + a sample of foreground text on top to check readability/contrast.
.html - One animation driver — the CSS , the shader rAF loop, or the canvas rAF loop; just one.
@keyframes - Include the freeze harness below, matched to the technique, so any moment can be screenshotted deterministically.
Freeze harness — pin a frame for screenshots. Match the mechanism to the technique:
html
<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>Verify loop — render → freeze → screenshot → check:
- Open the file frozen at start / mid / end across one loop period: ,
…/bg.html?t=0,?t=<period/2>.?t=<period> - Screenshot each frozen frame.
- Check fidelity (subtle, on-brand, seamless) and artifacts — text contrast holds at every frame, no banding in gradients/shader, the loop seam (vs
t=0) matches, no GPU/console errors.t=period
bash
npx playwright screenshot --wait-for-timeout=600 "file://$PWD/bg.html?t=12" frame-mid.pngBefore you finish:
- Opens standalone in a browser — no console/WebGL errors, no missing CDN.
- One driver; freezes the exact frame (shader renders one frame, canvas sim stepped deterministically).
?t=N - Screenshotted at start / mid / end — foreground text stays readable, no banding, loop seam invisible.
- honored (one static frame rendered, loop stopped) + pauses offscreen/hidden.
prefers-reduced-motion - capped at 2; motion is slow and subtle, never out-contrasting content.
devicePixelRatio
打包工具(目录):scripts/可冻结scripts/seek-shot.sh anim.html 0 1.5 3机制并截取每个时刻的截图;?t=N可将截图拼接成一张预览图。详见scripts/contact-sheet.sh sheet.png frame-*.png。scripts/README.md
动态背景应交付为可直接在浏览器中打开的单个文件——包含标记、背景代码,以及(针对着色器/Canvas)来自CDN的Three.js或Canvas JS代码,全部内嵌。单个文件是最优交付形式。
.html输出规范:
- 单个文件:包含背景层+示例前景文本,用于检查可读性/对比度。
.html - 单个动画驱动——CSS 、着色器rAF循环或Canvas rAF循环;仅保留一种。
@keyframes - 包含以下冻结工具,匹配所选技术,以便确定性地截取任意时刻的截图。
冻结工具——固定帧用于截图:根据技术类型选择对应机制:
html
<script>
const t = new URLSearchParams(location.search).get("t");
if (t !== null) {
const T = parseFloat(t);
// CSS网格渐变:
document.querySelectorAll(".bg").forEach(el => {
el.style.animationDelay = (-T) + "s";
el.style.animationPlayState = "paused";
});
// 若使用GLSL着色器?→ 在固定时间渲染一帧:
// uniforms.uTime.value = T; renderer.render(scene, camera); running = false;
// 若使用Canvas粒子?→ 确定性生成种子,将模拟步进至T,绘制一次后停止循环。
}
window.__ready = true; // 无头模式就绪信号
</script>验证循环——渲染→冻结→截图→检查:
- 在一个循环周期的开始/中间/结束时刻打开冻结文件:、
…/bg.html?t=0、?t=<period/2>。?t=<period> - 截取每个冻结帧的截图。
- 检查保真度(微妙、符合品牌风格、无缝)和瑕疵——每个帧的文本对比度都达标,渐变/着色器无条带,循环接缝(vs
t=0)匹配,无GPU/控制台错误。t=period
bash
npx playwright screenshot --wait-for-timeout=600 "file://$PWD/bg.html?t=12" frame-mid.png完成前检查:
- 可在浏览器中独立打开——无控制台/WebGL错误,无CDN缺失。
- 单个驱动;可冻结精确帧(着色器渲染一帧,Canvas模拟确定性步进)。
?t=N - 在开始/中间/结束时刻截图——前景文本保持可读,无条带,循环接缝不可见。
- 遵循设置(渲染一帧静态画面,停止循环)+ 在视口外/隐藏时暂停。
prefers-reduced-motion - 上限设为2;动画缓慢微妙,对比度永远不超过内容。
devicePixelRatio
Quick reference
快速参考
| Need | Do |
|---|---|
| Cheapest gradient | CSS layered |
| Organic flow | GLSL |
| Network/dots | canvas particles + distance-linked lines |
| No seam | drive motion by |
| Perf cap | |
| Accessibility | |
| Save battery | pause on |
| 需求 | 实现方式 |
|---|---|
| 最低成本渐变 | CSS分层 |
| 有机流动效果 | GLSL |
| 网络/点效果 | Canvas粒子+距离触发连线 |
| 无接缝循环 | 用 |
| 性能限制 | |
| 无障碍支持 | |
| 节省电量 | 在 |
Gotchas
注意事项
- Never let the background out-contrast the foreground text — add a scrim if it does.
- Uncapped on retina/4K murders the GPU; cap at 2.
devicePixelRatio - loops only if first and last keyframes match exactly.
background-position - Noise-scrolled shaders loop only if the scroll is an integer number of cells per period; otherwise the loop visibly jumps.
- Forgetting the offscreen/hidden pause drains battery on mobile even when the user can't see it.
- 绝不能让背景的对比度超过前景文本——若出现此情况,添加遮罩层。
- 在视网膜/4K屏幕上不限制会严重消耗GPU;上限设为2。
devicePixelRatio - 只有首尾关键帧完全相同时,循环才无缝。
background-position - 噪波滚动着色器只有在每个周期滚动整数个单元时才会无缝循环;否则循环会出现明显跳变。
- 忘记在视口外/隐藏时暂停动画,会在移动设备上消耗电量,即使用户看不到它。
Reference files
参考文件
- — fuller drop-in implementations: a richer multi-blob CSS mesh with blur, the complete Three.js aurora shader with mouse parallax and a true seamless-loop time wrap, a grid-optimized constellation field with mouse repulsion, a perfectly-looping noise-flow technique, and a complete reduced-motion + offscreen-pause manager wrapping all of them.
references/background-recipes.md
- ——更完整的可直接使用实现:包含带模糊效果的丰富多 blob CSS网格、带鼠标视差和真正无缝循环时间包装的完整Three.js极光着色器、网格优化的带鼠标排斥效果的星座场、完美循环的噪波流动技术,以及封装所有功能的完整减少动效+视口外暂停管理器。
references/background-recipes.md