Compare commits
10 Commits
d8b622860c
...
b5a35ab198
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5a35ab198 | ||
|
|
7f42516fa3 | ||
|
|
4212d8877e | ||
|
|
6ccf372f65 | ||
|
|
55eb4bbf00 | ||
|
|
1032d20080 | ||
|
|
0f897354ec | ||
|
|
695dad519d | ||
|
|
462a53853f | ||
|
|
99f255fc29 |
81
.gitignore
vendored
81
.gitignore
vendored
@@ -1,30 +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
|
||||||
|
|
||||||
|
video/
|
||||||
|
|||||||
44
Makefile
44
Makefile
@@ -1,4 +1,10 @@
|
|||||||
.PHONY: help serve size validate clean
|
.PHONY: help serve size validate tidy video video-list video-frames video-bundle clean
|
||||||
|
|
||||||
|
# ASCII render resolution (override on the command line):
|
||||||
|
# make video NAME=nature1 SIZE=160x48 SCALE=640
|
||||||
|
SIZE ?= 80x24
|
||||||
|
SCALE ?= 320
|
||||||
|
FPS ?= 12
|
||||||
|
|
||||||
help: ## Show this help
|
help: ## Show this help
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||||
@@ -28,5 +34,39 @@ validate: ## Quick HTML sanity check (counts opening vs closing tags)
|
|||||||
diff <(echo "$$open") <(echo "$$close") || true; \
|
diff <(echo "$$open") <(echo "$$close") || true; \
|
||||||
done
|
done
|
||||||
|
|
||||||
clean: ## Remove generated artifacts
|
tidy: ## Run html-tidy on every .html
|
||||||
|
@for f in *.html; do echo "--- $$f ---"; tidy -e -q $$f || true; done
|
||||||
|
|
||||||
|
video: ## Convert a video from video/<name>.mp4 into ASCII bundle. Usage: make video NAME=nature1
|
||||||
|
@test -n "$(NAME)" || { echo "Usage: make video NAME=<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/<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 $(FPS) fps from video/$(NAME).mp4"
|
||||||
|
@ffmpeg -y -loglevel error -i video/$(NAME).mp4 -vf "fps=$(FPS),scale=$(SCALE):-1" build/$(NAME)/frame-%03d.png
|
||||||
|
@echo " chafa: $(SIZE) ascii per frame"
|
||||||
|
@for f in build/$(NAME)/frame-*.png; do \
|
||||||
|
chafa -f symbols -c none -s $(SIZE) --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
|
||||||
@rm -rf bin/ node_modules/ dist/
|
@rm -rf bin/ node_modules/ dist/
|
||||||
|
|||||||
125
README.md
125
README.md
@@ -1,125 +1,46 @@
|
|||||||
# tui-pages website
|
# tui-pages website
|
||||||
|
|
||||||
Static marketing site for the [`tui-pages`](https://gitlab.com/filipriec/tui-pages) Rust crate.
|
Static marketing site for the [`tui-pages`](https://gitlab.com/filipriec/tui-pages) Rust crate.
|
||||||
No build step required — open `index.html` in a browser and you're done.
|
No build step — it's plain HTML/CSS/JS.
|
||||||
|
|
||||||
## Stack
|
## Build & serve
|
||||||
|
|
||||||
| Layer | Tech | Source |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| HTML | Hand-written semantic | `*.html` |
|
|
||||||
| CSS framework | Tailwind CSS v3 (Play CDN) | runtime |
|
|
||||||
| Components | DaisyUI v4 (prebuilt CSS) | runtime |
|
|
||||||
| Interactivity | HTMX 2 | runtime |
|
|
||||||
| Light JS | Alpine.js 3 | runtime |
|
|
||||||
| Code highlight | highlight.js 11 (Rust, TOML, Bash) | runtime |
|
|
||||||
| Fonts | Inter + JetBrains Mono (Google Fonts) | runtime |
|
|
||||||
| Icons | Lucide (inline SVG) | hand-written |
|
|
||||||
| Page transitions | View Transitions API (native) | n/a |
|
|
||||||
|
|
||||||
Everything is loaded from public CDNs. The only thing served from this repo is
|
|
||||||
the HTML, our small `static/css/site.css` layer, and the SVG assets in
|
|
||||||
`static/img/`.
|
|
||||||
|
|
||||||
## Local development
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Just open the file
|
make serve # serve the working tree on http://localhost:8000
|
||||||
xdg-open index.html # Linux
|
|
||||||
open index.html # macOS
|
|
||||||
|
|
||||||
# Or serve it (recommended — gives you a stable URL for htmx)
|
|
||||||
python3 -m http.server 8000
|
|
||||||
# then visit http://localhost: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 size # report file sizes
|
||||||
make validate # quick HTML syntax check (search for unclosed tags)
|
make validate # quick HTML syntax check
|
||||||
```
|
```
|
||||||
|
|
||||||
## Going to production
|
Just edit the `.html` / `static/` files and refresh.
|
||||||
|
|
||||||
The CDN approach is fine for marketing pages. For better performance
|
## ASCII video background
|
||||||
(smaller CSS, no runtime Tailwind compile), swap the Play CDN for the
|
|
||||||
**Tailwind standalone CLI**:
|
|
||||||
|
|
||||||
```bash
|
The full-page background is a video re-rendered into animated ASCII. `ffmpeg`
|
||||||
# 1. Download the standalone CLI
|
extracts frames, `chafa` turns each into an ASCII grid, and
|
||||||
# https://tailwindcss.com/blog/standalone-cli
|
`tools/build_mountain_js.py` bundles them into `static/js/mountain.js`, which
|
||||||
|
the page plays back.
|
||||||
|
|
||||||
# 2. Put the binary in ./bin/tailwindcss
|
### Render a video
|
||||||
|
|
||||||
# 3. Create src/site.css that imports Tailwind and DaisyUI:
|
1. Drop your clip in `video/`, e.g. `video/nature1.mp4`.
|
||||||
# @import "tailwindcss";
|
2. Render + bundle (needs `ffmpeg` + `chafa`, so run in the nix shell):
|
||||||
# @plugin "daisyui";
|
|
||||||
|
|
||||||
# 4. Build
|
|
||||||
./bin/tailwindcss -i src/site.css -o static/css/site.css --minify
|
|
||||||
```
|
|
||||||
|
|
||||||
Then in `index.html` remove the Tailwind Play CDN `<script>` and the DaisyUI
|
|
||||||
`<link>`, and ensure `/static/css/site.css` is loaded last in `<head>`.
|
|
||||||
|
|
||||||
## File layout
|
|
||||||
|
|
||||||
```
|
|
||||||
tui-pages-web/
|
|
||||||
├── index.html # landing page
|
|
||||||
├── examples.html # examples gallery
|
|
||||||
├── 404.html # not found
|
|
||||||
├── robots.txt # search engine hints
|
|
||||||
├── sitemap.xml # sitemap
|
|
||||||
├── static/
|
|
||||||
│ ├── css/
|
|
||||||
│ │ └── site.css # our custom layer (small)
|
|
||||||
│ ├── img/
|
|
||||||
│ │ ├── favicon.svg
|
|
||||||
│ │ ├── logo.svg
|
|
||||||
│ │ ├── og-image.svg
|
|
||||||
│ │ ├── terminal-default.svg
|
|
||||||
│ │ ├── terminal-canvas.svg
|
|
||||||
│ │ └── terminal-keybindings.svg
|
|
||||||
│ └── demos/ # (empty — drop asciinema .cast files here)
|
|
||||||
├── content/ # (empty — markdown for blog/changelog later)
|
|
||||||
├── Makefile
|
|
||||||
├── .gitignore
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding an asciinema demo to the hero
|
|
||||||
|
|
||||||
The hero currently shows a static SVG terminal mockup. To replace it with a
|
|
||||||
real asciinema recording:
|
|
||||||
|
|
||||||
1. Record one of the examples:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ../komp_ac/tui-pages/examples/default
|
nix develop -c make video NAME=nature1 SIZE=480x144 SCALE=1920
|
||||||
asciinema rec ../../tui-pages-web/static/demos/intro.cast
|
|
||||||
# ... run the app for a few seconds, hit ctrl-d to stop
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. In `index.html`, replace the hero terminal block with:
|
3. `make serve` and hard-refresh the browser (Ctrl+Shift+R).
|
||||||
|
|
||||||
```html
|
- `NAME` — the file name without `.mp4`.
|
||||||
<div class="tp-terminal">
|
- `SIZE` — ASCII grid `cols×rows`. Bigger = sharper + heavier bundle. Keep
|
||||||
<asciinema-player src="/static/demos/intro.cast" autoplay loop></asciinema-player>
|
the ~3.33:1 ratio (e.g. `80x24`, `160x48`, `240x72`, `480x144`).
|
||||||
</div>
|
- `SCALE` — source pixel width fed to chafa; scale it up with `SIZE`.
|
||||||
```
|
|
||||||
|
|
||||||
3. Add the asciinema player to the `<head>`:
|
After changing `SIZE`, set a matching `font-size` in `static/css/ascii.css`
|
||||||
|
(it halves each time you double the grid) so the ASCII fills the screen.
|
||||||
|
|
||||||
```html
|
Current setup: `480x144`, font-size `max(0.35vw, 0.695vh)`.
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/asciinema-player@3.7.0/dist/bundle/asciinema-player.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/asciinema-player@3.7.0/dist/bundle/asciinema-player.js" defer></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
The website source is MIT-licensed. The terminal mockup SVGs in `static/img/`
|
MIT. Crate: <https://gitlab.com/filipriec/tui-pages>.
|
||||||
are hand-drawn and original. The crate itself is at
|
|
||||||
<https://gitlab.com/filipriec/tui-pages>.
|
|
||||||
|
|||||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1779560665,
|
||||||
|
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
189
flake.nix
Normal file
189
flake.nix
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
{
|
||||||
|
description = "Nix flake for the tui-pages website — static HTML/CSS/SVG, CDN-loaded JS";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
# NOTE: No rust-overlay — this is a pure-static site, no Rust toolchain required.
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, ... }:
|
||||||
|
let
|
||||||
|
systems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
forAllSystems = f: nixpkgs.lib.genAttrs systems f;
|
||||||
|
|
||||||
|
version = "0.1.0";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# `nix fmt` — format this flake
|
||||||
|
formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.nixpkgs-fmt);
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# packages.<system>.default
|
||||||
|
#
|
||||||
|
# The whole website as a single derivation. `nix build` produces ./result/
|
||||||
|
# containing the static files, ready to:
|
||||||
|
# - serve with any static file server, or
|
||||||
|
# - upload to Netlify / Cloudflare Pages / GitHub Pages / S3 / etc.
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
packages = forAllSystems (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
src = ./.;
|
||||||
|
in {
|
||||||
|
default = pkgs.stdenvNoCC.mkDerivation {
|
||||||
|
pname = "tui-pages-website";
|
||||||
|
inherit version;
|
||||||
|
inherit src;
|
||||||
|
|
||||||
|
dontBuild = true;
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
mkdir -p $out
|
||||||
|
# Use cp -R with --no-preserve=mode so the read-only Nix-store
|
||||||
|
# permissions don't leak into $out (a few hosts reject the
|
||||||
|
# resulting 0444 file modes when serving).
|
||||||
|
cp -R --no-preserve=mode ${src}/. $out/
|
||||||
|
chmod -R u+w $out
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "Static website for the tui-pages Rust crate";
|
||||||
|
homepage = "https://tui-pages.dev";
|
||||||
|
license = licenses.mit;
|
||||||
|
platforms = platforms.unix;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# apps.<system>.serve
|
||||||
|
#
|
||||||
|
# `nix run .#serve` — build the site and boot `python3 -m http.server`
|
||||||
|
# against the built result. Honours $PORT (defaults to 8000).
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
apps = forAllSystems (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
site = self.packages.${system}.default;
|
||||||
|
in {
|
||||||
|
serve = {
|
||||||
|
type = "app";
|
||||||
|
program = toString (pkgs.writeShellScript "tui-pages-web-serve" ''
|
||||||
|
echo "Serving ${site} on http://localhost:''${PORT:-8000}"
|
||||||
|
cd ${site}
|
||||||
|
exec ${pkgs.python3}/bin/python3 -m http.server "''${PORT:-8000}"
|
||||||
|
'');
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "Build the tui-pages website and serve it on $PORT (default 8000)";
|
||||||
|
platforms = platforms.unix;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# devShells.<system>.default
|
||||||
|
#
|
||||||
|
# `nix develop` — everything the Makefile, the README, and future
|
||||||
|
# workflows need. No Rust toolchain by design.
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
devShells = forAllSystems (system:
|
||||||
|
let pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
# --- Build / serve / validate (the Makefile uses these) ----
|
||||||
|
gnumake
|
||||||
|
python3
|
||||||
|
gawk
|
||||||
|
gnused
|
||||||
|
gnugrep
|
||||||
|
coreutils
|
||||||
|
|
||||||
|
# --- HTML validation (optional, for the tidy check) --------
|
||||||
|
html-tidy
|
||||||
|
|
||||||
|
# --- Smoke-test the local server ---------------------------
|
||||||
|
curl
|
||||||
|
|
||||||
|
# --- Record TUI demos (used to replace the SVG mockups) ----
|
||||||
|
asciinema
|
||||||
|
|
||||||
|
# --- Generate the animated ASCII background from a video.
|
||||||
|
# `make video NAME=<name>` extracts frames from video/<name>.mp4
|
||||||
|
# with ffmpeg at 12 fps, runs chafa on each to get 80x24 ASCII,
|
||||||
|
# and bundles them into static/js/mountain.js for the cycler.
|
||||||
|
ffmpeg
|
||||||
|
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 --
|
||||||
|
# Uncomment if you switch from the Play CDN to a precompiled stylesheet.
|
||||||
|
# nodejs
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
# Some distros ship `python` but not `python3`. Make the
|
||||||
|
# `python3` invocation in the Makefile portable.
|
||||||
|
if ! command -v python3 >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
|
||||||
|
alias python3=python
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<'EOF'
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ tui-pages website dev shell │
|
||||||
|
│ │
|
||||||
|
│ make serve python3 -m http.server 8000 │
|
||||||
|
│ make size report file sizes │
|
||||||
|
│ make validate HTML tag balance check │
|
||||||
|
│ make tidy run html-tidy on every .html │
|
||||||
|
│ 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 │
|
||||||
|
│ make video-list show available videos in video/ │
|
||||||
|
│ make video-frames extract + chafa frames for $NAME │
|
||||||
|
│ make video-bundle bundle frames into mountain.js │
|
||||||
|
│ │
|
||||||
|
│ nix build build site → ./result/ │
|
||||||
|
│ nix run .#serve build + serve (honours $PORT) │
|
||||||
|
│ nix fmt format this flake │
|
||||||
|
│ nix flake check run all checks │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# checks.<system>.tidy
|
||||||
|
#
|
||||||
|
# HTML sanity check using tidy. Non-blocking — prints warnings to the
|
||||||
|
# build log, never fails. `nix flake check` runs this.
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
checks = forAllSystems (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
site = self.packages.${system}.default;
|
||||||
|
in {
|
||||||
|
tidy = pkgs.runCommand "tui-pages-website-html-check"
|
||||||
|
{
|
||||||
|
nativeBuildInputs = [ pkgs.html-tidy ];
|
||||||
|
} ''
|
||||||
|
cd ${site}
|
||||||
|
for f in *.html; do
|
||||||
|
echo "─── $f ───"
|
||||||
|
${pkgs.html-tidy}/bin/tidy -e -q -utf8 "$f" 2>&1 | head -20 || true
|
||||||
|
done
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
20
index.html
20
index.html
@@ -77,6 +77,8 @@
|
|||||||
|
|
||||||
<!-- Our custom layer (after Tailwind + DaisyUI compile) -->
|
<!-- Our custom layer (after Tailwind + DaisyUI compile) -->
|
||||||
<link rel="stylesheet" href="/static/css/site.css">
|
<link rel="stylesheet" href="/static/css/site.css">
|
||||||
|
<!-- Animated ASCII art layer (chafa-rendered) -->
|
||||||
|
<link rel="stylesheet" href="/static/css/ascii.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body
|
<body
|
||||||
@@ -93,9 +95,22 @@
|
|||||||
class="min-h-screen bg-base-100 text-base-content antialiased"
|
class="min-h-screen bg-base-100 text-base-content antialiased"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<!-- ============================ ASCII BACKGROUND ============================ -->
|
||||||
|
<!-- Full-bleed 3D mountain flyover. tools/mountain.py renders 60 PNG
|
||||||
|
frames; chafa converts each to 80x24 ASCII; tools/build_mountain_js.py
|
||||||
|
bundles them into static/js/mountain.js. The background <pre> is
|
||||||
|
mounted by static/js/mountain-bg.js, which cycles the 60 frames at
|
||||||
|
12 fps. Style lives in static/css/ascii.css. -->
|
||||||
|
<pre id="tp-mountain-bg" class="tp-mountain-bg" aria-hidden="true"></pre>
|
||||||
|
|
||||||
|
|
||||||
<a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:px-3 focus:py-2 focus:bg-accent focus:text-accent-fg focus:rounded">Skip to content</a>
|
<a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:px-3 focus:py-2 focus:bg-accent focus:text-accent-fg focus:rounded">Skip to content</a>
|
||||||
|
|
||||||
<!-- ============================ NAV ============================ -->
|
<!-- 3D mountain flyover: pre-baked ASCII frames cycled at 12 fps -->
|
||||||
|
<script src="/static/js/mountain.js" defer></script>
|
||||||
|
<script src="/static/js/mountain-bg.js" defer></script>
|
||||||
|
|
||||||
|
<!-- ============================ NAV ============================ -->
|
||||||
<header class="tp-nav sticky top-0 z-40">
|
<header class="tp-nav sticky top-0 z-40">
|
||||||
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center gap-4">
|
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center gap-4">
|
||||||
<a href="/" class="flex items-center gap-2.5 group" aria-label="tui-pages home">
|
<a href="/" class="flex items-center gap-2.5 group" aria-label="tui-pages home">
|
||||||
@@ -189,7 +204,7 @@
|
|||||||
|
|
||||||
<h1 class="mt-6 text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-50 leading-[1.05]">
|
<h1 class="mt-6 text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-50 leading-[1.05]">
|
||||||
A framework for<br>
|
A framework for<br>
|
||||||
building <span class="text-accent">TUIs</span> in Rust<span class="tp-cursor" aria-hidden="true"></span>
|
building <span class="text-accent">TUIs</span> in Rust
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="mt-6 text-lg text-zinc-300 max-w-xl leading-relaxed">
|
<p class="mt-6 text-lg text-zinc-300 max-w-xl leading-relaxed">
|
||||||
@@ -228,7 +243,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-3 text-xs text-zinc-500 font-mono">or: <code class="text-zinc-400">cargo install tui-pages-cli</code></p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hero terminal mockup -->
|
<!-- Hero terminal mockup -->
|
||||||
|
|||||||
91
static/css/ascii.css
Normal file
91
static/css/ascii.css
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
tui-pages website - animated ASCII mountain background (chafa-rendered)
|
||||||
|
|
||||||
|
The mountain background is JS-driven: tools/mountain.py renders 60
|
||||||
|
procedural 3D mountain-flyover PNG frames; chafa converts each to 80x24
|
||||||
|
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 the
|
||||||
|
<pre id="tp-mountain-bg" class="tp-mountain-bg"> placeholder in index.html.
|
||||||
|
|
||||||
|
This CSS file:
|
||||||
|
- positions the frame as a full-screen top layer
|
||||||
|
- tints the dense characters rust-orange with text-shadow + glow
|
||||||
|
- adds a CRT scanline overlay
|
||||||
|
- disables the animation under prefers-reduced-motion
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Full-screen ASCII mountain top layer ----------------------------------- */
|
||||||
|
.tp-mountain-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: block;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100svh;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: max(0.35vw, 0.695vh);
|
||||||
|
font-size: max(0.35vw, 0.695svh);
|
||||||
|
line-height: 1.0;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: #f4a26b;
|
||||||
|
text-shadow: 0 0 8px rgba(183, 65, 14, 0.35);
|
||||||
|
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
|
||||||
|
fight the page content. */
|
||||||
|
.tp-mountain-bg::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0,
|
||||||
|
transparent 2px,
|
||||||
|
rgba(0, 0, 0, 0.20) 2px,
|
||||||
|
rgba(0, 0, 0, 0.20) 3px
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
animation: tp-mtn-scanline 8s linear infinite;
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tp-mtn-scanline {
|
||||||
|
0% { background-position: 0 0; }
|
||||||
|
100% { background-position: 0 6px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme: dim the background, drop the glow, switch to dark text. */
|
||||||
|
:root[data-theme="winter"] .tp-mountain-bg {
|
||||||
|
opacity: 0.20;
|
||||||
|
color: #2a1810;
|
||||||
|
text-shadow: none;
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="winter"] .tp-mountain-bg::after {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0,
|
||||||
|
transparent 2px,
|
||||||
|
rgba(255, 255, 255, 0.12) 2px,
|
||||||
|
rgba(255, 255, 255, 0.12) 3px
|
||||||
|
);
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduced motion: keep the first frame static, no scanline drift. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.tp-mountain-bg::after {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,14 +29,16 @@ html {
|
|||||||
--tp-grid-line: rgba(63, 63, 70, 0.35);
|
--tp-grid-line: rgba(63, 63, 70, 0.35);
|
||||||
--tp-hero-from: #0b0b0f;
|
--tp-hero-from: #0b0b0f;
|
||||||
--tp-hero-to: #18181b;
|
--tp-hero-to: #18181b;
|
||||||
|
--tp-text-outline: rgba(0, 0, 0, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
: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);
|
||||||
--tp-hero-from: #fafafa;
|
--tp-hero-from: #fafafa;
|
||||||
--tp-hero-to: #f4f4f5;
|
--tp-hero-to: #f4f4f5;
|
||||||
|
--tp-text-outline: rgba(255, 255, 255, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Body --------------------------------------------------------- */
|
/* ---------- Body --------------------------------------------------------- */
|
||||||
@@ -55,13 +57,30 @@ body {
|
|||||||
|
|
||||||
/* ---------- Hero --------------------------------------------------------- */
|
/* ---------- Hero --------------------------------------------------------- */
|
||||||
.tp-hero {
|
.tp-hero {
|
||||||
background:
|
background: transparent; /* let the mountain ASCII show through */
|
||||||
radial-gradient(ellipse 80% 50% at 50% 0%, rgba(183, 65, 14, 0.15), transparent 70%),
|
|
||||||
linear-gradient(180deg, var(--tp-hero-from) 0%, var(--tp-hero-to) 100%);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tp-hero > .max-w-7xl {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-hero h1,
|
||||||
|
.tp-hero p {
|
||||||
|
text-shadow:
|
||||||
|
0 -1px 0 var(--tp-text-outline),
|
||||||
|
1px 0 0 var(--tp-text-outline),
|
||||||
|
0 1px 0 var(--tp-text-outline),
|
||||||
|
-1px 0 0 var(--tp-text-outline),
|
||||||
|
-1px -1px 0 var(--tp-text-outline),
|
||||||
|
1px -1px 0 var(--tp-text-outline),
|
||||||
|
-1px 1px 0 var(--tp-text-outline),
|
||||||
|
1px 1px 0 var(--tp-text-outline),
|
||||||
|
0 2px 16px rgba(0, 0, 0, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
.tp-hero::before {
|
.tp-hero::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -75,6 +94,7 @@ body {
|
|||||||
mask-image: radial-gradient(ellipse 60% 50% at 50% 0%, #000 30%, transparent 80%);
|
mask-image: radial-gradient(ellipse 60% 50% at 50% 0%, #000 30%, transparent 80%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
opacity: 0.5; /* subtle grid, not a wall */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subtle floating code lines in the hero, behind the content */
|
/* Subtle floating code lines in the hero, behind the content */
|
||||||
@@ -89,30 +109,6 @@ body {
|
|||||||
radial-gradient(circle at 85% 70%, rgba(126, 231, 135, 0.05), transparent 35%);
|
radial-gradient(circle at 85% 70%, rgba(126, 231, 135, 0.05), transparent 35%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Cursor caret animation --------------------------------------- */
|
|
||||||
@keyframes tp-blink {
|
|
||||||
0%, 49% { opacity: 1; }
|
|
||||||
50%, 100% { opacity: 0; }
|
|
||||||
}
|
|
||||||
.tp-cursor {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.55em;
|
|
||||||
height: 1em;
|
|
||||||
background: var(--tp-accent);
|
|
||||||
margin-left: 0.15em;
|
|
||||||
vertical-align: -0.12em;
|
|
||||||
animation: tp-blink 1.1s steps(1) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Soft float for the hero terminal ----------------------------- */
|
|
||||||
@keyframes tp-float {
|
|
||||||
0%, 100% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-6px); }
|
|
||||||
}
|
|
||||||
.tp-float {
|
|
||||||
animation: tp-float 6s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Glass nav ---------------------------------------------------- */
|
/* ---------- Glass nav ---------------------------------------------------- */
|
||||||
.tp-nav {
|
.tp-nav {
|
||||||
background: rgba(9, 9, 11, 0.65);
|
background: rgba(9, 9, 11, 0.65);
|
||||||
@@ -120,7 +116,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);
|
||||||
}
|
}
|
||||||
@@ -139,11 +135,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);
|
||||||
}
|
}
|
||||||
@@ -196,7 +192,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;
|
||||||
@@ -206,13 +202,12 @@ body {
|
|||||||
.tp-terminal {
|
.tp-terminal {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 0.875rem;
|
border-radius: 0.875rem;
|
||||||
background: #0b0b0f;
|
background: transparent; /* let the body ASCII art show through */
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 1px 0 rgba(255, 255, 255, 0.04) inset,
|
0 0 0 1px rgba(244, 162, 107, 0.18), /* rust-orange hairline, brand */
|
||||||
0 30px 60px -20px rgba(0, 0, 0, 0.6),
|
0 30px 60px -20px rgba(0, 0, 0, 0.55),
|
||||||
0 18px 36px -18px rgba(0, 0, 0, 0.5);
|
0 18px 36px -18px rgba(0, 0, 0, 0.45);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(63, 63, 70, 0.4);
|
|
||||||
}
|
}
|
||||||
.tp-terminal::after {
|
.tp-terminal::after {
|
||||||
content: "";
|
content: "";
|
||||||
@@ -220,7 +215,14 @@ body {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(11, 11, 15, 0.55) 0%,
|
||||||
|
rgba(11, 11, 15, 0) 18%,
|
||||||
|
rgba(11, 11, 15, 0) 82%,
|
||||||
|
rgba(11, 11, 15, 0.45) 100%
|
||||||
|
); /* darken only the top + bottom strips, leave the middle transparent
|
||||||
|
so the body ASCII art shows through the terminal content */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Selection --------------------------------------------------- */
|
/* ---------- Selection --------------------------------------------------- */
|
||||||
@@ -249,7 +251,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,17 +1,17 @@
|
|||||||
<svg viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" role="img" aria-label="examples/default terminal screenshot">
|
<svg viewBox="0 0 640 400" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" role="img" aria-label="examples/default terminal screenshot">
|
||||||
<style>
|
<style>
|
||||||
.bg { fill: #18181b; }
|
.bg { fill: transparent; stroke: #3f3f46; stroke-width: 1; }
|
||||||
.chrome { fill: #27272a; }
|
.chrome { fill: transparent; }
|
||||||
.dot { fill: #52525b; }
|
.dot { fill: #52525b; }
|
||||||
.sep { stroke: #27272a; stroke-width: 1; }
|
.sep { stroke: #3f3f46; stroke-width: 1; }
|
||||||
.tab { fill: #18181b; }
|
.tab { fill: transparent; }
|
||||||
text { font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; fill: #f4f4f5; }
|
text { font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; fill: #f4f4f5; }
|
||||||
.t-md { font-size: 15px; }
|
.t-md { font-size: 15px; }
|
||||||
.t-sm { font-size: 13px; }
|
.t-sm { font-size: 13px; }
|
||||||
.t-xs { font-size: 11px; }
|
.t-xs { font-size: 11px; }
|
||||||
.muted { fill: #a1a1aa; }
|
.muted { fill: #a1a1aa; }
|
||||||
.dim { fill: #71717a; }
|
.dim { fill: #71717a; }
|
||||||
.sel-bg { fill: #b7410e; }
|
.sel-bg { fill: #b7410e; fill-opacity: 0.85; }
|
||||||
.sel-fg { fill: #fff5f0; }
|
.sel-fg { fill: #fff5f0; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.6 KiB |
73
static/js/mountain-bg.js
Normal file
73
static/js/mountain-bg.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
var pre = document.querySelector("#tp-mountain-bg");
|
||||||
|
if (!pre) return;
|
||||||
|
pre.setAttribute("data-tp-mountain", "");
|
||||||
|
pre.setAttribute("aria-hidden", "true");
|
||||||
|
pre.textContent = window.TP_MOUNTAIN_FRAMES[0];
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
35337
static/js/mountain.js
Normal file
35337
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