Home

01. PC demoscene · early 1990s · Retro techniques

Plasma

Demos are animating. Following your system setting.
Colors and knobs

These rewrite the live file. Copy HTML picks up whatever you set.

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));
  }
})();