Live demo
TypeScript source
This TypeScript code computes a plasma value per-pixel using several sine functions and maps the result into RGB. Below is a clear, commented TS implementation you can copy into your project or compile with tsc / esbuild / vite.
// TypeScript: plasma.ts const canvas = document.getElementById('plasma') as HTMLCanvasElement; const ctx = canvas.getContext('2d')!; let width = canvas.width; let height = canvas.height; const scaleInput = document.getElementById('scale') as HTMLInputElement; const speedInput = document.getElementById('speed') as HTMLInputElement; const shiftInput = document.getElementById('shift') as HTMLInputElement; const pauseBtn = document.getElementById('pause') as HTMLButtonElement; const resetBtn = document.getElementById('reset') as HTMLButtonElement; let running = true; let time = 0; // Resize helper to keep internal buffer size matching canvas pixel size function resize() { const dpr = Math.max(1, window.devicePixelRatio || 1); const rect = canvas.getBoundingClientRect(); width = Math.floor(rect.width * dpr); height = Math.floor(rect.height * dpr); canvas.width = width; canvas.height = height; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } window.addEventListener('resize', resize); resize(); // Fast sin/cos helpers const sin = Math.sin; const cos = Math.cos; const PI = Math.PI; // Color mapping: use smooth polychromatic palette function palette(t: number) { // t in [0,1) -> return [r,g,b] in [0,255] const r = Math.floor(128 + 127 * sin(2.0 * PI * (t) + 0.0)); const g = Math.floor(128 + 127 * sin(2.0 * PI * (t) + 2.0)); const b = Math.floor(128 + 127 * sin(2.0 * PI * (t) + 4.0)); return [r, g, b]; } // Compute a single frame into an ImageData buffer function renderFrame(tSec: number) { const image = ctx.createImageData(width, height); const data = image.data; const scale = parseFloat(scaleInput.value); const speed = parseFloat(speedInput.value); const shift = parseFloat(shiftInput.value); // Precompute commonly used values const timeFactor = tSec * speed; let idx = 0; // Loop over pixels for (let y = 0; y < height; y++) { const ny = (y / height - 0.5) * scale; for (let x = 0; x < width; x++) { const nx = (x / width - 0.5) * scale; // Combine multiple sine waves for richness const v1 = sin( (nx * 3.0) + timeFactor ); const v2 = sin( (ny * 3.0) + timeFactor * 0.7 ); const v3 = sin( (nx + ny) * 2.0 + sin(timeFactor * 0.5) ); const v4 = sin( Math.hypot(nx * 1.5, ny * 1.5) * 3.0 - timeFactor ); // Mix values to produce final scalar const mix = 0.5 * (v1 + v2) + 0.25 * v3 + 0.25 * v4; // Normalize mix to [0,1] const normalized = (mix + 1) * 0.5; // Apply color palette and optional shift const t = (normalized + (shift / (2 * PI))) % 1; const [r,g,b] = palette(t); // Write pixel data[idx++] = r; data[idx++] = g; data[idx++] = b; data[idx++] = 255; } } ctx.putImageData(image, 0, 0); } // Animation loop using requestAnimationFrame let last = performance.now(); function animate(now: number) { if (!running) { last = now; requestAnimationFrame(animate); return; } const dt = (now - last) / 1000; last = now; time += dt; renderFrame(time); requestAnimationFrame(animate); } requestAnimationFrame(animate); // Controls pauseBtn.addEventListener('click', () => { running = !running; pauseBtn.textContent = running ? 'Pause' : 'Resume'; }); resetBtn.addEventListener('click', () => { time = 0; });
How it works
- For each pixel we map its canvas coordinates into a small continuous domain centered at zero so patterns tile and scale nicely.
- We evaluate several sine-based functions of x, y and radius to create interference patterns that evolve over time.
- We combine those values into a single scalar and map that scalar through a smooth color palette.
- We write the resulting RGB into an ImageData buffer and paint it once per animation frame for maximum performance.
Performance tips
- Work on an offscreen image buffer and call putImageData once per frame to minimize canvas overhead.
- Use width/height that match device pixel ratio to avoid blurry results and unnecessary stretching.
- Try lowering resolution (compute at half size then scale) for weak devices and mobile.
- Consider WebGL or fragment shaders (GLSL) for extremely heavy patterns and very high frame rates.
Optional: WebGL / shader idea
Same math can be ported to a fragment shader where x,y are normalized gl_FragCoord and sine combinations are evaluated per-fragment for hardware-accelerated speed.
JavaScript tutorials:
Spinning squares - visual effect (25 lines)
Oldschool fire effect (20 lines)
Fireworks (60 lines)
Animated fractal (32 lines)
Physics engine for beginners