Spaces:
Running
Running
First screen is the playable game
Browse files- index.html +16 -16
- play.html +10 -2
- static/css/v1.css +26 -35
- static/js/v1.js +43 -28
index.html
CHANGED
|
@@ -24,12 +24,26 @@
|
|
| 24 |
</div>
|
| 25 |
</nav>
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
<header class="hero" id="top">
|
| 28 |
<div class="hero-bg" aria-hidden="true">
|
| 29 |
<video id="hero-video" muted loop playsinline preload="auto" data-src="hero_evo.mp4" data-poster="hero_evo.webp"></video>
|
| 30 |
</div>
|
| 31 |
-
<div class="hero-inner
|
| 32 |
-
<div class="hero-text">
|
| 33 |
<p class="eyebrow">Agentic verification · Progressive development</p>
|
| 34 |
<h1>EvoGame</h1>
|
| 35 |
<p class="hero-sub">Self-Evolving Game Generation through Agentic Verification and Progressive Development</p>
|
|
@@ -40,20 +54,6 @@
|
|
| 40 |
<a class="btn" href="#" aria-disabled="true">Code <small>soon</small></a>
|
| 41 |
<a class="btn" href="#" aria-disabled="true">Dataset <small>soon</small></a>
|
| 42 |
</div>
|
| 43 |
-
</div>
|
| 44 |
-
<section class="play-card" id="play" aria-labelledby="play-title" hidden>
|
| 45 |
-
<div class="play-top">
|
| 46 |
-
<span class="play-tag">Playable</span>
|
| 47 |
-
<b id="play-title"></b>
|
| 48 |
-
<span class="play-who" id="play-who"></span>
|
| 49 |
-
</div>
|
| 50 |
-
<div class="play-screen" id="play-screen"></div>
|
| 51 |
-
<div class="play-ctl">
|
| 52 |
-
<div class="seg" id="play-rounds" role="group" aria-label="Which build to play"></div>
|
| 53 |
-
<span class="play-keys" id="play-keys"></span>
|
| 54 |
-
<button class="btn btn-sm play-fs" id="play-fs" type="button" hidden>⛶ Fullscreen</button>
|
| 55 |
-
</div>
|
| 56 |
-
</section>
|
| 57 |
</div>
|
| 58 |
</header>
|
| 59 |
|
|
|
|
| 24 |
</div>
|
| 25 |
</nav>
|
| 26 |
|
| 27 |
+
<section class="stage" id="stage" aria-label="Playable game" hidden>
|
| 28 |
+
<div class="stage-game" id="stage-game"></div>
|
| 29 |
+
<div class="stage-bar">
|
| 30 |
+
<div class="stage-about">
|
| 31 |
+
<b id="stage-title"></b>
|
| 32 |
+
<span id="stage-who"></span>
|
| 33 |
+
</div>
|
| 34 |
+
<div class="stage-mid">
|
| 35 |
+
<div class="seg" id="stage-rounds" role="group" aria-label="Which build to play"></div>
|
| 36 |
+
<span class="stage-keys" id="stage-keys"></span>
|
| 37 |
+
</div>
|
| 38 |
+
<a class="btn btn-primary stage-down" href="#top">Explore EvoGame ↓</a>
|
| 39 |
+
</div>
|
| 40 |
+
</section>
|
| 41 |
+
|
| 42 |
<header class="hero" id="top">
|
| 43 |
<div class="hero-bg" aria-hidden="true">
|
| 44 |
<video id="hero-video" muted loop playsinline preload="auto" data-src="hero_evo.mp4" data-poster="hero_evo.webp"></video>
|
| 45 |
</div>
|
| 46 |
+
<div class="hero-inner">
|
|
|
|
| 47 |
<p class="eyebrow">Agentic verification · Progressive development</p>
|
| 48 |
<h1>EvoGame</h1>
|
| 49 |
<p class="hero-sub">Self-Evolving Game Generation through Agentic Verification and Progressive Development</p>
|
|
|
|
| 54 |
<a class="btn" href="#" aria-disabled="true">Code <small>soon</small></a>
|
| 55 |
<a class="btn" href="#" aria-disabled="true">Dataset <small>soon</small></a>
|
| 56 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
</div>
|
| 58 |
</header>
|
| 59 |
|
play.html
CHANGED
|
@@ -12,7 +12,10 @@
|
|
| 12 |
<style>
|
| 13 |
html, body { margin: 0; height: 100%; background: #05060a; overflow: hidden; }
|
| 14 |
#canvas { display: block; width: 100%; height: 100%; outline: none; }
|
| 15 |
-
#boot { position: absolute; inset: 0; display: grid; place-items: center; color: #b4bccd; font: 500 13px/1.5 ui-monospace, "JetBrains Mono", Menlo, monospace; text-align: center; padding: 16px; }
|
|
|
|
|
|
|
|
|
|
| 16 |
#boot[hidden] { display: none; }
|
| 17 |
#bar { width: min(320px, 70vw); height: 4px; margin: 10px auto 0; border-radius: 2px; background: #1c2130; overflow: hidden; }
|
| 18 |
#bar i { display: block; height: 100%; width: 0; background: #c6f432; transition: width .15s; }
|
|
@@ -20,7 +23,7 @@
|
|
| 20 |
</head>
|
| 21 |
<body>
|
| 22 |
<canvas id="canvas" tabindex="0">Your browser does not support the canvas element.</canvas>
|
| 23 |
-
<div id="boot"><div><div id="msg">Loading engine…</div><div id="bar"><i></i></div></div></div>
|
| 24 |
<script>
|
| 25 |
(() => {
|
| 26 |
const q = new URLSearchParams(location.search);
|
|
@@ -29,6 +32,9 @@
|
|
| 29 |
const pckSize = +q.get("pck_size") || 0;
|
| 30 |
const wasmGz = +q.get("wasm_gz") || 0;
|
| 31 |
const msg = document.getElementById("msg"), bar = document.querySelector("#bar i");
|
|
|
|
|
|
|
|
|
|
| 32 |
const mb = (n) => (n / 1048576).toFixed(1);
|
| 33 |
const show = (text, frac) => { msg.textContent = text; if (frac != null) bar.style.width = `${Math.min(100, frac * 100)}%`; };
|
| 34 |
const fail = (e) => { console.error(e); show(`Could not start the game: ${e && e.message ? e.message : e}`); };
|
|
@@ -88,6 +94,8 @@
|
|
| 88 |
};
|
| 89 |
document.head.append(script);
|
| 90 |
addEventListener("pointerdown", () => document.getElementById("canvas").focus());
|
|
|
|
|
|
|
| 91 |
})();
|
| 92 |
</script>
|
| 93 |
</body>
|
|
|
|
| 12 |
<style>
|
| 13 |
html, body { margin: 0; height: 100%; background: #05060a; overflow: hidden; }
|
| 14 |
#canvas { display: block; width: 100%; height: 100%; outline: none; }
|
| 15 |
+
#boot { position: absolute; inset: 0; display: grid; place-items: center; color: #b4bccd; font: 500 13px/1.5 ui-monospace, "JetBrains Mono", Menlo, monospace; text-align: center; padding: 16px; background: #05060a center / cover no-repeat; }
|
| 16 |
+
#boot::before { content: ""; position: absolute; inset: 0; background: rgba(5, 6, 10, .6); backdrop-filter: blur(3px); }
|
| 17 |
+
#boot > div { position: relative; }
|
| 18 |
+
#boot h1 { margin: 0 0 6px; font: 700 clamp(22px, 4vw, 40px)/1.1 system-ui, sans-serif; color: #eef1f7; }
|
| 19 |
#boot[hidden] { display: none; }
|
| 20 |
#bar { width: min(320px, 70vw); height: 4px; margin: 10px auto 0; border-radius: 2px; background: #1c2130; overflow: hidden; }
|
| 21 |
#bar i { display: block; height: 100%; width: 0; background: #c6f432; transition: width .15s; }
|
|
|
|
| 23 |
</head>
|
| 24 |
<body>
|
| 25 |
<canvas id="canvas" tabindex="0">Your browser does not support the canvas element.</canvas>
|
| 26 |
+
<div id="boot"><div><h1 id="boot-title"></h1><div id="msg">Loading engine…</div><div id="bar"><i></i></div></div></div>
|
| 27 |
<script>
|
| 28 |
(() => {
|
| 29 |
const q = new URLSearchParams(location.search);
|
|
|
|
| 32 |
const pckSize = +q.get("pck_size") || 0;
|
| 33 |
const wasmGz = +q.get("wasm_gz") || 0;
|
| 34 |
const msg = document.getElementById("msg"), bar = document.querySelector("#bar i");
|
| 35 |
+
document.getElementById("boot-title").textContent = q.get("title") || "";
|
| 36 |
+
if (/^(https?:|\/|\.)/.test(q.get("poster") || "") || /^[\w/.-]+$/.test(q.get("poster") || ""))
|
| 37 |
+
document.getElementById("boot").style.backgroundImage = `url("${encodeURI(q.get("poster"))}")`;
|
| 38 |
const mb = (n) => (n / 1048576).toFixed(1);
|
| 39 |
const show = (text, frac) => { msg.textContent = text; if (frac != null) bar.style.width = `${Math.min(100, frac * 100)}%`; };
|
| 40 |
const fail = (e) => { console.error(e); show(`Could not start the game: ${e && e.message ? e.message : e}`); };
|
|
|
|
| 94 |
};
|
| 95 |
document.head.append(script);
|
| 96 |
addEventListener("pointerdown", () => document.getElementById("canvas").focus());
|
| 97 |
+
// the game fills the embedding page's first screen: let the mouse wheel keep scrolling that page
|
| 98 |
+
if (parent !== window) addEventListener("wheel", (e) => { try { parent.scrollBy(0, e.deltaY); } catch { /* cross-origin parent */ } }, { passive: true, capture: true });
|
| 99 |
})();
|
| 100 |
</script>
|
| 101 |
</body>
|
static/css/v1.css
CHANGED
|
@@ -82,38 +82,29 @@
|
|
| 82 |
.modal.simple .m-prompt, .modal.simple .m-h, .modal.simple #m-reqhint, .modal.simple #m-reqs, .modal.simple #m-evo { display: none; }
|
| 83 |
@media (max-width: 760px) { .modal.simple .modal-info { grid-template-columns: 1fr; } .modal.simple .m-score { grid-column: 1; grid-row: auto; } }
|
| 84 |
|
| 85 |
-
/* ----------
|
| 86 |
-
.
|
| 87 |
-
.
|
| 88 |
-
.
|
| 89 |
-
.
|
| 90 |
-
.
|
| 91 |
-
.
|
| 92 |
-
.
|
| 93 |
-
.
|
| 94 |
-
.
|
| 95 |
-
.
|
| 96 |
-
.
|
| 97 |
-
.
|
| 98 |
-
.
|
| 99 |
-
.
|
| 100 |
-
.
|
| 101 |
-
.
|
| 102 |
-
.
|
| 103 |
-
.
|
| 104 |
-
.
|
| 105 |
-
.
|
| 106 |
-
.
|
| 107 |
-
.
|
| 108 |
-
.
|
| 109 |
-
|
| 110 |
-
.
|
| 111 |
-
.play-ctl .seg button.on b { color: var(--accent); }
|
| 112 |
-
.play-keys { font: 500 12px var(--mono); color: var(--muted); }
|
| 113 |
-
.play-fs { margin-left: auto; }
|
| 114 |
-
@media (max-width: 1020px) {
|
| 115 |
-
.hero-inner.has-play { grid-template-columns: 1fr; text-align: center; max-width: 820px; }
|
| 116 |
-
.has-play .hero-sub { margin-inline: auto; }
|
| 117 |
-
.has-play .hero-cta { justify-content: center; }
|
| 118 |
-
.play-card { text-align: left; }
|
| 119 |
-
}
|
|
|
|
| 82 |
.modal.simple .m-prompt, .modal.simple .m-h, .modal.simple #m-reqhint, .modal.simple #m-reqs, .modal.simple #m-evo { display: none; }
|
| 83 |
@media (max-width: 760px) { .modal.simple .modal-info { grid-template-columns: 1fr; } .modal.simple .m-score { grid-column: 1; grid-row: auto; } }
|
| 84 |
|
| 85 |
+
/* ---------- the first screen is a game ---------- */
|
| 86 |
+
.stage { position: relative; height: 100vh; height: 100svh; display: grid; grid-template-rows: minmax(0, 1fr) auto; background: var(--media-bg); }
|
| 87 |
+
.stage-game { position: relative; min-height: 0; overflow: hidden; }
|
| 88 |
+
.stage-game iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }
|
| 89 |
+
.stage-cover { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; padding: 24px; }
|
| 90 |
+
.stage-cover img, .stage-cover video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
| 91 |
+
.stage-cover img { filter: brightness(.45) blur(2px); transform: scale(1.03); }
|
| 92 |
+
.stage-cover > div { position: relative; display: grid; gap: 12px; justify-items: center; max-width: 560px; }
|
| 93 |
+
.stage-cover h2 { font-size: clamp(28px, 5vw, 48px); text-wrap: balance; }
|
| 94 |
+
.stage-cover p { margin: 0; color: var(--text-2); }
|
| 95 |
+
.stage-wipe { position: absolute; inset: 0; border: 0; border-radius: 0; aspect-ratio: auto; }
|
| 96 |
+
.stage-hint { position: absolute; left: 50%; top: 16px; transform: translateX(-50%); z-index: 2; pointer-events: none; font: 600 13px var(--mono); color: var(--accent-ink); background: var(--accent); padding: 6px 12px; border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.4); transition: opacity .4s; white-space: nowrap; }
|
| 97 |
+
.stage-hint.gone { opacity: 0; }
|
| 98 |
+
.stage-bar { display: flex; align-items: center; gap: 12px 20px; flex-wrap: wrap; padding: 10px var(--gutter); background: var(--bg); border-top: 1px solid var(--border); }
|
| 99 |
+
.stage-about { display: grid; gap: 1px; min-width: 0; }
|
| 100 |
+
.stage-about b { font: 700 16px var(--display); }
|
| 101 |
+
.stage-about span { color: var(--muted); font-size: 13px; display: inline-flex; align-items: center; gap: 6px; }
|
| 102 |
+
.stage-mid { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-inline: auto; }
|
| 103 |
+
.stage-mid .seg button { display: inline-flex; gap: 8px; align-items: baseline; }
|
| 104 |
+
.stage-mid .seg button b { font: 700 12px var(--mono); color: var(--muted); }
|
| 105 |
+
.stage-mid .seg button.on b { color: var(--accent); }
|
| 106 |
+
.stage-keys { font: 500 12px var(--mono); color: var(--muted); }
|
| 107 |
+
body.on-stage .nav { transform: translateY(-100%); }
|
| 108 |
+
.nav { transition: transform .25s, border-color .2s, background .2s; }
|
| 109 |
+
@media (max-width: 720px) { .stage-mid { margin-inline: 0; } .stage-down { margin-left: auto; } .stage-keys { display: none; } }
|
| 110 |
+
.btn-reset { border: 0; padding: 24px; margin: 0; width: 100%; height: 100%; cursor: pointer; color: var(--text); font: inherit; background: none; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/js/v1.js
CHANGED
|
@@ -12,41 +12,39 @@ Evo.boot(({ games, arms }) => {
|
|
| 12 |
const hv = $("#hero-video");
|
| 13 |
Evo.mountHeroVideo(hv, { src: hv.dataset.src, poster: hv.dataset.poster });
|
| 14 |
|
| 15 |
-
// ----------
|
| 16 |
-
(async function
|
| 17 |
const cfg = window.EVO_PLAY || await fetch(Evo.data("play.json")).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
| 18 |
const game = cfg?.games?.[0];
|
|
|
|
| 19 |
if (!game) return;
|
| 20 |
-
const
|
| 21 |
const ROUNDS = [["r0", "Round 0"], ["r30", "Round 30"]];
|
| 22 |
const label = (r) => ROUNDS.find(([k]) => k === r)[1];
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
$("#
|
| 26 |
-
$("#
|
|
|
|
| 27 |
|
| 28 |
-
function
|
| 29 |
-
running = false;
|
| 30 |
-
fsBtn.hidden = true;
|
| 31 |
-
const mbs = Math.round((cfg.engine.wasm_gz + game.builds[sel].size) / 1048576);
|
| 32 |
-
const poster = h("button", { class: "play-poster", type: "button", "aria-label": `Play ${game.title}, ${label(sel)}` },
|
| 33 |
-
h("img", { src: Evo.media(`${game.pair}/${sel === "r0" ? "p0" : "final"}.webp`), alt: "" }),
|
| 34 |
-
h("span", { class: "play-go" }, h("i"), h("b", {}, `Play ${label(sel)} in your browser`), h("span", {}, `${mbs} MB download · keyboard`)));
|
| 35 |
-
poster.addEventListener("click", start);
|
| 36 |
-
screen.replaceChildren(poster);
|
| 37 |
-
}
|
| 38 |
-
function start() {
|
| 39 |
-
running = true;
|
| 40 |
-
Evo.pauseAll();
|
| 41 |
-
$("#hero-video").pause();
|
| 42 |
const b = game.builds[sel];
|
| 43 |
const src = "play.html?" + new URLSearchParams({
|
| 44 |
engine: Evo.media(cfg.engine.base), wasm_gz: cfg.engine.wasm_gz, pck: Evo.media(b.pck), pck_size: b.size,
|
|
|
|
| 45 |
});
|
| 46 |
-
|
|
|
|
| 47 |
frame.addEventListener("load", () => frame.contentWindow?.focus());
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
rounds.replaceChildren(...ROUNDS.map(([r, t]) => h("button", {
|
| 52 |
type: "button", class: r === sel ? "on" : "", "data-r": r,
|
|
@@ -54,12 +52,29 @@ Evo.boot(({ games, arms }) => {
|
|
| 54 |
if (sel === r) return;
|
| 55 |
sel = r;
|
| 56 |
rounds.querySelectorAll("button").forEach((x) => x.classList.toggle("on", x.dataset.r === r));
|
| 57 |
-
|
| 58 |
},
|
| 59 |
}, t, h("b", {}, s100(game.scores[r])))));
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
})();
|
| 64 |
|
| 65 |
// ---------- headline: one row per generator ----------
|
|
|
|
| 12 |
const hv = $("#hero-video");
|
| 13 |
Evo.mountHeroVideo(hv, { src: hv.dataset.src, poster: hv.dataset.poster });
|
| 14 |
|
| 15 |
+
// ---------- first screen: a real game, already running ----------
|
| 16 |
+
(async function stage() {
|
| 17 |
const cfg = window.EVO_PLAY || await fetch(Evo.data("play.json")).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
| 18 |
const game = cfg?.games?.[0];
|
| 19 |
+
const el = $("#stage");
|
| 20 |
if (!game) return;
|
| 21 |
+
const box = $("#stage-game"), rounds = $("#stage-rounds");
|
| 22 |
const ROUNDS = [["r0", "Round 0"], ["r30", "Round 30"]];
|
| 23 |
const label = (r) => ROUNDS.find(([k]) => k === r)[1];
|
| 24 |
+
const canPlay = matchMedia("(pointer: fine)").matches && !navigator.connection?.saveData;
|
| 25 |
+
let sel = "r30", frame = null;
|
| 26 |
+
$("#stage-title").textContent = canPlay ? `You're playing ${game.title}` : `${game.title}: round 0 → round 30`;
|
| 27 |
+
$("#stage-who").replaceChildren(dot(game.model), `made by ${game.model}, then improved over 30 EvoGame rounds`);
|
| 28 |
+
$("#stage-keys").textContent = canPlay ? game.controls : "";
|
| 29 |
|
| 30 |
+
function run() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
const b = game.builds[sel];
|
| 32 |
const src = "play.html?" + new URLSearchParams({
|
| 33 |
engine: Evo.media(cfg.engine.base), wasm_gz: cfg.engine.wasm_gz, pck: Evo.media(b.pck), pck_size: b.size,
|
| 34 |
+
title: `${game.title} · ${label(sel)}`, poster: Evo.media(`${game.pair}/${sel === "r0" ? "p0" : "final"}.webp`),
|
| 35 |
});
|
| 36 |
+
frame = h("iframe", { src, title: `${game.title}, ${label(sel)}`, allow: "autoplay; fullscreen; gamepad" });
|
| 37 |
+
const hint = h("div", { class: "stage-hint" }, `Live game · ${game.controls}`);
|
| 38 |
frame.addEventListener("load", () => frame.contentWindow?.focus());
|
| 39 |
+
box.replaceChildren(frame, hint);
|
| 40 |
+
const dismiss = () => hint.classList.add("gone");
|
| 41 |
+
setTimeout(dismiss, 6000);
|
| 42 |
+
frame.addEventListener("load", () => frame.contentWindow?.addEventListener("keydown", dismiss, { once: true }));
|
| 43 |
+
}
|
| 44 |
+
function watch() { // touch screens and data saver: the same two builds, replayed
|
| 45 |
+
const wipe = Evo.wipeFrame(game.pair, { alt: game.title, eager: true, cls: "wipe stage-wipe", after: "Round 30" });
|
| 46 |
+
box.replaceChildren(wipe);
|
| 47 |
+
Evo.observe(wipe);
|
| 48 |
}
|
| 49 |
rounds.replaceChildren(...ROUNDS.map(([r, t]) => h("button", {
|
| 50 |
type: "button", class: r === sel ? "on" : "", "data-r": r,
|
|
|
|
| 52 |
if (sel === r) return;
|
| 53 |
sel = r;
|
| 54 |
rounds.querySelectorAll("button").forEach((x) => x.classList.toggle("on", x.dataset.r === r));
|
| 55 |
+
if (canPlay) run();
|
| 56 |
},
|
| 57 |
}, t, h("b", {}, s100(game.scores[r])))));
|
| 58 |
+
if (!canPlay) rounds.hidden = true;
|
| 59 |
+
el.hidden = false;
|
| 60 |
+
canPlay ? run() : watch();
|
| 61 |
+
|
| 62 |
+
// hide the site nav while the game has the screen; free the game once it is far out of view
|
| 63 |
+
new IntersectionObserver(([e]) => {
|
| 64 |
+
document.body.classList.toggle("on-stage", e.intersectionRatio > 0.6);
|
| 65 |
+
if (e.intersectionRatio > 0.2) $("#hero-video").pause();
|
| 66 |
+
}, { threshold: [0, 0.2, 0.6, 1] }).observe(el);
|
| 67 |
+
new IntersectionObserver(([e]) => {
|
| 68 |
+
if (!canPlay) return;
|
| 69 |
+
if (!e.isIntersecting && frame) {
|
| 70 |
+
frame = null;
|
| 71 |
+
const again = h("button", { class: "stage-cover btn-reset", type: "button" },
|
| 72 |
+
h("img", { src: Evo.media(`${game.pair}/${sel === "r0" ? "p0" : "final"}.webp`), alt: "" }),
|
| 73 |
+
h("div", {}, h("h2", {}, `${game.title} is paused`), h("span", { class: "btn btn-primary" }, "Play again")));
|
| 74 |
+
again.addEventListener("click", run);
|
| 75 |
+
box.replaceChildren(again);
|
| 76 |
+
}
|
| 77 |
+
}, { rootMargin: "300px 0px" }).observe(el);
|
| 78 |
})();
|
| 79 |
|
| 80 |
// ---------- headline: one row per generator ----------
|