video
This commit is contained in:
169
README.md
169
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 `<pre>` 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 `<pre>` 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 <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
|
||||
|
||||
- A single `<div class="tp-ascii-rain" aria-hidden="true">` is placed at
|
||||
the top of `<body>`, fixed full-bleed, with `z-index: -1` and
|
||||
`pointer-events: none` so it never blocks the UI.
|
||||
- Inside, a `<pre class="tp-ascii-rain-track">` holds **two byte-identical
|
||||
copies** of the chafa output stacked vertically for a seamless
|
||||
`translateY(0 → -50%)` loop over 90 s.
|
||||
- Color is `#f4a26b` (warm rust-orange) at 14% opacity with a faint
|
||||
`text-shadow` glow — picks up the brand colour and reads as a subtle
|
||||
background, not a wall of `@` blocks.
|
||||
- A `::after` overlay draws a 2 px CRT scanline pattern that ticks
|
||||
downward at 8 s/cycle.
|
||||
- The hero section has a `.tp-ascii-hero-overlay` class that paints a
|
||||
vertical gradient over the top of the page, so the hero text stays
|
||||
readable while the ASCII art shows through on the bottom half and below.
|
||||
- The light theme inverts to dark text on light, dimmer, no glow.
|
||||
- All animations are disabled inside
|
||||
`@media (prefers-reduced-motion: reduce)`.
|
||||
- `static/js/mountain.js` exposes `window.TP_MOUNTAIN_FRAMES` (an array of
|
||||
60 template strings) plus `TP_MOUNTAIN_N_FRAMES = 60` and
|
||||
`TP_MOUNTAIN_FPS = 12`.
|
||||
- `static/js/mountain-bg.js` mounts a `<pre class="tp-mountain-frame"
|
||||
aria-hidden="true">` to `document.body`, sets the first frame as
|
||||
`textContent`, then cycles with `requestAnimationFrame` and a
|
||||
`setInterval`-style accumulator so frame drops don't desync.
|
||||
- The script pauses when `document.hidden` is true (no wasted CPU on
|
||||
backgrounded tabs) and respects `prefers-reduced-motion: reduce` by
|
||||
showing a single mid-loop frame and not animating.
|
||||
- Style lives in `static/css/ascii.css`:
|
||||
- `position: fixed; inset: 0; z-index: 0` — full-bleed, behind page content
|
||||
- `color: #f4a26b` (rust-orange) with a 0.45 opacity and a soft
|
||||
`text-shadow` glow — picks up the brand colour
|
||||
- `font-family: 'JetBrains Mono'` for the in-browser renderer
|
||||
- A `::after` pseudo-element overlays a 2 px CRT scanline pattern that
|
||||
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
|
||||
`static/img/terminal-default.svg` mockup, exactly as designed.
|
||||
The hero (`#hero`) is **not** part of the mountain background — it keeps
|
||||
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)
|
||||
|
||||
@@ -169,20 +243,23 @@ tui-pages-web/
|
||||
├── 404.html # not found
|
||||
├── robots.txt # search engine hints
|
||||
├── sitemap.xml # sitemap
|
||||
├── tools/
|
||||
│ ├── mountain.py # pure-Python 3D-mountain PNG renderer
|
||||
│ └── build_mountain_js.py # bundles 60 ASCII frames into mountain.js
|
||||
├── static/
|
||||
│ ├── css/
|
||||
│ │ ├── 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/
|
||||
│ │ ├── favicon.svg
|
||||
│ │ ├── logo.svg
|
||||
│ │ ├── og-image.svg # 1200x630 social share card (used as-is)
|
||||
│ │ ├── og-image-bg.svg # 1200x500 cropped variant (chafa source)
|
||||
│ │ ├── og-image.svg # 1200x630 social share card
|
||||
│ │ ├── terminal-default.svg
|
||||
│ │ ├── terminal-canvas.svg
|
||||
│ │ └── terminal-keybindings.svg
|
||||
│ ├── ascii/
|
||||
│ │ └── rain.txt # chafa render of og-image-bg.svg
|
||||
│ ├── js/
|
||||
│ │ ├── mountain.js # 60-frame ASCII array (≈120 kB)
|
||||
│ │ └── mountain-bg.js # requestAnimationFrame cycler
|
||||
│ └── demos/ # (empty — drop asciinema .cast files here)
|
||||
├── content/ # (empty — markdown for blog/changelog later)
|
||||
├── 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>
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user