L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Design et création · SKILL.md

canvas-webgl

canvas-webgl

Browser graphics via the Canvas 2D API, WebGL and creative-coding libraries. Covers React canvas setup, the 2D API, particle systems, pixel manipulation, OffscreenCanvas in a worker, raw WebGL, p5.js and noise functions. USE WHEN: user mentions "canvas", "getContext('2d')", "WebGL", "p5.js", "particle system", "pixel manipulation", "OffscreenCanvas", asks …

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: canvas-webgl description: | Browser graphics via the Canvas 2D API, WebGL and creative-coding libraries. Covers React canvas setup, the 2D API, particle systems, pixel manipulation, OffscreenCanvas in a worker, raw WebGL, p5.js and noise functions.

USE WHEN: user mentions "canvas", "getContext('2d')", "WebGL", "p5.js", "particle system", "pixel manipulation", "OffscreenCanvas", asks about drawing or generative graphics in the browser

DO NOT USE FOR: 3D scene graphs (use three-js), vector graphics and SVG (use svg-animation), DOM animation (use framer-motion or gsap) allowed-tools: Read, Grep, Glob, Write, Edit

Canvas / WebGL Skill

Browser graphics via Canvas 2D API, WebGL, and creative coding libraries.

Canvas 2D — React Setup

import { useEffect, useRef } from "react";

function Canvas2D({ draw }: { draw: (ctx: CanvasRenderingContext2D, t: number) => void }) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current!;
    const ctx = canvas.getContext("2d")!;
    let raf: number;
    let start: number | null = null;

    // Retina scaling
    const dpr = window.devicePixelRatio || 1;
    canvas.width  = canvas.offsetWidth  * dpr;
    canvas.height = canvas.offsetHeight * dpr;
    ctx.scale(dpr, dpr);

    const loop = (ts: number) => {
      if (!start) start = ts;
      const elapsed = (ts - start) / 1000; // seconds
      ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
      draw(ctx, elapsed);
      raf = requestAnimationFrame(loop);
    };

    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, [draw]);

  return <canvas ref={canvasRef} style={{ width: "100%", height: "100%" }} />;
}

Canvas 2D API Reference

Drawing

// Paths
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x, y);
ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise?);
ctx.arcTo(x1, y1, x2, y2, radius);
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
ctx.quadraticCurveTo(cpx, cpy, x, y);
ctx.rect(x, y, w, h);
ctx.roundRect(x, y, w, h, radii); // Chrome 99+
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.clip();

// Shapes
ctx.fillRect(x, y, w, h);
ctx.strokeRect(x, y, w, h);
ctx.clearRect(x, y, w, h);

// Text
ctx.font = "bold 16px system-ui";
ctx.textAlign = "center";      // left | right | center | start | end
ctx.textBaseline = "middle";   // top | middle | alphabetic | bottom
ctx.fillText("text", x, y);
ctx.strokeText("text", x, y);
const metrics = ctx.measureText("text"); // .width, .actualBoundingBoxHeight

Styles

ctx.fillStyle = "#6366f1";
ctx.fillStyle = "rgba(99, 102, 241, 0.5)";
ctx.strokeStyle = "#fff";
ctx.lineWidth = 2;
ctx.lineCap = "round";      // butt | round | square
ctx.lineJoin = "round";     // miter | round | bevel
ctx.globalAlpha = 0.8;
ctx.globalCompositeOperation = "screen"; // multiply | screen | overlay | lighten | darken | etc.

// Gradient
const grad = ctx.createLinearGradient(0, 0, 200, 0);
grad.addColorStop(0, "#6366f1");
grad.addColorStop(1, "#a855f7");
ctx.fillStyle = grad;

// Radial gradient
const rGrad = ctx.createRadialGradient(cx, cy, rInner, cx, cy, rOuter);

// Pattern
const img = new Image(); img.src = "/pattern.png";
ctx.fillStyle = ctx.createPattern(img, "repeat")!;

Transforms

ctx.save();                          // push state
ctx.translate(x, y);
ctx.rotate(angle);                   // radians
ctx.scale(sx, sy);
ctx.transform(a, b, c, d, e, f);    // raw matrix
ctx.setTransform(a, b, c, d, e, f); // reset + set
ctx.restore();                       // pop state

Particle System Pattern

class Particle {
  x: number; y: number; vx: number; vy: number;
  life: number; maxLife: number; size: number; color: string;

  constructor(w: number, h: number) {
    this.x = Math.random() * w;
    this.y = Math.random() * h;
    this.vx = (Math.random() - 0.5) * 2;
    this.vy = (Math.random() - 0.5) * 2;
    this.maxLife = 60 + Math.random() * 120;
    this.life = this.maxLife;
    this.size = 2 + Math.random() * 4;
    this.color = `hsl(${220 + Math.random() * 60}, 80%, 65%)`;
  }

