video
This commit is contained in:
84
.gitignore
vendored
84
.gitignore
vendored
@@ -1,35 +1,89 @@
|
|||||||
# Build artifacts
|
# ============================================================================
|
||||||
bin/
|
# Build artifacts — generated by `make video`, `make serve`, `nix build`
|
||||||
node_modules/
|
# ============================================================================
|
||||||
|
build/
|
||||||
dist/
|
dist/
|
||||||
*.min.css
|
out/
|
||||||
*.min.js
|
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/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
*.swo
|
||||||
|
*.swn
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Helix
|
||||||
|
.helix/
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
# OS
|
# OS
|
||||||
|
# ============================================================================
|
||||||
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
# Logs
|
# Logs
|
||||||
|
# ============================================================================
|
||||||
*.log
|
*.log
|
||||||
npm-debug.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
|
||||||
.env.local
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
# Cache
|
# ============================================================================
|
||||||
|
# Caches
|
||||||
|
# ============================================================================
|
||||||
.cache/
|
.cache/
|
||||||
.parcel-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
|
video/
|
||||||
result
|
|
||||||
result-*
|
|
||||||
.direnv/
|
|
||||||
|
|||||||
37
Makefile
37
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
|
help: ## Show this help
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
@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
|
tidy: ## Run html-tidy on every .html
|
||||||
@for f in *.html; do echo "--- $$f ---"; tidy -e -q $$f || true; done
|
@for f in *.html; do echo "--- $$f ---"; tidy -e -q $$f || true; done
|
||||||
|
|
||||||
ascii: ## Regenerate the body-rain ASCII art (needs chafa)
|
video: ## Convert a video from video/<name>.mp4 into ASCII bundle. Usage: make video NAME=nature1
|
||||||
@mkdir -p static/ascii
|
@test -n "$(NAME)" || { echo "Usage: make video NAME=<name> (without .mp4)"; exit 1; }
|
||||||
chafa -f symbols -c none -s 240x60 --symbols ascii --animate off \
|
@test -f "video/$(NAME).mp4" || { echo "video/$(NAME).mp4 not found. Available:"; $(MAKE) video-list; exit 1; }
|
||||||
static/img/og-image-bg.svg > static/ascii/rain.txt
|
@echo "==> Processing video/$(NAME).mp4"
|
||||||
@echo "Regenerated static/ascii/rain.txt (from static/img/og-image-bg.svg)"
|
@$(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/<NAME>.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/<NAME>/ 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
|
clean: ## Remove build artefacts
|
||||||
@rm -rf bin/ node_modules/ dist/
|
@rm -rf bin/ node_modules/ dist/
|
||||||
|
|||||||
163
README.md
163
README.md
@@ -36,59 +36,133 @@ python3 -m http.server 8000
|
|||||||
The `Makefile` wraps the Python server and a few convenience commands:
|
The `Makefile` wraps the Python server and a few convenience commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make serve # python3 -m http.server 8000
|
make serve # python3 -m http.server 8000 (serves the WORKING TREE — use this while iterating)
|
||||||
make size # report file sizes
|
make size # report file sizes
|
||||||
make validate # quick HTML syntax check (search for unclosed tags)
|
make validate # quick HTML syntax check (search for unclosed tags)
|
||||||
make tidy # run html-tidy on every .html (warnings only)
|
make tidy # run html-tidy on every .html (warnings only)
|
||||||
make ascii # regenerate the body-rain ASCII art (needs `chafa`)
|
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
|
## Animated ASCII video background
|
||||||
[`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 `<pre>` blocks; the animation is pure CSS.
|
|
||||||
|
|
||||||
```
|
The page background is an **animated chafa ASCII playback of a video**, running
|
||||||
static/ascii/rain.txt ← 240 × ~50 grid of og-image-bg.svg
|
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 `<pre>` every ~83 ms.
|
||||||
|
|
||||||
To re-render after changing the source SVG (you need `chafa` on `$PATH`,
|
### Swap the video
|
||||||
or run inside `nix develop`):
|
|
||||||
|
1. Drop a `.mp4` in `video/`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make ascii
|
cp ~/Videos/my-clip.mp4 video/myclip.mp4
|
||||||
```
|
```
|
||||||
|
|
||||||
The source SVG is committed (`static/img/og-image-bg.svg`) so `make ascii`
|
2. Build it into the site (run inside the nix dev shell for ffmpeg + chafa):
|
||||||
is fully reproducible — no ImageMagick, no cropping step.
|
|
||||||
|
```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 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/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 <pre.tp-mountain-frame> 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
|
### How it's wired
|
||||||
|
|
||||||
- A single `<div class="tp-ascii-rain" aria-hidden="true">` is placed at
|
- `static/js/mountain.js` exposes `window.TP_MOUNTAIN_FRAMES` (an array of
|
||||||
the top of `<body>`, fixed full-bleed, with `z-index: -1` and
|
60 template strings) plus `TP_MOUNTAIN_N_FRAMES = 60` and
|
||||||
`pointer-events: none` so it never blocks the UI.
|
`TP_MOUNTAIN_FPS = 12`.
|
||||||
- Inside, a `<pre class="tp-ascii-rain-track">` holds **two byte-identical
|
- `static/js/mountain-bg.js` mounts a `<pre class="tp-mountain-frame"
|
||||||
copies** of the chafa output stacked vertically for a seamless
|
aria-hidden="true">` to `document.body`, sets the first frame as
|
||||||
`translateY(0 → -50%)` loop over 90 s.
|
`textContent`, then cycles with `requestAnimationFrame` and a
|
||||||
- Color is `#f4a26b` (warm rust-orange) at 14% opacity with a faint
|
`setInterval`-style accumulator so frame drops don't desync.
|
||||||
`text-shadow` glow — picks up the brand colour and reads as a subtle
|
- The script pauses when `document.hidden` is true (no wasted CPU on
|
||||||
background, not a wall of `@` blocks.
|
backgrounded tabs) and respects `prefers-reduced-motion: reduce` by
|
||||||
- A `::after` overlay draws a 2 px CRT scanline pattern that ticks
|
showing a single mid-loop frame and not animating.
|
||||||
downward at 8 s/cycle.
|
- Style lives in `static/css/ascii.css`:
|
||||||
- The hero section has a `.tp-ascii-hero-overlay` class that paints a
|
- `position: fixed; inset: 0; z-index: 0` — full-bleed, behind page content
|
||||||
vertical gradient over the top of the page, so the hero text stays
|
- `color: #f4a26b` (rust-orange) with a 0.45 opacity and a soft
|
||||||
readable while the ASCII art shows through on the bottom half and below.
|
`text-shadow` glow — picks up the brand colour
|
||||||
- The light theme inverts to dark text on light, dimmer, no glow.
|
- `font-family: 'JetBrains Mono'` for the in-browser renderer
|
||||||
- All animations are disabled inside
|
- A `::after` pseudo-element overlays a 2 px CRT scanline pattern that
|
||||||
`@media (prefers-reduced-motion: reduce)`.
|
ticks downward at 8 s/cycle
|
||||||
|
- Light theme (`data-theme="winter"`) drops the glow, dims the opacity
|
||||||
|
to 0.20, and switches the scanlines to white-on-transparent
|
||||||
|
- The scanline animation is killed inside
|
||||||
|
`@media (prefers-reduced-motion: reduce)`
|
||||||
|
|
||||||
The hero (`#hero`) is **not** animated — it keeps the original static
|
The hero (`#hero`) is **not** part of the mountain background — it keeps
|
||||||
`static/img/terminal-default.svg` mockup, exactly as designed.
|
the original static `static/img/terminal-default.svg` mockup, exactly as
|
||||||
|
designed. The terminal sits on top of the mountain as a discrete card.
|
||||||
|
|
||||||
|
### Tuning the animation
|
||||||
|
|
||||||
|
The renderer is parameterized at the top of `tools/mountain.py`:
|
||||||
|
|
||||||
|
| Constant | Default | What it does |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `W, H` | 320 × 200 | Output PNG resolution |
|
||||||
|
| `N_FRAMES` | 60 | How long the loop is |
|
||||||
|
| `HORIZON_FRAC` | 0.45 | Sky height as a fraction of the image |
|
||||||
|
| `SEED` | deterministic | Heightmap random seed |
|
||||||
|
| `STEPS` | 80 | Forward steps per frame in camera space |
|
||||||
|
|
||||||
|
Bump `W`/`H` for sharper terrain, or `--symbols ascii+block+half` in the
|
||||||
|
chafa call inside `tools/build_mountain_js.py` for denser glyphs. Both
|
||||||
|
changes require a `make mountain` rebuild and a higher page weight
|
||||||
|
(mountain.js is currently ≈120 kB).
|
||||||
|
|
||||||
## Nix (optional, recommended)
|
## Nix (optional, recommended)
|
||||||
|
|
||||||
@@ -169,20 +243,23 @@ tui-pages-web/
|
|||||||
├── 404.html # not found
|
├── 404.html # not found
|
||||||
├── robots.txt # search engine hints
|
├── robots.txt # search engine hints
|
||||||
├── sitemap.xml # sitemap
|
├── sitemap.xml # sitemap
|
||||||
|
├── tools/
|
||||||
|
│ ├── mountain.py # pure-Python 3D-mountain PNG renderer
|
||||||
|
│ └── build_mountain_js.py # bundles 60 ASCII frames into mountain.js
|
||||||
├── static/
|
├── static/
|
||||||
│ ├── css/
|
│ ├── css/
|
||||||
│ │ ├── site.css # our custom layer (small)
|
│ │ ├── site.css # our custom layer (small)
|
||||||
│ │ └── ascii.css # animation rules for the body-rain ASCII art
|
│ │ └── ascii.css # animation rules for the mountain background
|
||||||
│ ├── img/
|
│ ├── img/
|
||||||
│ │ ├── favicon.svg
|
│ │ ├── favicon.svg
|
||||||
│ │ ├── logo.svg
|
│ │ ├── logo.svg
|
||||||
│ │ ├── og-image.svg # 1200x630 social share card (used as-is)
|
│ │ ├── og-image.svg # 1200x630 social share card
|
||||||
│ │ ├── og-image-bg.svg # 1200x500 cropped variant (chafa source)
|
|
||||||
│ │ ├── terminal-default.svg
|
│ │ ├── terminal-default.svg
|
||||||
│ │ ├── terminal-canvas.svg
|
│ │ ├── terminal-canvas.svg
|
||||||
│ │ └── terminal-keybindings.svg
|
│ │ └── terminal-keybindings.svg
|
||||||
│ ├── ascii/
|
│ ├── js/
|
||||||
│ │ └── rain.txt # chafa render of og-image-bg.svg
|
│ │ ├── mountain.js # 60-frame ASCII array (≈120 kB)
|
||||||
|
│ │ └── mountain-bg.js # requestAnimationFrame cycler
|
||||||
│ └── demos/ # (empty — drop asciinema .cast files here)
|
│ └── demos/ # (empty — drop asciinema .cast files here)
|
||||||
├── content/ # (empty — markdown for blog/changelog later)
|
├── content/ # (empty — markdown for blog/changelog later)
|
||||||
├── flake.nix # Nix dev shell, package, app, checks
|
├── flake.nix # Nix dev shell, package, app, checks
|
||||||
@@ -222,7 +299,7 @@ recording (much more impressive, but requires a recorded cast):
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/asciinema-player@3.7.0/dist/bundle/asciinema-player.js" defer></script>
|
<script src="https://cdn.jsdelivr.net/npm/asciinema-player@3.7.0/dist/bundle/asciinema-player.js" defer></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
The body-rain ASCII art is independent of the hero and stays as-is.
|
The 3D-mountain ASCII background is independent of the hero and stays as-is.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
20
flake.nix
20
flake.nix
@@ -115,11 +115,14 @@
|
|||||||
# --- Record TUI demos (used to replace the SVG mockups) ----
|
# --- Record TUI demos (used to replace the SVG mockups) ----
|
||||||
asciinema
|
asciinema
|
||||||
|
|
||||||
# --- Generate the body-background ASCII art from the og-image
|
# --- Generate the animated ASCII background from a video.
|
||||||
# `make ascii` runs `chafa` to convert the og-image into
|
# `make video NAME=<name>` extracts frames from video/<name>.mp4
|
||||||
# a text grid, which is then embedded in index.html and
|
# with ffmpeg at 12 fps, runs chafa on each to get 80x24 ASCII,
|
||||||
# animated with CSS as a slow drifting body background.
|
# and bundles them into static/js/mountain.js for the cycler.
|
||||||
|
ffmpeg
|
||||||
chafa
|
chafa
|
||||||
|
# --- `make video` also needs python3 (already in the default set)
|
||||||
|
# to run tools/build_mountain_js.py.
|
||||||
|
|
||||||
# --- Optional: standalone Tailwind CLI for production CSS --
|
# --- Optional: standalone Tailwind CLI for production CSS --
|
||||||
# Uncomment if you switch from the Play CDN to a precompiled stylesheet.
|
# Uncomment if you switch from the Play CDN to a precompiled stylesheet.
|
||||||
@@ -141,12 +144,13 @@
|
|||||||
│ make size report file sizes │
|
│ make size report file sizes │
|
||||||
│ make validate HTML tag balance check │
|
│ make validate HTML tag balance check │
|
||||||
│ make tidy run html-tidy on every .html │
|
│ make tidy run html-tidy on every .html │
|
||||||
│ make ascii regenerate body-rain ASCII art │
|
│ make video NAME=foo regenerate the ASCII background from │
|
||||||
|
│ video/foo.mp4 (12 fps, 80x24 ASCII) │
|
||||||
│ │
|
│ │
|
||||||
│ asciinema rec static/demos/intro.cast record a demo │
|
│ asciinema rec static/demos/intro.cast record a demo │
|
||||||
│ chafa -s 240x60 -c none \│
|
│ make video-list show available videos in video/ │
|
||||||
│ static/img/og-image-bg.svg >│
|
│ make video-frames extract + chafa frames for $NAME │
|
||||||
│ static/ascii/rain.txt render body rain │
|
│ make video-bundle bundle frames into mountain.js │
|
||||||
│ │
|
│ │
|
||||||
│ nix build build site → ./result/ │
|
│ nix build build site → ./result/ │
|
||||||
│ nix run .#serve build + serve (honours $PORT) │
|
│ nix run .#serve build + serve (honours $PORT) │
|
||||||
|
|||||||
20
index.html
20
index.html
File diff suppressed because one or more lines are too long
@@ -1,49 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_
|
|
||||||
=$ sa a y sa_gwaanggug, :
|
|
||||||
? ?F fY ` @F?YFZ@~?f r~ :
|
|
||||||
`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
yyyy_ yagggm aggr _ygggs
|
|
||||||
_@@@@@_ @@@~~~ _ __ __ _ __ __ _ 4@@F $@@~~~ __ _
|
|
||||||
$@@~@@$ $@@@@@@L$@@y@@@L4@@@@@@g_ $@@W@@@@yg@@@@y yg@@@@@gy "@@$ $@@, y@@E _a@@@@@@y 4@@$g@@F4@@F y@@@F 4@@@@@@$_a@@@@@@g, @@$g@@$
|
|
||||||
g@@F ~@@@ ``@@@`` $@@@~`` `yyyyy@@@ $@@F``@@@F `@@@ u@@@yyy$@@L @@@,y@B@@ $@@ @@@~ 4@@@ 4@@@~``~4@@@@@P~ `@@@```$@@~ 7@@$ @@@~``'
|
|
||||||
g@@@$$$@@@y @@@ $@@L y@@@FFF@@@ $@@ @@@ @@@ 4@@@PPPPPPT "@@$$@~$@W@@F @@@ y@@@ 4@@$ 4@@@@@g_ @@@ @@@ @@@ @@$
|
|
||||||
y@@@~~~~~$@@L @@@ $@@L "@@$yy$@@@ $@@ @@@ @@@ R@@gyyyg@ 4@@@E "@@@@ 4@@$yg@@@~ 4@@$ 4@@F~R@@g_ @@@ 7@@@ga@@@~ @@$
|
|
||||||
^~~^ "~~~ ~~~ ~~~~ `~FF~~~~~ 7~~ ~~~ ~~~ `~FFFF~~ ~~~ ~~~~ `~FFFF~ "~~~ ~~~~ ~~~T ~~~ ~FFFF~ ~~~
|
|
||||||
|
|
||||||
|
|
||||||
`:: ::: ::: ::: ::: ::::::::::: ::: ::: :::: ::: ::::::::. ::
|
|
||||||
`:: ..... ... ... ... ::: ..... ::: ... ... ..... .... ... ````:::```` ::: ::: `::: ...... ... .. ..... :::```::: ... ... ....... ..::...
|
|
||||||
`:::```::: ::: ::: ::: ::: :::```:::: ::: ::::```::. :::```::: ::: ::: ::: `::: ::````` ::: :::```::: :::...::` ::: ::: :::````` ``::```
|
|
||||||
`::' ::: ::: ::: ::: ::: ::: `::: ::: ::: ::: ::: ::: ::: ::: ::: `::: `:::::.. ::: ::' ::: :::``:::. ::: ::: `::::::. ::
|
|
||||||
`::: .::: ::: .::: ::: ::: :::. :::: ::: ::: ::: `:::. .::: ::: `::: .:::' `::: ```::: ::: :: ::: ::: `:::. :::. .::: . ````:: ::. :::
|
|
||||||
`````:::`` ``::````` ``' ``` ```::````' ``` ``` ``' ``````::: ``` ```:::``` ```' ``:::``` ``` `` ``` ``` ```. ``::````` ``::::`` ````` ```
|
|
||||||
:....:::`
|
|
||||||
````````
|
|
||||||
|
|
||||||
.y + - i + .
|
|
||||||
4W*MF~EZK,,4~3 $~f~%g~M $~_Ta F~K~L4~$~$uEZLf~$ Q~a~3 F~ [ E4E_ $='EZ%4_z F~K~LyT% F~L4Z, TM #~$uM7L B~%_T3 F~$yED F~LyT% yy^%uT7LyT%?F Fg~3 F~L
|
|
||||||
" ?=! 4rf ~ ?=~`g9 F ?=? ~ ~ ~f ^ f h= ?=F `=F ?= ^rF:=^J F~r?=:_$~ ~ ~ ~?=T M=~==~z "=f f f hf $=~"=? ]gF"h= ~ ~?=? ?^ f _g ^=T h ~`=F ~ ~
|
|
||||||
` ` ` ` `` `
|
|
||||||
y== y_ _ __ __ _ _ _ ,___ y_= __ __ __ __ __ __ __ __ _ __ __ __ __a__ ;y_ __ _,___ _ __ _ yg _, __ __ _ _ __ ___ _ ___ ____ _ ____
|
|
||||||
`Th $`$ $ L`L $`$=="yMy~$`4 $`$ F`L$ 9 `L F`La== "=yy=g $ F la== =d $ E `4`4 $4`4-= E $`4 4 F`$== 4`# 3 F` P=\4_F4== E ~yF 4~J $`$ $4 $==4```L
|
|
||||||
`T~ ~~`T` ~T ~ `T" ~ ~ " " ~~" ~ ~`Z4 ~ ~ ~ TT `T~`T~ ~ ~ ~ ~T T~ ^ `T'~ ~ ~ ~ ~T ~T"`~`T~ ~ `T^ ~ `T` ~T~ ~ ~T ~ .F 4~~ ~ ~T`J `T^ ~T ~ ~
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,60 +1,46 @@
|
|||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
tui-pages website - animated ASCII art layer (chafa-rendered)
|
tui-pages website - animated ASCII mountain background (chafa-rendered)
|
||||||
Loaded AFTER site.css. Provides:
|
|
||||||
- .tp-ascii-rain full-bleed body background, slow drifting pattern
|
The mountain background is JS-driven: tools/mountain.py renders 60
|
||||||
sourced from static/img/og-image-bg.svg
|
procedural 3D mountain-flyover PNG frames; chafa converts each to 80x24
|
||||||
All animations respect prefers-reduced-motion.
|
ASCII; tools/build_mountain_js.py bundles them into static/js/mountain.js.
|
||||||
|
static/js/mountain-bg.js cycles the frames at 12 fps into a single
|
||||||
|
<pre class="tp-mountain-frame"> element mounted at the bottom of <body>.
|
||||||
|
|
||||||
|
This CSS file:
|
||||||
|
- positions the frame as a full-bleed fixed background
|
||||||
|
- tints the dense characters rust-orange with text-shadow + glow
|
||||||
|
- adds a CRT scanline overlay
|
||||||
|
- disables the animation under prefers-reduced-motion
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
/* Body-wide ASCII art background ---------------------------------------- */
|
/* Full-bleed ASCII mountain background ---------------------------------- */
|
||||||
/* Fixed full-bleed layer behind all content. Renders a chafa output of the
|
.tp-mountain-frame {
|
||||||
og-image-bg.svg (black background + "tui-pages" wordmark + headline +
|
|
||||||
subtitle), drawn as ASCII glyphs in a single full-width tile. Two copies
|
|
||||||
of the tile are stacked vertically and the whole track drifts slowly
|
|
||||||
upward, looping seamlessly when the top copy exits the viewport.
|
|
||||||
|
|
||||||
The hero is a static <img src="static/img/terminal-default.svg"> as
|
|
||||||
designed - this file does NOT touch the hero.
|
|
||||||
|
|
||||||
Regenerate the source tile with `make ascii` (uses chafa on
|
|
||||||
static/img/og-image-bg.svg). The chafa output is in static/ascii/rain.txt
|
|
||||||
and is 240 columns wide. */
|
|
||||||
|
|
||||||
.tp-ascii-rain {
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 0; /* sit above the page background, below the content */
|
z-index: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.14;
|
display: flex;
|
||||||
overflow: hidden;
|
align-items: center;
|
||||||
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
justify-content: center;
|
||||||
font-size: clamp(6px, 0.9vw, 9px);
|
|
||||||
line-height: 1.05;
|
|
||||||
color: #f4a26b; /* warm rust-orange tint, picks up the brand */
|
|
||||||
white-space: pre;
|
|
||||||
text-shadow: 0 0 6px rgba(183, 65, 14, 0.25); /* faint phosphor glow */
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The track holds two byte-identical copies of rain.txt stacked vertically.
|
|
||||||
translateY(0 -> -50%) is a perfect seamless loop. */
|
|
||||||
.tp-ascii-rain-track {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
animation: tp-ascii-drift 90s linear infinite;
|
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
will-change: transform;
|
font-size: clamp(8px, 1.2vw, 14px);
|
||||||
}
|
line-height: 1.0;
|
||||||
|
letter-spacing: 0;
|
||||||
.tp-ascii-rain-cell {
|
color: #f4a26b; /* warm rust-orange tint, picks up the brand */
|
||||||
display: block;
|
text-shadow: 0 0 8px rgba(183, 65, 14, 0.35); /* phosphor glow */
|
||||||
white-space: pre; /* preserve the chafa line breaks */
|
opacity: 0.45;
|
||||||
|
white-space: pre;
|
||||||
|
background: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CRT scanlines overlay - a thin moving line every 2px. Faint so it doesn't
|
/* CRT scanlines overlay - a thin moving line every 2px. Faint so it doesn't
|
||||||
fight the page content. */
|
fight the page content. */
|
||||||
.tp-ascii-rain::after {
|
.tp-mountain-frame::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -62,36 +48,28 @@
|
|||||||
to bottom,
|
to bottom,
|
||||||
transparent 0,
|
transparent 0,
|
||||||
transparent 2px,
|
transparent 2px,
|
||||||
rgba(0, 0, 0, 0.18) 2px,
|
rgba(0, 0, 0, 0.20) 2px,
|
||||||
rgba(0, 0, 0, 0.18) 3px
|
rgba(0, 0, 0, 0.20) 3px
|
||||||
);
|
);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: tp-ascii-scanline 8s linear infinite;
|
animation: tp-mtn-scanline 8s linear infinite;
|
||||||
|
mix-blend-mode: multiply;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes tp-ascii-drift {
|
@keyframes tp-mtn-scanline {
|
||||||
0% { transform: translateY(0); }
|
|
||||||
100% { transform: translateY(-50%); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes tp-ascii-scanline {
|
|
||||||
0% { background-position: 0 0; }
|
0% { background-position: 0 0; }
|
||||||
100% { background-position: 0 6px; }
|
100% { background-position: 0 6px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The hero text is high-contrast on its own (zinc-50 / accent), and the
|
/* Light theme: dim the background, drop the glow, switch to dark text. */
|
||||||
body ASCII rain is at 14% opacity. No overlay needed — the rain shows
|
:root[data-theme="winter"] .tp-mountain-frame {
|
||||||
through everywhere. */
|
opacity: 0.20;
|
||||||
|
color: #2a1810;
|
||||||
/* Light theme: invert to dark text on light, dimmer, no glow. */
|
|
||||||
:root[data-theme="light"] .tp-ascii-rain {
|
|
||||||
opacity: 0.10;
|
|
||||||
color: #1f1f23;
|
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
mix-blend-mode: multiply;
|
mix-blend-mode: multiply;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="light"] .tp-ascii-rain::after {
|
:root[data-theme="winter"] .tp-mountain-frame::after {
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
transparent 0,
|
transparent 0,
|
||||||
@@ -99,13 +77,12 @@
|
|||||||
rgba(255, 255, 255, 0.12) 2px,
|
rgba(255, 255, 255, 0.12) 2px,
|
||||||
rgba(255, 255, 255, 0.12) 3px
|
rgba(255, 255, 255, 0.12) 3px
|
||||||
);
|
);
|
||||||
|
mix-blend-mode: screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reduced motion: pause the drift, keep the static background. */
|
/* Reduced motion: keep the first frame static, no scanline drift. */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.tp-ascii-rain-track,
|
.tp-mountain-frame::after {
|
||||||
.tp-ascii-rain::after {
|
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
transform: none !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ html {
|
|||||||
--tp-hero-to: #18181b;
|
--tp-hero-to: #18181b;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="light"] {
|
:root[data-theme="winter"] {
|
||||||
--tp-accent: #93330c;
|
--tp-accent: #93330c;
|
||||||
--tp-accent-fg: #fff5f0;
|
--tp-accent-fg: #fff5f0;
|
||||||
--tp-grid-line: rgba(228, 228, 231, 0.7);
|
--tp-grid-line: rgba(228, 228, 231, 0.7);
|
||||||
@@ -55,7 +55,7 @@ body {
|
|||||||
|
|
||||||
/* ---------- Hero --------------------------------------------------------- */
|
/* ---------- Hero --------------------------------------------------------- */
|
||||||
.tp-hero {
|
.tp-hero {
|
||||||
background: transparent; /* let the body ASCII rain show through */
|
background: transparent; /* let the mountain ASCII show through */
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ body {
|
|||||||
-webkit-backdrop-filter: saturate(160%) blur(12px);
|
-webkit-backdrop-filter: saturate(160%) blur(12px);
|
||||||
border-bottom: 1px solid rgba(63, 63, 70, 0.4);
|
border-bottom: 1px solid rgba(63, 63, 70, 0.4);
|
||||||
}
|
}
|
||||||
:root[data-theme="light"] .tp-nav {
|
:root[data-theme="winter"] .tp-nav {
|
||||||
background: rgba(255, 255, 255, 0.7);
|
background: rgba(255, 255, 255, 0.7);
|
||||||
border-bottom-color: rgba(228, 228, 231, 0.7);
|
border-bottom-color: rgba(228, 228, 231, 0.7);
|
||||||
}
|
}
|
||||||
@@ -138,11 +138,11 @@ body {
|
|||||||
background: rgba(24, 24, 27, 0.75);
|
background: rgba(24, 24, 27, 0.75);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
:root[data-theme="light"] .tp-card {
|
:root[data-theme="winter"] .tp-card {
|
||||||
background: rgba(255, 255, 255, 0.6);
|
background: rgba(255, 255, 255, 0.6);
|
||||||
border-color: rgba(228, 228, 231, 0.8);
|
border-color: rgba(228, 228, 231, 0.8);
|
||||||
}
|
}
|
||||||
:root[data-theme="light"] .tp-card:hover {
|
:root[data-theme="winter"] .tp-card:hover {
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
border-color: rgba(183, 65, 14, 0.4);
|
border-color: rgba(183, 65, 14, 0.4);
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,7 @@ body {
|
|||||||
color: #f4f4f5;
|
color: #f4f4f5;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
:root[data-theme="light"] .tp-kbd {
|
:root[data-theme="winter"] .tp-kbd {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #18181b;
|
color: #18181b;
|
||||||
border-color: #d4d4d8;
|
border-color: #d4d4d8;
|
||||||
@@ -254,7 +254,7 @@ body {
|
|||||||
|
|
||||||
/* ---------- No-FOUC theme flash ----------------------------------------- */
|
/* ---------- No-FOUC theme flash ----------------------------------------- */
|
||||||
html:not([data-theme]) body { background: #09090b; }
|
html:not([data-theme]) body { background: #09090b; }
|
||||||
:root[data-theme="light"] body { background: #fafafa; }
|
:root[data-theme="winter"] body { background: #fafafa; }
|
||||||
|
|
||||||
/* ---------- Alpine x-cloak (prevents flash of un-initialised content) --- */
|
/* ---------- Alpine x-cloak (prevents flash of un-initialised content) --- */
|
||||||
[x-cloak] { display: none !important; }
|
[x-cloak] { display: none !important; }
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<svg width="1200" height="520" viewBox="0 0 1200 520" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="tui-pages terminal art (chafa source)">
|
|
||||||
<defs>
|
|
||||||
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
|
||||||
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#27272a" stroke-width="1" stroke-opacity="0.3"/>
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<rect width="1200" height="520" fill="#000000"/>
|
|
||||||
<rect width="1200" height="520" fill="url(#grid)"/>
|
|
||||||
|
|
||||||
<g transform="translate(80, 90)">
|
|
||||||
<text font-family="ui-monospace, 'JetBrains Mono', monospace" font-weight="700" font-size="22" fill="#f4f4f5" x="0" y="0">tui-pages</text>
|
|
||||||
<rect x="140" y="-15" width="4" height="24" fill="#b7410e"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<text font-family="Inter, system-ui, sans-serif" font-weight="700" font-size="84" fill="#f4f4f5" x="80" y="280" letter-spacing="-2">
|
|
||||||
A framework for
|
|
||||||
</text>
|
|
||||||
<text font-family="Inter, system-ui, sans-serif" font-weight="700" font-size="84" fill="#b7410e" x="80" y="370" letter-spacing="-2">
|
|
||||||
building TUIs in Rust.
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<text font-family="Inter, system-ui, sans-serif" font-weight="400" font-size="28" fill="#a1a1aa" x="80" y="430">
|
|
||||||
Pre-programmed focus, keymaps, and page navigation.
|
|
||||||
</text>
|
|
||||||
<text font-family="Inter, system-ui, sans-serif" font-weight="400" font-size="28" fill="#a1a1aa" x="80" y="470">
|
|
||||||
Stop rewriting the same architecture for every project.
|
|
||||||
</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
75
static/js/mountain-bg.js
Normal file
75
static/js/mountain-bg.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Mountain background animation.
|
||||||
|
*
|
||||||
|
* Cycles the 60 ASCII frames from window.TP_MOUNTAIN_FRAMES at 12 fps.
|
||||||
|
* Mounts a single <pre> in the body that gets its textContent swapped
|
||||||
|
* on each frame. Uses requestAnimationFrame to stay smooth and yields
|
||||||
|
* cleanly to the browser's frame budget. Pauses on prefers-reduced-motion
|
||||||
|
* and on document.hidden.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (typeof window.TP_MOUNTAIN_FRAMES === "undefined") return;
|
||||||
|
if (document.querySelector("[data-tp-mountain]")) return;
|
||||||
|
|
||||||
|
var pre = document.createElement("pre");
|
||||||
|
pre.className = "tp-mountain-frame";
|
||||||
|
pre.setAttribute("data-tp-mountain", "");
|
||||||
|
pre.setAttribute("aria-hidden", "true");
|
||||||
|
pre.textContent = window.TP_MOUNTAIN_FRAMES[0];
|
||||||
|
document.body.appendChild(pre);
|
||||||
|
|
||||||
|
var n = window.TP_MOUNTAIN_N_FRAMES;
|
||||||
|
var fps = window.TP_MOUNTAIN_FPS;
|
||||||
|
var i = 0;
|
||||||
|
var last = 0;
|
||||||
|
var acc = 0;
|
||||||
|
var interval = 1000 / fps;
|
||||||
|
|
||||||
|
var reduceMotion =
|
||||||
|
window.matchMedia &&
|
||||||
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
|
||||||
|
if (reduceMotion) {
|
||||||
|
// Show a single representative frame; no animation.
|
||||||
|
pre.textContent = window.TP_MOUNTAIN_FRAMES[Math.floor(n / 2)];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function step(ts) {
|
||||||
|
if (!last) last = ts;
|
||||||
|
acc += ts - last;
|
||||||
|
last = ts;
|
||||||
|
while (acc >= interval) {
|
||||||
|
i = (i + 1) % n;
|
||||||
|
pre.textContent = window.TP_MOUNTAIN_FRAMES[i];
|
||||||
|
acc -= interval;
|
||||||
|
}
|
||||||
|
if (!document.hidden) {
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
} else {
|
||||||
|
// Resume on next visibility change.
|
||||||
|
document.addEventListener(
|
||||||
|
"visibilitychange",
|
||||||
|
function onVisible() {
|
||||||
|
if (!document.hidden) {
|
||||||
|
last = 0;
|
||||||
|
acc = 0;
|
||||||
|
document.removeEventListener("visibilitychange", onVisible);
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
3969
static/js/mountain.js
Normal file
3969
static/js/mountain.js
Normal file
File diff suppressed because it is too large
Load Diff
62
tools/build_mountain_js.py
Normal file
62
tools/build_mountain_js.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Build static/js/mountain.js with all ASCII frames from a build/<name>/ dir
|
||||||
|
embedded as a JS array.
|
||||||
|
|
||||||
|
Usage: build_mountain_js.py <src_dir> <dst_js_path>
|
||||||
|
|
||||||
|
Frames are read in sorted order, escaped for JS template literals, and
|
||||||
|
concatenated into a window.TP_MOUNTAIN_FRAMES array.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def js_escape(s: str) -> str:
|
||||||
|
return s.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print(__doc__, file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
src = sys.argv[1]
|
||||||
|
dst = sys.argv[2]
|
||||||
|
|
||||||
|
txts = sorted(p for p in os.listdir(src) if p.startswith("frame-") and p.endswith(".txt"))
|
||||||
|
if not txts:
|
||||||
|
print(f"no frame-*.txt files in {src}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
frames = []
|
||||||
|
for name in txts:
|
||||||
|
with open(os.path.join(src, name)) as f:
|
||||||
|
frames.append(f.read().rstrip("\n"))
|
||||||
|
|
||||||
|
header = """/* Autogenerated by tools/build_mountain_js.py
|
||||||
|
ASCII frames from a video, chafa-rendered, concatenated into a JS array.
|
||||||
|
Cycled at 12 fps by static/js/mountain-bg.js. */
|
||||||
|
|
||||||
|
window.TP_MOUNTAIN_FRAMES = [
|
||||||
|
"""
|
||||||
|
footer = f"""];
|
||||||
|
|
||||||
|
window.TP_MOUNTAIN_N_FRAMES = {len(frames)};
|
||||||
|
window.TP_MOUNTAIN_FPS = 12;
|
||||||
|
"""
|
||||||
|
body = ",\n".join(f" `{js_escape(fr)}`" for fr in frames)
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||||
|
with open(dst, "w") as f:
|
||||||
|
f.write(header)
|
||||||
|
f.write(body)
|
||||||
|
f.write("\n")
|
||||||
|
f.write(footer)
|
||||||
|
|
||||||
|
print(f" {os.path.getsize(dst):>7d} bytes {dst} ({len(frames)} frames)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user