Glitch Image
Every few seconds the image has a short fit: slices jump sideways, the red and cyan channels tear apart, the frame hops and blocks of noise flash. A heartbeat later it snaps back as if nothing happened, until the next one.
How to use it
- Paste the snippet just before
</head>. - Wrap an image in an element with
data-glitch-imageand give the wrapper a size. The image fills it, cropped to cover. - It works with any image, even from another site.
Options
Add any of these attributes to the same element.
data-interval | Average time between bursts. Default 2400 ms. |
data-intensity | How wild each burst is, from 0 to 2. Default 1. |
data-hover | Glitch non-stop while hovered or touched, instead of on a timer. |
With reduced motion the image stays clean.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Glitch Image: every few seconds the image has a short fit. Horizontal
// slices jump sideways, the red and cyan channels tear apart, the frame
// hops and blocks of noise flash, then it snaps back to normal.
// Usage: <figure data-glitch-image><img src="photo.jpg" alt="…"></figure>
// The image (an <img>, or an inline <svg>) fills the box, cropped to cover,
// and stays in the page for screen readers.
// Optional: data-interval (average ms between bursts, default 2400),
// data-intensity (how wild, 0 to 2, default 1), data-hover (glitch
// non-stop while hovered or touched, instead of on a timer).
var DEFAULTS = { interval: 2400, intensity: 1 };
var css =
'.gim{position:relative;overflow:hidden}' +
'.gim-src{opacity:0}' +
'.gim-canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// Turn the source (<img> or inline <svg>) into something drawImage can use.
function loadSource(node, done) {
if (node.tagName.toLowerCase() === 'svg') {
var vb = node.viewBox && node.viewBox.baseVal, r = node.getBoundingClientRect();
var vw = (vb && vb.width) || r.width || 300, vh = (vb && vb.height) || r.height || 150;
var k = 1400 / Math.max(vw, vh);
var clone = node.cloneNode(true);
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
clone.setAttribute('width', Math.round(vw * k));
clone.setAttribute('height', Math.round(vh * k));
var im = new Image();
im.onload = function () { done(im, im.width, im.height); };
im.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(new XMLSerializer().serializeToString(clone));
} else if (node.complete && node.naturalWidth) {
done(node, node.naturalWidth, node.naturalHeight);
} else {
node.addEventListener('load', function () { done(node, node.naturalWidth, node.naturalHeight); });
}
}
function cover(ctx, img, iw, ih, w, h) {
var s = Math.max(w / iw, h / ih);
ctx.drawImage(img, (w - iw * s) / 2, (h - ih * s) / 2, iw * s, ih * s);
}
function rnd(a, b) { return a + Math.random() * (b - a); }
function setup(el, img, iw, ih) {
var d = el.dataset;
var interval = +d.interval || DEFAULTS.interval;
var amt = d.intensity !== undefined ? +d.intensity : DEFAULTS.intensity;
var hover = d.hover !== undefined && d.hover !== 'false';
var canvas = document.createElement('canvas');
canvas.className = 'gim-canvas';
canvas.setAttribute('aria-hidden', 'true');
el.appendChild(canvas);
var ctx = canvas.getContext('2d');
// The plain image, plus a red-only and a cyan-only copy for tearing the channels.
var base = document.createElement('canvas');
var red = document.createElement('canvas');
var cyan = document.createElement('canvas');
var w = 0, h = 0, dpr = 1;
function tint(c, color) {
c.width = base.width;
c.height = base.height;
var x = c.getContext('2d');
x.drawImage(base, 0, 0);
x.globalCompositeOperation = 'multiply';
x.fillStyle = color;
x.fillRect(0, 0, c.width, c.height);
}
function build() {
dpr = Math.min(2, window.devicePixelRatio || 1);
w = el.clientWidth;
h = el.clientHeight;
if (!w || !h) return;
canvas.width = base.width = Math.round(w * dpr);
canvas.height = base.height = Math.round(h * dpr);
var b = base.getContext('2d');
b.imageSmoothingQuality = 'high';
b.setTransform(dpr, 0, 0, dpr, 0, 0);
cover(b, img, iw, ih, w, h);
tint(red, '#f00');
tint(cyan, '#0ff');
}
function plain() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.globalCompositeOperation = 'source-over';
ctx.drawImage(base, 0, 0);
}
// One glitched frame: torn channels, shifted slices, a hop and a few noise blocks.
function glitchFrame(k) {
var W = canvas.width, H = canvas.height, s = dpr;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, W, H);
var hop = Math.random() < 0.3 ? rnd(-6, 6) * s * k : 0;
var split = rnd(5, 16) * s * k;
ctx.globalCompositeOperation = 'lighter';
ctx.drawImage(red, -split, hop);
ctx.drawImage(cyan, split * 0.6, hop);
ctx.globalCompositeOperation = 'source-over';
var n = Math.round(rnd(4, 10) * Math.max(0.5, k));
for (var i = 0; i < n; i++) {
var sy = Math.random() * H, sh = rnd(3, Math.max(6, H * 0.16)) * (Math.random() < 0.6 ? 0.4 : 1);
sh = Math.min(sh, H - sy);
if (sh < 1) continue;
var dx = (Math.random() < 0.5 ? -1 : 1) * rnd(0.03, 0.16) * W * k;
var src = Math.random() < 0.25 ? (Math.random() < 0.5 ? red : cyan) : base;
ctx.drawImage(src, 0, sy, W, sh, dx, sy, W, sh);
// Fill the gap the slice leaves with the image wrapped round.
if (dx > 0) ctx.drawImage(src, W - dx, sy, dx, sh, 0, sy, dx, sh);
else if (dx < 0) ctx.drawImage(src, 0, sy, -dx, sh, W + dx, sy, -dx, sh);
}
var blocks = Math.random() < 0.6 ? Math.round(rnd(1, 4) * k) : 0;
for (var b = 0; b < blocks; b++) {
ctx.fillStyle = ['rgba(255,0,90,.55)', 'rgba(0,255,230,.5)', 'rgba(255,255,255,.7)', 'rgba(0,0,0,.45)'][(Math.random() * 4) | 0];
ctx.fillRect(Math.random() * W, Math.random() * H, rnd(20, 120) * s, rnd(2, 8) * s);
}
// A thin bright scan line now and then.
if (Math.random() < 0.4) {
ctx.fillStyle = 'rgba(255,255,255,.35)';
ctx.fillRect(0, Math.random() * H, W, s);
}
}
build();
if (window.ResizeObserver) new ResizeObserver(function () { build(); plain(); }).observe(el);
plain();
if (reduced()) return;
var visible = false, hovering = false, burstEnd = 0, nextBurst = 0, nextFrame = 0, glitched = false;
function schedule(now) { nextBurst = now + interval * rnd(0.5, 1.4); }
if (hover) {
var on = function () { hovering = true; };
var off = function () { hovering = false; };
el.addEventListener('pointerenter', on);
el.addEventListener('pointerdown', on);
el.addEventListener('pointerleave', off);
el.addEventListener('pointercancel', off);
el.addEventListener('pointerup', function (e) { if (e.pointerType !== 'mouse') off(); });
}
var timer = anime.createTimer({
loop: true, duration: 1000, autoplay: false,
onUpdate: function () {
var now = performance.now();
var active;
if (hover) active = hovering;
else {
if (!nextBurst) schedule(now - interval * 0.6);
if (now >= nextBurst && now > burstEnd) {
burstEnd = now + rnd(220, 620);
schedule(burstEnd);
}
active = now < burstEnd;
}
if (!active) {
if (glitched) { glitched = false; plain(); }
return;
}
// Change the glitch at an uneven, jerky rate, with the odd clean frame between.
if (now < nextFrame) return;
nextFrame = now + rnd(40, 110);
if (Math.random() < 0.18) { plain(); glitched = false; return; }
glitchFrame(amt * rnd(0.5, 1.2));
glitched = true;
}
});
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
if (visible) timer.play();
else { timer.pause(); plain(); glitched = false; }
}, { threshold: 0.1 }).observe(el);
}
function init() {
document.querySelectorAll('[data-glitch-image]').forEach(function (el) {
if (el._gim) return;
var src = el.querySelector('img, svg');
if (!src) return;
el._gim = true;
el.classList.add('gim');
src.classList.add('gim-src');
loadSource(src, function (img, iw, ih) { setup(el, img, iw, ih); });
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><figure data-glitch-image style="height:400px">
<img src="photo.jpg" alt="Northern lights">
</figure>