KeiSeiKit-1.0/skills/web-effects/SKILL.md
Parfii-bot a4e667de10 KeiSeiKit-public — clean state
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.
2026-05-01 12:09:03 +08:00

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.
name description required
effect Effect: distortion, particles, noise, hover, displacement, gradient, blur (auto-detect if omitted) false
name description required
approach Approach: css-only, webgl, canvas, library (auto-detect by complexity) 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.

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

  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