npx claudepluginhub sacredvoid/skillkit --plugin skillkitThis skill uses the workspace's default tool permissions.
Generate interactive generative art and creative coding sketches. Inspired by [Leisure Lab](https://lab.xubh.top/) (43 sketches by KainXu).
Generates algorithmic philosophies and p5.js sketches for generative art, procedural visuals, flow fields, and particle systems.
Generates algorithmic art philosophies and p5.js sketches with seeded randomness, particles, flows, and interactive parameters for generative art requests.
Creates algorithmic art philosophies in Markdown and interactive p5.js sketches (HTML/JS) for generative art, flow fields, particle systems using seeded randomness.
Share bugs, ideas, or general feedback.
Generate interactive generative art and creative coding sketches. Inspired by Leisure Lab (43 sketches by KainXu).
Creative coding = algorithms made visible. Every sketch transforms math into something you can see, touch, and feel.
The output format is flexible: single self-contained HTML file, React component, Next.js page, p5.js sketch, or raw Canvas/WebGL - whatever fits the user's project.
Pick the right approach for the visual:
What visual are you building?
|
+-- Flowing, organic motion?
| +-- Particles following forces --> Flow Field (Perlin noise)
| +-- Smooth color blending --> Fluid Shader (WebGL fragment)
| +-- Swirling paint effect --> Fluid Swirl (iterative sine + spiral distortion)
|
+-- Nature / biology?
| +-- Trees, plants, corals --> L-System (recursive branching)
| +-- Cell division, growth --> Cellular Automata (neighbor rules)
| +-- Veins, rivers, cracks --> Diffusion-Limited Aggregation
|
+-- Geometric / mathematical?
| +-- Mondrian, grids --> Recursive Subdivision
| +-- Space-filling patterns --> Hilbert/Peano Curves
| +-- Spirals, roses --> Polar Coordinate Math
| +-- Vortex patterns --> Parametric equations in (r, theta)
|
+-- Text effects?
| +-- Text from particles --> Canvas pixel sampling + particle system
| +-- Neon glow --> CSS text-shadow stacking or shader
| +-- Glitch effect --> Random clip-path + color channel offset
| +-- Blur/reveal --> CSS filter animation or shader blur
|
+-- Interactive / mouse-driven?
| +-- Particles scatter from cursor --> Distance-based force repulsion
| +-- Drawing / painting --> Canvas stroke with velocity-based width
| +-- Cursor trail effects --> Ring buffer of positions + fade
|
+-- 3D scenes?
| +-- Product showcase --> Three.js + GLTF + orbit controls
| +-- Abstract geometry --> Three.js + custom shaders
| +-- Camera depth blur --> Three.js postprocessing (DOF)
|
+-- Image transformation?
+-- Photo to painting --> Algorithmic brush strokes on canvas
+-- Pointillism / dots --> Pixel sampling + circle rendering
+-- Pixel decomposition --> Grid sampling + animated scatter
The foundation of organic-looking particle motion. Particles follow a vector field generated by noise.
// Core concept: noise(x, y) returns smooth random value 0-1
// Map to angle: angle = noise(x * scale, y * scale) * TWO_PI
// Each particle follows the angle at its grid position
class FlowField {
constructor(canvas, particleCount = 2000) {
this.ctx = canvas.getContext('2d');
this.w = canvas.width;
this.h = canvas.height;
this.scale = 0.005; // Noise zoom (smaller = smoother)
this.speed = 2;
this.particles = Array.from({ length: particleCount }, () => ({
x: Math.random() * this.w,
y: Math.random() * this.h,
prevX: 0, prevY: 0
}));
}
// Attempt at simplex-like noise (for self-contained sketches)
// For production, use a proper noise library
noise2D(x, y) {
const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
return n - Math.floor(n);
}
update(time) {
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.02)'; // Trail fade
this.ctx.fillRect(0, 0, this.w, this.h);
for (const p of this.particles) {
p.prevX = p.x;
p.prevY = p.y;
const angle = this.noise2D(
p.x * this.scale + time * 0.0001,
p.y * this.scale
) * Math.PI * 4;
p.x += Math.cos(angle) * this.speed;
p.y += Math.sin(angle) * this.speed;
// Wrap around edges
if (p.x < 0) p.x = this.w;
if (p.x > this.w) p.x = 0;
if (p.y < 0) p.y = this.h;
if (p.y > this.h) p.y = 0;
this.ctx.strokeStyle = `hsla(${angle * 30}, 70%, 60%, 0.3)`;
this.ctx.beginPath();
this.ctx.moveTo(p.prevX, p.prevY);
this.ctx.lineTo(p.x, p.y);
this.ctx.stroke();
}
}
}
Key parameters to tune:
scale (0.001-0.01): Noise zoom. Smaller = wider, smoother curvesspeed (1-5): How fast particles moveparticleCount: More = denser field, watch performanceRecursive string rewriting that draws botanical structures.
function lSystem(axiom, rules, iterations) {
let current = axiom;
for (let i = 0; i < iterations; i++) {
current = current.split('').map(c => rules[c] || c).join('');
}
return current;
}
function drawLSystem(ctx, instructions, len, angle) {
const stack = [];
for (const char of instructions) {
switch (char) {
case 'F': // Draw forward
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -len);
ctx.stroke();
ctx.translate(0, -len);
break;
case '+': ctx.rotate(angle); break; // Turn right
case '-': ctx.rotate(-angle); break; // Turn left
case '[': // Save state (branch start)
stack.push(ctx.getTransform());
break;
case ']': // Restore state (branch end)
ctx.setTransform(stack.pop());
break;
}
}
}
// Classic tree
const tree = lSystem('F', { 'F': 'FF+[+F-F-F]-[-F+F+F]' }, 4);
// Fern
const fern = lSystem('X', { 'X': 'F+[[X]-X]-F[-FX]+X', 'F': 'FF' }, 6);
Common L-System presets:
| Name | Axiom | Rules | Angle |
|---|---|---|---|
| Binary tree | F | F -> FF+[+F-F]-[-F+F] | 25deg |
| Fern | X | X -> F+[[X]-X]-F[-FX]+X, F -> FF | 25deg |
| Bush | F | F -> F[+FF][-FF]F[-F][+F]F | 20deg |
| Seaweed | F | F -> FF-[-F+F+F]+[+F-F-F] | 22deg |
For GPU-accelerated visuals. Single HTML file with inline shader.
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl');
canvas.width = innerWidth;
canvas.height = innerHeight;
const vertSrc = `attribute vec2 p; void main(){gl_Position=vec4(p,0,1);}`;
const fragSrc = `
precision mediump float;
uniform float t;
uniform vec2 r; // resolution
uniform vec2 m; // mouse
void main() {
vec2 uv = gl_FragCoord.xy / r;
vec2 mouse = m / r;
// Fluid swirl: iterative sine distortion
for (int i = 0; i < 8; i++) {
uv = vec2(
sin(uv.y * 4.0 + t + float(i)) * 0.4 + uv.x,
cos(uv.x * 4.0 + t + float(i)) * 0.4 + uv.y
);
}
// Distance from mouse adds interaction
float d = length(uv - mouse) * 2.0;
vec3 col = vec3(
sin(uv.x * 3.0 + t) * 0.5 + 0.5,
sin(uv.y * 3.0 + t * 1.3) * 0.5 + 0.5,
sin((uv.x + uv.y) * 2.0 + t * 0.7) * 0.5 + 0.5
);
gl_FragColor = vec4(col * (1.0 - d * 0.3), 1.0);
}`;
// Boilerplate: compile, link, draw fullscreen quad
function compile(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
return s;
}
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, vertSrc));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fragSrc));
gl.linkProgram(prog);
gl.useProgram(prog);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW);
const p = gl.getAttribLocation(prog, 'p');
gl.enableVertexAttribArray(p);
gl.vertexAttribPointer(p, 2, gl.FLOAT, false, 0, 0);
const tLoc = gl.getUniformLocation(prog, 't');
const rLoc = gl.getUniformLocation(prog, 'r');
const mLoc = gl.getUniformLocation(prog, 'm');
let mx = 0, my = 0;
canvas.addEventListener('mousemove', e => { mx = e.clientX; my = canvas.height - e.clientY; });
(function loop(now) {
gl.viewport(0, 0, canvas.width, canvas.height);
gl.uniform1f(tLoc, now * 0.001);
gl.uniform2f(rLoc, canvas.width, canvas.height);
gl.uniform2f(mLoc, mx, my);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(loop);
})(0);
</script>
Shader effect recipes (modify the main() body):
sin(uv.x*10+t) + sin(uv.y*10+t) + sin((uv.x+uv.y)*10+t)sin() at increasing frequencies (octaves)texture2D(tex, uv + vec2(sin(uv.y*10+t)*0.02))Sample pixels from text rendered on a hidden canvas, spawn particles at dark pixel positions.
function createTextParticles(text, fontSize, canvas) {
const ctx = canvas.getContext('2d');
const w = canvas.width, h = canvas.height;
// Render text to hidden canvas to sample pixels
const offscreen = new OffscreenCanvas(w, h);
const offCtx = offscreen.getContext('2d');
offCtx.fillStyle = 'white';
offCtx.font = `bold ${fontSize}px sans-serif`;
offCtx.textAlign = 'center';
offCtx.textBaseline = 'middle';
offCtx.fillText(text, w / 2, h / 2);
// Sample pixels at grid intervals
const imageData = offCtx.getImageData(0, 0, w, h).data;
const particles = [];
const gap = 4; // Density: lower = more particles
for (let y = 0; y < h; y += gap) {
for (let x = 0; x < w; x += gap) {
const alpha = imageData[(y * w + x) * 4 + 3];
if (alpha > 128) {
particles.push({
targetX: x, targetY: y,
x: Math.random() * w, y: Math.random() * h, // Start scattered
vx: 0, vy: 0
});
}
}
}
// Animate: spring toward target, repel from mouse
function animate(mouseX, mouseY) {
ctx.clearRect(0, 0, w, h);
for (const p of particles) {
const dx = mouseX - p.x, dy = mouseY - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Repel from mouse
if (dist < 100) {
p.vx -= dx / dist * 5;
p.vy -= dy / dist * 5;
}
// Spring back to target
p.vx += (p.targetX - p.x) * 0.05;
p.vy += (p.targetY - p.y) * 0.05;
p.vx *= 0.9; // Damping
p.vy *= 0.9;
p.x += p.vx;
p.y += p.vy;
ctx.fillStyle = '#fff';
ctx.fillRect(p.x, p.y, 2, 2);
}
}
return { animate, particles };
}
Emergent patterns from simple local rules.
function cellularField(canvas, gridSize = 100) {
const ctx = canvas.getContext('2d');
const cols = gridSize, rows = gridSize;
const cellW = canvas.width / cols, cellH = canvas.height / rows;
// Initialize grid with random values
let grid = Array.from({ length: cols }, () =>
Array.from({ length: rows }, () => Math.random())
);
// Random locked anchor points
const anchors = Array.from({ length: 20 }, () => ({
x: Math.floor(Math.random() * cols),
y: Math.floor(Math.random() * rows),
value: Math.random()
}));
function step() {
const next = grid.map(row => [...row]);
for (let x = 1; x < cols - 1; x++) {
for (let y = 1; y < rows - 1; y++) {
// Average neighbors
next[x][y] = (
grid[x-1][y] + grid[x+1][y] +
grid[x][y-1] + grid[x][y+1]
) / 4 + (Math.random() - 0.5) * 0.01;
}
}
// Re-apply anchors
for (const a of anchors) next[a.x][a.y] = a.value;
grid = next;
}
function draw() {
for (let x = 0; x < cols; x++) {
for (let y = 0; y < rows; y++) {
const v = grid[x][y];
ctx.fillStyle = `hsl(${v * 360}, 70%, 50%)`;
ctx.fillRect(x * cellW, y * cellH, cellW, cellH);
}
}
}
return { step, draw };
}
Mathematical beauty from (r, theta) parametric equations.
function polarArt(canvas) {
const ctx = canvas.getContext('2d');
const cx = canvas.width / 2, cy = canvas.height / 2;
function draw(time) {
ctx.fillStyle = 'rgba(0,0,0,0.05)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let theta = 0; theta < Math.PI * 20; theta += 0.02) {
// The art is in this formula - experiment!
const r = 150 * Math.sin(theta * 3 + time * 0.001)
+ 50 * Math.cos(theta * 7 - time * 0.002);
const x = cx + r * Math.cos(theta);
const y = cy + r * Math.sin(theta);
ctx.fillStyle = `hsl(${theta * 20 + time * 0.05}, 80%, 60%)`;
ctx.fillRect(x, y, 2, 2);
}
}
return { draw };
}
Formula playground (swap into the r = ... line):
r = 200 * cos(n * theta) (n controls petals)r = theta * 5x = A*sin(a*t+d), y = B*sin(b*t) (use cartesian)r = exp(sin(theta)) - 2*cos(4*theta) + sin((2*theta-PI)/24)^5For 3D creative coding with models, lighting, and camera.
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
function create3DScene(container) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);
container.appendChild(renderer.domElement);
// Lighting
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 5, 5);
scene.add(dirLight);
// Controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
camera.position.z = 5;
// Animation loop
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// Handle resize
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
return { scene, camera, renderer };
}
Upload an image, decompose into artistic representation.
function imageToPointillism(sourceCanvas, outputCanvas, dotSize = 6) {
const src = sourceCanvas.getContext('2d');
const out = outputCanvas.getContext('2d');
const w = sourceCanvas.width, h = sourceCanvas.height;
const imageData = src.getImageData(0, 0, w, h).data;
out.fillStyle = '#000';
out.fillRect(0, 0, w, h);
const dots = [];
for (let y = 0; y < h; y += dotSize) {
for (let x = 0; x < w; x += dotSize) {
const i = (y * w + x) * 4;
const r = imageData[i], g = imageData[i+1], b = imageData[i+2];
const brightness = (r + g + b) / 3;
dots.push({
x: x + (Math.random() - 0.5) * dotSize * 0.5,
y: y + (Math.random() - 0.5) * dotSize * 0.5,
radius: (brightness / 255) * dotSize * 0.5 + 1,
color: `rgb(${r},${g},${b})`
});
}
}
// Animate dots appearing
let drawn = 0;
function drawBatch() {
const batch = Math.min(drawn + 200, dots.length);
for (let i = drawn; i < batch; i++) {
const d = dots[i];
out.beginPath();
out.arc(d.x, d.y, d.radius, 0, Math.PI * 2);
out.fillStyle = d.color;
out.fill();
}
drawn = batch;
if (drawn < dots.length) requestAnimationFrame(drawBatch);
}
drawBatch();
}
Art style variants (change the rendering loop):
.:-=+*#%@Adapt output to the user's needs:
| User wants | Output format |
|---|---|
| Quick standalone demo | Single .html file, no dependencies |
| React/Next.js component | .tsx with useRef + useEffect + canvas |
| p5.js sketch | sketch.js with setup() / draw() |
| Background for existing site | CSS + minimal JS, or shader-only |
| npm package / reusable | ES module with config options |
| Three.js scene | Module with importmap or bundler setup |
For self-contained HTML: Inline all JS/CSS, use CDN imports via <script type="importmap"> for Three.js, or raw Canvas/WebGL with no dependencies.
For React: Use useRef for canvas, useEffect for animation loop, cleanup on unmount. Use 'use client' directive for Next.js.
fillRect with low alpha instead of clearRectrequestAnimationFrame, offload to GPUfillRect over arc for dots.devicePixelRatio: Always set for sharp rendering on retina displayswill-change: transform: Hint to browser for composited layers| Mistake | Fix |
|---|---|
| Clearing canvas every frame (no trails) | Use semi-transparent fill instead of clearRect |
| Noise that looks random, not smooth | Lower the scale factor (0.001-0.01 range) |
| Too many particles, low FPS | Start with 500, increase until you hit 30fps |
| Shader compiles but shows black | Check gl.getShaderInfoLog() for errors |
| Mouse coords wrong on canvas | Account for canvas offset and devicePixelRatio |
| Animation jerky on resize | Debounce resize handler, recalculate dimensions |
| Colors look muddy | Use HSL with fixed saturation/lightness, vary only hue |
When the user describes a vibe, map to technique:
| Vibe | Technique |
|---|---|
| "Organic, flowing" | Perlin noise flow field |
| "Geometric, structured" | Recursive subdivision, Mondrian |
| "Natural, growing" | L-systems, fractal trees |
| "Psychedelic, trippy" | WebGL shader with iterative distortion |
| "Minimal, elegant" | Single curve, polar coordinates |
| "Chaotic, energetic" | High-count particles with physics |
| "Retro, pixelated" | Low-res canvas + nearest-neighbor scaling |
| "Dreamy, soft" | Gaussian blur + slow float + pastels |
| "Interactive, playful" | Mouse-reactive particles or deformation |
| "Data-driven" | Map dataset values to visual properties |