mcpField
M
motion-background

motion-background

github.com/iart-ai设计与 UI1722.9k 次安装18 周前更新
代码仓库

关于 motion-background

motion-background 是用于制作内容背后动态背景的技能,提供 CSS、GLSL 和 Canvas 实现方案。它强调循环衔接、文字可读性、减少动态效果偏好及性能控制。

主要功能

  1. 01CSS 渐变与关键帧动画
  2. 02Three.js GLSL 流动着色器
  3. 03Canvas 粒子与连线
  4. 04无缝循环与静帧截图
  5. 05减少动态效果及暂停控制

适用场景

  1. 01为落地页制作柔和动态背景
  2. 02为登录页或空状态添加氛围
  3. 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>

验证流程——渲染 → 冻结 → 截图 → 检查:

  1. 在一个循环周期内,分别打开文件并冻结在开始/中间/结束时刻:…/bg.html?t=0、?t=<period/2>、?t=<period>。
  2. 为每个冻结的画面截图。
  3. 检查视觉效果(细腻、符合品牌风格、无缝)和瑕疵——每一帧中的文字对比度都足够,渐变/着色器没有色带,循环接缝(t=0 与 t=period)一致,且没有 GPU/控制台错误。
npx playwright screenshot --wait-for-timeout=600 "file://$PWD/bg.html?t=12" frame-mid.png

完成前:

  1. 可在浏览器中独立打开——没有控制台/WebGL 错误,也没有缺失的 CDN 资源。
  2. 只有一个驱动器;?t=N 可冻结到精确帧(着色器仅渲染一帧,canvas 仿真以确定性方式步进)。
  3. 已在开始/中间/结束时刻截图——前景文字始终清晰可读,没有色带,循环接缝不可见。
  4. 遵循 prefers-reduced-motion(渲染一帧静态画面并停止循环),并在元素离屏/页面隐藏时暂停。
  5. 将 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 极光着色器、针对网格优化且带鼠标排斥效果的星群场、可完美循环的噪声流动技术,以及一个将上述效果封装起来的完整动态效果减少设置 + 离屏暂停管理器。

评论

支持 Markdown