Film Grain
Lay a fine, moving grain over an image, a hero or a whole page, like a frame of film. A few frames of noise are made once and shuffled at film speed, blended softly into whatever is underneath.
How to use it
- Paste the snippet just before
</head>. - Add
data-film-grainto any element. The grain goes on top and lets clicks through.
Options
Add any of these attributes to the same element.
data-opacity | Strength of the grain, from 0 to 1. Default 0.35. |
data-size | Size of each grain in pixels. Default 1.5. |
data-fps | Frames per second. Default 24. |
data-blend | How it mixes with the content: any mix-blend-mode. Default overlay; try soft-light or multiply on light designs. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Film Grain: lays a fine, moving grain over a section, like a frame of
// film. A few frames of noise are made once and shuffled about at film
// speed, blended softly into whatever is underneath. Paused off screen.
// Usage: <div data-film-grain>…an image, a hero, anything…</div>
// Optional: data-opacity (strength, 0 to 1, default 0.35), data-size (grain
// size in px, default 1.5), data-fps (frames per second, default 24),
// data-blend (any mix-blend-mode, default "overlay"; try "soft-light" or
// "multiply" on light designs).
var DEFAULTS = { opacity: 0.35, size: 1.5, fps: 24, blend: 'overlay' };
var TILE = 160; // noise tile size, in grains
var FRAMES = 6; // different noise tiles to shuffle
var css =
'.fgr{position:relative;overflow:hidden;isolation:isolate}' +
'.fgr-canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:2}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// One tile of grey noise, with a little clumping so it reads as grain, not static.
function tile() {
var c = document.createElement('canvas');
c.width = c.height = TILE;
var g = c.getContext('2d');
var img = g.createImageData(TILE, TILE), p = img.data;
for (var i = 0; i < p.length; i += 4) {
var v = (Math.random() + Math.random() + Math.random()) / 3; // bell-shaped, mostly mid-grey
v = 128 + (v - 0.5) * 2 * 150;
p[i] = p[i + 1] = p[i + 2] = v < 0 ? 0 : v > 255 ? 255 : v;
p[i + 3] = 255;
}
g.putImageData(img, 0, 0);
return c;
}
function setup(el) {
var d = el.dataset;
el.classList.add('fgr');
var canvas = document.createElement('canvas');
canvas.className = 'fgr-canvas';
canvas.setAttribute('aria-hidden', 'true');
canvas.style.opacity = d.opacity !== undefined ? d.opacity : DEFAULTS.opacity;
canvas.style.mixBlendMode = d.blend || DEFAULTS.blend;
el.appendChild(canvas);
var ctx = canvas.getContext('2d');
var size = +d.size || DEFAULTS.size;
var fps = +d.fps || DEFAULTS.fps;
var tiles = [];
for (var i = 0; i < FRAMES; i++) tiles.push(tile());
var w = 0, h = 0, dpr = 1;
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
w = el.clientWidth;
h = el.clientHeight;
canvas.width = Math.ceil(w * dpr);
canvas.height = Math.ceil(h * dpr);
}
// Tile a random frame across the canvas from a random offset.
function draw() {
var t = tiles[(Math.random() * FRAMES) | 0];
var s = size * dpr, step = TILE * s;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.imageSmoothingEnabled = s > 1.6;
var ox = -Math.random() * step, oy = -Math.random() * step;
for (var y = oy; y < canvas.height; y += step) {
for (var x = ox; x < canvas.width; x += step) ctx.drawImage(t, x, y, step, step);
}
}
resize();
draw();
if (window.ResizeObserver) new ResizeObserver(function () { resize(); draw(); }).observe(el);
if (reduced()) return;
var acc = 0, last = 0;
var timer = anime.createTimer({
loop: true, duration: 1000, autoplay: false,
onUpdate: function () {
var now = performance.now();
acc += last ? now - last : 0;
last = now;
if (acc < 1000 / fps) return;
acc = 0;
draw();
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-film-grain]').forEach(function (el) {
if (el._fgr) return;
el._fgr = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><figure data-film-grain>
<img src="photo.jpg" alt="…">
</figure>