diff --git a/.gitignore b/.gitignore index b89ad7e..d3e849e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,35 +1,89 @@ -# Build artifacts -bin/ -node_modules/ +# ============================================================================ +# Build artifacts — generated by `make video`, `make serve`, `nix build` +# ============================================================================ +build/ dist/ -*.min.css -*.min.js +out/ +bin/ -# Editor +# Nix build outputs (symlinks to /nix/store) +result +result-* +result-*.drv +.direnv/ + +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.venv/ +venv/ + +# Node (if/when we add a Tailwind CLI build step) +node_modules/ +.npm/ +.yarn/ + +# ============================================================================ +# Generated runtime assets — the .mp4 in video/ IS tracked, the build/ output +# is not. The static/js/mountain.js bundle is *committed* (regeneratable, but +# useful to have a built version in the repo so the site works without +# ffmpeg + chafa installed). +# ============================================================================ + +# ============================================================================ +# Editor / IDE +# ============================================================================ .vscode/ .idea/ *.swp -.DS_Store +*.swo +*.swn +*~ +# Helix +.helix/ + +# ============================================================================ # OS +# ============================================================================ +.DS_Store Thumbs.db +desktop.ini +# ============================================================================ # Logs +# ============================================================================ *.log npm-debug.log* +yarn-debug.log* +yarn-error.log* +pip-log.txt +pip-log/ -# Env +# ============================================================================ +# Env / secrets (in case a future step needs API keys) +# ============================================================================ .env .env.local +.env.*.local -# Cache +# ============================================================================ +# Caches +# ============================================================================ .cache/ .parcel-cache/ +.eslintcache +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ -# Generated -static/css/site.compiled.css +# ============================================================================ +# Tailwind (if/when we swap the Play CDN for a standalone build) +# ============================================================================ +static/css/dist/ +static/css/*.compiled.css +static/css/tailwind.config.js -# Nix -result -result-* -.direnv/ +video/ diff --git a/Makefile b/Makefile index c09cd2c..e92ab85 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help serve size validate tidy ascii clean +.PHONY: help serve size validate tidy video video-list video-frames video-bundle clean help: ## Show this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ @@ -31,11 +31,36 @@ validate: ## Quick HTML sanity check (counts opening vs closing tags) tidy: ## Run html-tidy on every .html @for f in *.html; do echo "--- $$f ---"; tidy -e -q $$f || true; done -ascii: ## Regenerate the body-rain ASCII art (needs chafa) - @mkdir -p static/ascii - chafa -f symbols -c none -s 240x60 --symbols ascii --animate off \ - static/img/og-image-bg.svg > static/ascii/rain.txt - @echo "Regenerated static/ascii/rain.txt (from static/img/og-image-bg.svg)" +video: ## Convert a video from video/.mp4 into ASCII bundle. Usage: make video NAME=nature1 + @test -n "$(NAME)" || { echo "Usage: make video NAME= (without .mp4)"; exit 1; } + @test -f "video/$(NAME).mp4" || { echo "video/$(NAME).mp4 not found. Available:"; $(MAKE) video-list; exit 1; } + @echo "==> Processing video/$(NAME).mp4" + @$(MAKE) video-frames NAME=$(NAME) --no-print-directory + @$(MAKE) video-bundle NAME=$(NAME) --no-print-directory + @echo "==> Done. static/js/mountain.js is now the ASCII version of $(NAME)." + +video-list: ## List available videos in video/ + @ls -1 video/*.mp4 2>/dev/null | sed 's|video/||; s|\.mp4$$||' || echo " (no videos found)" + +video-frames: ## Extract PNG frames from video/.mp4 and chafa each to ASCII + @test -n "$(NAME)" || { echo "NAME is required"; exit 1; } + @mkdir -p build/$(NAME) + @command -v ffmpeg >/dev/null 2>&1 || \ + { echo "ffmpeg not found. Run via: nix develop -c make video-frames NAME=$(NAME)"; exit 1; } + @command -v chafa >/dev/null 2>&1 || \ + { echo "chafa not found. Run via: nix develop -c make video-frames NAME=$(NAME)"; exit 1; } + @echo " extracting frames at 12 fps from video/$(NAME).mp4" + @ffmpeg -y -loglevel error -i video/$(NAME).mp4 -vf "fps=12,scale=320:-1" build/$(NAME)/frame-%03d.png + @echo " chafa: 80x24 ascii per frame" + @for f in build/$(NAME)/frame-*.png; do \ + chafa -f symbols -c none -s 80x24 --symbols ascii --animate off "$$f" > "$${f%.png}.txt"; \ + done + @echo " rendered $$(($$(ls build/$(NAME)/frame-*.png | wc -l))) PNG + ASCII frames in build/$(NAME)/" + +video-bundle: ## Bundle ASCII frames from build// into static/js/mountain.js + @test -n "$(NAME)" || { echo "NAME is required"; exit 1; } + @python3 tools/build_mountain_js.py build/$(NAME) static/js/mountain.js + @echo " bundled static/js/mountain.js (regenerated from build/$(NAME)/)" clean: ## Remove build artefacts @rm -rf bin/ node_modules/ dist/ diff --git a/README.md b/README.md index bc35865..2592bd4 100644 --- a/README.md +++ b/README.md @@ -36,59 +36,133 @@ python3 -m http.server 8000 The `Makefile` wraps the Python server and a few convenience commands: ```bash -make serve # python3 -m http.server 8000 -make size # report file sizes -make validate # quick HTML syntax check (search for unclosed tags) -make tidy # run html-tidy on every .html (warnings only) -make ascii # regenerate the body-rain ASCII art (needs `chafa`) +make serve # python3 -m http.server 8000 (serves the WORKING TREE — use this while iterating) +make size # report file sizes +make validate # quick HTML syntax check (search for unclosed tags) +make tidy # run html-tidy on every .html (warnings only) +make video NAME=foo # regenerate the ASCII background from video/foo.mp4 ``` -## Animated ASCII body background +> **Note:** `make serve` reads from the working tree (so it picks up the latest +> `static/js/mountain.js` after a `make video`). For a production build, use +> `nix run .#serve` — but note that the Nix build only includes **git-tracked** +> files, so you must `git add` your new `mountain.js` (and any other new files) +> before `nix run .#serve` will pick them up. -The page background is **animated ASCII art** generated by -[`chafa`](https://github.com/hpjansson/chafa) from a stripped-down version -of the Open Graph card (`static/img/og-image-bg.svg` — black background, -grid, the `tui-pages` wordmark, the "A framework for / building TUIs in -Rust." headline, and the subtitle, with the install button cropped out -because chafa was rendering it as a row of `@` symbols). The art is plain -text inside `
` blocks; the animation is pure CSS.
+## Animated ASCII video background
 
-```
-static/ascii/rain.txt     ← 240 × ~50 grid of og-image-bg.svg
-```
+The page background is an **animated chafa ASCII playback of a video**, running
+at 12 fps behind the entire page. `ffmpeg` extracts frames from a video you
+provide; `chafa` converts each frame to an 80 × 24 ASCII grid; a bundler
+(`tools/build_mountain_js.py`) concatenates all grids into a single JS array
+(`static/js/mountain.js`). A tiny cycler (`static/js/mountain-bg.js`) swaps
+`textContent` on a single `
` every ~83 ms.
 
-To re-render after changing the source SVG (you need `chafa` on `$PATH`,
-or run inside `nix develop`):
+### Swap the video
+
+1. Drop a `.mp4` in `video/`:
+
+   ```bash
+   cp ~/Videos/my-clip.mp4 video/myclip.mp4
+   ```
+
+2. Build it into the site (run inside the nix dev shell for ffmpeg + chafa):
+
+   ```bash
+   nix develop -c make video NAME=myclip
+   ```
+
+   This runs three steps:
+   - `make video-frames NAME=myclip` — `ffmpeg` extracts PNGs at 12 fps into `build/myclip/`, then `chafa -s 80x24 --symbols ascii` produces ASCII for each
+   - `make video-bundle NAME=myclip` — bundles all `frame-*.txt` into `static/js/mountain.js`
+   - the wrapper `make video` does both
+
+3. Serve and view:
+
+   ```bash
+   make serve
+   # → http://localhost:8000
+   ```
+
+### Helper targets
 
 ```bash
