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