UNPKG

skills-ws

Version:

85 agent skills for AI coding assistants — marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.

389 lines (310 loc) • 15.5 kB
--- name: ascii-banner description: "Build animated ASCII/Unicode banners for CLI tools and web UIs — frame animation, ANSI color, terminal capability detection, flicker-free rendering, accessibility, and canvas/WebGL ASCII shaders. Use when adding a CLI startup banner, building terminal-aesthetic web UIs, converting images/3D scenes to ASCII, or making banner animation TTY-safe." --- # Animated ASCII Banners ## Overview Animated ASCII banners create personality in CLI tools and terminal-aesthetic web UIs. This skill covers both terminal-native (Node.js/Python CLI) and web-based (canvas/WebGL) implementations. **Key challenges:** Terminal inconsistency, ANSI color fragmentation, screen reader accessibility, flicker prevention, and cross-platform rendering. ## Part 1: Terminal ASCII Animation (CLI) ### 1. Frame-Based Animation Architecture ``` project/ frames/ # Each .txt file is one animation frame frame-001.txt frame-002.txt ... colors/ # Color map per frame (optional) frame-001.json src/ renderer.ts # Animation engine palette.ts # ANSI color role mapping detect.ts # Terminal capability detection ``` ### 2. Basic Animation Loop (Node.js) ```javascript import fs from "fs"; import readline from "readline"; const frames = fs .readdirSync("./frames") .filter(f => f.endsWith(".txt")) .sort() .map(f => fs.readFileSync(`./frames/${f}`, "utf8")); let current = 0; let running = true; function render() { if (!running) return; readline.cursorTo(process.stdout, 0, 0); readline.clearScreenDown(process.stdout); process.stdout.write(frames[current]); current = (current + 1) % frames.length; } // 75ms = ~13fps — safe for most terminals const interval = setInterval(render, 75); // Graceful cleanup process.on("SIGINT", () => { running = false; clearInterval(interval); readline.cursorTo(process.stdout, 0, 0); readline.clearScreenDown(process.stdout); process.exit(0); }); // Auto-stop after one loop setTimeout(() => { clearInterval(interval); running = false; }, frames.length * 75); ``` ### 3. ANSI Color System **Use semantic color roles, not hardcoded values.** Terminals remap colors based on user themes. ```javascript // Color role mapping — degrade gracefully across terminals const ANSI_ROLES = { primary: "\x1b[32m", // Green (accent) secondary: "\x1b[36m", // Cyan highlight: "\x1b[97m", // Bright white shadow: "\x1b[90m", // Dark gray dim: "\x1b[2m", // Dim modifier reset: "\x1b[0m", }; function colorize(char, role) { if (!role || role === "none") return char; return `${ANSI_ROLES[role] || ""}${char}${ANSI_ROLES.reset}`; } ``` **ANSI color modes:** | Mode | Colors | Support | Use | |------|--------|---------|-----| | 4-bit | 16 colors | Universal | Safe default — use this | | 8-bit | 256 colors | Most modern terminals | Extended palette | | 24-bit (truecolor) | 16M colors | iTerm2, Kitty, modern terminals | Brand-exact colors | **Terminal detection:** ```javascript function getColorSupport() { const env = process.env; if (env.NO_COLOR) return "none"; if (env.COLORTERM === "truecolor" || env.COLORTERM === "24bit") return "24bit"; if (env.TERM_PROGRAM === "iTerm.app") return "24bit"; if (env.TERM?.includes("256color")) return "8bit"; if (process.stdout.isTTY) return "4bit"; return "none"; } ``` ### 4. Flicker Prevention **Problem:** `clearScreen` + full repaint causes visible flicker. **Solution:** Differential rendering — only repaint changed characters: ```javascript let previousFrame = ""; function renderDiff(frame) { const lines = frame.split("\n"); const prevLines = previousFrame.split("\n"); for (let y = 0; y < lines.length; y++) { if (lines[y] !== prevLines[y]) { readline.cursorTo(process.stdout, 0, y); process.stdout.write(lines[y] + "\x1b[K"); // Clear to end of line } } previousFrame = frame; } ``` **Additional techniques:** - Use alternate screen buffer (`\x1b[?1049h` to enter, `\x1b[?1049l` to exit) - Hide cursor during animation (`\x1b[?25l`, restore with `\x1b[?25h`) - Batch writes using a string buffer, write once per frame ### 5. Accessibility Terminals have **no `aria-live` equivalent** — a screen reader reads stdout linearly, so every cursor-repositioned frame you write risks being announced as new text (a "chatterbox" of garbage). The accessible pattern is the opposite of the web one: **emit a single static text line, then do all animation with raw escape codes that assistive tech ignores or that you suppress entirely when not on an interactive TTY.** **Mandatory requirements:** | Requirement | Concrete CLI implementation | |-------------|------------------------------| | Opt-in / opt-out | Animate only behind intent (`--banner`/`--animate`), and always honor `--no-banner`. Never auto-play in CI or pipes. | | Static alternative | Before animating, print one plain line (e.g. `skills.ws — CLI v1.2.0`). This is what a screen reader and a logfile actually read; the animation is decoration layered on top. | | Don't re-announce | Never rewrite the *text content* every frame. Either animate inside the alternate screen buffer (which most SR setups skip) or only repaint changed cells (see §4) so unchanged glyphs aren't re-emitted. | | Reduced motion | There is **no standardized terminal "reduce motion" signal.** Disable animation when stdout is not a TTY, when `NO_COLOR` is set (users who set it generally want quiet output), when `TERM=dumb`, and via your own opt-out flag/config. On the **web** use the real standard: `@media (prefers-reduced-motion: reduce)`. | | Graceful degradation | Static ASCII art fallback (no escapes) whenever animation is disabled by any check above. | | Color-independent | Art must read by shape, not color — verify it's recognizable in `NO_COLOR=1` mode. | ```javascript // CLI gate: only animate when it's safe and wanted. // Note: NO_ANIMATION / REDUCE_MOTION are conventions, not standards — // support them defensively but rely on the TTY/flag checks as the real signal. function shouldAnimate({ flags = {} } = {}) { if (flags.noBanner) return false; // explicit opt-out wins if (!process.stdout.isTTY) return false; // piped, redirected, or CI if (process.env.CI) return false; // build logs if (process.env.TERM === "dumb") return false; if (process.env.NO_COLOR) return false; // user wants quiet output if (process.env.NO_ANIMATION) return false; // de-facto convention if (process.env.REDUCE_MOTION) return false; // some apps export this; non-standard return true; } // Always emit the accessible line; animate only as decoration on top. function showBanner(flags) { console.log("skills.ws — CLI v1.2.0"); // read by SR / captured in logs if (shouldAnimate({ flags })) startAnimation(); } ``` > Web equivalent: gate animation with the standardized media query, not an env var: > ```javascript > const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; > if (!reduce) startWebAnimation(); > ``` ### 6. ASCII Art Design **Pick a character set deliberately — "ASCII" and "terminal art" are not the same.** True ASCII is bytes 0x20–0x7E and renders identically everywhere (logs, dumb terminals, Windows `cmd`, CI). The box/block/arrow glyphs below are **Unicode** — gorgeous in iTerm2/Kitty/Windows Terminal but they mojibake on legacy code pages or under the wrong locale. Treat them as two distinct modes and pick based on `process.stdout.isTTY` + a UTF-8 locale check (`/utf-?8/i.test(process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || "")`). **Mode A — ASCII-safe (universal, 0x20–0x7E only):** ``` Shading (light → dense): . : - = + * # % @ Borders: + - | = (corners: + , edges: - and |) Fills: . , ; * # @ (no solid blocks exist in ASCII) Geometry / arrows: / \ < > ^ v (use v for down-arrow) ``` **Mode B — Unicode-enhanced (UTF-8 TTYs only; falls back to Mode A):** ``` Box-drawing: ┌ ─ ┐ │ └ ┘ ╔ ═ ╗ ║ ╚ ╝ ├ ┤ ┬ ┴ ┼ Block fills: ░ ▒ ▓ █ ▄ ▀ ▐ ▌ (▁▂▃▄▅▆▇█ for vertical bars/sparklines) Geometry: ╱ ╲ △ ▽ ◇ ○ ● Arrows: → ← ↑ ↓ ⟶ ⟵ ``` ```javascript const supportsUnicode = process.stdout.isTTY && /utf-?8/i.test(process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || ""); const charset = supportsUnicode ? UNICODE_SET : ASCII_SET; ``` **figlet for text banners.** `npm install figlet` installs the **library** locally — it does *not* put a `figlet` binary on your PATH. Use `npx` (downloads/runs the CLI without a global install), or install a CLI globally, or call the library from code. ```bash # No-install, one-off (recommended): npx fetches the CLI on demand npx figlet-cli -f Slant "SKILLS" # OR install a CLI globally so `figlet` is on PATH npm i -g figlet-cli # provides the `figlet` command figlet -f Slant "SKILLS" # Python equivalent (pyfiglet ships a console script): pip install pyfiglet pyfiglet -f slant "SKILLS" ``` ```javascript // Or use the figlet npm package as a library (after `npm install figlet`): import figlet from "figlet"; console.log(figlet.textSync("SKILLS", { font: "Slant" })); ``` **Popular figlet fonts:** `Slant`, `Banner3`, `Big`, `Doom`, `Standard`, `Small` (npm font names are capitalized; the system `figlet`/`pyfiglet` binaries accept lowercase like `slant`). List installed fonts with `figlet -l` or `pyfiglet -l`. ## Part 2: Web ASCII Animation (Canvas/WebGL) ### 7. Canvas-Based ASCII Renderer Convert any visual (3D scene, video, image) to ASCII in the browser: ```javascript const CHARS = " .:-=+*#%@"; function renderAscii(ctx, canvas, source, cellW, cellH) { // Draw source to small offscreen canvas const cols = Math.floor(canvas.width / cellW); const rows = Math.floor(canvas.height / cellH); const offscreen = new OffscreenCanvas(cols, rows); const offCtx = offscreen.getContext("2d"); offCtx.drawImage(source, 0, 0, cols, rows); const pixels = offCtx.getImageData(0, 0, cols, rows).data; ctx.fillStyle = "#0a0a0a"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.font = `${cellH - 2}px monospace`; for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { const i = (y * cols + x) * 4; const brightness = (pixels[i] * 0.299 + pixels[i+1] * 0.587 + pixels[i+2] * 0.114) / 255; if (brightness < 0.02) continue; const char = CHARS[Math.floor(brightness * (CHARS.length - 1))]; const green = Math.floor(40 + brightness * 215); ctx.fillStyle = `rgba(0,${green},${Math.floor(green*0.55)},${0.3 + brightness * 0.7})`; ctx.fillText(char, x * cellW, y * cellH + cellH - 2); } } } ``` ### 8. Three.js + ASCII Post-Processing Render an animated 3D scene to an offscreen WebGL buffer, then feed that buffer to the `renderAscii` function from §7. Complete, runnable example (Three.js r150+; verify the import path and API against your installed version — `THREE.WebGLRenderer` and ES-module imports are stable through mid-2026, but minor APIs drift): ```javascript import * as THREE from "three"; // --- Sizing (drives BOTH the WebGL buffer and the visible ASCII canvas) --- const WIDTH = 480, HEIGHT = 320; const CELL_W = 8, CELL_H = 14; // monospace cell size in px // --- Visible ASCII canvas (what the user sees) --- const asciiCanvas = document.getElementById("ascii"); asciiCanvas.width = WIDTH; asciiCanvas.height = HEIGHT; const asciiCtx = asciiCanvas.getContext("2d", { willReadFrequently: false }); // --- Scene --- const scene = new THREE.Scene(); const geometry = new THREE.TorusKnotGeometry(1, 0.35, 128, 32); const material = new THREE.MeshStandardMaterial({ color: 0x00ff88 }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // --- Camera (REQUIRED — was missing) --- const camera = new THREE.PerspectiveCamera(50, WIDTH / HEIGHT, 0.1, 100); camera.position.z = 4; // --- Lighting (MeshStandardMaterial renders black without lights) --- scene.add(new THREE.AmbientLight(0xffffff, 0.4)); const key = new THREE.DirectionalLight(0xffffff, 1.2); key.position.set(3, 4, 5); scene.add(key); // --- Offscreen WebGL renderer (its canvas is the SOURCE for renderAscii) --- const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(WIDTH, HEIGHT); // renderer.domElement is NOT added to the DOM — it's our pixel source. const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; function animate() { mesh.rotation.x += 0.01; mesh.rotation.y += 0.007; renderer.render(scene, camera); // renderAscii is defined in §7; it reads pixels and draws characters. renderAscii(asciiCtx, asciiCanvas, renderer.domElement, CELL_W, CELL_H); requestAnimationFrame(animate); } if (reduceMotion) { // Honor reduced motion: render a single static frame instead of looping. renderer.render(scene, camera); renderAscii(asciiCtx, asciiCanvas, renderer.domElement, CELL_W, CELL_H); } else { animate(); } ``` ### 9. Performance Optimization | Technique | Impact | Implementation | |-----------|--------|---------------| | Skip black pixels | 30-50% fewer draw calls | `if (brightness < threshold) continue` | | Throttle FPS | Reduce CPU usage | `requestAnimationFrame` with timestamp check | | Reduce resolution | Fewer cells to render | Smaller offscreen canvas | | Cache character metrics | Avoid repeated `measureText` | Pre-compute once | | Use `willReadFrequently` | Faster `getImageData` | Pass to canvas context options | | Gradient fade | Visual polish | CSS gradient overlay at edges | ### 10. Static ASCII Art Generation **From image to ASCII (Python):** ```python from PIL import Image CHARS = " .:-=+*#%@" def image_to_ascii(path, width=80): img = Image.open(path).convert("L") aspect = img.height / img.width height = int(width * aspect * 0.5) # Terminal chars are ~2:1 img = img.resize((width, height)) ascii_art = "" for y in range(height): for x in range(width): brightness = img.getpixel((x, y)) / 255 ascii_art += CHARS[int(brightness * (len(CHARS) - 1))] ascii_art += "\n" return ascii_art ``` **From text to ASCII banner** (uses `npx figlet-cli`; swap for a global `figlet` if installed — see §6): ```bash # Quick branded banner, indented two spaces npx figlet-cli -f Slant "skills.ws" | sed 's/^/ /' # With green color (bash) — wrap in ANSI SGR codes echo -e "\033[32m$(npx figlet-cli -f Slant 'skills.ws')\033[0m" ``` ## Checklist - [ ] Terminal capability detection (TTY + UTF-8 locale) before rendering - [ ] Print a static text alternative first; fall back to static art when animation disabled - [ ] Disable animation when not a TTY, in CI, with `NO_COLOR`, or `TERM=dumb`; web uses `prefers-reduced-motion` - [ ] Choose ASCII-safe vs Unicode charset by capability (don't assume UTF-8) - [ ] Hide cursor during animation, restore after - [ ] Use alternate screen buffer for full-screen animations - [ ] Differential rendering to prevent flicker - [ ] Test on: iTerm2, Terminal.app, Windows Terminal, Alacritty, VS Code terminal - [ ] Cleanup on SIGINT (restore cursor, clear buffer) - [ ] Keep animation under 3 seconds (respect user's time) - [ ] Web: add gradient fade, throttle to 30fps max