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
- Paste the snippet just before
</head>. - Add
data-raster-barsto 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-bars | How many bars. Default 7. |
data-colors | Bar colours, used in turn, e.g. #ff2a6d,#2ec4ff. Default a rainbow. |
data-speed | How fast they bounce. Default 1. |
data-pixel | Height of each line, for a chunkier or finer look. Default 2 px. |
data-scroller | Text for a wavy scroller along the bottom. |
data-bg | Background colour. Default #05030c. |
<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><div data-raster-bars data-scroller="HELLO FROM 1989 ... " style="height:360px"></div>