← Backgrounds

Plasma

The classic demoscene plasma: overlapping waves of colour flow and pulse across a section while the palette slowly cycles. Keep it smooth, or turn on chunky pixels for a retro look.

Greetings, demoscene

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-plasma to a section, hero or card.

Options

Add any of these attributes to the same element.

data-colorsComma-separated colours, cycled in a loop. Default purple, pink, amber, yellow, cyan and blue.
data-speedSpeed multiplier. Default 1.
data-scaleSize of the waves. Default 1.
data-pixelChunky retro pixels. Give a size in px, e.g. data-pixel="10". Default 6 when set.

It’s drawn small on a canvas and scaled up, and it pauses when off screen. With reduced motion it shows one still frame.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Plasma: the classic demoscene effect. Overlapping waves of colour flow
    // and pulse across a section while the palette slowly cycles. Drawn small
    // on a canvas behind your content and scaled up, either smooth or as
    // chunky retro pixels, and paused when off screen.
    // Usage: <section data-plasma>…</section>
    // Optional: data-colors (comma-separated, cycled in a loop; default a
    // purple, pink, amber and cyan rainbow), data-speed (default 1),
    // data-scale (size of the waves, default 1), data-pixel (chunky pixels;
    // give a size in px, e.g. data-pixel="8", default 6 when set).
    var DEFAULTS = { colors: ['#4c1d95', '#db2777', '#f59e0b', '#fde68a', '#06b6d4', '#1e3a8a'], speed: 1, scale: 1, pixel: 6 };
    var MAX_PIXELS = 30000;

    var css =
      '[data-plasma]{position:relative;isolation:isolate;overflow:hidden}' +
      '.plz-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none}' +
      '.plz-pixel{image-rendering:pixelated}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var probe = document.createElement('canvas').getContext('2d');
    function rgb(c) {
      probe.fillStyle = '#000';
      probe.fillStyle = c;
      var v = probe.fillStyle;
      if (v[0] === '#') return [parseInt(v.slice(1, 3), 16), parseInt(v.slice(3, 5), 16), parseInt(v.slice(5, 7), 16)];
      return v.match(/[\d.]+/g).slice(0, 3).map(Number);
    }

    // A looping 256-step palette that blends smoothly through the colours.
    function palette(colors) {
      var n = colors.length, lut = new Uint8ClampedArray(256 * 3);
      for (var i = 0; i < 256; i++) {
        var p = (i / 256) * n, a = Math.floor(p), f = p - a;
        f = f * f * (3 - 2 * f);
        var c1 = colors[a % n], c2 = colors[(a + 1) % n];
        for (var k = 0; k < 3; k++) lut[i * 3 + k] = c1[k] + (c2[k] - c1[k]) * f;
      }
      return lut;
    }

    function build(el) {
      var d = el.dataset;
      var lut = palette((d.colors ? d.colors.split(',') : DEFAULTS.colors).map(function (c) { return rgb(c.trim()); }));
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var scale = +d.scale || DEFAULTS.scale;
      var pixel = d.pixel !== undefined && d.pixel !== 'false' ? +d.pixel || DEFAULTS.pixel : 0;
      var canvas = document.createElement('canvas');
      canvas.className = 'plz-canvas' + (pixel ? ' plz-pixel' : '');
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var w = 0, h = 0, step = 3, img = null, t = 4;

      function resize() {
        var cw = Math.max(1, el.clientWidth), ch = Math.max(1, el.clientHeight);
        step = pixel ? Math.max(pixel, Math.sqrt((cw * ch) / MAX_PIXELS)) : Math.max(3, Math.sqrt((cw * ch) / MAX_PIXELS));
        w = Math.ceil(cw / step);
        h = Math.ceil(ch / step);
        canvas.width = w;
        canvas.height = h;
        img = ctx.createImageData(w, h);
        draw();
      }

      function draw() {
        var data = img.data;
        var k = step / (38 * scale);   // wave units per canvas pixel
        // Two points that wander about, each the centre of a set of rings.
        var ax = w * (0.5 + 0.35 * Math.sin(t * 0.31)) * k, ay = h * (0.5 + 0.35 * Math.cos(t * 0.23)) * k;
        var bx = w * (0.5 + 0.4 * Math.cos(t * 0.17)) * k, by = h * (0.5 + 0.4 * Math.sin(t * 0.27)) * k;
        var shift = t * 18;
        for (var y = 0; y < h; y++) {
          var py = y * k;
          for (var x = 0; x < w; x++) {
            var px = x * k;
            // The plasma is just four waves added together.
            var v = Math.sin(px * 0.6 + t * 0.9)
              + Math.sin((py * 0.7 + t * 0.6) * 0.8)
              + Math.sin(Math.sqrt((px - ax) * (px - ax) + (py - ay) * (py - ay)) * 0.75 - t)
              + Math.sin(Math.sqrt((px - bx) * (px - bx) + (py - by) * (py - by)) * 0.6 + t * 0.7);
            var idx = ((((v + 4) * 32 + shift) | 0) & 255) * 3;
            var o = (y * w + x) * 4;
            data[o] = lut[idx];
            data[o + 1] = lut[idx + 1];
            data[o + 2] = lut[idx + 2];
            data[o + 3] = 255;
          }
        }
        ctx.putImageData(img, 0, 0);
      }

      resize();
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;

      var last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = last ? Math.min(50, now - last) : 16;
          last = now;
          t += (dt / 1000) * speed;
          draw();
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-plasma]').forEach(function (el) {
        if (el._plz) return;
        el._plz = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-plasma data-pixel style="color: #fff">
  <h1>Greetings, demoscene</h1>
</section>