01. PC demoscene · early 1990s · Retro techniques
Plasma
About
The classic PC demoscene plasma: a sum of sine fields in the plane, sampled through a cycling palette. The 1990s versions added precomputed sine tables and drew through a 256-colour palette because a 386 could not afford a sine per pixel. This one is a modern reading of the look: a single WebGL 1 fragment shader evaluates about nine sines per pixel. Two triangles, a time uniform, no textures. Drop the file on a page and it fills the window.
Reference
Interpretation: this evokes a period look and does not reproduce one specific original.
Browser APIs
- WebGL 1
- GLSL ES 1.0
- requestAnimationFrame
- matchMedia
If WebGL is missing, the demo draws a message on the canvas instead of a blank frame. By default a demo holds a still frame when your system asks for reduced motion; the Motion buttons override that, and a standalone copy animates with #motion=play on its URL.
Source
(function () {
const PARAMS = {
color: "#6ee7b7",
speed: 1,
scale: 1
};
function hexToRgb(hex) {
const n = parseInt(String(hex).replace("#", ""), 16);
if (isNaN(n)) return [0.43, 0.91, 0.72];
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
}
const canvas = document.getElementById("c");
const fallback = document.getElementById("fallback");
function fail(message) {
fallback.hidden = false;
fallback.textContent = message;
}
const gl = canvas.getContext("webgl", { alpha: false, antialias: false, powerPreference: "high-performance" });
if (!gl) {
fail("WebGL is not available in this browser. Try a current Firefox, Chrome, Safari, or Edge.");
return;
}
const VERT = `attribute vec2 a_pos;
void main() {
gl_Position = vec4(a_pos, 0.0, 1.0);
}`;
const FRAG = `precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec3 u_color;
uniform float u_speed;
uniform float u_scale;
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);
uv *= u_scale;
float t = u_time * 0.55 * u_speed;
float v = 0.0;
v += sin(uv.x * 9.0 + t);
v += sin(uv.y * 8.0 + t * 1.15);
v += sin((uv.x + uv.y) * 6.0 + t * 0.7);
vec2 c = uv + vec2(sin(t * 0.31), cos(t * 0.27)) * 0.45;
v += sin(length(c) * 14.0 - t * 1.4);
v *= 0.25;
float r = 0.55 + 0.45 * sin(v * 6.28318 + t);
float g = 0.45 + 0.45 * sin(v * 6.28318 + t * 1.3 + 2.1);
float b = 0.55 + 0.45 * sin(v * 6.28318 + t * 0.7 + 4.2);
vec3 col = vec3(r, g, b);
col = mix(vec3(0.05, 0.18, 0.22), col, 0.92);
col = mix(col, vec3(0.43, 0.91, 0.72), 0.12 * (0.5 + 0.5 * sin(v * 12.0)));
vec2 q = gl_FragCoord.xy / u_resolution;
col *= 0.65 + 0.35 * pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.32);
col = mix(col, u_color, 0.32);
gl_FragColor = vec4(col, 1.0);
}`;
function compile(type, src) {
const sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(sh) || "Shader compile failed");
}
return sh;
}
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(prog) || "Program link failed");
}
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, 1, -1, 1, 1]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(prog, "a_pos");
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
const uTime = gl.getUniformLocation(prog, "u_time");
const uRes = gl.getUniformLocation(prog, "u_resolution");
const uColor = gl.getUniformLocation(prog, "u_color");
const uSpeed = gl.getUniformLocation(prog, "u_speed");
const uScale = gl.getUniformLocation(prog, "u_scale");
const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
let reduced = motionQuery.matches;
let raf = 0;
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
}
gl.viewport(0, 0, canvas.width, canvas.height);
}
function frame(now) {
resize();
const t = reduced ? 2.4 : now * 0.001;
const rgb = hexToRgb(PARAMS.color);
gl.uniform1f(uTime, t);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform3f(uColor, rgb[0], rgb[1], rgb[2]);
gl.uniform1f(uSpeed, PARAMS.speed);
gl.uniform1f(uScale, PARAMS.scale);
gl.drawArrays(gl.TRIANGLES, 0, 6);
if (!reduced) raf = requestAnimationFrame(frame);
}
canvas.addEventListener("webglcontextlost", function (e) {
e.preventDefault();
cancelAnimationFrame(raf);
fail("The WebGL context was lost. Reload to restore the demo.");
});
// Motion. The system's reduced-motion preference decides by default. A
// standalone copy animates anyway with "#motion=play" in its URL (or holds a
// frame with "#motion=still"); an embedding page can post {motion: "play"}.
let motionMode = "auto";
function applyMotion() {
const next = motionMode === "still" || (motionMode === "auto" && motionQuery.matches);
if (next === reduced) return;
reduced = next;
if (reduced) { cancelAnimationFrame(raf); raf = 0; frame(performance.now()); }
else if (!raf) raf = requestAnimationFrame(frame);
}
function setMotion(mode) {
if (mode === "play" || mode === "still" || mode === "auto") { motionMode = mode; applyMotion(); }
}
function readMotionHash() {
const m = /motion=(play|still|auto)/.exec(location.hash);
let mode = m ? m[1] : "auto";
// Embedded in the gallery, the page sets window.__demoMotion before any frame loads.
try { if (!m && window.parent !== window && typeof window.parent.__demoMotion === "string") mode = window.parent.__demoMotion; } catch (e) { /* cross-origin parent */ }
setMotion(mode);
}
function wake() { if (reduced) frame(performance.now()); }
if (motionQuery.addEventListener) motionQuery.addEventListener("change", applyMotion);
else if (motionQuery.addListener) motionQuery.addListener(applyMotion);
window.addEventListener("hashchange", readMotionHash);
window.addEventListener("message", function (e) { if (e.data && typeof e.data.motion === "string") setMotion(e.data.motion); });
window.addEventListener("resize", wake);
try {
if (reduced) frame(performance.now());
else raf = requestAnimationFrame(frame);
readMotionHash();
} catch (err) {
fail(err && err.message ? err.message : String(err));
}
})();