Bouncing Screensaver Logo
A logo drifts diagonally across a dark screen like an old DVD player left idle. It bounces off each edge and changes colour every time it hits one. Catch it landing exactly in a corner and the screen flashes.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-bouncing-screensaver-logo>for the built-in disc logo, or put your own logo inside it. - For your own logo, an
<svg>that usescurrentColorpicks up each new colour.
Options
Add any of these attributes to the same element.
data-text | Word on the built-in logo. Default LOGO. |
data-speed | How fast it drifts, in pixels a second. Default 90. |
data-size | Logo width as a fraction of the screen. Default 0.24. |
data-colors | Comma-separated colours to cycle through. Default seven bright colours. |
It is 8:5 with a near-black background by default; set your own height or background on the element to change that. It pauses when off screen. With reduced motion the logo stays put.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Bouncing Screensaver Logo: a logo drifts diagonally across a dark panel,
// bouncing off the edges like an old screensaver and changing colour every
// time it hits one. Hit a corner exactly and the panel flashes.
// Usage: <div data-bouncing-screensaver-logo></div>
// Put your own logo inside (an <svg> using currentColor, or any element)
// and it bounces that instead; otherwise it draws a disc logo with data-text.
// It is 8:5 with a near-black background by default; give it your own
// height or background to change that.
// Optional: data-text (default "LOGO"), data-speed (pixels a second,
// default 90), data-colors (comma-separated list to cycle through),
// data-size (logo width as a fraction of the panel, default 0.24).
var DEFAULTS = {
text: 'LOGO',
speed: 90,
size: 0.24,
colors: ['#a78bfa', '#f472b6', '#facc15', '#4ade80', '#38bdf8', '#fb923c', '#f87171']
};
var css =
'[data-bouncing-screensaver-logo]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5;background:#07060d}' +
'.bsv-logo{position:absolute;left:0;top:0;will-change:transform;transition:color .25s,filter .25s;line-height:0}' +
'.bsv-logo svg{display:block;width:100%;height:auto;overflow:visible}' +
'.bsv-flash{position:absolute;inset:0;pointer-events:none;opacity:0;background:radial-gradient(circle at var(--bsv-x,50%) var(--bsv-y,50%),rgba(255,255,255,.35),transparent 60%)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
function defaultLogo(text) {
// A bold italic word over a flattened disc with a hole, in currentColor.
var fs = text.length > 5 ? Math.max(16, 150 / text.length) : 30;
return '<svg viewBox="0 0 120 64" aria-hidden="true" focusable="false">' +
'<text x="60" y="34" text-anchor="middle" fill="currentColor" font-family="system-ui,-apple-system,\'Segoe UI\',sans-serif" font-weight="900" font-style="italic" font-size="' + fs.toFixed(1) + '" letter-spacing="-1" textLength="' + Math.min(112, text.length * fs * 0.7).toFixed(1) + '" lengthAdjust="spacingAndGlyphs">' + esc(text) + '</text>' +
'<path fill="currentColor" fill-rule="evenodd" d="M14 52a46 9 0 1 0 92 0a46 9 0 1 0 -92 0ZM48 52a12 3 0 1 0 24 0a12 3 0 1 0 -24 0Z"/>' +
'</svg>';
}
function build(el) {
var d = el.dataset;
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
var size = d.size !== undefined ? +d.size : DEFAULTS.size;
var colors = d.colors ? d.colors.split(',').map(function (c) { return c.trim(); }).filter(Boolean) : DEFAULTS.colors;
var ci = Math.floor(Math.random() * colors.length);
var logo = document.createElement('div');
logo.className = 'bsv-logo';
if (el.firstElementChild) {
while (el.firstChild) logo.appendChild(el.firstChild);
} else {
var text = d.text || DEFAULTS.text;
logo.innerHTML = defaultLogo(text);
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', text + ' logo bouncing round the screen');
if (!el.hasAttribute('role')) el.setAttribute('role', 'img');
}
var flash = document.createElement('div');
flash.className = 'bsv-flash';
flash.setAttribute('aria-hidden', 'true');
el.appendChild(flash);
el.appendChild(logo);
logo.style.color = colors[ci];
var W = 0, H = 0, lw = 0, lh = 0;
var x = 0, y = 0, vx = 1, vy = 1, placed = false;
function measure() {
W = el.clientWidth; H = el.clientHeight;
logo.style.width = Math.max(56, Math.min(220, W * size)) + 'px';
lw = logo.offsetWidth; lh = logo.offsetHeight;
if (!placed && W) {
x = Math.random() * Math.max(0, W - lw);
y = Math.random() * Math.max(0, H - lh);
vx = Math.random() < 0.5 ? -1 : 1;
vy = Math.random() < 0.5 ? -1 : 1;
placed = true;
}
x = Math.max(0, Math.min(x, W - lw));
y = Math.max(0, Math.min(y, H - lh));
draw();
}
function draw() { logo.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px)'; }
function nextColour() {
if (colors.length < 2) return;
var n = ci;
while (n === ci) n = Math.floor(Math.random() * colors.length);
ci = n;
logo.style.color = colors[ci];
}
function cornerFlash() {
el.style.setProperty('--bsv-x', (x + lw / 2) / W * 100 + '%');
el.style.setProperty('--bsv-y', (y + lh / 2) / H * 100 + '%');
anime.animate(flash, { opacity: [1, 0], duration: 900, ease: 'outQuad' });
anime.animate(logo.firstElementChild, { scale: [1.25, 1], duration: 600, ease: 'outElastic(1, .5)' });
}
new ResizeObserver(measure).observe(el);
measure();
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) / 1000 : 0.016;
last = now;
if (!W || !H) return;
// Keep the pace steady whatever the panel's size: scale with width, gently.
var v = speed * Math.max(0.6, Math.min(1.6, W / 600));
x += vx * v * dt; y += vy * v * dt;
var hitX = false, hitY = false;
if (x <= 0) { x = 0; vx = 1; hitX = true; }
else if (x >= W - lw) { x = W - lw; vx = -1; hitX = true; }
if (y <= 0) { y = 0; vy = 1; hitY = true; }
else if (y >= H - lh) { y = H - lh; vy = -1; hitY = true; }
if (hitX || hitY) nextColour();
// A corner: both walls within a few pixels of each other.
var nearX = x < 3 || x > W - lw - 3, nearY = y < 3 || y > H - lh - 3;
if ((hitX && nearY) || (hitY && nearX)) cornerFlash();
draw();
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-bouncing-screensaver-logo]').forEach(function (el) {
if (el._bsv) return; // idempotent
el._bsv = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-bouncing-screensaver-logo data-text="DVD"></div>
<!-- your own logo, in your brand colours: -->
<div data-bouncing-screensaver-logo data-colors="#7c3aed,#ec4899,#f59e0b">
<svg viewBox="0 0 100 40"><text x="50" y="28" text-anchor="middle" fill="currentColor">ACME</text></svg>
</div>