-make ascii
+make video-list              # show available videos in video/
+make video-frames NAME=foo   # only extract + chafa, skip the bundle
+make video-bundle NAME=foo   # only re-bundle from an existing build/foo/
 ```
 
-The source SVG is committed (`static/img/og-image-bg.svg`) so `make ascii`
-is fully reproducible — no ImageMagick, no cropping step.
+### Tuning
+
+- **Frame rate** — edit `Makefile` `fps=12` in the `video-frames` target. If
+  you change it, also update `TP_MOUNTAIN_FPS` at the bottom of
+  `static/js/mountain.js` (regenerated automatically each `make video`).
+- **ASCII resolution** — `-s 80x24` in the `chafa` call. Wider/higher = larger
+  bundle, more detail.
+- **Bundle size** — the resulting `static/js/mountain.js` is `~1.7 kB × N_FRAMES`
+  (333 kB for a 15 s clip at 12 fps). For long videos, drop `fps` or downscale
+  the source video first.
+```
+       ↓ chafa -s 80x24
+build/mountain/frame-*.txt   intermediate ASCII grids
+       ↓
+static/js/mountain.js        bundled 60-frame array (≈120 kB)
+       ↓ requestAnimationFrame
+DOM   what you actually see, 12 fps
+```
+
+To re-render (you need `chafa` and `python3` on `$PATH`, or use `nix develop`):
+
+```bash
+make mountain          # frames + bundle, end-to-end
+make mountain-frames   # just the PNG + ASCII frames
+make mountain-bundle   # just rebundle the JS array from existing frames
+```
 
 ### How it's wired
 
-- A single `