Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
8.2 KiB
| name | description | arguments | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| web-effects | Use when building visual web effects — WebGL shaders, image distortion, particles, noise/grain, hover effects, displacement maps. Covers curtains.js, OGL, tsParticles, custom WebGL, and CSS-only effects. |
|
Web Effects Skill
Decision Matrix — Pick Approach
| Effect | CSS Only | Canvas 2D | WebGL (library) | Custom WebGL |
|---|---|---|---|---|
| Image hover distortion | No | No | curtains.js | Possible |
| Particles (decorative) | Limited | Possible | tsParticles | Best perf |
| Noise/grain overlay | Yes | Yes | Shader | Overkill |
| Gradient animation | Yes | Possible | Unnecessary | No |
| Blur/glassmorphism | Yes | No | No | No |
| Displacement on scroll | No | No | curtains.js/OGL | Possible |
| Liquid/fluid effects | No | No | OGL | Yes |
| Image reveal/transition | CSS clip-path | Canvas | curtains.js | Possible |
Rule: Start with CSS. Escalate to Canvas/WebGL only when CSS cannot achieve the effect.
1. Curtains.js — DOM-Driven WebGL
Bundle: ~30KB min+gzip What it does: Converts HTML images/videos/canvases into WebGL textured planes that stay positioned with DOM layout.
Best for: Image hover distortion, displacement effects, WebGL transitions between slides.
import { Curtains, Plane } from "curtainsjs";
const curtains = new Curtains({ container: "#canvas" });
const plane = new Plane(curtains, document.querySelector(".image-wrapper"), {
vertexShader: vertexShaderSource,
fragmentShader: fragmentShaderSource,
uniforms: {
uMouse: { name: "uMouse", type: "2f", value: [0, 0] },
uTime: { name: "uTime", type: "1f", value: 0 },
}
});
plane.onRender(() => { plane.uniforms.uTime.value++; });
document.querySelector(".image-wrapper").addEventListener("mousemove", (e) => {
const rect = e.target.getBoundingClientRect();
plane.uniforms.uMouse.value = [
(e.clientX - rect.left) / rect.width,
1 - (e.clientY - rect.top) / rect.height
];
});
Displacement Shader (Hover Distortion)
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler0;
uniform sampler2D uDisplacement;
uniform vec2 uMouse;
void main() {
vec2 uv = vTextureCoord;
vec4 disp = texture2D(uDisplacement, uv);
float dist = distance(uv, uMouse);
float strength = smoothstep(0.3, 0.0, dist) * 0.05;
uv += disp.rg * strength;
gl_FragColor = texture2D(uSampler0, uv);
}
Note: gpu-curtains is a WebGPU successor worth watching.
2. OGL — Minimal WebGL
Bundle: ~8KB gzip, zero dependencies What it does: Thin WebGL abstraction, you write your own shaders.
Best for: Custom shader effects, fullscreen post-processing, when curtains.js is too opinionated.
import { Renderer, Camera, Program, Mesh, Plane } from "ogl";
const renderer = new Renderer();
const gl = renderer.gl;
document.body.appendChild(gl.canvas);
const camera = new Camera(gl);
camera.position.z = 1;
const geometry = new Plane(gl);
const program = new Program(gl, {
vertex: `
attribute vec3 position;
attribute vec2 uv;
varying vec2 vUv;
void main() { vUv = uv; gl_Position = vec4(position, 1.0); }
`,
fragment: `
precision highp float;
varying vec2 vUv;
uniform float uTime;
void main() {
gl_FragColor = vec4(vec3(sin(uTime + vUv.x * 6.28) * 0.5 + 0.5), 1.0);
}
`,
uniforms: { uTime: { value: 0 } }
});
const mesh = new Mesh(gl, { geometry, program });
function update(t) {
requestAnimationFrame(update);
program.uniforms.uTime.value = t * 0.001;
renderer.render({ scene: mesh, camera });
}
requestAnimationFrame(update);
OGL vs Three.js: OGL is 8KB vs Three.js ~150KB. Use OGL for shader effects where you do not need a scene graph, models, or physics.
3. Particles
tsParticles (Library)
Install: npm i tsparticles
Bundle: ~40KB min+gzip (core), modular
Frameworks: React, Vue, Svelte, Angular, Solid, vanilla
import Particles from "@tsparticles/react";
import { loadSlim } from "@tsparticles/slim";
function Background() {
const init = useCallback(async (engine) => { await loadSlim(engine); }, []);
return (
<Particles
init={init}
options={{
particles: {
number: { value: 50 },
size: { value: { min: 1, max: 3 } },
move: { enable: true, speed: 0.5 },
opacity: { value: { min: 0.1, max: 0.5 } },
links: { enable: true, distance: 150, opacity: 0.2 },
},
detectRetina: true,
}}
/>
);
}
Custom WebGL Particles (Performance-Critical)
When you need 10K+ particles at 60fps, do everything in shaders:
attribute vec3 position;
attribute vec2 velocity;
attribute float life;
uniform float uTime;
uniform float uDelta;
void main() {
vec3 pos = position + vec3(velocity * uDelta, 0.0);
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
gl_PointSize = mix(3.0, 0.0, life);
}
Decision: tsParticles for <1000 particles with config flexibility. Custom WebGL for >1000 particles or specific visual needs.
4. CSS-Only Effects
Animated Gradient
.gradient-bg {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient-shift 15s ease infinite;
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
Noise/Grain Overlay (CSS)
.grain::after {
content: "";
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,...");
opacity: 0.05;
pointer-events: none;
z-index: 9999;
mix-blend-mode: overlay;
}
Glassmorphism
.glass {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px) saturate(150%);
-webkit-backdrop-filter: blur(12px) saturate(150%);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 16px;
}
Image Reveal (Clip-Path)
.reveal {
clip-path: inset(0 100% 0 0);
transition: clip-path 0.8s cubic-bezier(0.77, 0, 0.175, 1);
}
.reveal.visible { clip-path: inset(0 0 0 0); }
Hover Magnetic Effect (JS Required)
const btn = document.querySelector(".magnetic-btn");
btn.addEventListener("mousemove", (e) => {
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
btn.style.transform = `translate(${x * 0.3}px, ${y * 0.3}px)`;
});
btn.addEventListener("mouseleave", () => {
btn.style.transform = "translate(0, 0)";
btn.style.transition = "transform 0.5s ease";
});
5. Performance Rules
GPU-Composited Properties (animate these)
transform — translate, rotate, scale
opacity — fade in/out
filter — blur, brightness
clip-path — reveal/hide
Layout-Triggering Properties (avoid animating)
width, height, top, left, right, bottom
margin, padding, border-width
font-size, line-height
will-change
.about-to-animate { will-change: transform, opacity; }
/* Do NOT: * { will-change: transform; } */
Frame Budget
- 60fps target: 16.66ms per frame
- Pause offscreen: IntersectionObserver to stop animations outside viewport
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) startRenderLoop();
else stopRenderLoop();
});
observer.observe(canvasElement);
Workflow
- Define the effect — what visual result is needed?
- Try CSS first — gradient, blur, clip-path, mix-blend-mode
- Escalate to Canvas/WebGL — only if CSS cannot achieve it
- Pick library — curtains.js for DOM-synced, OGL for custom shaders
- Write shader — keep fragment shaders simple, profile on mobile
- Add IntersectionObserver — pause offscreen effects
- Test performance — Chrome DevTools Performance, GPU memory
- Add prefers-reduced-motion — disable or simplify effects