← Filters & retro

Raster Bars

The demo-scene classic. Shiny colour bars, each a stack of chunky pixel lines shading from dark to white-hot and back, bounce up and down on sine waves and weave in front of and behind each other. Add a message and it rides along the bottom in a wavy scroller.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-raster-bars to an element and give it a height. The bars are drawn behind its content.

Options

Add any of these attributes to the same element.

data-barsHow many bars. Default 7.
data-colorsBar colours, used in turn, e.g. #ff2a6d,#2ec4ff. Default a rainbow.
data-speedHow fast they bounce. Default 1.
data-pixelHeight of each line, for a chunkier or finer look. Default 2 px.
data-scrollerText for a wavy scroller along the bottom.
data-bgBackground colour. Default #05030c.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Raster Bars: the demo-scene classic. Shiny horizontal colour bars, each a
    // stack of chunky pixel lines from dark to white-hot and back, bounce up and
    // down on sine waves, weaving in front of and behind each other. An
    // optional sine-wave text scroller runs along the bottom.
    // Usage: <div data-raster-bars></div>   (give it a height; any content sits on top)
    // Optional: data-bars (how many, default 7), data-colors (comma list of bar
    // colours, used in turn), data-speed (default 1), data-pixel (size of each
    // line, px, default 2), data-scroller (text for a wavy scroller along the
    // bottom), data-bg (background colour, default #05030c).
    var DEFAULTS = {
      bars: 7, speed: 1, pixel: 2, bg: '#05030c',
      colors: '#ff2a6d,#ff9f1c,#ffe14d,#3ee86b,#2ec4ff,#7b5cff,#ff4fd8'
    };

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

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function hex(c) {
      c = c.trim().replace('#', '');
      if (c.length === 3) c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2];
      var n = parseInt(c, 16);
      return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
    }

    // A bar's shading, centre to edge: white-hot, the colour, then down to near black.
    function ramp(rgb, steps) {
      var out = [];
      for (var i = 0; i < steps; i++) {
        var q = i / (steps - 1), c;
        if (q < 0.15) {
          var f = q / 0.15;
          c = [255 + (rgb[0] - 255) * f, 255 + (rgb[1] - 255) * f, 255 + (rgb[2] - 255) * f];
        } else {
          var g = 1 - 0.8 * Math.pow((q - 0.15) / 0.85, 1.6);
          c = [rgb[0] * g, rgb[1] * g, rgb[2] * g];
        }
        out.push('rgb(' + (c[0] | 0) + ',' + (c[1] | 0) + ',' + (c[2] | 0) + ')');
      }
      return out;
    }

    function setup(el) {
      var d = el.dataset;
      var count = Math.max(1, +d.bars || DEFAULTS.bars);
      var colors = (d.colors || DEFAULTS.colors).split(',').map(hex);
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var pixel = Math.max(1, +d.pixel || DEFAULTS.pixel);
      var text = d.scroller || '';
      var bg = d.bg || DEFAULTS.bg;
      var canvas = document.createElement('canvas');
      canvas.className = 'rbr-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var w = 0, h = 0, dpr = 1, ramps = [], half = 0, font = 0, textW = 0;

      function build() {
        dpr = Math.min(2, window.devicePixelRatio || 1);
        w = el.clientWidth;
        h = el.clientHeight;
        if (!w || !h) return;
        canvas.width = Math.round(w * dpr);
        canvas.height = Math.round(h * dpr);
        // Each bar is about a twelfth of the height, in whole pixel lines.
        half = Math.max(5, Math.round(h / 24 / pixel));
        ramps = [];
        for (var i = 0; i < count; i++) ramps.push(ramp(colors[i % colors.length], half + 1));
        font = Math.max(18, Math.min(44, h * 0.11));
        ctx.font = '900 ' + font + 'px ui-monospace, Menlo, Consolas, monospace';
        textW = text ? ctx.measureText(text + '   ').width : 0;
      }

      function draw(t) {
        // Hidden or not laid out yet (e.g. a filtered-out card): nothing to draw on.
        if (!w || !h || !ramps.length) return;
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.fillStyle = bg;
        ctx.fillRect(0, 0, w, h);
        var area = text ? h * 0.74 : h;
        // Faint background copper lines that slowly cycle through the palette.
        for (var y = 0; y < h; y += pixel * 2) {
          var c = colors[Math.floor((y / pixel / 6 + t / 400) % colors.length)];
          ctx.fillStyle = 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',.05)';
          ctx.fillRect(0, y, w, pixel);
        }
        // Each bar orbits on a tilted circle: the sine gives its height, the cosine its depth.
        var bars = [], sp = t / 1000 * speed;
        for (var i = 0; i < count; i++) {
          var ph = sp * 1.7 + i * (Math.PI * 2 / count) * 0.55;
          var amp = (area / 2 - half * pixel - 4) * (0.82 + 0.18 * Math.sin(sp * 0.6));
          bars.push({ i: i, y: area / 2 + Math.sin(ph) * amp, z: Math.cos(ph) });
        }
        bars.sort(function (a, b) { return a.z - b.z; });
        for (var b = 0; b < bars.length; b++) {
          var bar = bars[b], r = ramps[bar.i];
          // Bars further back are a touch thinner and dimmer.
          var scale = 0.75 + 0.25 * (bar.z + 1) / 2;
          var n = Math.max(2, Math.round(half * scale));
          var cy = Math.round(bar.y / pixel) * pixel;
          ctx.globalAlpha = 0.65 + 0.35 * (bar.z + 1) / 2;
          for (var k = -n; k <= n; k++) {
            ctx.fillStyle = r[Math.min(half, Math.round(Math.abs(k) / n * half))];
            ctx.fillRect(0, cy + k * pixel, w, pixel);
          }
        }
        ctx.globalAlpha = 1;
        if (!text) return;
        // The scroller: each letter rides its own point on a sine wave.
        ctx.font = '900 ' + font + 'px ui-monospace, Menlo, Consolas, monospace';
        ctx.textBaseline = 'middle';
        var base = h * 0.86, off = (t / 1000 * speed * font * 3.2) % textW;
        var x = -off, cw = textW / (text.length + 3);
        var grad = ctx.createLinearGradient(0, base - font * 0.8, 0, base + font * 0.8);
        grad.addColorStop(0, '#fff');
        grad.addColorStop(0.5, '#9be7ff');
        grad.addColorStop(1, '#3a7bff');
        ctx.fillStyle = grad;
        ctx.shadowColor = 'rgba(0,0,0,.8)';
        ctx.shadowOffsetY = pixel * 1.5;
        // Copies of the text laid end to end, so the line is unbroken.
        for (var rep = 0; x + rep * textW < w; rep++) {
          for (var j = 0; j < text.length; j++) {
            var lx = x + rep * textW + j * cw;
            if (lx < -cw || lx > w + cw) continue;
            var ly = base + Math.sin(lx / w * 7 + t / 300) * h * 0.045;
            ctx.fillText(text[j], lx, Math.round(ly / pixel) * pixel);
          }
        }
        ctx.shadowColor = 'transparent';
        ctx.shadowOffsetY = 0;
      }

      build();
      var t = 1200;
      if (window.ResizeObserver) new ResizeObserver(function () { build(); draw(t); }).observe(el);
      draw(t);
      if (reduced()) 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;
          draw(t);
        }
      });

      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); }
        else timer.pause();
      }, { threshold: 0.05 }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-raster-bars]').forEach(function (el) {
        if (el._rbr) return;
        el._rbr = true;
        el.classList.add('rbr');
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-raster-bars data-scroller="HELLO FROM 1989 ... " style="height:360px"></div>