--- name: motion-background description: This skill should be used when the user asks to "add an animated background", "build a mesh/gradient background", "make an aurora/shader background", "add constellation/particle background", "animated hero background", or "a subtle looping background behind content". Covers CSS mesh gradients, GLSL shader gradients (Three.js), canvas particle constellations, seamless loops, and reduced-motion/performance handling — fully self-contained. version: 0.1.0 --- # Motion 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. ## 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. ## 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. ## 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 (`linear-gradient` overlay or `backdrop`) if needed. - **Seamless loop**: drive motion with periodic functions so time wraps with no visible jump (see below). - **Respect motion preferences and battery**: honor `prefers-reduced-motion`, and pause when offscreen or the tab is hidden. ## Core techniques (inlined, runnable) ### 1. Animated CSS mesh gradient (no JS) Layered radial gradients whose positions drift. Animating `background-position` on a larger-than-viewport gradient loops seamlessly. ```css .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 `0%` and `100%` keyframes are identical, so the loop has no seam. ### 2. Full-screen GLSL gradient + noise shader (Three.js) A flowing aurora/gradient using value noise in a fragment shader on a full-screen plane. `uTime` advances each frame; to loop, feed it a wrapped time (section 4). ```js 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 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