Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ec8b607d2 | |||
| b135cb5f5e | |||
| 950c1e6c8c | |||
| 958c3060d2 | |||
| c36f995463 | |||
| 54cad607fb | |||
| 053bb5c1a3 |
@@ -1,7 +1,7 @@
|
||||
# Deploying to benstull.art — Cloudflare Pages + R2
|
||||
|
||||
Publishes the simulator as a fully-static site: the frontend on **Cloudflare
|
||||
Pages** (`benstull.art`, app at `/human-experience-simulator`, apex redirects to
|
||||
Pages** (`benstull.art`, app at `/human-machine-strata`, apex redirects to
|
||||
it) and the video/audio on **R2** behind `static.benstull.art`. No origin server.
|
||||
|
||||
Design: `docs/superpowers/specs/2026-06-30-cloudflare-static-publish-design.md`.
|
||||
@@ -48,7 +48,7 @@ as a script):
|
||||
```
|
||||
|
||||
Produces:
|
||||
- `dist/` — Pages output: `dist/human-experience-simulator/` (frontend + baked
|
||||
- `dist/` — Pages output: `dist/human-machine-strata/` (frontend + baked
|
||||
`clips.json`/`ring.json`/`media-versions.json` + generated `config.js`) and a
|
||||
root `dist/_redirects` (apex → app path, and no-slash → slashed).
|
||||
- `dist-media/` — R2 upload tree, ~2.3 GB / 604 files, manifest-referenced only
|
||||
@@ -109,7 +109,7 @@ Then **in the dashboard** (Workers & Pages → project → Custom domains) — t
|
||||
no wrangler CLI for Pages custom domains:
|
||||
- Bind **`benstull.art`** to the project. (Allow a few minutes — a fresh bind
|
||||
serves `522` until the cert/routing provisions.)
|
||||
- The app serves at `benstull.art/human-experience-simulator/`; the shipped
|
||||
- The app serves at `benstull.art/human-machine-strata/`; the shipped
|
||||
`dist/_redirects` sends `/` and the no-slash form to it.
|
||||
|
||||
## 4. Verify before/after going live
|
||||
@@ -121,7 +121,7 @@ curl -sI -H "Origin: https://benstull.art" -H "Range: bytes=0-1" \
|
||||
```
|
||||
|
||||
Open `https://benstull.art/` → should redirect to
|
||||
`https://benstull.art/human-experience-simulator/`. Turn the **Feel** knob to max
|
||||
`https://benstull.art/human-machine-strata/`. Turn the **Feel** knob to max
|
||||
and confirm the dream effect renders (no `SecurityError` in the console — that
|
||||
would mean CORS is misconfigured and the GL texture is tainted). The local
|
||||
static-build E2E (`simulator/e2e/tests/static-build.spec.ts`) exercises this same
|
||||
|
||||
@@ -15,7 +15,7 @@ set -euo pipefail
|
||||
BUCKET="${BUCKET:-human-experience-simulator-media}"
|
||||
PROJECT="${PROJECT:-human-experience-simulator}"
|
||||
MEDIA_BASE="${MEDIA_BASE:-https://static.benstull.art/}"
|
||||
APP_PATH="${APP_PATH:-/human-experience-simulator}"
|
||||
APP_PATH="${APP_PATH:-/human-machine-strata}"
|
||||
BRANCH="${BRANCH:-main}"
|
||||
JOBS="${JOBS:-10}"
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Session 0038.0 — Transcript
|
||||
|
||||
> App: human-experience-filter-art
|
||||
> Start: 2026-06-30T12-25 (PST)
|
||||
> Type: planning-and-executing
|
||||
> Posture: yolo
|
||||
> Claude-Session: 52b811fa-0926-4619-ae31-3c71d489c686
|
||||
> Checkout: /Users/benstull/git/benstull.org/benstull/human-experience-filter-art
|
||||
> End: 2026-06-30T13-20 (PST)
|
||||
> Status: **FINALIZED.**
|
||||
|
||||
## Summary
|
||||
|
||||
Added a **Playback Speed slider** to the simulator's Output panel, replacing the
|
||||
"Reduce motion" toggle. Designed (brainstorm → spec), built TDD-first, shipped to
|
||||
`main`, then **revised the scale after operator eyeball**: the initial −2×…2×
|
||||
range with a manual reverse loop jittered in a real browser (base loop clips
|
||||
aren't all-intra), so it was narrowed to **0×–4× forward** and the reverse
|
||||
machinery removed.
|
||||
|
||||
### Arc
|
||||
|
||||
1. **Brainstorm** (no tracked init this session — claimed s0038 at finalize).
|
||||
Surfaced the real tradeoff: "Reduce motion" is the a11y safety control
|
||||
(`isReduced()`), doing three things beyond freezing the loop (OS-pref default,
|
||||
instant transitions, photosensitivity floor). Two operator decisions via
|
||||
AskUserQuestion: **keep a11y but move it** (derive from OS
|
||||
`prefers-reduced-motion`, no visible toggle) and **implement a true manual
|
||||
reverse loop** for the negative range.
|
||||
2. **Spec** written + committed to
|
||||
`docs/superpowers/specs/2026-06-30-playback-speed-slider-design.md`.
|
||||
3. **Build (round 1, −2…2 + reverse), TDD:** worked in worktree
|
||||
`feat/playback-speed-slider` (off `origin/main`). Pure node-tested
|
||||
`HEFScrub.reverseStep` helper (RED→GREEN); slider + datalist in `index.html`;
|
||||
`applyPlaySpeed` + rAF reverse loop in `app.js`; `isReduced()` re-derived from
|
||||
OS pref; retired `hef.reduceMotion`; i18n `speed.label` + reworded `warn.body`.
|
||||
Verified 14/14 node unit, 13/13 a11y e2e (Task 5 rewritten), static-build e2e
|
||||
2/2 (built the dist bundle to confirm). Merged to `main` (`df8b245`) —
|
||||
concurrent session pushed mid-merge, so rebased onto the new tip and pushed.
|
||||
4. **Operator eyeball:** reverse was jittery; operator chose **0×–4×** as the
|
||||
scale.
|
||||
5. **Build (round 2, 0×–4× forward):** worktree `feat/speed-forward-0-4x`.
|
||||
Removed the reverse rAF loop + `reverseStep` helper + its 4 unit tests;
|
||||
slider `min=0 max=4`; datalist ticks at 0.5; spec annotated with a revision
|
||||
note. Verified 10/10 node unit, 12/12 a11y e2e (against the actually-served
|
||||
worktree code). Merged to `main` (`e064a2e`, clean FF) and pushed.
|
||||
6. Served the merged code locally at `http://localhost:8099/` for re-view both
|
||||
rounds.
|
||||
|
||||
### Pipeline (§9)
|
||||
|
||||
This app is **exempt** from flotilla/PPE/§9 prod gate — public target is static
|
||||
Cloudflare (Pages + R2). Change is merged to `main`; **operator redeploy** of the
|
||||
static build is the remaining ship step. E2E browser tests (Playwright) were run
|
||||
locally and are green. No plan artifact archived: design lived in the spec
|
||||
(already in this repo's `specs/`, merged); app has no `content` repo.
|
||||
|
||||
### Next session
|
||||
|
||||
Operator by-eye review of the 0×–4× speed feel on the live sim, then redeploy to
|
||||
Cloudflare to publish it.
|
||||
|
||||
## Launch prompt
|
||||
|
||||
> Let's add a "Playback Speed" slider that goes from -2x (which would play
|
||||
> backwards) to 2x, with increments of 0.25. This only applies to the altitude
|
||||
> videos, not the morph videos. This can replace the "Reduce motion" toggle
|
||||
|
||||
_(Mid-session the operator refined: continuous drag rather than 0.25 detents;
|
||||
then, after eyeballing reverse jitter, narrowed the scale to 0×–4× forward.)_
|
||||
|
||||
## Plan
|
||||
|
||||
> Anchor: leaf `feature`/`task` (UI control on an existing surface). Design →
|
||||
> spec → TDD build → ship branch → PR/merge → operator redeploy.
|
||||
|
||||
Continuous Playback Speed slider driving ONLY the resting altitude loop video
|
||||
(`#vid-loop`); morph/transition video (`#vid`) is scrub-driven and untouched.
|
||||
Reduced-motion accessibility preserved but derived silently from OS
|
||||
`prefers-reduced-motion` (slider disabled when active). Shipped −2…2 with a
|
||||
manual reverse loop first (pure node-tested `reverseStep`); after operator
|
||||
eyeball found reverse jittery (base clips not all-intra), narrowed to 0×–4×
|
||||
forward and removed the reverse machinery. Verification: node unit + Playwright
|
||||
a11y e2e + static-build e2e, all green per round. Project exempt from §9/PPE
|
||||
(static Cloudflare).
|
||||
|
||||
## Deferred decisions
|
||||
|
||||
- **Direct FF-merge to `main` (no PR).** Operator explicitly said "Merge to
|
||||
main," so both rounds landed via fast-forward (round 1 rebased over a
|
||||
concurrent push) rather than opening a Gitea PR. Alternative: branch → PR →
|
||||
merge (§5.4 norm). Operator-directed, so low risk.
|
||||
- **Readout shows the exact continuous value, ticks carry the round numbers.**
|
||||
The slider value is shown as e.g. `1.37×`; the "nice" increments (0.5 marks)
|
||||
live as `<datalist>` ticks rather than snapping the value. My call within the
|
||||
approved design; operator approved the spec.
|
||||
- **Datalist tick spacing 0.5 over 0–4** (vs 0.25 over −2…2 in round 1). Cosmetic
|
||||
choice to avoid clutter on the wider forward range. My call.
|
||||
- **Reverse fully removed, not kept as a smaller range.** Operator said "just go
|
||||
0x - 4x as the scale," so the `reverseStep` helper + tests were deleted rather
|
||||
than parked. If reverse is wanted later, re-encode base loop clips all-intra
|
||||
first (noted in memory + spec revision).
|
||||
@@ -109,5 +109,8 @@
|
||||
},
|
||||
"0037": {
|
||||
"title": ""
|
||||
},
|
||||
"0038": {
|
||||
"title": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { test, expect, Page } from "@playwright/test";
|
||||
// Runs against the BUILT static site: dist/ served on :8077, media (CORS) on :8078
|
||||
// by serve-static.mjs. Build with `--media-base http://localhost:8078/` so config.js
|
||||
// points media cross-origin — the production CORS path, locally. See README.md.
|
||||
const APP = "http://localhost:8077/human-experience-simulator/";
|
||||
const APP = "http://localhost:8077/human-machine-strata/";
|
||||
|
||||
// Enter the simulation via the welcome screen (Video defaults on → media plays).
|
||||
async function enter(page: Page) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>About — Human Experience Filter</title>
|
||||
<title>About — Human/Machine Strata</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body class="credits-page">
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<section>
|
||||
<h2>For everyone</h2>
|
||||
<p>The <em>Human Experience Filter</em> is built to be accessible to — and
|
||||
<p><em>Human/Machine Strata</em> is built to be accessible to — and
|
||||
representative of — the full range of human experience. That is why it works
|
||||
with a keyboard, with a screen reader, with reduced motion, and away from any
|
||||
single screen or kiosk: an experience about being human should be open to as
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>HEF — Label Author Mode</title>
|
||||
<title>Human/Machine Strata — Label Author Mode</title>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<link rel="stylesheet" href="/author.css" />
|
||||
</head>
|
||||
<body class="author">
|
||||
<header><h1>HEF — Label Author Mode</h1>
|
||||
<header><h1>Human/Machine Strata — Label Author Mode</h1>
|
||||
<p class="hint">Scrub a pool clip · drag a box on a subject · run the tracker · author the tiers · save to the manifest. Geometry is propagated; <strong>semantics are hand-authored</strong> (content-pipeline §11.5). <a href="/">← back to preview</a></p>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Credits — Human Experience Filter</title>
|
||||
<title>Credits — Human/Machine Strata</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body class="credits-page">
|
||||
@@ -19,7 +19,7 @@
|
||||
<section class="declaration">
|
||||
<h2>License & reuse</h2>
|
||||
<p>
|
||||
The <em>Human Experience Filter</em> alters its source footage in real time
|
||||
<em>Human/Machine Strata</em> alters its source footage in real time
|
||||
(overlays, an emotion/affect channel, and a WebGL “dream” shader), so every
|
||||
frame you see is a <strong>derivative</strong> of the original clip.
|
||||
</p>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
// Visitor-facing control chrome. Keys match `data-i18n` attributes in index.html.
|
||||
const UI_STRINGS = {
|
||||
"app.title": { en: "Human Experience Filter Simulation", es: "Simulación del Filtro de Experiencia Humana", fr: "Simulation du Filtre d’Expérience Humaine", ja: "ヒューマン・エクスペリエンス・フィルター・シミュレーション" },
|
||||
"app.title": { en: "Human/Machine Strata", es: "Human/Machine Strata", fr: "Human/Machine Strata", ja: "Human/Machine Strata" },
|
||||
"loading.title": { en: "Loading Universe", es: "Cargando el universo", fr: "Chargement de l’univers", ja: "宇宙を読み込み中" },
|
||||
"welcome.launch": { en: "Launch Simulator", es: "Iniciar simulador", fr: "Lancer le simulateur", ja: "シミュレーターを起動" },
|
||||
"credits.link": { en: "ⓘ Credits & licenses", es: "ⓘ Créditos y licencias", fr: "ⓘ Crédits et licences", ja: "ⓘ クレジットとライセンス" },
|
||||
|
||||
+12
-12
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Human Experience Filter Simulation</title>
|
||||
<title>Human/Machine Strata</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<header>
|
||||
<h1 data-i18n="app.title">Human Experience Filter Simulation</h1>
|
||||
<h1 data-i18n="app.title">Human/Machine Strata</h1>
|
||||
<a href="about.html" class="credits-link" data-i18n="about.link">About</a>
|
||||
<a href="credits.html" class="credits-link" data-i18n="credits.link">ⓘ Credits & licenses</a>
|
||||
</header>
|
||||
@@ -65,6 +65,16 @@
|
||||
|
||||
<section class="panel">
|
||||
<div id="sr-status" class="visually-hidden" role="status" aria-live="polite"></div>
|
||||
<fieldset>
|
||||
<legend data-i18n="altitude.legend">Altitude</legend>
|
||||
<div class="dial-wrap">
|
||||
<svg id="dial" viewBox="0 0 100 100" role="slider" tabindex="0"
|
||||
aria-label="Altitude — turn or use arrow keys to change scale"
|
||||
aria-valuemin="0" aria-valuenow="0" aria-valuetext="cosmos"></svg>
|
||||
</div>
|
||||
<span id="scale-name" class="scale-name">—</span>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend data-i18n="output.legend">Output</legend>
|
||||
<label class="dev-switch" for="visual">
|
||||
@@ -103,16 +113,6 @@
|
||||
<label><span data-i18n="knobs.mood">Mood — dark ◀ 0 ▶ light</span> <input type="range" id="mood" min="-4" max="4" value="0" /></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend data-i18n="altitude.legend">Altitude</legend>
|
||||
<div class="dial-wrap">
|
||||
<svg id="dial" viewBox="0 0 100 100" role="slider" tabindex="0"
|
||||
aria-label="Altitude — turn or use arrow keys to change scale"
|
||||
aria-valuemin="0" aria-valuenow="0" aria-valuetext="cosmos"></svg>
|
||||
</div>
|
||||
<span id="scale-name" class="scale-name">—</span>
|
||||
</fieldset>
|
||||
|
||||
<!-- Dev Mode: a single toggle; everything dev (pool picker + analysis) lives below it. -->
|
||||
<label class="dev-switch" for="dev-mode">
|
||||
<input type="checkbox" id="dev-mode" />
|
||||
|
||||
@@ -9,9 +9,9 @@ def test_build_emits_frontend_baked_json_and_only_referenced_media(tmp_path):
|
||||
result = build_static(
|
||||
out, media,
|
||||
media_base="https://static.benstull.art/",
|
||||
app_path="/human-experience-simulator",
|
||||
app_path="/human-machine-strata",
|
||||
)
|
||||
app_dir = out / "human-experience-simulator"
|
||||
app_dir = out / "human-machine-strata"
|
||||
|
||||
# frontend present under the app path; dev/author surfaces excluded
|
||||
assert (app_dir / "index.html").exists()
|
||||
@@ -25,10 +25,12 @@ def test_build_emits_frontend_baked_json_and_only_referenced_media(tmp_path):
|
||||
assert not (app_dir / "author.html").exists()
|
||||
assert not list(app_dir.glob("review*.html"))
|
||||
|
||||
# apex + no-slash redirect rules
|
||||
# apex + no-slash redirect rules, plus the permanent legacy-path redirect
|
||||
redirects = (out / "_redirects").read_text()
|
||||
assert "/ /human-experience-simulator/ 308" in redirects
|
||||
assert "/human-experience-simulator /human-experience-simulator/ 308" in redirects
|
||||
assert "/ /human-machine-strata/ 308" in redirects
|
||||
assert "/human-machine-strata /human-machine-strata/ 308" in redirects
|
||||
assert "/human-experience-simulator/ /human-machine-strata/ 301" in redirects
|
||||
assert "/human-experience-simulator/* /human-machine-strata/:splat 301" in redirects
|
||||
|
||||
# baked JSON matches the API shape
|
||||
clips = json.loads((app_dir / "clips.json").read_text())
|
||||
|
||||
+20
-6
@@ -59,13 +59,27 @@ def _write_config(app_dir: Path, media_base: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
# The app's original deploy path, kept as a permanent redirect so old links to
|
||||
# /human-experience-simulator/… still land on the renamed /human-machine-strata/….
|
||||
LEGACY_APP_PATH = "human-experience-simulator"
|
||||
|
||||
|
||||
def _write_redirects(out: Path, seg: str) -> None:
|
||||
# Apex → app path, and the no-trailing-slash form → slashed (so RELATIVE asset
|
||||
# urls resolve under the prefix). Cloudflare Pages `_redirects` syntax.
|
||||
(out / "_redirects").write_text(
|
||||
f"/ /{seg}/ 308\n"
|
||||
f"/{seg} /{seg}/ 308\n"
|
||||
)
|
||||
# urls resolve under the prefix). Plus a permanent (301) redirect from the legacy
|
||||
# path to the current one. Cloudflare Pages `_redirects` syntax.
|
||||
lines = [
|
||||
f"/ /{seg}/ 308\n",
|
||||
f"/{seg} /{seg}/ 308\n",
|
||||
]
|
||||
if LEGACY_APP_PATH and LEGACY_APP_PATH != seg:
|
||||
# Exact root (trailing slash) first — the `/*` splat does NOT match the empty
|
||||
# case, so the bare /legacy/ needs its own rule. Then the splat for subpaths,
|
||||
# then the no-trailing-slash form.
|
||||
lines.append(f"/{LEGACY_APP_PATH}/ /{seg}/ 301\n")
|
||||
lines.append(f"/{LEGACY_APP_PATH}/* /{seg}/:splat 301\n")
|
||||
lines.append(f"/{LEGACY_APP_PATH} /{seg}/ 301\n")
|
||||
(out / "_redirects").write_text("".join(lines))
|
||||
|
||||
|
||||
def _write_headers(out: Path, seg: str) -> None:
|
||||
@@ -158,7 +172,7 @@ if __name__ == "__main__":
|
||||
ap.add_argument("--out", default="dist")
|
||||
ap.add_argument("--media-out", default="dist-media")
|
||||
ap.add_argument("--media-base", default="https://static.benstull.art/")
|
||||
ap.add_argument("--app-path", default="/human-experience-simulator")
|
||||
ap.add_argument("--app-path", default="/human-machine-strata")
|
||||
a = ap.parse_args()
|
||||
r = build_static(a.out, a.media_out, media_base=a.media_base, app_path=a.app_path)
|
||||
print(json.dumps(r, indent=2))
|
||||
|
||||
Reference in New Issue
Block a user