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.
315 lines
8.2 KiB
Markdown
315 lines
8.2 KiB
Markdown
---
|
|
name: web-effects
|
|
description: 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.
|
|
arguments:
|
|
- name: effect
|
|
description: "Effect: distortion, particles, noise, hover, displacement, gradient, blur (auto-detect if omitted)"
|
|
required: false
|
|
- name: approach
|
|
description: "Approach: css-only, webgl, canvas, library (auto-detect by complexity)"
|
|
required: false
|
|
---
|
|
|
|
# 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.
|
|
|
|
```js
|
|
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)
|
|
|
|
```glsl
|
|
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.
|
|
|
|
```js
|
|
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
|
|
|
|
```jsx
|
|
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:
|
|
|
|
```glsl
|
|
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
|
|
|
|
```css
|
|
.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)
|
|
|
|
```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
|
|
|
|
```css
|
|
.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)
|
|
|
|
```css
|
|
.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)
|
|
|
|
```js
|
|
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
|
|
|
|
```css
|
|
.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
|
|
|
|
```js
|
|
const observer = new IntersectionObserver(([entry]) => {
|
|
if (entry.isIntersecting) startRenderLoop();
|
|
else stopRenderLoop();
|
|
});
|
|
observer.observe(canvasElement);
|
|
```
|
|
|
|
---
|
|
|
|
## Workflow
|
|
|
|
1. **Define the effect** — what visual result is needed?
|
|
2. **Try CSS first** — gradient, blur, clip-path, mix-blend-mode
|
|
3. **Escalate to Canvas/WebGL** — only if CSS cannot achieve it
|
|
4. **Pick library** — curtains.js for DOM-synced, OGL for custom shaders
|
|
5. **Write shader** — keep fragment shaders simple, profile on mobile
|
|
6. **Add IntersectionObserver** — pause offscreen effects
|
|
7. **Test performance** — Chrome DevTools Performance, GPU memory
|
|
8. **Add prefers-reduced-motion** — disable or simplify effects
|