- name
- ascii-motion
- description
- Make original animated ASCII art as a self-contained web page, in the ascii.rest style — halftone colour scenes, shaded mono 3D/fields, line-drawn sprites, particle sims, type/UI. Use for ASCII art, character art, halftone or dot-matrix animation, text-mode animation, or "make something like ascii.rest". Requests may come in any language. Always ask which style first.
- license
- MIT (portions adapted from ascii.rest, MIT; see THIRD_PARTY_NOTICES.md)
- compatibility
- Any agent that can write files and run python3. Headless Chromium (Playwright or Puppeteer) is optional, for the verify step.
- metadata
- {"version":"1.0.0"}
# ASCII Motion
Original, animated ASCII pieces drawn on a character grid, rendered to a `<canvas>` (colour) or `<pre>` (one ink). The craft follows ascii.rest by @bas3line (MIT): every piece is a pure function `frame(t) → text`, the colour is a palette index per cell, and a tiny runtime plays it. You write new pieces with this technique. You do not copy theirs.
## Step 1 — Ask before building (mandatory)
Before writing any code, ask the user these questions, in the user's language, all in one turn. If the agent has a question tool (for example AskUserQuestion in Claude Code), use it. Otherwise, ask in chat as a short numbered list with the options, and wait for the reply. Skip a question only if the user's message already answers it.
**Q1 Style (header "Style"):** offer the four main families. Mention in the question text that type/UI/data and logo glint are also available (through "Other" in a question tool, or as extra choices in chat).
| Option | Look | Reference pieces on ascii.rest |
|---|---|---|
| Halftone scene | full-colour landscape in dots ` ·•●`, 200×100, cinematic light | night-coast, ocean-sunset, kyoto-dusk, deep-reef |
| Shaded mono | one ink, brightness ramp `.,-~:;=!*#$@`, lit 3D forms or maths fields | donut, plasma, black-hole, mandelbrot |
| Line sprite | hand-drawn characters `/\_()'` with timed poses: creatures, objects | owl, cat, whale, train, windmill |
| Particles & sim | many small marks moving by simple physics | fireworks, rain, falling-sand, flow-field |
**Q2 Subject & mood (header "Subject"):** offer three or four concrete subjects that fit the chosen style. If the style is still unknown, offer neutral ones: night sea, mountain dawn, a creature, an abstract form. Each option description names the light source and the one motion, e.g. "moonlight; slow swell + drifting cloud".
**Q3 Delivery (header "Delivery"):** a standalone HTML file (default), a published Artifact page (only where the agent can publish one), or a drop-in `<script>`+element snippet for the user's own site.
If the session is unattended, choose the style that best fits the request, state it in one line, and proceed.
## Step 2 — Design on paper (one short paragraph, internal)
Decide these before coding. They are what taste comes from:
1. **One light story.** Name the key light (moon, sun, lamp, lantern) and where it sits. Everything else is lit *from* it: halos, rim light on the facing edge, reflections, a road of glints on water.
2. **Two or three silhouettes** big enough to read as a thumbnail. Silhouettes are *absence* of dots (low `floor`), not dark dots.
3. **One primary motion, one or two secondary ones.** Examples: a beam turns, clouds drift, water swells. Never animate everything.
4. **A loop.** Make periods whole divisors of one loop (e.g. 8 s, 12 s, 30 s) so nothing jumps.
5. **The palette:** 25–46 hand-picked hexes in ramps (sky blues dark→light, warm lamp 3–4 steps, land greens/greys, one accent). Ground colour ≈ the darkest sky, not pure black.
## Step 3 — Build with the kit
Copy `scripts/ascii-kit.js` from this skill (the same code is below; write it out verbatim if the file is not reachable) and write one piece file into the working directory, then bundle them into a single self-contained HTML file with `bundle.py`. Keep the piece contract exact; it is also compatible with ascii.rest's own `mount`.
**Piece contract**
```js
export const meta = { name, note, cols, rows, fps, cell /* 1 = square cells (halftone), 2 = text cells */, palette /* colour pieces only */, ground, options };
export default function make(options) { /* build static layers once */ return (t, { paper, color }) => text; }
```
- `t` is seconds. `paper` is true on a light page: flip ramps so dense ink means dark. `color` is a `Uint8Array(cols*rows)`; write palette indices into it.
- Precompute everything static (terrain masks, sprite strings, cloud fields that wrap with `noise(..., period)`) in `make`, outside the frame function. Frames must be cheap: 200×100 at 15 fps.
### ascii-kit.js
```js
// ascii-kit: runtime + helpers for animated ASCII pieces.
// Part of ascii-motion (MIT License, Copyright (c) 2026 ascii-motion contributors).
// mount(), halftone(), hash() and noise() are adapted from ascii.rest by bas3line
// (https://github.com/bas3line/ascii), used under this license:
//
// MIT License
//
// Copyright (c) 2026 bas3line (https://github.com/bas3line)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Piece contract (compatible with ascii.rest, MIT):
// export const meta = { name, note, cols, rows, fps, cell?, palette?, ground?, options? }
// export default (options) => (t, { paper, color }) => string // rows joined by "\n"
// Coloured pieces write a palette index per cell into color[y*cols+x].
// ---------- math ----------
export const clamp = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
export const smooth = (a, b, v) => { const k = clamp((v - a) / (b - a)); return k * k * (3 - 2 * k); };
export const mix = (a, b, k) => a + (b - a) * k;
export const ease = (x) => (x <= 0 ? 0 : x >= 1 ? 1 : x * x * (3 - 2 * x));
export function hash(x, y) {
let h = Math.imul(x | 0, 374761393) + Math.imul(y | 0, 668265263);
h = Math.imul(h ^ (h >>> 13), 1274126177);
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
}
// value noise; wraps every `period` lattice cells in x when period > 0 (for endless drift)
export function noise(x, y, period = 0) {
const xi = Math.floor(x), yi = Math.floor(y), fx = x - xi, fy = y - yi;
const u = fx * fx * (3 - 2 * fx), v = fy * fy * (3 - 2 * fy);
let x0 = xi, x1 = xi + 1;
if (period) { x0 = ((xi % period) + period) % period; x1 = (x0 + 1) % period; }
const a = hash(x0, yi), b = hash(x1, yi), c = hash(x0, yi + 1), d = hash(x1, yi + 1);
return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
}
export function fbm(x, y, oct = 4, period = 0) {
let s = 0, n = 0, amp = 0.5, f = 1;
for (let i = 0; i < oct; i++) { s += amp * noise(x * f, y * f, period * f); n += amp; amp *= 0.5; f *= 2; }
return s / n;
}
// ---------- halftone scenes (cell: 1, ~200×100, " ·•●") ----------
const DOTS = " ·•●", COVER = [0, 0.3, 0.6, 1];
const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5].map((v) => v / 16 - 0.47);
const hex = (s) => [1, 3, 5].map((i) => parseInt(s.slice(i, i + 2), 16) / 255);
// shade(s, x, y, t, col, row): set s.r s.g s.b (linear-ish 0..1+), optionally s.floor (min dot level), s.fade.
export function halftone(meta, shade) {
const W = meta.cols, H = meta.rows, P = meta.palette.map(hex);
const lut = new Uint8Array(32768).fill(255);
const nearest = (r, g, b) => {
const k = (Math.min(31, (r * 31.99) | 0) << 10) | (Math.min(31, (g * 31.99) | 0) << 5) | Math.min(31, (b * 31.99) | 0);
if (lut[k] !== 255) return lut[k];
let best = 0, bd = 1e9;
for (let i = 0; i < P.length; i++) {
const dr = P[i][0] - r, dg = P[i][1] - g, db = P[i][2] - b, d = 0.3 * dr * dr + 0.5 * dg * dg + 0.2 * db * db;
if (d < bd) (bd = d), (best = i);
}
return (lut[k] = best);
};
const out = new Array(W), lines = new Array(H), s = { r: 0, g: 0, b: 0, floor: 0.3, fade: 1 };
return (t, { color } = {}) => {
for (let r = 0; r < H; r++) {
for (let x = 0; x < W; x++) {
s.r = s.g = s.b = 0; s.floor = 0.3; s.fade = 1;
shade(s, x + 0.5, r + 0.5, t, x, r);
// dot size from brightness, dithered; colour makes up what dot size could not
const peak = Math.max(s.r, s.g, s.b, 1e-4);
const level = clamp(s.floor + (1 - s.floor) * Math.pow(peak, 0.85) * 0.95) * s.fade;
const step = Math.max(0, Math.min(3, Math.round(level * 3 + BAYER[(r & 3) * 4 + (x & 3)])));
out[x] = DOTS[step];
if (color) {
const want = step ? Math.min(1, (level + 0.06) / COVER[step]) : 0, k = (0.3 + 0.7 * want) / peak;
color[r * W + x] = nearest(clamp(s.r * k), clamp(s.g * k), clamp(s.b * k));
}
}
lines[r] = out.join("");
}
return lines.join("\n");
};
}
// ---------- mono ramp fields (cell: 2, text cells are ~2× taller than wide) ----------
// field(x, y, t) → 0..1 brightness, or -1 for empty. x,y in cell widths from centre (y already ×2).
export function ramp(meta, field, RAMP = ".,-~:;=!*#$@") {
const { cols, rows } = meta, N = RAMP.length, out = new Array(cols), lines = new Array(rows);
return (t, { paper = false } = {}) => {
for (let r = 0; r < rows; r++) {
const y = (r + 0.5 - rows / 2) * 2;
for (let c = 0; c < cols; c++) {
const v = field(c + 0.5 - cols / 2, y, t);
if (v < 0) { out[c] = " "; continue; }
const i = Math.min(N - 1, Math.floor(clamp(v) * N));
out[c] = RAMP[paper ? N - 1 - i : i]; // light page: dense ink = dark, so flip
}
lines[r] = out.join("");
}
return lines.join("\n");
};
}
// ---------- sprites ----------
export const FLIP = { "/": "\\", "\\": "/", "(": ")", ")": "(", "<": ">", ">": "<", "[": "]", "]": "[", "{": "}", "}": "{", "`": "'", "'": "`" };
export const mirror = (half, w = Math.max(...half.map((l) => l.length))) =>
half.map((l) => { l = l.padEnd(w); return l + [...l.slice(0, w - 1)].reverse().map((c) => FLIP[c] || c).join(""); });
export function grid(cols, rows) {
const g = Array.from({ length: rows }, () => new Array(cols).fill(" "));
return {
g,
在 GitHub 查看