Image Generation Reveal
An image resolves out of coloured static, the way an AI image generator shows its work. Rough blobs of colour surface first, detail sharpens step by step and the last grain clears away, while a little label counts the steps. Use your own picture, or leave it out and a fresh landscape is painted every time.
How to use it
- Paste the snippet just before
</head>. - Add
data-image-generation-revealto a box with a size (an aspect ratio works well), with your<img>inside. The image is drawn to cover the box. - Leave the image out to have it paint a new landscape each time instead.
Options
Add any of these attributes to the same element.
data-loop | Dissolve back to noise and generate again. |
data-duration | How long the image takes to resolve. Default 4200 ms. |
data-steps | Number of steps the label counts through. Default 30. |
data-hold | How long the finished image shows before looping. Default 2600 ms. |
data-label="false" | Hide the step label. |
The image’s alt text is kept for screen readers. With reduced motion turned on the finished image shows straight away.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Image Generation Reveal: an image resolves out of noise, the way an AI
// image generator shows its work. It starts as coloured static, rough blobs
// of colour surface, detail sharpens step by step and the last grain clears
// away. Use your own image, or leave it out and a fresh landscape is painted
// for every run. A small label counts the steps.
// Usage: <div data-image-generation-reveal><img src="photo.jpg" alt="…"></div>
// (the box sets the size; the image is drawn to cover it)
// Optional: data-loop (dissolve back to noise and generate again),
// data-duration (ms to resolve, default 4200), data-steps (denoising steps
// shown in the label, default 30), data-label="false" (no label),
// data-hold (ms the finished image shows before looping, default 2600).
var DEFAULTS = { duration: 4200, steps: 30, hold: 2600 };
var css =
'[data-image-generation-reveal]{position:relative;overflow:hidden}' +
'[data-image-generation-reveal]>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0}' +
'.igr-canvas{position:absolute;inset:0;width:100%;height:100%;display:block}' +
'.igr-label{position:absolute;left:10px;bottom:10px;display:flex;align-items:center;gap:6px;padding:4px 10px 4px 8px;' +
'border-radius:999px;background:rgba(10,8,20,.55);color:#fff;font:600 12px/1.4 system-ui,sans-serif;' +
'font-variant-numeric:tabular-nums;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);transition:opacity .4s}' +
'.igr-label i{width:7px;height:7px;border-radius:50%;background:#a78bfa;box-shadow:0 0 8px #a78bfa;animation:igr-blink 1s infinite}' +
'.igr-ready .igr-label i{animation:none;background:#4ade80;box-shadow:0 0 8px #4ade80}' +
'@keyframes igr-blink{50%{opacity:.3}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// Paint a simple landscape (sky, sun, layered hills, water) with a random
// palette, onto a canvas of the given size.
var PALETTES = [
['#1e1b4b', '#7c3aed', '#f472b6', '#fde68a', ['#4c1d95', '#3b0764', '#1e0838']],
['#0c4a6e', '#38bdf8', '#fef3c7', '#fff7ed', ['#0f766e', '#115e59', '#042f2e']],
['#431407', '#ea580c', '#fbbf24', '#fffbeb', ['#7c2d12', '#451a03', '#1c0a02']],
['#082f49', '#6366f1', '#f0abfc', '#ffffff', ['#312e81', '#1e1b4b', '#0b0a25']]
];
function paintScene(c) {
var g = c.getContext('2d'), W = c.width, H = c.height;
var p = PALETTES[Math.floor(Math.random() * PALETTES.length)];
var sky = g.createLinearGradient(0, 0, 0, H * 0.7);
sky.addColorStop(0, p[0]); sky.addColorStop(0.55, p[1]); sky.addColorStop(1, p[2]);
g.fillStyle = sky; g.fillRect(0, 0, W, H);
var sx = W * (0.25 + Math.random() * 0.5), sy = H * (0.32 + Math.random() * 0.12), sr = Math.min(W, H) * 0.11;
var glow = g.createRadialGradient(sx, sy, 0, sx, sy, sr * 4);
glow.addColorStop(0, p[3]); glow.addColorStop(0.25, 'rgba(255,255,255,.35)'); glow.addColorStop(1, 'rgba(255,255,255,0)');
g.fillStyle = glow; g.fillRect(0, 0, W, H);
g.fillStyle = p[3]; g.beginPath(); g.arc(sx, sy, sr, 0, Math.PI * 2); g.fill();
// Three ranges of hills, back to front.
for (var k = 0; k < 3; k++) {
var base = H * (0.5 + k * 0.1), amp = H * (0.12 - k * 0.025), ph = Math.random() * 10, f = 1.5 + Math.random() * 2;
g.fillStyle = p[4][k];
g.beginPath(); g.moveTo(0, H);
for (var x = 0; x <= W + 4; x += 4) {
var u = x / W;
g.lineTo(x, base - amp * (0.6 * Math.sin(u * f * Math.PI + ph) + 0.3 * Math.sin(u * f * 3.1 + ph * 2) + 0.1 * Math.sin(u * 23 + ph)));
}
g.lineTo(W, H); g.closePath(); g.fill();
}
// Still water with the sun's reflection.
var wy = H * 0.8;
var water = g.createLinearGradient(0, wy, 0, H);
water.addColorStop(0, p[2]); water.addColorStop(1, p[0]);
g.globalAlpha = 0.85; g.fillStyle = water; g.fillRect(0, wy, W, H - wy); g.globalAlpha = 1;
g.fillStyle = 'rgba(255,255,255,.5)';
for (var i = 0; i < 9; i++) {
var ww = sr * (1.6 - i * 0.14) * (0.7 + Math.random() * 0.5);
g.fillRect(sx - ww / 2, wy + 4 + i * (H - wy) / 10, ww, Math.max(1, H * 0.006));
}
}
function build(el) {
var d = el.dataset;
var duration = +d.duration || DEFAULTS.duration;
var steps = +d.steps || DEFAULTS.steps;
var hold = d.hold !== undefined ? +d.hold : DEFAULTS.hold;
var img = el.querySelector('img');
var canvas = document.createElement('canvas');
canvas.className = 'igr-canvas';
if (img && img.alt) { canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', img.alt); }
else canvas.setAttribute('aria-hidden', 'true');
el.appendChild(canvas);
var label = null;
if (d.label !== 'false') {
label = document.createElement('div');
label.className = 'igr-label';
label.setAttribute('aria-hidden', 'true');
label.innerHTML = '<i></i><span>Generating…</span>';
el.appendChild(label);
}
var ctx = canvas.getContext('2d');
var src = document.createElement('canvas'); // the finished picture
var small = document.createElement('canvas'); // for the blocky, blurry stages
var noise = document.createElement('canvas'); // a tile of coloured static
noise.width = noise.height = 96;
var w = 0, h = 0, dpr = 1, raf = 0, visible = false, phase = 'idle', t0 = 0, p = 0, last = 0, lastStep = -1;
function makeNoise() {
var g = noise.getContext('2d'), id = g.createImageData(96, 96), a = id.data;
for (var i = 0; i < a.length; i += 4) {
a[i] = Math.random() * 255; a[i + 1] = Math.random() * 255; a[i + 2] = Math.random() * 255; a[i + 3] = 255;
}
g.putImageData(id, 0, 0);
}
function prepare() {
src.width = Math.max(1, Math.round(w)); src.height = Math.max(1, Math.round(h));
var g = src.getContext('2d');
if (img && img.complete && img.naturalWidth) {
var s = Math.max(src.width / img.naturalWidth, src.height / img.naturalHeight);
var iw = img.naturalWidth * s, ih = img.naturalHeight * s;
g.drawImage(img, (src.width - iw) / 2, (src.height - ih) / 2, iw, ih);
} else paintScene(src);
}
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
var nw = el.clientWidth, nh = el.clientHeight;
var changed = Math.abs(nw - w) > 1 || Math.abs(nh - h) > 1;
w = nw; h = nh;
canvas.width = Math.round(w * dpr); canvas.height = Math.round(h * dpr);
if (changed) prepare();
draw(p);
}
// Draw the picture at progress q (0 = pure noise, 1 = sharp).
function draw(q) {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
if (!w || !h) return;
// Coarse to fine: the picture is drawn tiny and stretched back up, so
// early on only big smooth blobs of colour show.
var res = Math.max(3, Math.round(Math.pow(q, 2.2) * w));
if (q < 0.985) {
var sw = res, sh = Math.max(2, Math.round(res * h / w));
small.width = sw; small.height = sh;
var sg = small.getContext('2d');
sg.imageSmoothingEnabled = true;
sg.drawImage(src, 0, 0, sw, sh);
ctx.imageSmoothingEnabled = true;
ctx.drawImage(small, 0, 0, w, h);
if (q > 0.55) {
ctx.globalAlpha = (q - 0.55) / 0.45;
ctx.drawImage(src, 0, 0, w, h);
}
ctx.globalAlpha = 1;
} else ctx.drawImage(src, 0, 0, w, h);
// Static on top, fading as the steps go by.
var n = Math.pow(1 - q, 1.4);
if (n > 0.005) {
ctx.globalAlpha = n;
ctx.imageSmoothingEnabled = false;
var tile = Math.max(2, 3 * (1 - q) + 1.5) * 96, ox = -Math.random() * tile, oy = -Math.random() * tile;
for (var x = ox; x < w; x += tile) for (var y = oy; y < h; y += tile) ctx.drawImage(noise, x, y, tile, tile);
ctx.imageSmoothingEnabled = true;
ctx.globalAlpha = 1;
}
}
function setLabel(text, ready) {
if (!label) return;
label.lastChild.textContent = text;
el.classList.toggle('igr-ready', !!ready);
}
function frame(now) {
raf = 0;
if (!visible) return;
if (!t0) t0 = now;
var e = now - t0;
if (phase === 'gen') {
var lin = Math.min(1, e / duration);
// The progress jumps in discrete denoising steps, eased in between.
var step = Math.floor(lin * steps);
var within = lin * steps - step;
var q0 = 1 - Math.pow(1 - step / steps, 2), q1 = 1 - Math.pow(1 - (step + 1) / steps, 2);
p = Math.min(1, q0 + (q1 - q0) * Math.min(1, within * 2.5));
if (step !== lastStep) { lastStep = step; makeNoise(); setLabel('Step ' + Math.min(steps, step + 1) + ' / ' + steps); }
if (now - last > 40 || lin >= 1) { draw(p); last = now; }
if (lin >= 1) {
p = 1; draw(1); setLabel('Done', true);
phase = 'hold'; t0 = now;
if (d.loop === undefined) return;
}
} else if (phase === 'hold') {
if (e > hold) { phase = 'fade'; t0 = now; setLabel('Generating…'); }
} else if (phase === 'fade') {
var f = Math.min(1, e / 700);
p = 1 - f;
if (now - last > 40) { makeNoise(); draw(p); last = now; }
if (f >= 1) { if (!img) prepare(); phase = 'gen'; t0 = now; lastStep = -1; }
}
raf = requestAnimationFrame(frame);
}
function start() { if (!raf) raf = requestAnimationFrame(frame); }
makeNoise();
new ResizeObserver(resize).observe(el);
resize();
if (img && !img.complete) img.addEventListener('load', function () { prepare(); draw(p); });
if (reduced()) { p = 1; setLabel('Done', true); draw(1); el._igr = true; return; }
el._igr = true;
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
if (!visible) return;
if (phase === 'idle') { phase = 'gen'; t0 = 0; }
// Resume where it left off.
else t0 = performance.now() - (phase === 'gen' ? (lastStep / steps) * duration : 0);
start();
}, { threshold: 0.2 }).observe(el);
}
function init() {
document.querySelectorAll('[data-image-generation-reveal]').forEach(function (el) {
if (el._igr) return;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-image-generation-reveal style="aspect-ratio: 16 / 10; border-radius: 16px">
<img src="castle.jpg" alt="A castle on a misty hill">
</div>