  update() {
    this.x += this.vx; this.y += this.vy; this.life--;
  }

  draw(ctx: CanvasRenderingContext2D) {
    const alpha = this.life / this.maxLife;
    ctx.save();
    ctx.globalAlpha = alpha;
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.size * alpha, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  isDead() { return this.life <= 0; }
}

// In animation loop:
particles = particles.filter(p => !p.isDead());
while (particles.length < 100) particles.push(new Particle(w, h));
particles.forEach(p => { p.update(); p.draw(ctx); });

Pixel Manipulation

// Read/write every pixel
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // Uint8ClampedArray: [r,g,b,a, r,g,b,a, ...]

for (let i = 0; i < data.length; i += 4) {
  const r = data[i], g = data[i+1], b = data[i+2]; // a = data[i+3]
  const gray = 0.299 * r + 0.587 * g + 0.114 * b;
  data[i] = data[i+1] = data[i+2] = gray; // grayscale
}

ctx.putImageData(imageData, 0, 0);

OffscreenCanvas (Web Worker)

// main.ts
const worker = new Worker(new URL("./canvas.worker.ts", import.meta.url));
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);

// canvas.worker.ts
self.onmessage = (e) => {
  const canvas: OffscreenCanvas = e.data.canvas;
  const ctx = canvas.getContext("2d")!;
  // Heavy drawing here — doesn't block main thread
};

WebGL — Raw (when needed)

const gl = canvas.getContext("webgl2")!;

// Minimal setup
const vert = `#version 300 es
in vec2 a_position;
void main() {
  gl_Position = vec4(a_position, 0.0, 1.0);
}`;

const frag = `#version 300 es
precision highp float;
uniform float u_time;
out vec4 outColor;
void main() {
  outColor = vec4(sin(u_time) * 0.5 + 0.5, 0.3, 0.7, 1.0);
}`;

function compileShader(gl: WebGL2RenderingContext, type: number, src: string) {
  const shader = gl.createShader(type)!;
  gl.shaderSource(shader, src);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    throw new Error(gl.getShaderInfoLog(shader)!);
  }
  return shader;
}

For most WebGL work in React, use Three.js / R3F instead of raw WebGL.

p5.js — Creative Coding

import { useEffect, useRef } from "react";

export function P5Sketch() {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let p5Instance: any;

    import("p5").then(({ default: P5 }) => {
      p5Instance = new P5((p: any) => {
        p.setup = () => {
          p.createCanvas(containerRef.current!.offsetWidth, 400).parent(containerRef.current!);
          p.colorMode(p.HSB, 360, 100, 100);
        };

        p.draw = () => {
          p.background(0, 0, 10, 10); // slight trail
          const x = p.width / 2 + Math.cos(p.frameCount * 0.02) * 150;
          const y = p.height / 2 + Math.sin(p.frameCount * 0.03) * 80;
          p.noStroke();
          p.fill((p.frameCount * 2) % 360, 80, 90);
          p.ellipse(x, y, 20, 20);
        };
      });
    });

    return () => p5Instance?.remove();
  }, []);

  return <div ref={containerRef} />;
}

Noise Functions

// Simple 2D noise without library (value noise)
function hash(n: number) {
  return Math.sin(n) * 43758.5453 % 1;
}

function noise2d(x: number, y: number) {
  const ix = Math.floor(x), iy = Math.floor(y);
  const fx = x - ix, fy = y - iy;
  const ux = fx * fx * (3 - 2 * fx); // smoothstep
  const uy = fy * fy * (3 - 2 * fy);
  return (
    hash(ix + iy * 57) * (1-ux) * (1-uy) +
    hash(ix+1 + iy*57) * ux * (1-uy) +
    hash(ix + (iy+1)*57) * (1-ux) * uy +
    hash(ix+1 + (iy+1)*57) * ux * uy
  );
}

// Better: use "simplex-noise" or "noisejs" packages

Performance Patterns

TechniqueWhen
ctx.save()/restore() batchingMinimize state changes
Object poolingParticle systems — avoid GC pauses
ImageBitmap + createImageBitmap()Off-thread image decode
OffscreenCanvas + WorkerHeavy per-frame computation
path2D cachingReuse expensive path objects
Layer canvas techniqueStatic + dynamic layers as separate canvases
// Path2D caching (reuse complex path):
const cachedPath = new Path2D("M 0 0 L 100 0 L 50 80 Z");
ctx.fill(cachedPath);

Pitfalls

  • Always cancel requestAnimationFrame on unmount — memory leak if loop continues
  • Canvas must be explicitly sized — CSS size ≠ canvas resolution (causes blurriness)
  • Retina: multiply by devicePixelRatio, then scale ctx, then set CSS size
  • ctx.clearRect must cover full canvas including dpr-scaled dimensions
  • getImageData is slow — avoid every frame; batch pixel operations
Skills enregistrées