artwall / index.html
MikePGxAI's picture
Update index.html
4f9c723 verified
Raw
History Blame Contribute Delete
7.12 kB
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Death of the Author</title>
<style>
html, body { width:100%; height:100%; margin:0; background:#000; overflow:hidden; }
canvas { width:100vw; height:100vh; display:block; }
#hint {
position: fixed;
left: 16px; bottom: 12px;
font: 12px/1.2 system-ui, -apple-system, Segoe UI, Roboto, Arial;
color: rgba(255,255,255,0.45);
user-select: none;
pointer-events: none;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hint"></div>
<script>
const API_NEXT = "/next";
const FADE_MS = 1600;
const TRANSITION = "dissolve";
// Медленное, монотонное отдаление
// Попробуйте: 20000 (20с) / 30000 (30с) / 45000 (45с)
const DRIFT_MS = 20000;
// Насколько “близко” стартуем
// 1.08–1.18 обычно выглядит естественно
const ZOOM_FROM = 1.14;
const ZOOM_TO = 1.00;
const MASK_SIZE = 256;
const DPR_MAX = 2;
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d", { alpha: false });
const buf = document.createElement("canvas");
const bctx = buf.getContext("2d", { alpha: true });
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, DPR_MAX);
canvas.width = Math.floor(innerWidth * dpr);
canvas.height = Math.floor(innerHeight * dpr);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
buf.width = canvas.width;
buf.height = canvas.height;
}
addEventListener("resize", resize);
resize();
const maskCanvas = document.createElement("canvas");
maskCanvas.width = MASK_SIZE;
maskCanvas.height = MASK_SIZE;
const mctx = maskCanvas.getContext("2d", { willReadFrequently: true });
const maskData = mctx.createImageData(MASK_SIZE, MASK_SIZE);
const noise = new Uint8Array(MASK_SIZE * MASK_SIZE);
(crypto || window.crypto).getRandomValues(noise);
function regenNoise() {
(crypto || window.crypto).getRandomValues(noise);
}
function updateMask(progress01) {
const t = Math.max(0, Math.min(255, Math.floor(progress01 * 255)));
const d = maskData.data;
for (let i = 0, p = 0; i < noise.length; i++, p += 4) {
const a = (noise[i] < t) ? 255 : 0;
d[p] = 255; d[p+1] = 255; d[p+2] = 255; d[p+3] = a;
}
mctx.putImageData(maskData, 0, 0);
}
function blobToImage(blob) {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => { URL.revokeObjectURL(url); resolve(img); };
img.onerror = (e) => { URL.revokeObjectURL(url); reject(e); };
img.src = url;
});
}
async function fetchNext() {
const r = await fetch(API_NEXT, { cache: "no-store" });
if (!r.ok) throw new Error("fetch /next failed: " + r.status);
const id = Number(r.headers.get("x-frame-id") || "0");
const blob = await r.blob();
const img = await blobToImage(blob);
return { id, img };
}
let current = null;
let incoming = null;
let swapping = false;
let fadeStart = 0;
// ВАЖНО: отдельные “старты отдаления” для текущей и входящей картинки
let driftStartCurrent = performance.now();
let driftStartIncoming = performance.now();
function clamp01(x){ return Math.max(0, Math.min(1, x)); }
function easeInOut(x){
x = clamp01(x);
return x * x * (3 - 2 * x); // smoothstep
}
function coverDraw(targetCtx, img, alpha, zoom) {
const cw = targetCtx.canvas.width;
const ch = targetCtx.canvas.height;
const iw = img.naturalWidth || img.width;
const ih = img.naturalHeight || img.height;
const s = Math.max(cw / iw, ch / ih) * zoom;
const w = iw * s, h = ih * s;
const x = (cw - w) * 0.5;
const y = (ch - h) * 0.5;
targetCtx.globalAlpha = alpha;
targetCtx.drawImage(img, x, y, w, h);
targetCtx.globalAlpha = 1;
}
function drawLoading(now) {
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
const t = (Math.sin(now / 450) * 0.5 + 0.5);
ctx.fillStyle = `rgba(255,255,255,${0.22 + 0.22 * t})`;
ctx.font = `${Math.floor(canvas.width / 40)}px system-ui, -apple-system, Segoe UI, Roboto, Arial`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Генерация…", canvas.width/2, canvas.height/2);
}
function zoomFor(now, driftStart) {
const k = easeInOut(clamp01((now - driftStart) / DRIFT_MS));
return ZOOM_FROM + (ZOOM_TO - ZOOM_FROM) * k;
}
function tick(now) {
if (!current) {
drawLoading(now);
requestAnimationFrame(tick);
return;
}
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
const zoomCurrent = zoomFor(now, driftStartCurrent);
coverDraw(ctx, current.img, 1, zoomCurrent);
if (swapping && incoming) {
const t = clamp01((now - fadeStart) / FADE_MS);
const e = easeInOut(t);
// Входящая картинка отдаляется со своего старта (без скачка после перехода)
const zoomIncoming = zoomFor(now, driftStartIncoming);
if (TRANSITION === "fade") {
coverDraw(ctx, incoming.img, e, zoomIncoming);
} else {
updateMask(e);
bctx.clearRect(0, 0, buf.width, buf.height);
bctx.globalCompositeOperation = "source-over";
bctx.drawImage(maskCanvas, 0, 0, buf.width, buf.height);
bctx.globalCompositeOperation = "source-in";
coverDraw(bctx, incoming.img, 1, zoomIncoming);
ctx.drawImage(buf, 0, 0);
}
if (t >= 1) {
// commit swap — и ВАЖНО: не сбрасываем drift заново, а переносим его
current = incoming;
incoming = null;
swapping = false;
driftStartCurrent = driftStartIncoming; // никаких резких “приближений”
regenNoise();
}
}
requestAnimationFrame(tick);
}
async function run() {
while (true) {
try {
const fr = await fetchNext();
if (!current) {
current = fr;
driftStartCurrent = performance.now();
requestAnimationFrame(tick);
continue;
}
incoming = fr;
if (!swapping) {
swapping = true;
const now = performance.now();
fadeStart = now;
driftStartIncoming = now; // входящая сразу стартует “ближе” и начинает отдаляться
}
} catch (e) {
console.error(e);
await new Promise(r => setTimeout(r, 1000));
}
}
}
document.addEventListener("click", () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen?.().catch(() => {});
}
});
run();
</script>
</body>
</html>