Spaces:
Running
Running
Add the build scripts and docs; one index.html for every host
Browse files- .gitignore +3 -0
- README.md +94 -1
- _build/build_hero.py +63 -0
- _build/build_media.py +197 -0
- _build/build_pairs.py +253 -0
- _build/build_web.py +89 -0
- _build/web_export_presets.cfg +34 -0
- index.html +1 -2
- static/css/v2.css +224 -0
- static/css/v3.css +269 -0
- static/css/v4.css +230 -0
- static/js/core.js +7 -5
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
media
|
| 2 |
+
__pycache__/
|
| 3 |
+
_shots/
|
README.md
CHANGED
|
@@ -7,4 +7,97 @@ sdk: static
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
EvoGame project page
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# EvoGame — project page
|
| 11 |
+
|
| 12 |
+
Static site: plain HTML/CSS/JS, no build step, no framework. It shows every game we ran EvoGame on
|
| 13 |
+
twice — the initial generation (round 0) and the same game after 30 EvoGame rounds — plus one game
|
| 14 |
+
that visitors play in the browser.
|
| 15 |
+
|
| 16 |
+
Live: https://wenyiwu0111-evogame-page.static.hf.space (a public HF static Space)
|
| 17 |
+
|
| 18 |
+
## Run it locally
|
| 19 |
+
|
| 20 |
+
```bash
|
| 21 |
+
python -m http.server 8765 # then open http://127.0.0.1:8765
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
The page `fetch()`es JSON, so open it over HTTP, not as a `file://` path.
|
| 25 |
+
|
| 26 |
+
Video, images and the playable builds are **not in this repo** (see *Media* below). By default they
|
| 27 |
+
stream from the published site, so a fresh clone works with no extra files. To use a local copy
|
| 28 |
+
instead, put it (or a symlink to it) at `./media` and open the page with `?media=local`:
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
ln -s /path/to/evogame-page-media media # then http://127.0.0.1:8765/?media=local
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## Layout
|
| 35 |
+
|
| 36 |
+
```
|
| 37 |
+
index.html the page: game screen, hero, before/after, gallery, results, citation
|
| 38 |
+
play.html standalone Godot web player (one shared engine, one game pack per build)
|
| 39 |
+
static/css/base.css tokens + everything shared (cards, modal, charts, wipe comparison)
|
| 40 |
+
static/css/v1.css this design's own pieces (game screen, spotlight, pair cards)
|
| 41 |
+
static/js/core.js data loading, video scheduling, before/after wipe, game dialog, charts
|
| 42 |
+
static/js/v1.js this page: what goes where
|
| 43 |
+
static/data/evo.json one row per game: round-0 and round-30 scores, chosen demo, score curve
|
| 44 |
+
static/data/evo/<arm>/<task>.json per-requirement before/after + judge rationales + 30-round timeline
|
| 45 |
+
static/data/play.json the playable builds (engine + per-round .pck sizes)
|
| 46 |
+
static/data/games.json the 280 initial generations across both engines (used by v2–v4 only)
|
| 47 |
+
_build/*.py regenerate everything from the experiment runs (see below)
|
| 48 |
+
v2/ v3/ v4/ earlier design drafts kept for reference; the live page is v1 (root)
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
`static/js/core.js` is shared by all four designs — change it carefully.
|
| 52 |
+
|
| 53 |
+
## Media (not in this repo)
|
| 54 |
+
|
| 55 |
+
Built into `/storage/admin/wenyi/evogame-page-media` on the lab machine:
|
| 56 |
+
|
| 57 |
+
```
|
| 58 |
+
pairs/<arm>/<task>/p0.mp4, final.mp4, p0.webp, final.webp the same demo on both builds (54 MB)
|
| 59 |
+
play/engine/godot.js, godot.wasm.gz, godot.audio*.js one shared Godot 4.6.2 web engine (9 MB gz)
|
| 60 |
+
play/<arm>/<task>/<r0|r30>/index.pck one game pack per playable build
|
| 61 |
+
hero_evo.mp4 / .webp the moving background: 30 games in one video
|
| 62 |
+
baseline/... clips for the v2–v4 drafts
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
Everything the live site needs is also in the Space repo, so the quickest way to get a full working
|
| 66 |
+
copy is to clone the Space (below).
|
| 67 |
+
|
| 68 |
+
## Rebuild the data
|
| 69 |
+
|
| 70 |
+
Needs the experiment runs on the lab machine, and Godot 4.6.2 with web export templates for `build_web.py`.
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
python _build/build_pairs.py # before/after clips + evo.json + per-game detail JSON
|
| 74 |
+
python _build/build_hero.py --evo --out hero_evo --cols 5 --rows 6 --tile 320x180 --gap 3
|
| 75 |
+
python _build/build_web.py --games godot_qwen/platformer-dig-descent # playable web builds
|
| 76 |
+
python _build/build_media.py # only for the v2–v4 drafts (280 baseline games)
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
`build_pairs.py --reuse-media` rebuilds the JSON without re-encoding video.
|
| 80 |
+
|
| 81 |
+
## Deploy
|
| 82 |
+
|
| 83 |
+
The live site is the HF Space `WenyiWU0111/evogame-page` (sdk: static). It holds the page **and** its
|
| 84 |
+
media, so a clone is a complete, runnable copy:
|
| 85 |
+
|
| 86 |
+
```bash
|
| 87 |
+
git clone https://huggingface.co/spaces/WenyiWU0111/evogame-page
|
| 88 |
+
cd evogame-page && python -m http.server 8765
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
Push to that repo to publish (needs write access on the Space). The Space copy differs from this repo
|
| 92 |
+
in two small ways, both injected at deploy time: `window.EVO_MEDIA = "media"` pins the media folder,
|
| 93 |
+
and `window.EVO_NO_SWITCH = true` hides the v1–v4 switcher.
|
| 94 |
+
|
| 95 |
+
## Notes for whoever picks this up
|
| 96 |
+
|
| 97 |
+
- Scores are ×100 of the GameCraft-Bench reward, judged by Qwen3.8-27B over three passes; round 0 and
|
| 98 |
+
round 30 use the same scorer, judge and demo scripts, so the two sides are comparable.
|
| 99 |
+
- Six games are missing from the gallery because their round 30 had not finished scoring; rerun
|
| 100 |
+
`build_pairs.py` to pick them up.
|
| 101 |
+
- Authors, paper/code/dataset links and the BibTeX entry are still placeholders in `index.html`.
|
| 102 |
+
- Videos never autoplay more than a handful at a time: cards hold a poster until they scroll into
|
| 103 |
+
view, at most 12 clips play at once, and the hero is one pre-composited video rather than 30.
|
_build/build_hero.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Composite the hero background: a COLSxROWS grid of highlight clips as ONE looping video.
|
| 2 |
+
|
| 3 |
+
One decoder instead of COLS*ROWS - the page stays smooth. Picks the top games by reward,
|
| 4 |
+
alternating engines, skipping duplicate tasks.
|
| 5 |
+
|
| 6 |
+
python _build/build_hero.py [--cols 6 --rows 3]
|
| 7 |
+
"""
|
| 8 |
+
import argparse, io, json, subprocess
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
from PIL import Image
|
| 12 |
+
|
| 13 |
+
PAGE = Path(__file__).resolve().parents[1]
|
| 14 |
+
MEDIA = Path("/storage/admin/wenyi/evogame-page-media")
|
| 15 |
+
|
| 16 |
+
ap = argparse.ArgumentParser()
|
| 17 |
+
ap.add_argument("--cols", type=int, default=6)
|
| 18 |
+
ap.add_argument("--rows", type=int, default=3)
|
| 19 |
+
ap.add_argument("--seconds", type=float, default=10)
|
| 20 |
+
ap.add_argument("--evo", action="store_true", help="use the round-30 clips of the before/after pairs")
|
| 21 |
+
ap.add_argument("--out", default="hero_mosaic")
|
| 22 |
+
ap.add_argument("--tile", default="320x180", help="cell size in the composite, WxH")
|
| 23 |
+
ap.add_argument("--gap", type=int, default=0, help="dark gutter inside each cell, px")
|
| 24 |
+
a = ap.parse_args()
|
| 25 |
+
n = a.cols * a.rows
|
| 26 |
+
|
| 27 |
+
if a.evo:
|
| 28 |
+
games = json.loads((PAGE / "static/data/evo.json").read_text())
|
| 29 |
+
for g in games:
|
| 30 |
+
g["clip"] = MEDIA / g["pair"] / "final.mp4"
|
| 31 |
+
g["reward"] = g["repaired"]["reward"]
|
| 32 |
+
else:
|
| 33 |
+
games = [g for g in json.loads((PAGE / "static/data/games.json").read_text()) if g["media"]]
|
| 34 |
+
for g in games:
|
| 35 |
+
g["clip"] = MEDIA / g["media"] / "highlight.mp4"
|
| 36 |
+
by_arm = {}
|
| 37 |
+
for g in sorted(games, key=lambda g: -g["reward"]):
|
| 38 |
+
by_arm.setdefault(g["arm"], []).append(g)
|
| 39 |
+
picked, seen = [], set()
|
| 40 |
+
while len(picked) < n and any(by_arm.values()):
|
| 41 |
+
for arm in list(by_arm):
|
| 42 |
+
while by_arm[arm]:
|
| 43 |
+
g = by_arm[arm].pop(0)
|
| 44 |
+
if g["task"] not in seen:
|
| 45 |
+
picked.append(g); seen.add(g["task"]); break
|
| 46 |
+
picked = picked[:n]
|
| 47 |
+
|
| 48 |
+
W, H = map(int, a.tile.split("x"))
|
| 49 |
+
g2 = a.gap * 2
|
| 50 |
+
cmd = ["ffmpeg", "-y"]
|
| 51 |
+
for g in picked:
|
| 52 |
+
cmd += ["-stream_loop", "-1", "-ss", "2", "-t", str(a.seconds), "-i", str(g["clip"])]
|
| 53 |
+
fl = [f"[{i}:v]scale={W - g2}:{H - g2},pad={W}:{H}:{a.gap}:{a.gap}:color=0x05060a,setsar=1,fps=24[v{i}]" for i in range(n)]
|
| 54 |
+
layout = "|".join(f"{(i % a.cols) * W}_{(i // a.cols) * H}" for i in range(n))
|
| 55 |
+
fl.append("".join(f"[v{i}]" for i in range(n)) + f"xstack=inputs={n}:layout={layout}[out]")
|
| 56 |
+
out = MEDIA / f"{a.out}.mp4"
|
| 57 |
+
cmd += ["-filter_complex", ";".join(fl), "-map", "[out]", "-t", str(a.seconds), "-c:v", "libx264",
|
| 58 |
+
"-preset", "slow", "-crf", "30", "-pix_fmt", "yuv420p", "-movflags", "+faststart", "-an", str(out)]
|
| 59 |
+
subprocess.run(cmd, check=True, stderr=subprocess.DEVNULL)
|
| 60 |
+
png = subprocess.run(["ffmpeg", "-ss", "1", "-i", str(out), "-frames:v", "1", "-f", "image2pipe", "-c:v", "png", "-"],
|
| 61 |
+
check=True, capture_output=True).stdout
|
| 62 |
+
Image.open(io.BytesIO(png)).save(MEDIA / f"{a.out}.webp", "WEBP", quality=70)
|
| 63 |
+
print(out, out.stat().st_size // 1024, "KB;", ", ".join(g["task"] for g in picked))
|
_build/build_media.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build the page's media + data from the r0 baseline scoring runs.
|
| 2 |
+
|
| 3 |
+
For every (arm, task) it writes, under MEDIA_OUT/baseline/<arm>/<task>/:
|
| 4 |
+
highlight.mp4 ~10 s montage of the most active window of up to 4 demos (640x360, no audio)
|
| 5 |
+
poster.webp the busiest frame, for the card before the video loads
|
| 6 |
+
demos/<id>.mp4 every pass-1 demo, remuxed with faststart so it streams
|
| 7 |
+
and into the page repo:
|
| 8 |
+
static/data/games.json one row per game (what the gallery needs)
|
| 9 |
+
static/data/details/<arm>/<task>.json per-requirement scores + judge rationales (loaded on open)
|
| 10 |
+
|
| 11 |
+
Scores come from pass 1 of r0_scores (judge qwen38-27b); reward/M/D/V/A are the CSV means.
|
| 12 |
+
|
| 13 |
+
python _build/build_media.py --arms godot_gpt phaser_gpt [--tasks a b] [--jobs 16] [--data-only]
|
| 14 |
+
"""
|
| 15 |
+
import argparse, csv, json, re, subprocess, sys
|
| 16 |
+
from concurrent.futures import ProcessPoolExecutor
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
from PIL import Image
|
| 21 |
+
|
| 22 |
+
SCORES = Path("/storage/admin/wenyi/r0_scores")
|
| 23 |
+
MEDIA_OUT = Path("/storage/admin/wenyi/evogame-page-media")
|
| 24 |
+
PAGE = Path(__file__).resolve().parents[1]
|
| 25 |
+
TASKS = {"godot": Path("/home/admin/wenyi/gamecraft-bench/tasks"),
|
| 26 |
+
"phaser": Path("/home/admin/wenyi/gamecraft-bench-web/tasks-web")}
|
| 27 |
+
ARMS = {
|
| 28 |
+
"godot_gpt": {"engine": "Godot 4", "model": "GPT-5.5", "harness": "Codex CLI"},
|
| 29 |
+
"phaser_gpt": {"engine": "Phaser", "model": "GPT-5.5", "harness": "OpenGame"},
|
| 30 |
+
"godot_qwen": {"engine": "Godot 4", "model": "Qwen3.8-27B", "harness": ""},
|
| 31 |
+
"phaser_qwen": {"engine": "Phaser", "model": "Qwen3.8-27B", "harness": ""},
|
| 32 |
+
"godot_kimi": {"engine": "Godot 4", "model": "Kimi-K2.6", "harness": ""},
|
| 33 |
+
"godot_glm": {"engine": "Godot 4", "model": "GLM-5.3-Flash", "harness": ""},
|
| 34 |
+
}
|
| 35 |
+
CATS = {"M": "Core Mechanics", "D": "Content Depth", "V": "Functional Visuals", "A": "Presentation & Art"}
|
| 36 |
+
SEG_S, MAX_SEGS, FRAME_STEP_S = 2.5, 4, 0.5
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def run(cmd):
|
| 40 |
+
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def instruction_meta(engine_key, task):
|
| 44 |
+
md = (TASKS[engine_key] / task / "instruction.md").read_text()
|
| 45 |
+
title = re.search(r"^# (.+)$", md, re.M).group(1).strip()
|
| 46 |
+
m = re.search(r"## Core Vision\s+(.+?)(?:\n## |\Z)", md, re.S)
|
| 47 |
+
vision = re.sub(r"\s+", " ", m.group(1)).strip() if m else ""
|
| 48 |
+
toml = (TASKS[engine_key] / task / "task.toml").read_text()
|
| 49 |
+
d = re.search(r'^description\s*=\s*"(.*)"', toml, re.M)
|
| 50 |
+
return title, (d.group(1) if d else ""), vision
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def padded_from(task_dir):
|
| 54 |
+
"""demo_id -> first padded frame index (frozen tail we should not show)."""
|
| 55 |
+
p = task_dir / "tail_padding.json"
|
| 56 |
+
if not p.exists():
|
| 57 |
+
return {}
|
| 58 |
+
return {x["demo"]: x["from"] for x in json.loads(p.read_text()).get("padded", [])}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def activity(frames):
|
| 62 |
+
"""Per-step visual change between consecutive 0.5 s frames."""
|
| 63 |
+
thumbs = [np.asarray(Image.open(f).convert("L").resize((64, 36)), dtype=np.float32) for f in frames]
|
| 64 |
+
return [float(np.abs(a - b).mean()) for a, b in zip(thumbs, thumbs[1:])]
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def best_window(demo):
|
| 68 |
+
"""(start_s, score, busiest_frame) of the most active SEG_S window in the unpadded part."""
|
| 69 |
+
frames = sorted((demo["dir"] / "frames").glob("frame_*.png"))
|
| 70 |
+
usable = demo["dur"] - 0.3
|
| 71 |
+
if demo["pad_from_frame"] is not None:
|
| 72 |
+
usable = min(usable, demo["pad_from_frame"] / 30.0)
|
| 73 |
+
n = int(usable / FRAME_STEP_S)
|
| 74 |
+
frames = frames[: max(n, 2)]
|
| 75 |
+
if len(frames) < 2:
|
| 76 |
+
return 0.0, 0.0, frames[0] if frames else None
|
| 77 |
+
act = activity(frames)
|
| 78 |
+
w = max(1, int(SEG_S / FRAME_STEP_S))
|
| 79 |
+
sums = [sum(act[i:i + w]) for i in range(max(1, len(act) - w + 1))]
|
| 80 |
+
i = int(np.argmax(sums))
|
| 81 |
+
start = min(i * FRAME_STEP_S, max(0.0, usable - SEG_S))
|
| 82 |
+
busiest = frames[i + 1 + int(np.argmax(act[i:i + w]))] if act[i:i + w] else frames[i]
|
| 83 |
+
return start, sums[i], busiest
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def build_game(arm, task, data_only=False):
|
| 87 |
+
tdir = SCORES / arm / task
|
| 88 |
+
p1 = tdir / "p1"
|
| 89 |
+
out = MEDIA_OUT / "baseline" / arm / task
|
| 90 |
+
pads = padded_from(tdir)
|
| 91 |
+
demos = []
|
| 92 |
+
if (p1 / "demos").is_dir():
|
| 93 |
+
for d in sorted((p1 / "demos").iterdir()):
|
| 94 |
+
mp4 = d / f"{d.name}.mp4"
|
| 95 |
+
if not mp4.exists():
|
| 96 |
+
continue
|
| 97 |
+
dur = float(subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
|
| 98 |
+
"-of", "csv=p=0", str(mp4)], capture_output=True, text=True).stdout or 0)
|
| 99 |
+
demos.append({"id": d.name, "dir": d, "mp4": mp4, "dur": dur, "pad_from_frame": pads.get(d.name)})
|
| 100 |
+
has_media = bool(demos)
|
| 101 |
+
if has_media and not (data_only and (out / "highlight.mp4").exists()):
|
| 102 |
+
(out / "demos").mkdir(parents=True, exist_ok=True)
|
| 103 |
+
for d in demos:
|
| 104 |
+
run(["ffmpeg", "-y", "-i", str(d["mp4"]), "-c", "copy", "-an", "-movflags", "+faststart",
|
| 105 |
+
str(out / "demos" / f"{d['id']}.mp4")])
|
| 106 |
+
wins = [(d, *best_window(d)) for d in demos]
|
| 107 |
+
# the MAX_SEGS most active demos, shown in their original order
|
| 108 |
+
picked = sorted(sorted(wins, key=lambda x: -x[2])[:MAX_SEGS], key=lambda x: x[0]["id"])
|
| 109 |
+
cmd, fl = ["ffmpeg", "-y"], []
|
| 110 |
+
for k, (d, start, _, _) in enumerate(picked):
|
| 111 |
+
cmd += ["-ss", f"{start:.2f}", "-t", str(SEG_S), "-i", str(d["mp4"])]
|
| 112 |
+
fl.append(f"[{k}:v]scale=640:360:force_original_aspect_ratio=decrease,pad=640:360:(ow-iw)/2:(oh-ih)/2,"
|
| 113 |
+
f"fps=24,setsar=1,format=yuv420p[v{k}]")
|
| 114 |
+
fl.append("".join(f"[v{k}]" for k in range(len(picked))) + f"concat=n={len(picked)}:v=1:a=0[out]")
|
| 115 |
+
cmd += ["-filter_complex", ";".join(fl), "-map", "[out]", "-c:v", "libx264", "-preset", "slow",
|
| 116 |
+
"-crf", "30", "-movflags", "+faststart", "-an", str(out / "highlight.mp4")]
|
| 117 |
+
run(cmd)
|
| 118 |
+
busiest = max(wins, key=lambda x: x[2])[3]
|
| 119 |
+
Image.open(busiest).convert("RGB").resize((640, 360)).save(out / "poster.webp", "WEBP", quality=72)
|
| 120 |
+
return arm, task, [{"id": d["id"], "dur": round(d["dur"], 1)} for d in demos], has_media
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def details(arm, task):
|
| 124 |
+
p1 = SCORES / arm / task / "p1"
|
| 125 |
+
bpath = p1 / "breakdown.json"
|
| 126 |
+
if not bpath.exists():
|
| 127 |
+
return None
|
| 128 |
+
b = json.loads(bpath.read_text())
|
| 129 |
+
rationale = {}
|
| 130 |
+
jl = p1 / "judge_log.json"
|
| 131 |
+
if jl.exists():
|
| 132 |
+
for r in json.loads(jl.read_text()):
|
| 133 |
+
rationale.setdefault(r["requirement_id"], {})[r["demo_id"]] = r.get("rationale", "")
|
| 134 |
+
return {
|
| 135 |
+
"reward_p1": b.get("reward"), "build_ok": b.get("build_ok"), "formula": b.get("formula"),
|
| 136 |
+
"judge": b.get("judge", {}).get("model"),
|
| 137 |
+
"requirements": [{
|
| 138 |
+
"id": r["id"], "cat": CATS.get(r["id"][0], ""), "description": r["description"],
|
| 139 |
+
"score": r.get("aggregated"), "agg": r.get("agg"),
|
| 140 |
+
"per_demo": r.get("per_demo", {}), "rationale": rationale.get(r["id"], {}),
|
| 141 |
+
} for r in b.get("requirements", [])],
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def main():
|
| 146 |
+
ap = argparse.ArgumentParser()
|
| 147 |
+
ap.add_argument("--arms", nargs="+", default=["godot_gpt", "phaser_gpt"])
|
| 148 |
+
ap.add_argument("--tasks", nargs="*")
|
| 149 |
+
ap.add_argument("--jobs", type=int, default=16)
|
| 150 |
+
ap.add_argument("--data-only", action="store_true", help="skip media that already exists")
|
| 151 |
+
a = ap.parse_args()
|
| 152 |
+
|
| 153 |
+
rows = {}
|
| 154 |
+
for arm in a.arms:
|
| 155 |
+
for r in csv.DictReader(open(SCORES / f"{arm}.csv")):
|
| 156 |
+
if not a.tasks or r["task"] in a.tasks:
|
| 157 |
+
rows[(arm, r["task"])] = r
|
| 158 |
+
|
| 159 |
+
media = {}
|
| 160 |
+
with ProcessPoolExecutor(a.jobs) as ex:
|
| 161 |
+
futs = [ex.submit(build_game, arm, task, a.data_only) for arm, task in rows]
|
| 162 |
+
for f in futs:
|
| 163 |
+
try:
|
| 164 |
+
arm, task, demos, has_media = f.result()
|
| 165 |
+
media[(arm, task)] = (demos, has_media)
|
| 166 |
+
print("ok", arm, task, len(demos), flush=True)
|
| 167 |
+
except subprocess.CalledProcessError as e:
|
| 168 |
+
print("FAIL", e.cmd[-1], e.stderr.decode()[-400:], file=sys.stderr, flush=True)
|
| 169 |
+
|
| 170 |
+
data = PAGE / "static" / "data"
|
| 171 |
+
games = []
|
| 172 |
+
for (arm, task), r in rows.items():
|
| 173 |
+
if (arm, task) not in media:
|
| 174 |
+
continue
|
| 175 |
+
engine_key = arm.split("_")[0]
|
| 176 |
+
title, blurb, vision = instruction_meta(engine_key, task)
|
| 177 |
+
demos, has_media = media[(arm, task)]
|
| 178 |
+
det = details(arm, task)
|
| 179 |
+
if det:
|
| 180 |
+
(data / "details" / arm).mkdir(parents=True, exist_ok=True)
|
| 181 |
+
(data / "details" / arm / f"{task}.json").write_text(json.dumps(det, ensure_ascii=False))
|
| 182 |
+
f = lambda k: round(float(r[k]), 4)
|
| 183 |
+
games.append({
|
| 184 |
+
"arm": arm, "task": task, **ARMS[arm], "genre": task.split("-")[0],
|
| 185 |
+
"title": title, "blurb": blurb, "vision": vision,
|
| 186 |
+
"reward": f("reward"), "build": f("BUILD"), "M": f("M"), "D": f("D"), "V": f("V"), "A": f("A"),
|
| 187 |
+
"media": f"baseline/{arm}/{task}" if has_media else None, "demos": demos,
|
| 188 |
+
"repaired": None,
|
| 189 |
+
})
|
| 190 |
+
games.sort(key=lambda g: (g["arm"], -g["reward"]))
|
| 191 |
+
data.mkdir(parents=True, exist_ok=True)
|
| 192 |
+
(data / "games.json").write_text(json.dumps(games, ensure_ascii=False, indent=0))
|
| 193 |
+
print(f"wrote {len(games)} games")
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
if __name__ == "__main__":
|
| 197 |
+
main()
|
_build/build_pairs.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Before/after pairs: the initial generation (round 0) next to the game after 30 EvoGame rounds.
|
| 2 |
+
|
| 3 |
+
For every game with a scored round 30 it writes, under MEDIA_OUT/pairs/<arm>/<task>/:
|
| 4 |
+
p0.mp4 / final.mp4 the SAME demo replayed on both builds, cut to the same length (no padded tail, <= MAX_S)
|
| 5 |
+
p0.webp / final.webp stills taken at the same moment of each clip
|
| 6 |
+
and into the page repo:
|
| 7 |
+
static/data/evo.json one row per game: scores before/after, the chosen demo, score curve
|
| 8 |
+
static/data/evo/<arm>/<task>.json per-requirement before/after + judge rationales + the 30-round timeline
|
| 9 |
+
|
| 10 |
+
Both sides are scored by the same scorer/judge (qwen38-27b, 3 passes); scores are pass means, categories are
|
| 11 |
+
pass means of the rubric variables. The demo shown is the common demo whose per-demo judge score rose the most.
|
| 12 |
+
|
| 13 |
+
python _build/build_pairs.py [--arms godot_gpt godot_qwen] [--tasks ...] [--jobs 16]
|
| 14 |
+
"""
|
| 15 |
+
import argparse, json, re, subprocess, sys
|
| 16 |
+
from io import BytesIO
|
| 17 |
+
from concurrent.futures import ProcessPoolExecutor
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
from PIL import Image
|
| 21 |
+
|
| 22 |
+
MEDIA_OUT = Path("/storage/admin/wenyi/evogame-page-media")
|
| 23 |
+
PAGE = Path(__file__).resolve().parents[1]
|
| 24 |
+
TASKS = Path("/home/admin/wenyi/gamecraft-bench/tasks")
|
| 25 |
+
FINAL_ROUND = 30
|
| 26 |
+
MAX_S = 20.0
|
| 27 |
+
FPS = 30 # Godot scorer recordings
|
| 28 |
+
REUSE = "--reuse-media" in sys.argv # keep already-encoded clips (data-only rebuild)
|
| 29 |
+
|
| 30 |
+
ARMS = {
|
| 31 |
+
"godot_gpt": {
|
| 32 |
+
"engine": "Godot 4", "model": "GPT-5.5", "harness": "Codex CLI",
|
| 33 |
+
"p0": lambda g: Path(f"/storage/admin/wenyi/r0_scores/godot_gpt/{g}"),
|
| 34 |
+
"loop": Path("/storage/admin/wenyi/newloop_self40/runs"),
|
| 35 |
+
},
|
| 36 |
+
"godot_qwen": {
|
| 37 |
+
"engine": "Godot 4", "model": "Qwen3.8-27B", "harness": "",
|
| 38 |
+
# the loop started from corpus_qwen; this is round 0 of exactly that build, scored the same way
|
| 39 |
+
"p0": lambda g: Path(f"/storage/admin/scored/qwen_{g}__G0"),
|
| 40 |
+
"loop": Path("/home/admin/wenyi/gamedoctor_run/newloop_selfqwen40/runs"),
|
| 41 |
+
},
|
| 42 |
+
}
|
| 43 |
+
CATS = {"M": "Core Mechanics", "D": "Content Depth", "V": "Functional Visuals", "A": "Presentation & Art"}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def clip_text(t, n):
|
| 47 |
+
t = re.sub(r"\s+", " ", t or "").strip()
|
| 48 |
+
return t if len(t) <= n else t[: n - 1].rsplit(" ", 1)[0] + "…"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def score_dir_ok(d):
|
| 52 |
+
s = d / "summary.json"
|
| 53 |
+
return s.exists() and not (d / "score_failed.txt").exists() and (d / "p1" / "breakdown.json").exists()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def scores(d):
|
| 57 |
+
"""reward and category means over the judging passes of one score dir."""
|
| 58 |
+
rewards = json.loads((d / "summary.json").read_text())["rewards"]
|
| 59 |
+
cats = {k: [] for k in CATS}
|
| 60 |
+
build = []
|
| 61 |
+
for p in sorted(d.glob("p[0-9]/breakdown.json")):
|
| 62 |
+
b = json.loads(p.read_text())
|
| 63 |
+
v = b.get("variables", {})
|
| 64 |
+
build.append(float(v.get("BUILD", 1.0 if b.get("build_ok") else 0.0)))
|
| 65 |
+
for k in CATS:
|
| 66 |
+
xs = [float(x) for n, x in v.items() if re.fullmatch(fr"{k}\d+", n)]
|
| 67 |
+
if xs:
|
| 68 |
+
cats[k].append(sum(xs) / len(xs))
|
| 69 |
+
out = {"reward": round(sum(rewards) / len(rewards), 4), "build": round(sum(build) / len(build), 2) if build else 0}
|
| 70 |
+
out.update({k: round(sum(xs) / len(xs), 4) if xs else 0.0 for k, xs in cats.items()})
|
| 71 |
+
return out
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def pad_from(d):
|
| 75 |
+
p = d / "tail_padding.json"
|
| 76 |
+
if not p.exists():
|
| 77 |
+
return {}
|
| 78 |
+
return {x["demo"]: x["from"] / FPS for x in json.loads(p.read_text()).get("padded", []) if x.get("from") is not None}
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def duration(mp4):
|
| 82 |
+
return float(subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", str(mp4)],
|
| 83 |
+
capture_output=True, text=True).stdout or 0)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def per_demo_mean(breakdown):
|
| 87 |
+
acc = {}
|
| 88 |
+
for r in breakdown["requirements"]:
|
| 89 |
+
for demo, s in r.get("per_demo", {}).items():
|
| 90 |
+
acc.setdefault(demo, []).append(float(s))
|
| 91 |
+
return {d: sum(v) / len(v) for d, v in acc.items()}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def rationales(d):
|
| 95 |
+
out = {}
|
| 96 |
+
jl = d / "p1" / "judge_log.json"
|
| 97 |
+
if jl.exists():
|
| 98 |
+
for r in json.loads(jl.read_text()):
|
| 99 |
+
out.setdefault(r["requirement_id"], {})[r["demo_id"]] = r.get("rationale", "")
|
| 100 |
+
return out
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def round_goal(e, plan):
|
| 104 |
+
"""What the round set out to do. Art rounds carry their goal in art/polish, not in the plan question."""
|
| 105 |
+
q = plan.get("question") or e.get("question") or ""
|
| 106 |
+
if q and not q.lower().startswith("no development question"):
|
| 107 |
+
return q
|
| 108 |
+
for src in (e.get("art"), e.get("polish")):
|
| 109 |
+
if isinstance(src, dict) and src.get("goal"):
|
| 110 |
+
return src["goal"]
|
| 111 |
+
landed = (e.get("art_list") or {}).get("landed") or []
|
| 112 |
+
asked = [x.get("asked") for x in landed if isinstance(x, dict) and x.get("asked")]
|
| 113 |
+
return "Art: " + "; ".join(asked[:3]) if asked else ""
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def timeline(loop_game):
|
| 117 |
+
rj = loop_game / "rounds.json"
|
| 118 |
+
if not rj.exists():
|
| 119 |
+
return []
|
| 120 |
+
out = []
|
| 121 |
+
for e in json.loads(rj.read_text()):
|
| 122 |
+
plan = e.get("plan") or {}
|
| 123 |
+
ver = e.get("verification") or {}
|
| 124 |
+
prim = next((i for i in ver.get("issues", []) if i.get("role") == "primary"), None)
|
| 125 |
+
verdict = ver.get("primary_verdict") or (prim or {}).get("verdict")
|
| 126 |
+
out.append({
|
| 127 |
+
"r": e.get("round"),
|
| 128 |
+
"kind": "art" if e.get("kind") == "art" else "repair",
|
| 129 |
+
"ask": clip_text(round_goal(e, plan), 240),
|
| 130 |
+
"found": clip_text(e.get("primary_issue") or "", 220),
|
| 131 |
+
"verdict": verdict,
|
| 132 |
+
"note": clip_text((prim or {}).get("note") or "", 260),
|
| 133 |
+
"files": len((e.get("repair") or {}).get("files_edited") or []),
|
| 134 |
+
"assets": (e.get("generated_files") or {}).get("n", 0),
|
| 135 |
+
"build_ok": e.get("build_ok"),
|
| 136 |
+
})
|
| 137 |
+
return out
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def build_game(arm, task):
|
| 141 |
+
cfg = ARMS[arm]
|
| 142 |
+
p0d, loop_game = cfg["p0"](task), cfg["loop"] / task
|
| 143 |
+
fin = loop_game / f"round_{FINAL_ROUND:02d}" / "score"
|
| 144 |
+
if not score_dir_ok(p0d):
|
| 145 |
+
return arm, task, None, "no round-0 score"
|
| 146 |
+
if not score_dir_ok(fin):
|
| 147 |
+
return arm, task, None, f"round {FINAL_ROUND} not scored (yet)"
|
| 148 |
+
|
| 149 |
+
b0 = json.loads((p0d / "p1" / "breakdown.json").read_text())
|
| 150 |
+
b1 = json.loads((fin / "p1" / "breakdown.json").read_text())
|
| 151 |
+
m0, m1 = per_demo_mean(b0), per_demo_mean(b1)
|
| 152 |
+
pads0, pads1 = pad_from(p0d), pad_from(fin)
|
| 153 |
+
cands = []
|
| 154 |
+
for demo in sorted(set(m0) & set(m1)):
|
| 155 |
+
v0, v1 = p0d / "p1" / "demos" / demo / f"{demo}.mp4", fin / "p1" / "demos" / demo / f"{demo}.mp4"
|
| 156 |
+
if not (v0.exists() and v1.exists()):
|
| 157 |
+
continue
|
| 158 |
+
usable = min(pads0.get(demo, 1e9), duration(v0), pads1.get(demo, 1e9), duration(v1), MAX_S)
|
| 159 |
+
if usable >= 4:
|
| 160 |
+
cands.append((m1[demo] - m0[demo], usable, demo, v0, v1))
|
| 161 |
+
if not cands:
|
| 162 |
+
return arm, task, None, "no common replayable demo"
|
| 163 |
+
gain, usable, demo, v0, v1 = max(cands)
|
| 164 |
+
|
| 165 |
+
out = MEDIA_OUT / "pairs" / arm / task
|
| 166 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 167 |
+
for src, name in ((v0, "p0"), (v1, "final")):
|
| 168 |
+
if REUSE and (out / f"{name}.mp4").exists() and (out / f"{name}.webp").exists():
|
| 169 |
+
continue
|
| 170 |
+
subprocess.run(["ffmpeg", "-y", "-v", "error", "-i", str(src), "-t", f"{usable:.2f}",
|
| 171 |
+
"-vf", "scale=854:480:force_original_aspect_ratio=decrease,pad=854:480:(ow-iw)/2:(oh-ih)/2,fps=24,format=yuv420p",
|
| 172 |
+
"-c:v", "libx264", "-preset", "slow", "-crf", "28", "-an", "-movflags", "+faststart",
|
| 173 |
+
str(out / f"{name}.mp4")], check=True)
|
| 174 |
+
png = subprocess.run(["ffmpeg", "-v", "error", "-ss", f"{usable * 0.45:.2f}", "-i", str(out / f"{name}.mp4"),
|
| 175 |
+
"-frames:v", "1", "-f", "image2pipe", "-c:v", "png", "-"], check=True, capture_output=True).stdout
|
| 176 |
+
Image.open(BytesIO(png)).convert("RGB").save(out / f"{name}.webp", "WEBP", quality=72)
|
| 177 |
+
|
| 178 |
+
s0, s1 = scores(p0d), scores(fin)
|
| 179 |
+
curve = [[0, s0["reward"]]]
|
| 180 |
+
for rd in sorted(loop_game.glob("round_*/score")):
|
| 181 |
+
r = int(rd.parent.name.split("_")[1])
|
| 182 |
+
if score_dir_ok(rd):
|
| 183 |
+
rw = json.loads((rd / "summary.json").read_text())["rewards"]
|
| 184 |
+
curve.append([r, round(sum(rw) / len(rw), 4)])
|
| 185 |
+
|
| 186 |
+
why0, why1 = rationales(p0d), rationales(fin)
|
| 187 |
+
reqs = []
|
| 188 |
+
for r in b1["requirements"]:
|
| 189 |
+
r0 = next((x for x in b0["requirements"] if x["id"] == r["id"]), None)
|
| 190 |
+
reqs.append({
|
| 191 |
+
"id": r["id"], "cat": CATS.get(r["id"][0], ""), "description": r["description"],
|
| 192 |
+
"p0": r0["aggregated"] if r0 else None, "final": r["aggregated"],
|
| 193 |
+
"p0_demo": r0["per_demo"].get(demo) if r0 else None, "final_demo": r["per_demo"].get(demo),
|
| 194 |
+
"why_p0": why0.get(r["id"], {}).get(demo, ""), "why_final": why1.get(r["id"], {}).get(demo, ""),
|
| 195 |
+
})
|
| 196 |
+
detail = {"demo": demo, "judge": b1.get("judge", {}).get("model"), "requirements": reqs, "rounds": timeline(loop_game)}
|
| 197 |
+
(PAGE / "static/data/evo" / arm).mkdir(parents=True, exist_ok=True)
|
| 198 |
+
(PAGE / "static/data/evo" / arm / f"{task}.json").write_text(json.dumps(detail, ensure_ascii=False))
|
| 199 |
+
|
| 200 |
+
return arm, task, {"s0": s0, "s1": s1, "demo": demo, "dur": round(usable, 1), "demo_gain": round(gain, 3), "curve": curve}, "ok"
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def instruction_meta(task):
|
| 204 |
+
md = (TASKS / task / "instruction.md").read_text()
|
| 205 |
+
title = re.search(r"^# (.+)$", md, re.M).group(1).strip()
|
| 206 |
+
m = re.search(r"## Core Vision\s+(.+?)(?:\n## |\Z)", md, re.S)
|
| 207 |
+
toml = (TASKS / task / "task.toml").read_text()
|
| 208 |
+
d = re.search(r'^description\s*=\s*"(.*)"', toml, re.M)
|
| 209 |
+
return title, (d.group(1) if d else ""), re.sub(r"\s+", " ", m.group(1)).strip() if m else ""
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def main():
|
| 213 |
+
ap = argparse.ArgumentParser()
|
| 214 |
+
ap.add_argument("--arms", nargs="+", default=list(ARMS))
|
| 215 |
+
ap.add_argument("--tasks", nargs="*")
|
| 216 |
+
ap.add_argument("--jobs", type=int, default=16)
|
| 217 |
+
ap.add_argument("--reuse-media", action="store_true", help="only rebuild data; keep encoded clips that exist")
|
| 218 |
+
a = ap.parse_args()
|
| 219 |
+
|
| 220 |
+
todo = [(arm, t.name) for arm in a.arms for t in sorted(ARMS[arm]["loop"].iterdir())
|
| 221 |
+
if t.is_dir() and (not a.tasks or t.name in a.tasks)]
|
| 222 |
+
rows, skipped = [], []
|
| 223 |
+
with ProcessPoolExecutor(a.jobs) as ex:
|
| 224 |
+
for f in [ex.submit(build_game, arm, task) for arm, task in todo]:
|
| 225 |
+
try:
|
| 226 |
+
arm, task, res, why = f.result()
|
| 227 |
+
except Exception as e: # one broken game must not sink the batch
|
| 228 |
+
print("FAIL", type(e).__name__, str(e)[-300:], file=sys.stderr, flush=True)
|
| 229 |
+
continue
|
| 230 |
+
if res is None:
|
| 231 |
+
skipped.append((arm, task, why))
|
| 232 |
+
continue
|
| 233 |
+
title, blurb, vision = instruction_meta(task)
|
| 234 |
+
cfg = ARMS[arm]
|
| 235 |
+
s0, s1 = res["s0"], res["s1"]
|
| 236 |
+
rows.append({
|
| 237 |
+
"arm": arm, "task": task, "engine": cfg["engine"], "model": cfg["model"], "harness": cfg["harness"],
|
| 238 |
+
"genre": task.split("-")[0], "title": title, "blurb": blurb, "vision": vision,
|
| 239 |
+
"reward": s0["reward"], "build": s0["build"], **{k: s0[k] for k in CATS},
|
| 240 |
+
"pair": f"pairs/{arm}/{task}", "demo": res["demo"], "dur": res["dur"], "demo_gain": res["demo_gain"],
|
| 241 |
+
"repaired": {"round": FINAL_ROUND, "reward": s1["reward"], "build": s1["build"], **{k: s1[k] for k in CATS}},
|
| 242 |
+
"curve": res["curve"], "media": None, "demos": [],
|
| 243 |
+
})
|
| 244 |
+
print("ok", arm, task, f"{s0['reward']:.3f} -> {s1['reward']:.3f}", res["demo"], flush=True)
|
| 245 |
+
rows.sort(key=lambda g: (g["arm"], -(g["repaired"]["reward"] - g["reward"])))
|
| 246 |
+
(PAGE / "static/data/evo.json").write_text(json.dumps(rows, ensure_ascii=False, indent=0))
|
| 247 |
+
print(f"wrote {len(rows)} pairs; skipped {len(skipped)}:")
|
| 248 |
+
for s in skipped:
|
| 249 |
+
print(" skip", *s)
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
if __name__ == "__main__":
|
| 253 |
+
main()
|
_build/build_web.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Playable builds: export a game's round-0 and round-30 Godot projects to the Web (single-threaded WASM).
|
| 2 |
+
|
| 3 |
+
All exports share one engine (same 4.6.2 template), so it is stored once, gzipped:
|
| 4 |
+
MEDIA_OUT/play/engine/godot.js, godot.wasm.gz, godot.audio.worklet.js, godot.audio.position.worklet.js
|
| 5 |
+
and each build only ships its game data:
|
| 6 |
+
MEDIA_OUT/play/<arm>/<task>/<r0|r30>/index.pck
|
| 7 |
+
plus static/data/play.json for the page (sizes feed the loading bar).
|
| 8 |
+
|
| 9 |
+
Needs Godot 4.6.2 with web export templates in ~/.local/share/godot/export_templates/4.6.2.stable.
|
| 10 |
+
|
| 11 |
+
python _build/build_web.py [--games godot_gpt/platformer-echo-climb ...]
|
| 12 |
+
"""
|
| 13 |
+
import argparse, gzip, json, shutil, subprocess, sys
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
PAGE = Path(__file__).resolve().parents[1]
|
| 17 |
+
MEDIA_OUT = Path("/storage/admin/wenyi/evogame-page-media")
|
| 18 |
+
WORK = Path("/storage/admin/wenyi/evogame-web/work")
|
| 19 |
+
PRESET = Path(__file__).with_name("web_export_presets.cfg")
|
| 20 |
+
SOURCES = {
|
| 21 |
+
"godot_gpt": {"r0": lambda t: Path(f"/storage/admin/wenyi/r0_trees/godot_gpt/{t}/game"),
|
| 22 |
+
"r30": lambda t: Path(f"/storage/admin/wenyi/newloop_self40/runs/{t}/round_30/tree")},
|
| 23 |
+
"godot_qwen": {"r0": lambda t: Path(f"/home/admin/wenyi/gamedoctor_run/godot/corpus_qwen/{t}"),
|
| 24 |
+
"r30": lambda t: Path(f"/home/admin/wenyi/gamedoctor_run/newloop_selfqwen40/runs/{t}/round_30/tree")},
|
| 25 |
+
}
|
| 26 |
+
# what a visitor needs to know to play; read off each game's own help text
|
| 27 |
+
CONTROLS = {
|
| 28 |
+
"godot_gpt/platformer-echo-climb": "A / D move · Space jump · R restart",
|
| 29 |
+
"godot_qwen/shooter-void-patrol": "WASD / arrows move · hold Space fire · X bomb",
|
| 30 |
+
"godot_gpt/shooter-sky-duel": "arrows fly · Space fire",
|
| 31 |
+
"godot_qwen/platformer-dig-descent": "Enter start · A / D steer · Space shoot down",
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def export(src, work, out):
|
| 36 |
+
shutil.rmtree(work, ignore_errors=True)
|
| 37 |
+
work.parent.mkdir(parents=True, exist_ok=True)
|
| 38 |
+
subprocess.run(["rsync", "-a", "--exclude", "*.py", "--exclude", ".godot", f"{src}/", f"{work}/"], check=True)
|
| 39 |
+
shutil.copy(PRESET, work / "export_presets.cfg")
|
| 40 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 41 |
+
log = work.with_suffix(".log").open("w")
|
| 42 |
+
subprocess.run(["godot", "--headless", "--path", str(work), "--import"], stdout=log, stderr=subprocess.STDOUT, timeout=900)
|
| 43 |
+
subprocess.run(["godot", "--headless", "--path", str(work), "--export-release", "Web", str(out / "index.html")],
|
| 44 |
+
stdout=log, stderr=subprocess.STDOUT, timeout=900, check=True)
|
| 45 |
+
return out
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def main():
|
| 49 |
+
ap = argparse.ArgumentParser()
|
| 50 |
+
ap.add_argument("--games", nargs="+", default=list(CONTROLS))
|
| 51 |
+
a = ap.parse_args()
|
| 52 |
+
evo = {f"{g['arm']}/{g['task']}": g for g in json.loads((PAGE / "static/data/evo.json").read_text())}
|
| 53 |
+
engine = MEDIA_OUT / "play" / "engine"
|
| 54 |
+
rows = []
|
| 55 |
+
for key in a.games:
|
| 56 |
+
arm, task = key.split("/")
|
| 57 |
+
g = evo.get(key)
|
| 58 |
+
builds = {}
|
| 59 |
+
for rnd in ("r0", "r30"):
|
| 60 |
+
out = export(SOURCES[arm][rnd](task), WORK / task / rnd, MEDIA_OUT / "play" / arm / task / rnd)
|
| 61 |
+
if not (engine / "godot.wasm.gz").exists():
|
| 62 |
+
engine.mkdir(parents=True, exist_ok=True)
|
| 63 |
+
shutil.copy(out / "index.js", engine / "godot.js")
|
| 64 |
+
for w in ("audio.worklet.js", "audio.position.worklet.js"):
|
| 65 |
+
shutil.copy(out / f"index.{w}", engine / f"godot.{w}")
|
| 66 |
+
raw = (out / "index.wasm").read_bytes()
|
| 67 |
+
(engine / "godot.wasm.gz").write_bytes(gzip.compress(raw, 9))
|
| 68 |
+
(engine / "sizes.json").write_text(json.dumps({"wasm": len(raw), "wasm_gz": (engine / "godot.wasm.gz").stat().st_size}))
|
| 69 |
+
# keep only what the shared player needs
|
| 70 |
+
for f in out.iterdir():
|
| 71 |
+
if f.name != "index.pck":
|
| 72 |
+
f.unlink()
|
| 73 |
+
builds[rnd] = {"pck": f"play/{arm}/{task}/{rnd}/index.pck", "size": (out / "index.pck").stat().st_size}
|
| 74 |
+
print("exported", key, rnd, builds[rnd]["size"] // 1024, "KB", flush=True)
|
| 75 |
+
rows.append({
|
| 76 |
+
"arm": arm, "task": task, "title": g["title"] if g else task, "model": g["model"] if g else "",
|
| 77 |
+
"controls": CONTROLS.get(key, ""), "pair": g["pair"] if g else None,
|
| 78 |
+
"scores": {"r0": g["reward"], "r30": g["repaired"]["reward"]} if g else {},
|
| 79 |
+
"builds": builds,
|
| 80 |
+
})
|
| 81 |
+
old = json.loads((PAGE / "static/data/play.json").read_text()).get("games", []) if (PAGE / "static/data/play.json").exists() else []
|
| 82 |
+
rows += [r for r in old if f"{r['arm']}/{r['task']}" not in a.games]
|
| 83 |
+
sizes = json.loads((engine / "sizes.json").read_text())
|
| 84 |
+
(PAGE / "static/data/play.json").write_text(json.dumps({"engine": {"base": "play/engine/godot", **sizes}, "games": rows}, indent=1))
|
| 85 |
+
print("wrote static/data/play.json")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
if __name__ == "__main__":
|
| 89 |
+
sys.exit(main())
|
_build/web_export_presets.cfg
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[preset.0]
|
| 2 |
+
|
| 3 |
+
name="Web"
|
| 4 |
+
platform="Web"
|
| 5 |
+
runnable=true
|
| 6 |
+
dedicated_server=false
|
| 7 |
+
custom_features=""
|
| 8 |
+
export_filter="all_resources"
|
| 9 |
+
include_filter="*.json"
|
| 10 |
+
exclude_filter="*.py, demo_outputs/*"
|
| 11 |
+
export_path="build/index.html"
|
| 12 |
+
patches=PackedStringArray()
|
| 13 |
+
encryption_include_filters=""
|
| 14 |
+
encryption_exclude_filters=""
|
| 15 |
+
seed=0
|
| 16 |
+
encrypt_pck=false
|
| 17 |
+
encrypt_directory=false
|
| 18 |
+
script_export_mode=2
|
| 19 |
+
|
| 20 |
+
[preset.0.options]
|
| 21 |
+
|
| 22 |
+
custom_template/debug=""
|
| 23 |
+
custom_template/release=""
|
| 24 |
+
variant/extensions_support=false
|
| 25 |
+
variant/thread_support=false
|
| 26 |
+
vram_texture_compression/for_desktop=true
|
| 27 |
+
vram_texture_compression/for_mobile=false
|
| 28 |
+
html/export_icon=true
|
| 29 |
+
html/custom_html_shell=""
|
| 30 |
+
html/head_include=""
|
| 31 |
+
html/canvas_resize_policy=2
|
| 32 |
+
html/focus_canvas_on_start=true
|
| 33 |
+
html/experimental_virtual_keyboard=false
|
| 34 |
+
progressive_web_app/enabled=false
|
index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
-
<title>EvoGame</title>
|
| 7 |
<meta name="description" content="EvoGame turns open-ended game improvement into goal-oriented, state-controlled progressive development through agentic verification.">
|
| 8 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
@@ -139,7 +139,6 @@
|
|
| 139 |
|
| 140 |
<footer class="footer"><div class="wrap">EvoGame project page · demo media hosted on Hugging Face</div></footer>
|
| 141 |
|
| 142 |
-
<script>window.EVO_MEDIA = "media"; window.EVO_NO_SWITCH = true;</script>
|
| 143 |
<script src="static/js/core.js"></script>
|
| 144 |
<script src="static/js/v1.js"></script>
|
| 145 |
</body>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>EvoGame: Self-Evolving Game Generation</title>
|
| 7 |
<meta name="description" content="EvoGame turns open-ended game improvement into goal-oriented, state-controlled progressive development through agentic verification.">
|
| 8 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
|
|
| 139 |
|
| 140 |
<footer class="footer"><div class="wrap">EvoGame project page · demo media hosted on Hugging Face</div></footer>
|
| 141 |
|
|
|
|
| 142 |
<script src="static/js/core.js"></script>
|
| 143 |
<script src="static/js/v1.js"></script>
|
| 144 |
</body>
|
static/css/v2.css
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* v2 — "paper page": light, academic, serif display. Loaded after base.css. */
|
| 2 |
+
:root {
|
| 3 |
+
color-scheme: light;
|
| 4 |
+
--bg: #fbfaf7;
|
| 5 |
+
--bg-alt: #f3f1ea;
|
| 6 |
+
--surface: #ffffff;
|
| 7 |
+
--surface-2: #f6f5f0;
|
| 8 |
+
--border: #e3e0d6;
|
| 9 |
+
--text: #1b1c20;
|
| 10 |
+
--text-2: #46474f;
|
| 11 |
+
--muted: #6f707a;
|
| 12 |
+
--accent: #0f6f66; /* deep teal */
|
| 13 |
+
--accent-soft: rgba(15, 111, 102, .08);
|
| 14 |
+
--accent-ink: #ffffff;
|
| 15 |
+
--godot: #2a78d6; /* series 1 (light step) */
|
| 16 |
+
--phaser: #eb6834; /* series 2 (light step) */
|
| 17 |
+
--bar: #0f6f66;
|
| 18 |
+
--track: #e8e5dc;
|
| 19 |
+
--hover-border: #c9c5b8;
|
| 20 |
+
--nav-bg: rgba(251, 250, 247, .8);
|
| 21 |
+
--nav-bg-solid: rgba(251, 250, 247, .95);
|
| 22 |
+
--chip-bg: rgba(20, 21, 26, .72);
|
| 23 |
+
--overlay: rgba(28, 27, 24, .5);
|
| 24 |
+
--stripe-a: #f1efe8;
|
| 25 |
+
--stripe-b: #e9e6dd;
|
| 26 |
+
--media-bg: #16171b;
|
| 27 |
+
--s-hi: #1e7a3c; --s-mid: #9a6100; --s-lo: #b8382a;
|
| 28 |
+
--radius: 10px;
|
| 29 |
+
--display: "Newsreader", "Iowan Old Style", "Palatino Linotype", Georgia, serif;
|
| 30 |
+
--font: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
|
| 31 |
+
--mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
body { font-size: 16px; line-height: 1.65; }
|
| 35 |
+
h1, h2, h3 { font-weight: 500; letter-spacing: -0.005em; }
|
| 36 |
+
.wrap.narrow { max-width: 1040px; }
|
| 37 |
+
.wrap.wide { max-width: 1360px; }
|
| 38 |
+
em { font-style: italic; }
|
| 39 |
+
|
| 40 |
+
/* ---------- nav ---------- */
|
| 41 |
+
.nav { height: 54px; border-bottom: 1px solid var(--border); background: var(--nav-bg-solid); }
|
| 42 |
+
.brand { font: 600 20px var(--display); letter-spacing: 0; }
|
| 43 |
+
.nav-links a { color: var(--text-2); font-size: 14px; }
|
| 44 |
+
.nav-links a:hover { background: var(--surface-2); }
|
| 45 |
+
.nav-links { min-width: 0; }
|
| 46 |
+
@media (max-width: 720px) { .nav .nav-links a[href="#compare"] { display: none; } }
|
| 47 |
+
html { scroll-padding-top: 64px; }
|
| 48 |
+
|
| 49 |
+
/* ---------- title block ---------- */
|
| 50 |
+
.title-block { padding-block: 116px 0; text-align: center; }
|
| 51 |
+
.kicker { font: 600 12px var(--font); letter-spacing: .14em; text-transform: uppercase; color: var(--accent); margin: 0 0 14px; }
|
| 52 |
+
.title-block h1 { font: 500 clamp(56px, 9vw, 104px)/1 var(--display); letter-spacing: -0.02em; }
|
| 53 |
+
.paper-title { font: 400 clamp(21px, 2.7vw, 30px)/1.3 var(--display); color: var(--text); max-width: 820px; margin: 16px auto 22px; text-wrap: balance; }
|
| 54 |
+
.authors { font-size: 16px; color: var(--text-2); margin: 0; }
|
| 55 |
+
.affil { font-size: 14px; color: var(--muted); margin: 2px 0 26px; }
|
| 56 |
+
.links { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }
|
| 57 |
+
.link-pill { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 999px; background: #202127; color: #fff; text-decoration: none; font-size: 14px; font-weight: 500; transition: background .15s; }
|
| 58 |
+
.link-pill svg { width: 15px; height: 15px; flex: none; }
|
| 59 |
+
.link-pill small { font: 500 10px var(--mono); text-transform: uppercase; letter-spacing: .06em; color: #a9abb5; }
|
| 60 |
+
.link-pill[aria-disabled="true"] { cursor: default; background: #4a4b52; }
|
| 61 |
+
.link-pill.solid { background: var(--accent); }
|
| 62 |
+
.link-pill.solid:hover { background: #0b5a53; }
|
| 63 |
+
|
| 64 |
+
.teaser { margin: 48px auto 0; max-width: 1180px; }
|
| 65 |
+
.teaser-frame { position: relative; aspect-ratio: 32 / 9; max-width: 100%; border-radius: 12px; overflow: hidden; background: var(--media-bg); box-shadow: 0 1px 2px rgba(0,0,0,.06), 0 12px 40px rgba(40, 36, 20, .12); }
|
| 66 |
+
.teaser-frame video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
| 67 |
+
@media (max-width: 700px) { .teaser-frame { aspect-ratio: 16 / 9; } }
|
| 68 |
+
.teaser figcaption, .fig figcaption, .diagram figcaption { font-size: 14px; color: var(--text-2); margin-top: 12px; text-align: left; max-width: 860px; }
|
| 69 |
+
.teaser figcaption { margin-inline: auto; text-align: center; }
|
| 70 |
+
figcaption b { color: var(--text); font-weight: 600; }
|
| 71 |
+
|
| 72 |
+
/* ---------- sections ---------- */
|
| 73 |
+
.section { padding-block: clamp(56px, 7vw, 88px); }
|
| 74 |
+
.section.alt { background: var(--bg-alt); border-block: 1px solid var(--border); }
|
| 75 |
+
.h-section { font: 500 clamp(30px, 3.6vw, 40px)/1.15 var(--display); margin-bottom: 14px; }
|
| 76 |
+
.lede { color: var(--text-2); margin: 0 0 28px; max-width: 760px; }
|
| 77 |
+
.narrow-head { max-width: 1040px; margin: 0 auto; }
|
| 78 |
+
.muted { color: var(--muted); }
|
| 79 |
+
|
| 80 |
+
.tldr { display: flex; gap: 18px; align-items: baseline; background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: 0 var(--radius) var(--radius) 0; padding: 18px 22px; margin-bottom: 56px; }
|
| 81 |
+
.tldr-tag { font: 700 12px var(--mono); color: var(--accent); flex: none; }
|
| 82 |
+
.tldr p { margin: 0; font: 400 19px/1.5 var(--display); color: var(--text); }
|
| 83 |
+
@media (max-width: 600px) { .tldr { flex-direction: column; gap: 6px; } }
|
| 84 |
+
|
| 85 |
+
.abstract { display: grid; grid-template-columns: 1fr 250px; gap: 44px; align-items: start; }
|
| 86 |
+
.prose p { margin: 0 0 16px; color: var(--text-2); text-align: justify; hyphens: auto; }
|
| 87 |
+
.prose b { color: var(--text); font-weight: 600; }
|
| 88 |
+
.facts { margin: 0; display: grid; gap: 0; border-top: 1px solid var(--border); }
|
| 89 |
+
.facts div { padding: 14px 0; border-bottom: 1px solid var(--border); }
|
| 90 |
+
.facts dt { font: 500 36px/1 var(--display); color: var(--accent); }
|
| 91 |
+
.facts dd { margin: 4px 0 0; font-size: 14px; color: var(--text-2); }
|
| 92 |
+
@media (max-width: 820px) {
|
| 93 |
+
.abstract { grid-template-columns: 1fr; gap: 20px; }
|
| 94 |
+
.facts { grid-template-columns: repeat(2, 1fr); column-gap: 20px; }
|
| 95 |
+
.prose p { text-align: left; }
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* ---------- method diagram ---------- */
|
| 99 |
+
.diagram { margin: 0; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 26px 26px 18px; }
|
| 100 |
+
.dg-row { display: grid; grid-template-columns: 132px 1fr; gap: 18px; align-items: center; padding-block: 16px; border-bottom: 1px dashed var(--border); }
|
| 101 |
+
.dg-row:first-child { padding-top: 0; }
|
| 102 |
+
.dg-row:nth-last-child(2) { border-bottom: 0; }
|
| 103 |
+
.dg-label { font: 600 11px var(--mono); text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
|
| 104 |
+
.dg-states, .dg-loop { list-style: none; margin: 0; padding: 0; display: flex; align-items: stretch; gap: 0; }
|
| 105 |
+
.dg-states li:not(.arrow) { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; }
|
| 106 |
+
.dg-states b, .dg-loop b { display: block; font: 600 17px var(--display); color: var(--text); }
|
| 107 |
+
.dg-states span, .dg-loop span { display: block; font-size: 13px; line-height: 1.4; color: var(--text-2); margin-top: 2px; }
|
| 108 |
+
.dg-states li:nth-child(1) { border-top: 3px solid #9fc9c3; }
|
| 109 |
+
.dg-states li:nth-child(3) { border-top: 3px solid #4f9d94; }
|
| 110 |
+
.dg-states li:nth-child(5) { border-top: 3px solid var(--accent); }
|
| 111 |
+
.dg-exp li:not(.arrow) { border-top: 1px solid var(--border) !important; background: var(--surface-2); }
|
| 112 |
+
.arrow { flex: 0 0 28px; position: relative; }
|
| 113 |
+
.arrow::before { content: ""; position: absolute; left: 5px; right: 9px; top: 50%; height: 1.5px; background: var(--muted); }
|
| 114 |
+
.arrow::after { content: ""; position: absolute; right: 5px; top: calc(50% - 4px); border: 4.5px solid transparent; border-left: 6px solid var(--muted); border-right: 0; }
|
| 115 |
+
.dg-loop { gap: 10px; counter-reset: step; }
|
| 116 |
+
.dg-loop li { flex: 1; position: relative; padding: 10px 14px 10px 14px; border-radius: 8px; background: var(--bg); border: 1px solid var(--border); }
|
| 117 |
+
.dg-loop li.verify { background: var(--accent-soft); border-color: rgba(15, 111, 102, .35); }
|
| 118 |
+
.dg-loop li.verify b { color: var(--accent); }
|
| 119 |
+
.dg-loop i { font: 600 11px var(--mono); font-style: normal; color: var(--muted); }
|
| 120 |
+
.dg-loop li:not(:last-child)::after { content: "→"; position: absolute; right: -10px; top: 50%; transform: translate(50%, -50%); color: var(--muted); font-size: 13px; background: var(--surface); line-height: 1; padding: 1px 0; z-index: 1; }
|
| 121 |
+
.dg-return { grid-column: 2; margin: 8px 0 0; font-size: 13px; color: var(--accent); }
|
| 122 |
+
.diagram figcaption { margin-top: 10px; }
|
| 123 |
+
@media (max-width: 820px) {
|
| 124 |
+
.dg-row { grid-template-columns: 1fr; gap: 10px; }
|
| 125 |
+
.dg-return { grid-column: 1; }
|
| 126 |
+
.dg-states, .dg-loop { flex-direction: column; }
|
| 127 |
+
.arrow { flex: 0 0 22px; }
|
| 128 |
+
.arrow::before { left: 50%; right: auto; top: 3px; bottom: 9px; width: 1.5px; height: auto; }
|
| 129 |
+
.arrow::after { left: calc(50% - 4px); right: auto; top: auto; bottom: 3px; border: 4.5px solid transparent; border-top: 6px solid var(--muted); border-bottom: 0; }
|
| 130 |
+
.dg-loop li:not(:last-child)::after { content: "↓"; right: 50%; top: auto; bottom: -10px; transform: translate(50%, 50%); padding: 0 4px; }
|
| 131 |
+
.diagram { padding: 18px 16px 14px; }
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/* ---------- before / after ---------- */
|
| 135 |
+
.compare { border-radius: 12px; padding: 18px 18px 20px; }
|
| 136 |
+
.compare-head h3 { font: 500 23px var(--display); }
|
| 137 |
+
.compare-head button:hover h3 { color: var(--accent); }
|
| 138 |
+
.compare-label { font: 600 11px var(--font); letter-spacing: .08em; }
|
| 139 |
+
.compare-label .after { color: var(--accent); }
|
| 140 |
+
.compare-label b { font-family: var(--mono); font-size: 14px; }
|
| 141 |
+
.pill { font-family: var(--font); border-radius: 999px; background: var(--surface-2); }
|
| 142 |
+
.frame { border-radius: 8px; }
|
| 143 |
+
.pending { color: var(--muted); }
|
| 144 |
+
.pending b { font: 500 18px var(--display); color: var(--text-2); }
|
| 145 |
+
|
| 146 |
+
/* ---------- gallery ---------- */
|
| 147 |
+
.filters { top: 54px; background: var(--bg-alt); padding-block: 14px; margin-bottom: 20px; border-bottom: 1px solid var(--border); }
|
| 148 |
+
.seg { border-radius: 999px; padding: 3px; }
|
| 149 |
+
.seg button { border-radius: 999px; padding: 5px 14px; }
|
| 150 |
+
.seg button.on { background: var(--text); color: #fff; box-shadow: none; }
|
| 151 |
+
.filters select, .filters input { border-radius: 999px; padding: 7px 14px; }
|
| 152 |
+
.filters select:focus, .filters input:focus { outline: 2px solid rgba(15, 111, 102, .45); }
|
| 153 |
+
.count { font-family: var(--font); }
|
| 154 |
+
|
| 155 |
+
.grid.dense { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 22px 16px; }
|
| 156 |
+
.card.v2 { min-width: 0; }
|
| 157 |
+
.card.v2 { background: transparent; border: 0; border-radius: 0; overflow: visible; box-shadow: none; }
|
| 158 |
+
.card.v2:hover, .card.v2:focus-visible { transform: none; box-shadow: none; }
|
| 159 |
+
.card.v2 .frame { border-radius: 8px; border: 1px solid var(--border); transition: box-shadow .18s, transform .18s; }
|
| 160 |
+
.card.v2:hover .frame, .card.v2:focus-visible .frame { box-shadow: 0 10px 28px rgba(40, 36, 20, .18); transform: translateY(-2px); }
|
| 161 |
+
.card.v2:focus-visible .frame { outline: 2px solid var(--accent); outline-offset: 2px; }
|
| 162 |
+
.card.v2 .c-body { padding: 9px 2px 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 2px; min-width: 0; }
|
| 163 |
+
.c-top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
|
| 164 |
+
.c-title { font: 500 17px/1.25 var(--display); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
| 165 |
+
.c-score { font: 600 14px var(--mono); color: var(--text); flex: none; }
|
| 166 |
+
.c-meta { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--muted); }
|
| 167 |
+
.c-cats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-top: 7px; }
|
| 168 |
+
.c-cats span { display: flex; gap: 4px; font: 500 10px var(--mono); color: var(--muted); border-top: 2px solid var(--track); padding-top: 4px; position: relative; }
|
| 169 |
+
.c-cats span i { position: absolute; left: 0; top: -2px; height: 2px; background: var(--bar); }
|
| 170 |
+
.c-cats span b { font-weight: 500; color: var(--text-2); }
|
| 171 |
+
.more .btn { border-radius: 999px; padding: 9px 22px; }
|
| 172 |
+
@media (max-width: 520px) { .grid.dense { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 10px; } .c-cats { display: none; } .c-title { font-size: 15px; } }
|
| 173 |
+
|
| 174 |
+
/* ---------- results ---------- */
|
| 175 |
+
.formula { font: 500 14px var(--mono); color: var(--text); background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px; margin: -12px 0 28px; overflow-x: auto; white-space: nowrap; }
|
| 176 |
+
.fig { margin: 0 0 28px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 18px 20px; min-width: 0; }
|
| 177 |
+
.fig-head { display: flex; justify-content: flex-end; }
|
| 178 |
+
.legend { margin: 0 0 8px; }
|
| 179 |
+
.fig-pair { display: grid; grid-template-columns: 1.15fr .85fr; gap: 20px; margin-bottom: 28px; }
|
| 180 |
+
.fig-pair .fig { margin: 0; }
|
| 181 |
+
.fig-note { padding: 6px 4px; }
|
| 182 |
+
.fig-note h3 { font: 500 20px var(--display); margin-bottom: 10px; }
|
| 183 |
+
.fig-note p { margin: 0 0 8px; font-size: 14px; color: var(--text-2); }
|
| 184 |
+
.fig-note b { color: var(--text); font-weight: 600; }
|
| 185 |
+
@media (max-width: 820px) { .fig-pair { grid-template-columns: 1fr; } }
|
| 186 |
+
.table-fig { padding: 0; overflow: hidden; }
|
| 187 |
+
.table-fig figcaption { margin: 0; padding: 14px 20px; border-bottom: 1px solid var(--border); max-width: none; }
|
| 188 |
+
.table-fig .table-wrap { margin: 0; border: 0; border-radius: 0; }
|
| 189 |
+
.table { font-size: 14px; }
|
| 190 |
+
.table th { font: 600 11px var(--font); letter-spacing: .06em; background: var(--surface-2); }
|
| 191 |
+
.row-label { font-family: var(--font); }
|
| 192 |
+
.strip-dot { stroke: #fff; }
|
| 193 |
+
.tip { background: #1b1c20; border-color: #1b1c20; }
|
| 194 |
+
|
| 195 |
+
/* ---------- citation / footer ---------- */
|
| 196 |
+
.bibtex { background: var(--surface); color: var(--text); }
|
| 197 |
+
.copy { background: var(--surface-2); }
|
| 198 |
+
.footer { background: var(--bg); }
|
| 199 |
+
|
| 200 |
+
/* ---------- modal ---------- */
|
| 201 |
+
.modal-panel { background: var(--surface); border-color: var(--border); border-radius: 14px; box-shadow: 0 30px 80px rgba(20, 18, 10, .3); }
|
| 202 |
+
.modal-media { background: var(--bg); }
|
| 203 |
+
.modal-close { background: var(--surface); color: var(--text); }
|
| 204 |
+
.player { border-radius: 8px; }
|
| 205 |
+
.demo-tabs button { border-radius: 999px; background: var(--surface); }
|
| 206 |
+
.demo-tabs button.on { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
|
| 207 |
+
.demo-tabs button.on small { color: var(--accent); }
|
| 208 |
+
.m-prompt h3 { font-family: var(--font); }
|
| 209 |
+
.m-prompt p { font: 400 16px/1.55 var(--display); color: var(--text-2); }
|
| 210 |
+
.modal-info h2 { font: 500 32px/1.15 var(--display); }
|
| 211 |
+
.m-score { background: var(--bg); }
|
| 212 |
+
.m-overall b { font-weight: 600; }
|
| 213 |
+
.m-overall span, .req-cat { font-family: var(--font); }
|
| 214 |
+
.twin { background: var(--surface); }
|
| 215 |
+
.twin:hover { border-color: var(--accent); }
|
| 216 |
+
.m-h { font: 500 21px var(--display); }
|
| 217 |
+
.req { background: var(--surface); }
|
| 218 |
+
.req.open { border-color: var(--hover-border); }
|
| 219 |
+
.req-demos button { background: var(--surface-2); }
|
| 220 |
+
.req-demos button.on { border-color: var(--accent); }
|
| 221 |
+
.rationale { background: var(--accent-soft); border-left-color: var(--accent); color: var(--text); }
|
| 222 |
+
.btn { border-radius: 999px; }
|
| 223 |
+
|
| 224 |
+
.brand, .h-section, .title-block h1 { color: var(--text); }
|
static/css/v3.css
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* v3 — Arcade. Layered on base.css: tokens first, then arcade-specific pieces. */
|
| 2 |
+
:root {
|
| 3 |
+
--bg: #0d0818;
|
| 4 |
+
--bg-alt: #120b22;
|
| 5 |
+
--surface: #1a1030;
|
| 6 |
+
--surface-2: #241640;
|
| 7 |
+
--border: #3a2764;
|
| 8 |
+
--text: #f4f0ff;
|
| 9 |
+
--text-2: #cbc1e8;
|
| 10 |
+
--muted: #9387b6;
|
| 11 |
+
--accent: #ffd23f; /* arcade yellow */
|
| 12 |
+
--accent-ink: #1a1030;
|
| 13 |
+
--cyan: #3ef2ff;
|
| 14 |
+
--magenta: #ff4fd8;
|
| 15 |
+
--bar: #ffd23f;
|
| 16 |
+
--track: #2d1f4d;
|
| 17 |
+
--hover-border: #6d52b8;
|
| 18 |
+
--nav-bg: rgba(13, 8, 24, .7);
|
| 19 |
+
--nav-bg-solid: rgba(13, 8, 24, .94);
|
| 20 |
+
--chip-bg: rgba(13, 8, 24, .88);
|
| 21 |
+
--overlay: rgba(6, 3, 14, .88);
|
| 22 |
+
--stripe-a: #150d28;
|
| 23 |
+
--stripe-b: #1b1233;
|
| 24 |
+
--media-bg: #05030b;
|
| 25 |
+
--s-hi: #7cf29a; --s-mid: #ffd23f; --s-lo: #ff7a8a;
|
| 26 |
+
--radius: 0px;
|
| 27 |
+
--pixel: "Press Start 2P", ui-monospace, "SFMono-Regular", Menlo, monospace;
|
| 28 |
+
--display: var(--pixel);
|
| 29 |
+
--font: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
|
| 30 |
+
--shadow: 4px 4px 0 #000;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
body.arcade { background:
|
| 34 |
+
radial-gradient(1200px 600px at 80% -10%, rgba(255, 79, 216, .10), transparent 60%),
|
| 35 |
+
radial-gradient(900px 500px at 0% 20%, rgba(62, 242, 255, .07), transparent 60%),
|
| 36 |
+
var(--bg); }
|
| 37 |
+
|
| 38 |
+
/* pixel font only where it's a label, heading or number */
|
| 39 |
+
h1, h2, .level h3, .chart-card h3, .m-h { font-family: var(--pixel); font-weight: 400; letter-spacing: 0; }
|
| 40 |
+
h3 { font-family: var(--font); font-weight: 700; letter-spacing: -0.01em; }
|
| 41 |
+
|
| 42 |
+
.scanlines {
|
| 43 |
+
position: fixed; inset: 0; z-index: 90; pointer-events: none;
|
| 44 |
+
background: repeating-linear-gradient(to bottom, rgba(0, 0, 0, .22) 0 1px, transparent 1px 3px);
|
| 45 |
+
opacity: .35; mix-blend-mode: multiply;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/* ---------- nav ---------- */
|
| 49 |
+
.nav { height: 60px; border-bottom: 3px solid transparent; }
|
| 50 |
+
.nav.scrolled { border-color: var(--border); }
|
| 51 |
+
.brand { font: 400 13px var(--pixel); gap: 10px; }
|
| 52 |
+
.coin { width: 14px; height: 14px; background: var(--accent); box-shadow: inset -3px -3px 0 #c79a12, 0 0 0 2px #000; }
|
| 53 |
+
.nav-links a { font: 400 10px var(--pixel); color: var(--text-2); border-radius: 0; padding: 8px 10px; }
|
| 54 |
+
.nav-links a:hover { background: var(--surface); color: var(--accent); }
|
| 55 |
+
@media (max-width: 720px) { .nav-links a:not([href="#gallery"]):not([href="#compare"]) { display: none; } }
|
| 56 |
+
|
| 57 |
+
/* ---------- buttons ---------- */
|
| 58 |
+
.btn { font: 400 10px/1 var(--pixel); border-radius: 0; border: 3px solid var(--border); background: var(--surface);
|
| 59 |
+
box-shadow: var(--shadow); padding: 12px 13px; transition: transform .08s, box-shadow .08s, border-color .15s; }
|
| 60 |
+
.btn:hover { border-color: var(--cyan); background: var(--surface-2); }
|
| 61 |
+
.btn:active { transform: translate(2px, 2px); box-shadow: 2px 2px 0 #000; }
|
| 62 |
+
.btn small { font: 400 7px var(--pixel); color: var(--magenta); letter-spacing: 0; }
|
| 63 |
+
.btn-start { background: var(--accent); color: var(--accent-ink); border-color: #000; }
|
| 64 |
+
.btn-start:hover { background: #ffe06e; border-color: #000; box-shadow: 4px 4px 0 var(--magenta); }
|
| 65 |
+
.btn-sm { padding: 8px 10px; font-size: 9px; }
|
| 66 |
+
.tri { display: inline-block; width: 8px; height: 10px; background: currentColor; clip-path: polygon(0 0, 100% 50%, 0 100%); margin-right: 10px; vertical-align: -1px; }
|
| 67 |
+
.blink { animation: blink 1s steps(2, start) infinite; }
|
| 68 |
+
@keyframes blink { to { visibility: hidden; } }
|
| 69 |
+
|
| 70 |
+
/* ---------- hero ---------- */
|
| 71 |
+
.hero { min-height: auto; display: block; padding-block: 108px 56px; overflow: visible; }
|
| 72 |
+
.hero-grid { max-width: 1240px; margin: 0 auto; padding-inline: var(--gutter); display: grid; grid-template-columns: 1.05fr 1fr; gap: clamp(28px, 5vw, 64px); align-items: center; }
|
| 73 |
+
.insert-coin { font: 400 10px/1.6 var(--pixel); color: var(--cyan); margin: 0 0 22px; }
|
| 74 |
+
.hero h1 { font-size: clamp(38px, 7.2vw, 84px); line-height: 1.05; letter-spacing: 0; text-shadow: none; display: flex; flex-wrap: wrap; }
|
| 75 |
+
.h1-evo { color: var(--cyan); text-shadow: 4px 4px 0 #0b3e52, 8px 8px 0 #000; }
|
| 76 |
+
.h1-game { color: var(--accent); text-shadow: 4px 4px 0 #9b2a86, 8px 8px 0 #000; }
|
| 77 |
+
.hero-sub { font-size: clamp(16px, 1.9vw, 20px); color: var(--text); margin: 26px 0 8px; max-width: 560px; text-shadow: none; }
|
| 78 |
+
.authors { color: var(--muted); margin: 0 0 28px; }
|
| 79 |
+
.hero-cta { justify-content: flex-start; gap: 12px; }
|
| 80 |
+
|
| 81 |
+
.cabinet { position: relative; background: linear-gradient(180deg, #2a1650, #1a0e33); border: 3px solid #000;
|
| 82 |
+
box-shadow: 0 0 0 3px var(--border), 10px 10px 0 #000; padding: 14px 16px 16px; }
|
| 83 |
+
.marquee { font: 400 clamp(9px, 1.1vw, 12px)/1 var(--pixel); text-align: center; color: #fff; padding: 12px 8px; margin-bottom: 14px;
|
| 84 |
+
background: linear-gradient(90deg, var(--magenta), #7a3cff 50%, var(--cyan)); border: 3px solid #000;
|
| 85 |
+
text-shadow: 2px 2px 0 #000; letter-spacing: .08em; }
|
| 86 |
+
.bezel { background: #07040f; border: 3px solid #000; padding: clamp(10px, 1.6vw, 18px); box-shadow: inset 0 0 0 3px #1f1340; }
|
| 87 |
+
.crt { position: relative; aspect-ratio: 16 / 9; max-width: 100%; overflow: hidden; border-radius: 14px / 18px; background: var(--media-bg);
|
| 88 |
+
box-shadow: inset 0 0 40px rgba(0, 0, 0, .9); }
|
| 89 |
+
.crt video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; filter: saturate(1.1) contrast(1.05); }
|
| 90 |
+
.crt-glare { position: absolute; inset: 0; pointer-events: none;
|
| 91 |
+
background: radial-gradient(ellipse at 30% 20%, rgba(255, 255, 255, .10), transparent 45%),
|
| 92 |
+
repeating-linear-gradient(to bottom, rgba(0, 0, 0, .25) 0 1px, transparent 1px 3px);
|
| 93 |
+
box-shadow: inset 0 0 60px rgba(0, 0, 0, .75); }
|
| 94 |
+
.controls { display: flex; align-items: center; gap: 14px; padding: 16px 10px 2px; }
|
| 95 |
+
.stick { width: 16px; height: 16px; border-radius: 50%; background: #e23; box-shadow: 0 0 0 3px #000, 0 10px 0 -4px #111; margin-right: auto; }
|
| 96 |
+
.btn-dot { width: 18px; height: 18px; border-radius: 50%; box-shadow: 0 0 0 3px #000, inset -3px -3px 0 rgba(0, 0, 0, .3); }
|
| 97 |
+
.b1 { background: var(--cyan); } .b2 { background: var(--accent); } .b3 { background: var(--magenta); }
|
| 98 |
+
@media (max-width: 900px) {
|
| 99 |
+
.hero { padding-top: 88px; }
|
| 100 |
+
.hero-grid { grid-template-columns: 1fr; }
|
| 101 |
+
.cabinet { order: -1; }
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/* ---------- hi-score ---------- */
|
| 105 |
+
.hiscore-section { padding-block: 24px 40px; }
|
| 106 |
+
.hiscore { max-width: 820px; margin: 0 auto; border: 3px solid var(--cyan); background: rgba(10, 6, 20, .85);
|
| 107 |
+
box-shadow: 0 0 0 3px #000, 8px 8px 0 #000, 0 0 40px rgba(62, 242, 255, .15); padding: clamp(18px, 3vw, 30px); }
|
| 108 |
+
.hiscore-title { font: 400 clamp(12px, 2vw, 16px) var(--pixel); color: var(--accent); text-align: center; margin-bottom: 20px; }
|
| 109 |
+
.star { font: 700 18px var(--font); color: var(--magenta); vertical-align: 1px; }
|
| 110 |
+
.hiscore-rows { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
|
| 111 |
+
.hiscore-rows li { display: grid; grid-template-columns: 60px 1fr auto; align-items: baseline; gap: 14px; }
|
| 112 |
+
.hiscore-rows .rank { font: 400 11px var(--pixel); color: var(--magenta); }
|
| 113 |
+
.hiscore-rows li:nth-child(1) .rank { color: var(--accent); }
|
| 114 |
+
.hiscore-rows li:nth-child(2) .rank { color: #d7deea; }
|
| 115 |
+
.hiscore-rows li:nth-child(3) .rank { color: #e59a62; }
|
| 116 |
+
.hiscore-rows .what { color: var(--text-2); font-size: 15px; border-bottom: 2px dotted var(--border); padding-bottom: 4px; min-width: 0; }
|
| 117 |
+
.hiscore-rows b { font: 400 clamp(16px, 2.6vw, 24px) var(--pixel); color: var(--text); }
|
| 118 |
+
@media (max-width: 520px) {
|
| 119 |
+
.hiscore-rows li { grid-template-columns: 44px 1fr auto; gap: 10px; }
|
| 120 |
+
.hiscore-rows .rank { font-size: 9px; }
|
| 121 |
+
.hiscore-rows .what { font-size: 13px; }
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/* ---------- sections ---------- */
|
| 125 |
+
.section-alt { background: var(--bg-alt); border-block: 3px solid var(--border); }
|
| 126 |
+
.section-head { max-width: 820px; }
|
| 127 |
+
.section-head h2 { font-size: clamp(18px, 3vw, 30px); line-height: 1.45; margin-bottom: 16px; }
|
| 128 |
+
.section-head p + p { margin-top: 12px; }
|
| 129 |
+
.section-head strong { color: var(--accent); }
|
| 130 |
+
.kicker { font: 400 10px var(--pixel) !important; color: var(--magenta) !important; margin: 0 0 14px !important; }
|
| 131 |
+
|
| 132 |
+
/* ---------- level select ---------- */
|
| 133 |
+
.level-select { list-style: none; margin: 8px 0 0; padding: 0; display: grid; grid-template-columns: 1fr auto 1fr auto 1fr; gap: 14px; align-items: stretch; }
|
| 134 |
+
.level { position: relative; background: var(--surface); border: 3px solid var(--border); box-shadow: var(--shadow); padding: 20px 18px 16px; }
|
| 135 |
+
.level-tag { display: inline-block; font: 400 10px var(--pixel); background: var(--cyan); color: #04121a; padding: 6px 8px; margin-bottom: 14px; box-shadow: 3px 3px 0 #000; }
|
| 136 |
+
.level h3 { font-size: 15px; color: var(--text); margin-bottom: 10px; }
|
| 137 |
+
.level p { margin: 0 0 12px; color: var(--text-2); font-size: 14px; }
|
| 138 |
+
.level-stars { font-size: 18px; color: var(--accent); letter-spacing: 4px; }
|
| 139 |
+
.level-boss { border-color: var(--accent); }
|
| 140 |
+
.level-boss .level-tag { background: var(--accent); color: var(--accent-ink); }
|
| 141 |
+
.level-arrow { align-self: center; color: var(--magenta); line-height: 0; }
|
| 142 |
+
.level-arrow .tri { width: 12px; height: 16px; margin: 0; }
|
| 143 |
+
.verify-loop { display: grid; grid-template-columns: 1fr auto 1fr; gap: 14px; align-items: stretch; margin-top: 28px; }
|
| 144 |
+
.verify { border: 3px dashed var(--hover-border); padding: 14px 16px; }
|
| 145 |
+
.verify-key { font: 400 10px var(--pixel); color: var(--cyan); }
|
| 146 |
+
.verify p { margin: 8px 0 0; color: var(--text-2); font-size: 14px; }
|
| 147 |
+
.verify-mid { align-self: center; font: 400 10px var(--pixel); color: var(--accent-ink); background: var(--magenta); padding: 10px 12px; box-shadow: 3px 3px 0 #000; }
|
| 148 |
+
.level-foot { color: var(--muted); margin: 18px 0 0; }
|
| 149 |
+
@media (max-width: 860px) {
|
| 150 |
+
.level-select, .verify-loop { grid-template-columns: 1fr; }
|
| 151 |
+
.level-arrow { justify-self: center; transform: rotate(90deg); }
|
| 152 |
+
.verify-mid { justify-self: center; }
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/* ---------- versus (before/after) ---------- */
|
| 156 |
+
.compare { border: 3px solid var(--border); box-shadow: 6px 6px 0 #000; background: var(--surface); }
|
| 157 |
+
.compare-head h3 { font-size: 18px; }
|
| 158 |
+
.compare-head button:hover h3 { color: var(--accent); }
|
| 159 |
+
.compare-pair { grid-template-columns: 1fr auto 1fr; align-items: center; }
|
| 160 |
+
.vs { font: 400 clamp(14px, 2vw, 22px) var(--pixel); color: var(--accent); text-shadow: 3px 3px 0 var(--magenta); padding-top: 24px; }
|
| 161 |
+
.compare-label { font: 400 9px var(--pixel); letter-spacing: 0; }
|
| 162 |
+
.compare-label .before { color: var(--cyan); }
|
| 163 |
+
.compare-label .after { color: var(--magenta); }
|
| 164 |
+
.compare-label b { font: 400 12px var(--pixel); color: var(--accent); }
|
| 165 |
+
.frame { border-radius: 0; border: 3px solid #000; }
|
| 166 |
+
.pending b { font: 400 10px/1.8 var(--pixel); color: var(--magenta); }
|
| 167 |
+
@media (max-width: 700px) { .compare-pair { grid-template-columns: 1fr; } .vs { justify-self: center; padding: 0; } }
|
| 168 |
+
|
| 169 |
+
/* ---------- gallery ---------- */
|
| 170 |
+
.filters { top: 60px; }
|
| 171 |
+
.seg { border-radius: 0; border: 3px solid var(--border); padding: 3px; }
|
| 172 |
+
.seg button { font: 400 9px var(--pixel); border-radius: 0; padding: 9px 12px; }
|
| 173 |
+
.seg button.on { background: var(--accent); color: var(--accent-ink); box-shadow: none; }
|
| 174 |
+
.filters select, .filters input { border-radius: 0; border: 3px solid var(--border); }
|
| 175 |
+
.filters select:focus, .filters input:focus { outline: 3px solid var(--cyan); outline-offset: 0; }
|
| 176 |
+
.count { font: 400 9px var(--pixel); }
|
| 177 |
+
|
| 178 |
+
.grid { gap: 22px; }
|
| 179 |
+
.cart { position: relative; border: 3px solid var(--border); border-radius: 0; background: var(--surface); box-shadow: var(--shadow); overflow: visible; }
|
| 180 |
+
.cart:hover, .cart:focus-visible { transform: translate(-3px, -3px); border-color: var(--cyan); box-shadow: 7px 7px 0 var(--magenta); }
|
| 181 |
+
.cart-notch { position: absolute; top: -3px; left: 50%; width: 38%; height: 9px; transform: translateX(-50%); background: var(--bg); border: 3px solid var(--border); border-top: 0; z-index: 2; }
|
| 182 |
+
.cart:hover .cart-notch, .cart:focus-visible .cart-notch { border-color: var(--cyan); }
|
| 183 |
+
.cart .frame { border: 0; border-bottom: 3px solid #000; }
|
| 184 |
+
.badge { font: 400 8px var(--pixel); border-radius: 0; padding: 6px 7px; box-shadow: 2px 2px 0 #000; }
|
| 185 |
+
.dot { border-radius: 0; }
|
| 186 |
+
.card-genre { font: 400 8px var(--pixel); color: var(--cyan); letter-spacing: 0; }
|
| 187 |
+
.cart h3 { font-size: 16px; }
|
| 188 |
+
.cart-score { display: flex; justify-content: space-between; align-items: baseline; border-top: 2px dotted var(--border); padding-top: 8px; }
|
| 189 |
+
.cart-score span { font: 400 8px var(--pixel); color: var(--muted); }
|
| 190 |
+
.cart-score b { font: 400 14px var(--pixel); color: var(--accent); }
|
| 191 |
+
.pmeters { display: grid; grid-template-columns: 1fr 1fr; gap: 7px 16px; }
|
| 192 |
+
.pmeter { display: grid; grid-template-columns: 10px 1fr 26px; gap: 5px; align-items: center; }
|
| 193 |
+
.pk, .pv { font: 400 8px var(--pixel); color: var(--muted); }
|
| 194 |
+
.pv { text-align: right; color: var(--text-2); }
|
| 195 |
+
.pbar { display: grid; grid-template-columns: repeat(10, 1fr); gap: 2px; height: 8px; }
|
| 196 |
+
.pbar i { background: var(--track); }
|
| 197 |
+
.pbar i.on { background: var(--bar); }
|
| 198 |
+
.no-media { font: 400 9px/1.8 var(--pixel); }
|
| 199 |
+
.more .btn { font-size: 11px; }
|
| 200 |
+
.empty { font: 400 11px var(--pixel); }
|
| 201 |
+
|
| 202 |
+
/* ---------- leaderboards ---------- */
|
| 203 |
+
.boards { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; margin-bottom: 22px; }
|
| 204 |
+
.board { border: 3px solid var(--border); background: #0a0616; box-shadow: 6px 6px 0 #000; padding: 16px; min-width: 0; }
|
| 205 |
+
.board-godot { border-top-color: var(--godot); }
|
| 206 |
+
.board-phaser { border-top-color: var(--phaser); }
|
| 207 |
+
.board-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
|
| 208 |
+
.board-head span { font: 400 12px var(--pixel); display: inline-flex; align-items: center; gap: 8px; }
|
| 209 |
+
.board-head small { font: 400 8px var(--pixel); color: var(--muted); }
|
| 210 |
+
.board ol { list-style: none; margin: 0; padding: 0; }
|
| 211 |
+
.board-row { width: 100%; display: grid; grid-template-columns: 50px 1fr auto; gap: 12px; align-items: center; padding: 8px 6px; background: none; border: 0; border-bottom: 2px dotted #231740; cursor: pointer; text-align: left; }
|
| 212 |
+
.board li:last-child .board-row { border-bottom: 0; }
|
| 213 |
+
.board-row:hover { background: var(--surface); }
|
| 214 |
+
.board-row:hover .board-title { color: var(--accent); }
|
| 215 |
+
.board-rank { font: 400 10px var(--pixel); color: var(--muted); }
|
| 216 |
+
.r1 .board-rank { color: var(--accent); } .r2 .board-rank { color: #d7deea; } .r3 .board-rank { color: #e59a62; }
|
| 217 |
+
.board-title { font-weight: 600; font-size: 14px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 218 |
+
.board-title small { display: block; font-weight: 400; font-size: 12px; color: var(--muted); }
|
| 219 |
+
.board-score { font: 400 12px var(--pixel); color: var(--text); }
|
| 220 |
+
@media (max-width: 860px) { .boards { grid-template-columns: 1fr; } }
|
| 221 |
+
|
| 222 |
+
/* ---------- charts & table ---------- */
|
| 223 |
+
.chart-card { border: 3px solid var(--border); box-shadow: 6px 6px 0 #000; }
|
| 224 |
+
.chart-card h3 { font-size: 12px; color: var(--accent); line-height: 1.5; }
|
| 225 |
+
.row-label { font-family: var(--font); }
|
| 226 |
+
.table-wrap { border: 3px solid var(--border); border-radius: 0; box-shadow: 6px 6px 0 #000; }
|
| 227 |
+
.table th { font: 400 8px var(--pixel); letter-spacing: 0; color: var(--cyan); }
|
| 228 |
+
|
| 229 |
+
/* ---------- citation / footer ---------- */
|
| 230 |
+
.bibtex { border: 3px solid var(--border); box-shadow: 6px 6px 0 #000; }
|
| 231 |
+
.copy { font: 400 9px var(--pixel); border-radius: 0; border: 2px solid var(--border); padding: 7px 9px; }
|
| 232 |
+
.footer { border-top: 3px solid var(--border); display: block; }
|
| 233 |
+
.footer .wrap { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: baseline; }
|
| 234 |
+
.footer-go { font: 400 10px var(--pixel); color: var(--magenta); }
|
| 235 |
+
|
| 236 |
+
/* ---------- modal ---------- */
|
| 237 |
+
.modal-panel { border: 3px solid var(--cyan); border-radius: 0; background: var(--bg-alt); box-shadow: 0 0 0 3px #000, 10px 10px 0 #000; }
|
| 238 |
+
.modal-close { border-radius: 0; border: 3px solid var(--border); font: 400 12px var(--pixel); box-shadow: 3px 3px 0 #000; }
|
| 239 |
+
.modal-close:hover { border-color: var(--magenta); color: var(--magenta); }
|
| 240 |
+
.modal-media { border-right: 3px solid var(--border); }
|
| 241 |
+
.player { border-radius: 0; border: 3px solid #000; }
|
| 242 |
+
.demo-tabs button { border-radius: 0; border: 2px solid var(--border); }
|
| 243 |
+
.demo-tabs button.on { border-color: var(--accent); background: rgba(255, 210, 63, .1); }
|
| 244 |
+
.m-prompt { border-top: 3px dotted var(--border); }
|
| 245 |
+
.m-prompt h3, .req-cat { font: 400 9px var(--pixel); color: var(--cyan); letter-spacing: 0; }
|
| 246 |
+
.modal-nav { font: 400 9px var(--pixel); }
|
| 247 |
+
.pill { border-radius: 0; font: 400 8px/1.4 var(--pixel); padding: 6px 7px; }
|
| 248 |
+
.modal-info h2 { font-size: clamp(15px, 2vw, 20px); line-height: 1.5; }
|
| 249 |
+
.m-score { border-radius: 0; border: 3px solid var(--border); background: #0a0616; }
|
| 250 |
+
.m-overall b { font: 400 30px/1 var(--pixel); color: var(--accent); }
|
| 251 |
+
.m-overall span { font: 400 8px var(--pixel); letter-spacing: 0; display: block; margin-top: 8px; }
|
| 252 |
+
.m-cat b { font: 400 9px var(--pixel); }
|
| 253 |
+
.m-cat .meter { height: 8px; border-radius: 0; }
|
| 254 |
+
.m-cat .meter i, .meter i { border-radius: 0; }
|
| 255 |
+
.meter { border-radius: 0; }
|
| 256 |
+
.twin { border-radius: 0; border: 2px dashed var(--hover-border); }
|
| 257 |
+
.twin b { font: 400 11px var(--pixel); color: var(--accent); }
|
| 258 |
+
.m-h { font-size: 12px; color: var(--accent); margin-top: 26px; }
|
| 259 |
+
.req { border-radius: 0; border: 2px solid var(--border); }
|
| 260 |
+
.req-id { font: 400 9px var(--pixel); padding-top: 3px; }
|
| 261 |
+
.req-score { font: 400 10px var(--pixel); padding-top: 2px; }
|
| 262 |
+
.req-demos button { border-radius: 0; font: 400 8px/1.4 var(--pixel); padding: 4px 6px; }
|
| 263 |
+
.rationale { border-radius: 0; border-left: 3px solid var(--accent); }
|
| 264 |
+
|
| 265 |
+
/* ---------- motion ---------- */
|
| 266 |
+
@media (prefers-reduced-motion: reduce) {
|
| 267 |
+
.blink { animation: none; }
|
| 268 |
+
.cart, .btn { transition: none; }
|
| 269 |
+
}
|
static/css/v4.css
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* v4 — "Cinema": warm near-black, editorial serif, one amber accent, task-first. Loads after base.css. */
|
| 2 |
+
:root {
|
| 3 |
+
--bg: #0b0a09;
|
| 4 |
+
--bg-alt: #100e0c;
|
| 5 |
+
--surface: #171512;
|
| 6 |
+
--surface-2: #201d19;
|
| 7 |
+
--border: #2a2621;
|
| 8 |
+
--text: #f3ede4;
|
| 9 |
+
--text-2: #c7bdaf;
|
| 10 |
+
--muted: #8d8376;
|
| 11 |
+
--accent: #f0a73a;
|
| 12 |
+
--accent-ink: #1b1206;
|
| 13 |
+
--godot: #3987e5;
|
| 14 |
+
--phaser: #d95926;
|
| 15 |
+
--bar: #f0a73a;
|
| 16 |
+
--track: #2c2823;
|
| 17 |
+
--hover-border: #4d443a;
|
| 18 |
+
--nav-bg: rgba(11, 10, 9, .5);
|
| 19 |
+
--nav-bg-solid: rgba(11, 10, 9, .93);
|
| 20 |
+
--chip-bg: rgba(8, 7, 6, .78);
|
| 21 |
+
--overlay: rgba(6, 5, 4, .9);
|
| 22 |
+
--stripe-a: #13110f;
|
| 23 |
+
--stripe-b: #181613;
|
| 24 |
+
--media-bg: #050403;
|
| 25 |
+
--s-hi: #9ad58a; --s-mid: #e8c36a; --s-lo: #ef8f7c;
|
| 26 |
+
--radius: 4px;
|
| 27 |
+
--display: "Instrument Serif", "Iowan Old Style", "Palatino Linotype", Georgia, serif;
|
| 28 |
+
--font: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
|
| 29 |
+
--mono: "JetBrains Mono", ui-monospace, Menlo, monospace;
|
| 30 |
+
--gutter: clamp(16px, 4vw, 48px);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
body.v4 { font-size: 15.5px; }
|
| 34 |
+
.v4 h1, .v4 h2, .v4 h3 { font-weight: 400; letter-spacing: 0; }
|
| 35 |
+
.v4 em { font-style: italic; }
|
| 36 |
+
.v4 .wrap { max-width: 1320px; }
|
| 37 |
+
.kicker, .v4 .section-head .kicker { font: 500 11px var(--mono); letter-spacing: .16em; text-transform: uppercase; color: var(--accent); margin: 0 0 14px; }
|
| 38 |
+
|
| 39 |
+
/* ---------- nav ---------- */
|
| 40 |
+
.v4 .brand { font: 400 24px var(--display); letter-spacing: .01em; }
|
| 41 |
+
.v4 .nav-links a { border-radius: 0; font-size: 13px; letter-spacing: .02em; }
|
| 42 |
+
.v4 .nav-links a:hover { background: none; color: var(--accent); }
|
| 43 |
+
|
| 44 |
+
/* ---------- buttons ---------- */
|
| 45 |
+
.v4 .btn { border-radius: 999px; padding: 10px 20px; background: transparent; border-color: var(--hover-border); font-weight: 500; }
|
| 46 |
+
.v4 .btn:hover { background: var(--surface); border-color: var(--text-2); }
|
| 47 |
+
.v4 .btn-primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
|
| 48 |
+
.v4 .btn-primary:hover { background: #f7b95c; border-color: #f7b95c; transform: none; }
|
| 49 |
+
|
| 50 |
+
/* ---------- masthead + theater ---------- */
|
| 51 |
+
.masthead { padding: 108px var(--gutter) 72px; max-width: 1480px; margin: 0 auto; }
|
| 52 |
+
.mast-grid { display: grid; grid-template-columns: minmax(0, 4.2fr) minmax(0, 7.8fr); gap: clamp(28px, 4vw, 64px); align-items: center; }
|
| 53 |
+
.mast-copy h1 { font-size: clamp(64px, 9vw, 132px); line-height: .9; margin: 0 0 18px; }
|
| 54 |
+
.mast-sub { font: 400 clamp(22px, 2.4vw, 30px)/1.2 var(--display); color: var(--text); margin: 0 0 14px; max-width: 30ch; }
|
| 55 |
+
.mast-sub em { color: var(--accent); }
|
| 56 |
+
.authors { color: var(--muted); margin: 0 0 28px; font-size: 14px; }
|
| 57 |
+
.mast-cta { display: flex; flex-wrap: wrap; gap: 8px; }
|
| 58 |
+
|
| 59 |
+
.theater { margin: 0; min-width: 0; }
|
| 60 |
+
.screen { position: relative; display: block; width: 100%; aspect-ratio: 16 / 9; max-width: 100%; padding: 0; border: 0; cursor: pointer; overflow: hidden; background: var(--media-bg); border-radius: 2px; box-shadow: 0 30px 80px rgba(0,0,0,.6); }
|
| 61 |
+
.screen-still, .screen-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
| 62 |
+
.screen-video { transition: opacity .45s ease; }
|
| 63 |
+
.screen.switching .screen-video { opacity: 0; }
|
| 64 |
+
.screen-shade { position: absolute; inset: 0; background: linear-gradient(to top, rgba(6,5,4,.88) 0%, rgba(6,5,4,.2) 38%, transparent 60%); pointer-events: none; }
|
| 65 |
+
.screen-caption { position: absolute; left: clamp(14px, 2.4vw, 28px); bottom: clamp(16px, 2.4vw, 28px); right: 140px; text-align: left; display: grid; gap: 4px; }
|
| 66 |
+
.now { font: 500 10px var(--mono); letter-spacing: .18em; text-transform: uppercase; color: var(--accent); }
|
| 67 |
+
.screen-title { font: 400 clamp(24px, 3.2vw, 42px)/1.05 var(--display); color: #fff; }
|
| 68 |
+
.screen-meta { font-size: 13px; color: #d9d0c3; display: inline-flex; align-items: center; gap: 8px; }
|
| 69 |
+
.screen-score { position: absolute; right: clamp(14px, 2.4vw, 28px); bottom: clamp(16px, 2.4vw, 28px); text-align: right; color: #fff; }
|
| 70 |
+
.screen-score b { display: block; font: 400 clamp(40px, 5vw, 64px)/.9 var(--display); }
|
| 71 |
+
.screen-score span { font: 500 10px var(--mono); letter-spacing: .16em; text-transform: uppercase; color: #d9d0c3; }
|
| 72 |
+
.screen-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: rgba(255,255,255,.12); }
|
| 73 |
+
.screen-progress i { display: block; height: 100%; background: var(--accent); transform-origin: left; transform: scaleX(0); }
|
| 74 |
+
.screen:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
| 75 |
+
|
| 76 |
+
.rail { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; margin-top: 10px; }
|
| 77 |
+
.rail-item { padding: 0; border: 0; background: none; cursor: pointer; text-align: left; opacity: .55; transition: opacity .2s; min-width: 0; }
|
| 78 |
+
.rail-item img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 2px; border-bottom: 2px solid transparent; }
|
| 79 |
+
.rail-item:hover, .rail-item.on { opacity: 1; }
|
| 80 |
+
.rail-item.on img { border-bottom-color: var(--accent); }
|
| 81 |
+
.rail-label { display: flex; align-items: center; gap: 6px; margin-top: 6px; font-size: 12px; color: var(--text-2); }
|
| 82 |
+
.rail-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 83 |
+
@media (max-width: 1080px) {
|
| 84 |
+
.mast-grid { grid-template-columns: 1fr; }
|
| 85 |
+
.mast-copy h1 { font-size: clamp(64px, 16vw, 120px); }
|
| 86 |
+
}
|
| 87 |
+
@media (max-width: 640px) {
|
| 88 |
+
.masthead { padding-top: 88px; }
|
| 89 |
+
.rail { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
| 90 |
+
.screen-caption { right: 92px; }
|
| 91 |
+
.screen-meta { font-size: 11px; }
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* ---------- sections ---------- */
|
| 95 |
+
.v4 .section { padding-block: clamp(72px, 10vw, 128px); }
|
| 96 |
+
.v4 .section-alt { background: var(--bg-alt); }
|
| 97 |
+
.v4 .section-head { max-width: 760px; margin-bottom: 44px; }
|
| 98 |
+
.v4 .section-head h2 { font-size: clamp(40px, 5.6vw, 68px); line-height: 1; margin-bottom: 16px; }
|
| 99 |
+
.v4 .section-head p:not(.kicker) { font-size: 16.5px; line-height: 1.65; }
|
| 100 |
+
|
| 101 |
+
/* ---------- overview ---------- */
|
| 102 |
+
.figures { display: grid; grid-template-columns: repeat(4, 1fr); border-block: 1px solid var(--border); margin-bottom: clamp(56px, 8vw, 96px); }
|
| 103 |
+
.figures div { padding: 26px 20px 22px; border-left: 1px solid var(--border); }
|
| 104 |
+
.figures div:first-child { border-left: 0; padding-left: 0; }
|
| 105 |
+
.figures b { display: block; font: 400 clamp(48px, 6vw, 76px)/1 var(--display); color: var(--text); }
|
| 106 |
+
.figures span { color: var(--muted); font-size: 13.5px; }
|
| 107 |
+
@media (max-width: 760px) {
|
| 108 |
+
.figures { grid-template-columns: repeat(2, 1fr); }
|
| 109 |
+
.figures div:nth-child(3) { border-left: 0; padding-left: 0; }
|
| 110 |
+
.figures div:nth-child(n+3) { border-top: 1px solid var(--border); }
|
| 111 |
+
}
|
| 112 |
+
.essay { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: clamp(28px, 5vw, 80px); }
|
| 113 |
+
.essay h2 { font-size: clamp(40px, 5.2vw, 64px); line-height: 1.02; }
|
| 114 |
+
.essay h2 em { color: var(--accent); }
|
| 115 |
+
.essay-body p { color: var(--text-2); font-size: 17px; line-height: 1.7; margin: 0 0 16px; }
|
| 116 |
+
.essay-body strong { color: var(--text); font-weight: 600; }
|
| 117 |
+
.acts { list-style: none; padding: 0; margin: clamp(48px, 7vw, 88px) 0 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; counter-reset: act; }
|
| 118 |
+
.acts li { padding: 26px 28px 8px 0; border-top: 1px solid var(--hover-border); position: relative; }
|
| 119 |
+
.acts li + li { padding-left: 28px; border-left: 1px solid var(--border); }
|
| 120 |
+
.acts li::after { content: "→"; position: absolute; right: -8px; top: 22px; color: var(--accent); font-size: 18px; }
|
| 121 |
+
.acts li:last-child::after { content: none; }
|
| 122 |
+
.act-no { font: italic 400 22px var(--display); color: var(--accent); }
|
| 123 |
+
.acts h3 { font-size: 36px; margin: 2px 0 6px; }
|
| 124 |
+
.acts p { color: var(--text-2); margin: 0 0 18px; }
|
| 125 |
+
.acts-foot { color: var(--muted); max-width: 780px; margin: 24px 0 0; font-size: 15px; }
|
| 126 |
+
@media (max-width: 860px) {
|
| 127 |
+
.essay { grid-template-columns: 1fr; }
|
| 128 |
+
.acts { grid-template-columns: 1fr; }
|
| 129 |
+
.acts li, .acts li + li { padding-left: 0; border-left: 0; }
|
| 130 |
+
.acts li::after { content: none; }
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/* ---------- before / after ---------- */
|
| 134 |
+
.v4 .compare-list { gap: 56px; }
|
| 135 |
+
.v4 .compare { background: none; border: 0; padding: 0; border-radius: 0; }
|
| 136 |
+
.v4 .compare-head { border-bottom: 1px solid var(--border); padding-bottom: 12px; margin-bottom: 16px; }
|
| 137 |
+
.v4 .compare-head h3 { font-size: clamp(28px, 3.2vw, 40px); }
|
| 138 |
+
.v4 .compare-pair { gap: 16px; }
|
| 139 |
+
.v4 .compare-label { font-size: 10.5px; letter-spacing: .16em; }
|
| 140 |
+
.v4 .compare-label b { font: 400 30px/1 var(--display); }
|
| 141 |
+
.v4 .frame { border-radius: 2px; border: 0; }
|
| 142 |
+
.v4 .pill { border-radius: 999px; background: transparent; }
|
| 143 |
+
|
| 144 |
+
/* ---------- gallery toolbar ---------- */
|
| 145 |
+
.v4 .filters { border-block: 1px solid var(--border); padding-block: 12px; margin-bottom: 36px; }
|
| 146 |
+
.v4 .seg { border-radius: 999px; background: transparent; }
|
| 147 |
+
.v4 .seg button { border-radius: 999px; }
|
| 148 |
+
.v4 .seg button.on { background: var(--text); color: var(--bg); box-shadow: none; }
|
| 149 |
+
.v4 .filters select, .v4 .filters input { border-radius: 999px; background: transparent; }
|
| 150 |
+
.v4 .filters select option { background: var(--surface); color: var(--text); }
|
| 151 |
+
.v4 .filters select:focus, .v4 .filters input:focus { outline-color: rgba(240,167,58,.6); }
|
| 152 |
+
|
| 153 |
+
/* ---------- pair cards ---------- */
|
| 154 |
+
.pairs { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 540px), 1fr)); gap: 56px 36px; }
|
| 155 |
+
.pairs.solo-mode { grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: 40px 24px; }
|
| 156 |
+
.pair { min-width: 0; }
|
| 157 |
+
.pair-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 12px; }
|
| 158 |
+
.pair-head > div:first-child { min-width: 0; }
|
| 159 |
+
.pair-genre { font: 500 10.5px var(--mono); letter-spacing: .16em; text-transform: uppercase; color: var(--muted); margin: 0 0 4px; }
|
| 160 |
+
.pair-head h3 { font-size: clamp(24px, 2.3vw, 30px); line-height: 1.05; }
|
| 161 |
+
.halves { display: grid; grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); gap: 6px; }
|
| 162 |
+
.half { padding: 0; border: 0; background: none; cursor: pointer; text-align: left; min-width: 0; display: block; }
|
| 163 |
+
.half .frame { border-radius: 2px; }
|
| 164 |
+
.half .frame img { transition: transform .5s ease, filter .3s; }
|
| 165 |
+
.half .frame video { opacity: 0; transition: opacity .3s; }
|
| 166 |
+
.half .frame video.ready { opacity: 1; }
|
| 167 |
+
.half:hover .frame img { transform: scale(1.03); }
|
| 168 |
+
.half:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
| 169 |
+
.half-foot { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; padding: 9px 2px 0; border-top: 2px solid transparent; }
|
| 170 |
+
.half:hover .half-foot { border-top-color: var(--accent); }
|
| 171 |
+
.half-engine { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--text-2); }
|
| 172 |
+
.half-score { font: 400 30px/1 var(--display); color: var(--text); }
|
| 173 |
+
|
| 174 |
+
.dumbbell { flex: none; width: 150px; display: grid; gap: 6px; justify-items: end; padding-bottom: 4px; }
|
| 175 |
+
.db-track { position: relative; width: 100%; height: 12px; }
|
| 176 |
+
.db-track::before { content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: var(--track); }
|
| 177 |
+
.db-span { position: absolute; top: calc(50% - 1px); height: 2px; background: var(--text-2); opacity: .55; }
|
| 178 |
+
.db-dot { position: absolute; top: 50%; width: 10px; height: 10px; margin: -5px 0 0 -5px; border-radius: 50%; box-shadow: 0 0 0 2px var(--bg); }
|
| 179 |
+
.db-dot.godot { background: var(--godot); } .db-dot.phaser { background: var(--phaser); }
|
| 180 |
+
.db-label { font: 500 11px var(--mono); color: var(--text-2); white-space: nowrap; }
|
| 181 |
+
@media (max-width: 520px) {
|
| 182 |
+
.pair-head { flex-direction: column; align-items: stretch; gap: 8px; }
|
| 183 |
+
.dumbbell { width: 100%; justify-items: start; grid-template-columns: 1fr auto; align-items: center; gap: 12px; }
|
| 184 |
+
.half-score { font-size: 24px; }
|
| 185 |
+
.half-engine { font-size: 12px; gap: 5px; }
|
| 186 |
+
}
|
| 187 |
+
.v4 .more .btn { padding: 12px 28px; }
|
| 188 |
+
|
| 189 |
+
/* ---------- results ---------- */
|
| 190 |
+
.h2h { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: 20px; align-items: start; }
|
| 191 |
+
.h2h-side { display: grid; gap: 20px; min-width: 0; }
|
| 192 |
+
@media (max-width: 960px) { .h2h { grid-template-columns: 1fr; } }
|
| 193 |
+
.v4 .chart-card { border-radius: 2px; background: var(--surface); padding: 22px 22px 14px; }
|
| 194 |
+
.v4 .chart-card h3 { font-size: 26px; }
|
| 195 |
+
.scatter { overflow-x: auto; }
|
| 196 |
+
.scatter svg { display: block; width: 100%; height: auto; min-width: 300px; margin-top: 10px; }
|
| 197 |
+
.diag { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 4 4; }
|
| 198 |
+
.axis-title { fill: var(--text-2); font: 500 12px var(--font); }
|
| 199 |
+
.zone { fill: var(--muted); font: italic 400 16px var(--display); }
|
| 200 |
+
.sc-dot { fill: var(--accent); stroke: var(--surface); stroke-width: 1.5; }
|
| 201 |
+
.tally { background: var(--surface); border: 1px solid var(--border); border-radius: 2px; padding: 20px 22px; display: grid; gap: 16px; }
|
| 202 |
+
.tally-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
| 203 |
+
.tally-row b { display: block; font: 400 52px/1 var(--display); }
|
| 204 |
+
.tally-row span { color: var(--text-2); font-size: 13px; }
|
| 205 |
+
.tally-big { display: grid; gap: 2px; text-align: left; background: none; border: 0; border-top: 1px solid var(--border); padding: 14px 0 0; cursor: pointer; }
|
| 206 |
+
.tally-big span { font: 500 10.5px var(--mono); letter-spacing: .16em; text-transform: uppercase; color: var(--muted); }
|
| 207 |
+
.tally-big strong { font: 400 26px var(--display); color: var(--text); }
|
| 208 |
+
.tally-big em { color: var(--accent); font-style: normal; font-size: 13px; }
|
| 209 |
+
.tally-big:hover strong { color: var(--accent); }
|
| 210 |
+
.v4 .table-wrap { border-radius: 2px; margin-top: 20px; }
|
| 211 |
+
.v4 .table th { background: var(--surface-2); }
|
| 212 |
+
|
| 213 |
+
/* ---------- citation / footer ---------- */
|
| 214 |
+
.v4 .bibtex { border-radius: 2px; }
|
| 215 |
+
.v4 .footer { font-size: 12px; }
|
| 216 |
+
|
| 217 |
+
/* ---------- modal ---------- */
|
| 218 |
+
.v4 .modal-panel { border-radius: 4px; background: var(--bg); }
|
| 219 |
+
.v4 .modal-close { background: transparent; }
|
| 220 |
+
.v4 .player { border-radius: 2px; }
|
| 221 |
+
.v4 .demo-tabs button { border-radius: 999px; background: transparent; }
|
| 222 |
+
.v4 .demo-tabs button.on { border-color: var(--accent); background: rgba(240,167,58,.1); }
|
| 223 |
+
.v4 .modal-info h2 { font-size: clamp(32px, 3.4vw, 44px); line-height: 1; margin-top: 4px; }
|
| 224 |
+
.v4 .m-score { border-radius: 2px; background: var(--surface); }
|
| 225 |
+
.v4 .m-overall b { font: 400 60px/.9 var(--display); }
|
| 226 |
+
.v4 .m-prompt p { font: 400 19px/1.45 var(--display); color: var(--text-2); }
|
| 227 |
+
.v4 .twin { border-radius: 2px; }
|
| 228 |
+
.v4 .m-h { font-size: 28px; margin-top: 28px; }
|
| 229 |
+
.v4 .req { border-radius: 2px; }
|
| 230 |
+
.v4 .rationale { font: italic 400 17px/1.45 var(--display); border-left-color: var(--accent); }
|
static/js/core.js
CHANGED
|
@@ -15,12 +15,14 @@
|
|
| 15 |
|
| 16 |
const SCRIPT = document.currentScript.src;
|
| 17 |
const ROOT = new URL("../../", SCRIPT).href; // site root, wherever the version page lives
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
| 19 |
const qs = new URLSearchParams(location.search);
|
| 20 |
-
const
|
| 21 |
-
// a page can pin the media location (e.g. a self-contained bundle ships its clips next to the page)
|
| 22 |
const MEDIA = window.EVO_MEDIA ? new URL(window.EVO_MEDIA, ROOT).href.replace(/\/$/, "")
|
| 23 |
-
: qs.get("media") === "
|
| 24 |
|
| 25 |
const MAX_PLAYING = 12;
|
| 26 |
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
@@ -780,7 +782,7 @@
|
|
| 780 |
|
| 781 |
// ---------- chrome ----------
|
| 782 |
function versionSwitcher() {
|
| 783 |
-
if (window.EVO_NO_SWITCH) return;
|
| 784 |
const here = location.pathname.match(/\/(v\d)\/?(index\.html)?$/)?.[1] || "v1";
|
| 785 |
document.body.append(h("nav", { class: "vswitch", "aria-label": "Design versions" },
|
| 786 |
VERSIONS.map(([name, path]) => h("a", { href: `${ROOT}${path}${location.search}`, class: name === here ? "on" : "" }, name))));
|
|
|
|
| 15 |
|
| 16 |
const SCRIPT = document.currentScript.src;
|
| 17 |
const ROOT = new URL("../../", SCRIPT).href; // site root, wherever the version page lives
|
| 18 |
+
// Clips, posters and the playable builds are not in this repo; by default they stream from the
|
| 19 |
+
// published site. `?media=local` uses ./media instead (a copy or a symlink of the media folder),
|
| 20 |
+
// and a deployed copy can pin its own location with window.EVO_MEDIA.
|
| 21 |
+
const REMOTE = "https://wenyiwu0111-evogame-page.static.hf.space/media";
|
| 22 |
const qs = new URLSearchParams(location.search);
|
| 23 |
+
const published = /(^|\.)static\.hf\.space$/.test(location.hostname) || location.hostname === "huggingface.co";
|
|
|
|
| 24 |
const MEDIA = window.EVO_MEDIA ? new URL(window.EVO_MEDIA, ROOT).href.replace(/\/$/, "")
|
| 25 |
+
: qs.get("media") === "local" || published ? `${ROOT}media` : REMOTE;
|
| 26 |
|
| 27 |
const MAX_PLAYING = 12;
|
| 28 |
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
|
|
| 782 |
|
| 783 |
// ---------- chrome ----------
|
| 784 |
function versionSwitcher() {
|
| 785 |
+
if (window.EVO_NO_SWITCH || published) return;
|
| 786 |
const here = location.pathname.match(/\/(v\d)\/?(index\.html)?$/)?.[1] || "v1";
|
| 787 |
document.body.append(h("nav", { class: "vswitch", "aria-label": "Design versions" },
|
| 788 |
VERSIONS.map(([name, path]) => h("a", { href: `${ROOT}${path}${location.search}`, class: name === here ? "on" : "" }, name))));
|