Skip to main content

ascii-motion:制作独立 HTML 字符动画

在字符网格上制作原创动画,并打包成独立 HTML 页面,支持彩色半色调场景、单色明暗形体、线条角色和粒子效果。

来源信息

仓库
zgyk4167/ascii-motion
最近来源活动
2026年10月8日 07:59
检测到的 SKILL.md 语言
英语
星标
0
分支
0

相关案例

作者的发布帖附有彩色花朵场景预览,以字符点呈现画面。

用途

制作前选择视觉类型、主题和氛围;文档支持独立页面,也支持放入已有网站的代码片段。

必要前提

Agent 需要能够写文件并运行 Python 3;验证阶段可选用无头 Chromium 浏览器。

操作说明

提供风格、主题、氛围和交付方式。流程先设计光照与运动,基于所附运行时编写新场景,再将场景和运行时打包为一个 HTML 文件。

限制

技术及部分运行时代码按 MIT 许可改编自 ascii.rest;场景应重新创作,不直接复制参考作品。文档中的验证用于检查渲染和运动,本页没有对生成动画作运行认证。

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
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 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看