<main class="studio">
<div class="frame">
<canvas id="canvas" width="512" height="512" role="img" aria-label="Image not generated yet"></canvas>
</div>
<section class="panel" aria-labelledby="prompt-label">
<h1 class="panel-label" id="prompt-label">Prompt</h1>
<p class="prompt">A calm lake at dusk below three mountain ridges</p>
<div class="progress">
<div class="progress-bar" id="progress" role="progressbar" aria-label="Generation progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<span class="progress-fill" id="progress-fill"></span>
</div>
<p class="progress-text">
<span id="step-text">Ready</span>
<span id="percent">0%</span>
</p>
</div>
<p class="seed">Seed <span id="seed">none yet</span></p>
<div class="buttons">
<button type="button" class="btn btn-primary" id="generate">Generate</button>
<button type="button" class="btn" id="cancel" disabled>Cancel</button>
<button type="button" class="btn" id="save" disabled>Save image</button>
</div>
<p class="sr-only" id="status" role="status"></p>
</section>
</main>*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
background: #0b0f19;
color: #e2e8f0;
}
.studio {
display: grid;
grid-template-columns: minmax(0, 460px) minmax(0, 340px);
justify-content: center;
align-items: center;
gap: 40px;
min-height: 100vh;
padding: 32px 20px;
}
.frame {
aspect-ratio: 1;
overflow: hidden;
border-radius: 18px;
background: #151b2b;
box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.18), 0 24px 60px -20px rgba(0, 0, 0, 0.7);
}
canvas {
display: block;
width: 100%;
height: 100%;
}
.panel-label {
margin: 0 0 6px;
font-size: 0.8125rem;
font-weight: 600;
color: #94a3b8;
}
.prompt {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
line-height: 1.4;
color: #f8fafc;
}
.progress {
margin: 28px 0 12px;
}
.progress-bar {
height: 6px;
overflow: hidden;
border-radius: 3px;
background: #1e293b;
}
.progress-fill {
display: block;
width: 0;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #3d7bfa, #9a5cf6, #ef4f98);
}
.progress-text {
display: flex;
justify-content: space-between;
margin: 10px 0 0;
font-size: 0.875rem;
font-variant-numeric: tabular-nums;
color: #cbd5e1;
}
.seed {
margin: 0 0 24px;
font-size: 0.875rem;
color: #94a3b8;
}
.seed span {
font-variant-numeric: tabular-nums;
color: #cbd5e1;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.btn {
min-height: 40px;
padding: 8px 14px;
border: 1px solid #334155;
border-radius: 10px;
background: transparent;
color: #e2e8f0;
font: inherit;
font-size: 0.9375rem;
font-weight: 600;
cursor: pointer;
}
.btn:hover:not(:disabled) {
background: #1e293b;
}
.btn-primary {
border-color: #f8fafc;
background: #f8fafc;
color: #0b0f19;
}
.btn-primary:hover:not(:disabled) {
border-color: #cbd5e1;
background: #cbd5e1;
}
.btn:focus-visible {
outline: 2px solid #93c5fd;
outline-offset: 2px;
}
.btn:disabled {
opacity: 0.4;
cursor: default;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@media (max-width: 760px) {
.studio {
grid-template-columns: minmax(0, 420px);
align-content: center;
gap: 18px;
padding: 20px 16px;
}
.frame {
width: min(100%, 58vh);
justify-self: center;
}
.prompt {
font-size: 1.0625rem;
}
.progress {
margin: 14px 0 8px;
}
.seed {
margin-bottom: 16px;
}
}
@media (forced-colors: active) {
.progress-bar {
border: 1px solid CanvasText;
}
.progress-fill {
forced-color-adjust: none;
background: Highlight;
}
}
@media (pointer: coarse) {
.btn {
min-height: 44px;
}
}const SIZE = 512;
const STEPS = 30;
const STEP_MS = 140;
const palettes = [
{ sky: ["#1b1740", "#6b3a78", "#f39a6b"], sun: "255, 211, 154", ridges: ["#8a5580", "#553463", "#2a1c40"], lake: ["#3a2552", "#140f26"] },
{ sky: ["#0f2438", "#3f5f7f", "#f2b36b"], sun: "255, 226, 168", ridges: ["#6b7f96", "#3d5068", "#1c2738"], lake: ["#26384d", "#0b1420"] },
{ sky: ["#2a1433", "#9c4466", "#ffb38a"], sun: "255, 240, 201", ridges: ["#9a5672", "#633152", "#2f172c"], lake: ["#4a2440", "#170a18"] },
{ sky: ["#07252a", "#1f6b6b", "#e8c07a"], sun: "255, 243, 196", ridges: ["#4a8a84", "#285a58", "#11302f"], lake: ["#154040", "#061617"] }
];
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d", { willReadFrequently: true });
const scene = document.createElement("canvas");
const sceneCtx = scene.getContext("2d");
const tiny = document.createElement("canvas");
const tinyCtx = tiny.getContext("2d");
const progress = document.getElementById("progress");
const fill = document.getElementById("progress-fill");
const stepText = document.getElementById("step-text");
const percent = document.getElementById("percent");
const seedText = document.getElementById("seed");
const generateBtn = document.getElementById("generate");
const cancelBtn = document.getElementById("cancel");
const saveBtn = document.getElementById("save");
const status = document.getElementById("status");
const calm = window.matchMedia("(prefers-reduced-motion: reduce)");
scene.width = SIZE;
scene.height = SIZE;
let run = 0;
let seed = 0;
let step = 0;
// Seeded random numbers, so one seed always paints one picture
function random(a) {
return function() {
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function paintScene(g, n) {
const rand = random(n);
const p = palettes[Math.floor(rand() * palettes.length)];
const horizon = 330;
const sky = g.createLinearGradient(0, 0, 0, horizon);
sky.addColorStop(0, p.sky[0]);
sky.addColorStop(0.62, p.sky[1]);
sky.addColorStop(1, p.sky[2]);
g.fillStyle = sky;
g.fillRect(0, 0, SIZE, horizon);
for (let i = 0; i < 70; i++) {
g.fillStyle = "rgba(255, 255, 255, " + (0.15 + rand() * 0.6).toFixed(2) + ")";
g.fillRect(rand() * SIZE, rand() * horizon * 0.45, 1.5, 1.5);
}
const sx = 140 + rand() * 232;
const sy = 240 + rand() * 40;
const sr = 30 + rand() * 16;
const glow = g.createRadialGradient(sx, sy, sr * 0.5, sx, sy, sr * 5);
glow.addColorStop(0, "rgba(" + p.sun + ", 0.5)");
glow.addColorStop(1, "rgba(" + p.sun + ", 0)");
g.fillStyle = glow;
g.fillRect(0, 0, SIZE, horizon);
g.fillStyle = "rgb(" + p.sun + ")";
g.beginPath();
g.arc(sx, sy, sr, 0, Math.PI * 2);
g.fill();
// Three ridges, far to near, each a sum of two sine waves
p.ridges.forEach(function(color, i) {
const base = 300 + i * 14;
const height = 120 - i * 36;
const f1 = 1 + rand() * 1.5;
const f2 = 3 + rand() * 4;
const p1 = rand() * Math.PI * 2;
const p2 = rand() * Math.PI * 2;
g.beginPath();
g.moveTo(0, horizon);
for (let x = 0; x <= SIZE; x += 4) {
const u = (x / SIZE) * Math.PI;
const h = 0.5 + 0.35 * Math.sin(u * f1 + p1) + 0.15 * Math.sin(u * f2 + p2);
g.lineTo(x, Math.min(horizon, base - height * h + rand() * 3));
}
g.lineTo(SIZE, horizon);
g.closePath();
g.fillStyle = color;
g.fill();
});
const lake = g.createLinearGradient(0, horizon, 0, SIZE);
lake.addColorStop(0, p.lake[0]);
lake.addColorStop(1, p.lake[1]);
g.fillStyle = lake;
g.fillRect(0, horizon, SIZE, SIZE - horizon);
g.save();
g.globalAlpha = 0.3;
g.translate(0, horizon * 2);
g.scale(1, -1);
g.drawImage(g.canvas, 0, 0, SIZE, horizon, 0, 0, SIZE, horizon);
g.restore();
for (let y = horizon + 3; y < SIZE; y += 3 + rand() * 5) {
const fade = 1 - (y - horizon) / (SIZE - horizon);
const width = sr * (0.6 + rand() * 1.8) * (0.3 + fade);
const shift = (rand() - 0.5) * sr * 0.8;
g.fillStyle = "rgba(" + p.sun + ", " + (0.2 + rand() * 0.45 * fade).toFixed(2) + ")";
g.fillRect(sx + shift - width / 2, y, width, 1.5);
}
}
// Blur the scene and add grain, a little less at every step
function renderStep(t) {
const block = Math.max(1, Math.round(48 * Math.pow(1 - t, 2)));
ctx.imageSmoothingEnabled = true;
if (block > 1) {
const small = Math.ceil(SIZE / block);
tiny.width = small;
tiny.height = small;
tinyCtx.drawImage(scene, 0, 0, small, small);
ctx.drawImage(tiny, 0, 0, SIZE, SIZE);
} else {
ctx.drawImage(scene, 0, 0);
}
const amount = calm.matches ? 0 : Math.pow(1 - t, 1.5) * 0.85;
if (amount < 0.01) return;
const image = ctx.getImageData(0, 0, SIZE, SIZE);
const d = image.data;
const spread = 400 * amount;
for (let i = 0; i < d.length; i += 4) {
d[i] += (Math.random() - 0.5) * spread;
d[i + 1] += (Math.random() - 0.5) * spread;
d[i + 2] += (Math.random() - 0.5) * spread;
}
ctx.putImageData(image, 0, 0);
}
function setProgress(value, label) {
const pct = Math.round(value * 100);
fill.style.width = pct + "%";
percent.textContent = pct + "%";
progress.setAttribute("aria-valuenow", String(pct));
stepText.textContent = label;
}
function setBusy(busy) {
const focused = document.activeElement;
generateBtn.disabled = busy;
cancelBtn.disabled = !busy;
saveBtn.disabled = busy || step < STEPS;
// Move focus off a button that was just disabled
if (focused && focused.disabled) (busy ? cancelBtn : generateBtn).focus();
}
function generate() {
const id = ++run;
seed = 10000 + Math.floor(Math.random() * 90000);
step = 0;
seedText.textContent = String(seed);
paintScene(sceneCtx, seed);
canvas.setAttribute("aria-label", "Image generating");
setBusy(true);
status.textContent = "Generating image";
function next() {
if (id !== run) return;
step++;
renderStep(step / STEPS);
setProgress(step / STEPS, "Step " + step + " of " + STEPS);
if (step < STEPS) {
setTimeout(next, STEP_MS);
return;
}
stepText.textContent = "Done";
generateBtn.textContent = "Generate again";
canvas.setAttribute("aria-label", "Generated image of a calm lake at dusk below three mountain ridges");
setBusy(false);
status.textContent = "Image ready";
}
renderStep(0);
setProgress(0, "Starting");
setTimeout(next, STEP_MS);
}
function cancel() {
run++;
stepText.textContent = "Canceled at step " + step;
canvas.setAttribute("aria-label", "Canceled image, partly generated");
setBusy(false);
status.textContent = "Canceled at step " + step + " of " + STEPS;
}
function save() {
const name = "generated-" + seed + ".png";
canvas.toBlob(function(blob) {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = name;
document.body.append(link);
link.click();
link.remove();
setTimeout(function() {
URL.revokeObjectURL(url);
}, 1000);
});
}
generateBtn.addEventListener("click", generate);
cancelBtn.addEventListener("click", cancel);
saveBtn.addEventListener("click", save);
ctx.fillStyle = "#151b2b";
ctx.fillRect(0, 0, SIZE, SIZE);
setTimeout(function() {
if (!run) generate();
}, 300);Click any element to find its code Esc
Check this code before you run it
It came from a shared link. It runs with fewer permissions, so it cannot open pop-ups, submit forms or start downloads.
element.animate() call shows up here as it plays.