train
[github]a steam train crossing on its track, smoke puffing behind
objects 76×26 15 fps 6.8 kB
..::::... :::::::::. .:::::....:::::::::::. ::: ..........::::::::::.:------:: ........ .::::::::.:--------:. :--: ........ ::-----:::.-=====-: .::::::...-====--: .. ::--:::. . =++= -=+==: _____ \ / _________ .-. _ | | \_______/_____/ \| |_____| |__[] | ___ | || || || \ _ || || || || || ) _\ _.-^^^^-._ ||___|| || || || ) | |^^^^^^^^^^| | |__||______||______||____/ || | | |_____|=========================[] || | | | | .---. .---. |\ _|= |__________|= |_| / | \ / | \ ____ |\\ | |=|=||=|=| |--(o)-o|=|==(o)=o|==#|____| |\\\ . .-..-..-..-. \ | / \ | / | \\\ ) (x)(x)(x)(x) '---' '---' |__\\\ ============================================================================ |__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__| ':'.,'.: . .' : , .. . , .: . : ,,:.. .,: ..:: :,: ..':., ' .'.' '..,..::::... :::::::::. .:::::....:::::::::::. ::: ..........::::::::::.:------:: ........ .::::::::.:--------:. :--: ........ ::-----:::.-=====-: .::::::...-====--: .. ::--:::. . =++= -=+==: _____ \ / _________ .-. _ | | \_______/_____/ \| |_____| |__[] | ___ | || || || \ _ || || || || || ) _\ _.-^^^^-._ ||___|| || || || ) | |^^^^^^^^^^| | |__||______||______||____/ || | | |_____|=========================[] || | | | | .---. .---. |\ _|= |__________|= |_| / | \ / | \ ____ |\\ | |=|=||=|=| |--(o)-o|=|==(o)=o|==#|____| |\\\ . .-..-..-..-. \ | / \ | / | \\\ ) (x)(x)(x)(x) '---' '---' |__\\\ ============================================================================ |__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__| ':'.,'.: . .' : , .. . , .: . : ,,:.. .,: ..:: :,: ..':., ' .'.' '..,
use it
html
<script type="module" src="https://ascii.rest/ascii.js"></script> <ascii-art piece="train"></ascii-art>
No install and no build step. ascii.js defines the <ascii-art> tag, loads the piece from ascii.rest, plays it while it is on screen, and holds the first frame for anyone who prefers reduced motion.
react
// npm install github:bas3line/ascii
import { Ascii } from "ascii.rest/react";
import { train } from "ascii.rest/pieces";
<Ascii piece={train} /> A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="train", to fetch the piece only when it mounts.
astro
--- // npm install github:bas3line/ascii import Ascii from "ascii.rest/astro"; --- <Ascii piece="train" />
Renders the first frame on the server, so the page is whole before any script runs, then plays the piece once it loads.
typescript
// npm install github:bas3line/ascii
import { mount } from "ascii.rest";
import { train } from "ascii.rest/pieces";
const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, train); mount plays a piece in a <pre>, or on a <canvas> for the coloured ones, and returns a function that stops it. Every piece, option and frame is typed.
/*
* train: a steam train crossing the frame on its track, a coach and tender
* behind the engine. Smoke puffs from the stack in time with the beat of the
* wheels, whose rods turn as they roll; it leaves on the right and comes back.
*/
import type { Frame, Meta } from "../types.ts";
export const meta = {
name: "train",
category: "objects",
note: "a steam train crossing on its track, smoke puffing behind",
cols: 76,
rows: 26,
fps: 15,
} satisfies Meta;
// The train side on as blocks of [top row, left column, lines]. Spaces are see-through.
const S = String.raw;
const PARTS: [number, number, string[]][] = [
[14, 0, [
S` ________________ `,
S`/________________\ `,
S`| __ __ __ __ | `,
S`|| || || || || `,
S`||__||__||__||__|| `,
S`|________________|=`,
S` |=|=| |=|=| `,
]],
[15, 19, [
S` _.-^^^^-._ `,
S` |^^^^^^^^^^| `,
S` | | `,
S` | | `,
S` |__________|=`,
S` |=|=||=|=| `,
]],
[9, 33, [
S` _____ `,
S` \ / `,
S` _________ .-. _ | | `,
S` \_______/_____/ \| |_____| |__[]`,
S` | ___ | || || || \ `,
S` || || || || || )`,
S` ||___|| || || || )`,
S` | |__||______||______||____/ `,
S` |_____|=========================[]`,
S` | | |\ `,
S` |_| ____ |\\ `,
S` |____| |\\\ `,
S` | \\\ `,
S` |__\\\ `,
]],
];
const LEN = 73; // the train's length, rear buffer to the tip of the cowcatcher
const RAIL = 23;
const SPEED = 8; // columns a second
const DRIVE = [42, 52]; // the coupled wheels' centre columns
const SMALL = [3, 6, 12, 15, 22, 25, 28, 31]; // the small wheels' columns
const CROSS = 58; // where the main rod meets the crosshead, mid stroke
const CYL = 60; // the back of the cylinder
const STACK = 62; // the chimney's column
const RIM = 2.5; // the driving wheels' radius, in rows
const LIFE = 5.5; // seconds the smoke lasts
const WIND = 2.4; // columns a second it drifts back
const SMOKE = " .:-=+*"; // thin to thick
// A driving wheel: the rim, then the spokes square on and an eighth of a turn on.
const WHEEL = [S` .---. `, S` / \ `, S`| (o) |`, S` \ / `, S` '---' `];
const SPOKES = [[S` | `, S` -- -- `, S` | `], [S` \ / `, S` `, S` / \ `]];
const hash = (x: number, y: number) => {
let h = Math.imul(x, 0x27d4eb2d) ^ Math.imul(y, 0x165667b1);
h = Math.imul(h ^ (h >>> 15), 0x85ebca6b);
return ((h ^ (h >>> 13)) >>> 0) / 4294967296;
};
export default function train(): Frame {
const { cols, rows } = meta;
const SPAN = cols + LEN + 1; // a lap: in from the left, across and out
const X0 = LEN - 16; // at t = 0 the engine and tender are in, the coach coming
const out: string[] = new Array(cols * rows);
const put = (r: number, c: number, ch: string) => {
if (r >= 0 && r < rows && c >= 0 && c < cols) out[r * cols + c] = ch;
};
const text = (r: number, c: number, s: string) => [...s].forEach((ch, i) => ch !== " " && put(r, c + i, ch));
const left = (t: number) => Math.floor((((X0 + SPEED * t) % SPAN) + SPAN) % SPAN) - LEN; // the train's left edge
const OMEGA = SPEED / (2 * RIM); // radians a second the wheels turn
const BEAT = Math.PI / 2 / OMEGA; // four beats to a turn
const DT = BEAT / 4; // smoke is let go in four puffs a beat, the first the strongest
// The track and the ballast under it, which never move.
const track: [number, number, string][] = [];
for (let c = 0; c < cols; c++) {
track.push([RAIL, c, "="], [RAIL + 1, c, c % 3 ? "_" : "|"]);
const h = hash(c, 5);
track.push([RAIL + 2, c, h < 0.16 ? ":" : h < 0.34 ? "," : h < 0.44 ? "'" : h < 0.7 ? "." : " "]);
}
const dens = new Float32Array(cols * rows);
return (t) => {
out.fill(" ");
const ox = left(t);
// Smoke: let go from the stack, left where it was let go, rising, swelling
// and thinning as it drifts back on the wind.
dens.fill(0);
for (let k = Math.ceil((t - LIFE) / BEAT); k * BEAT <= t; k++) {
const age = t - k * BEAT, u = age / LIFE;
const x = left(k * BEAT) + STACK - WIND * age + 0.8 * Math.sin(k * 1.7);
const y = 8.4 - 7.2 * (1 - Math.exp(-age / 2)) + 0.5 * Math.sin(k * 2.9);
const rad = 1.5 + 0.75 * age ** 0.8, w = (1 - u) ** 1.2;
for (let r = Math.floor(y - rad); r <= y + rad; r++)
for (let c = Math.floor(x - 2 * rad); c <= x + 2 * rad; c++) {
if (r < 0 || r >= rows || c < 0 || c >= cols) continue;
const dx = (c + 0.5 - x) / 2 / rad, dy = (r + 0.5 - y) / rad, q = dx * dx + dy * dy;
if (q >= 1) continue;
// Lit from above, so each billow keeps its round top where it overlaps the last.
const v = w * Math.sqrt(1 - q) * (0.6 - 0.4 * dy - 0.1 * dx);
const j = r * cols + c;
if (v > dens[j]) dens[j] = v;
}
}
for (let k = 0; k < cols * rows; k++) {
const v = dens[k];
if (v > 0.08) out[k] = SMOKE[Math.min(SMOKE.length - 1, Math.ceil(v * 7))];
}
for (const [r, c, ch] of track) put(r, c, ch);
for (const [r0, c0, lines] of PARTS) lines.forEach((s, i) => text(r0 + i, ox + c0, s));
// The wheels turn as far as they roll.
const a = OMEGA * t;
const eighth = Math.floor(a / (Math.PI / 4)) % 2;
for (const c of DRIVE) {
WHEEL.forEach((s, i) => text(RAIL - 5 + i, ox + c - 4, s));
SPOKES[eighth].forEach((s, i) => text(RAIL - 4 + i, ox + c - 4, s));
}
const roll = Math.floor((SPEED * t) / 1.4) % 2;
for (const c of SMALL) {
text(RAIL - 2, ox + c - 1, ".-.");
text(RAIL - 1, ox + c - 1, roll ? "(+)" : "(x)");
}
// The crank pins go round together with the rod between them, and the main
// rod runs on to the crosshead. Rods pass behind the rims and the hubs.
const hub = RAIL - 3, dr = -Math.round(1.2 * Math.sin(a));
const dc = Math.round((dr ? 2.4 : 3) * Math.cos(a)), pr = hub + dr;
const free = (r: number, c: number) => DRIVE.every((d) => Math.abs(c - d) !== (r === hub ? 4 : 3) && (r !== hub || Math.abs(c - d) > 1));
for (let c = DRIVE[0] + dc; c <= DRIVE[1] + dc; c++)
if (free(pr, c)) put(pr, ox + c, c === DRIVE[0] + dc || c === DRIVE[1] + dc ? "o" : "=");
const x0 = DRIVE[1] + dc, xh = CROSS + Math.round(Math.cos(a));
for (let c = x0 + 1; c < xh; c++) {
const r = Math.round(pr + ((hub - pr) * (c - x0)) / (xh - x0));
if (free(r, c)) put(r, ox + c, r === pr ? "=" : "-");
}
put(hub, ox + xh, "#");
for (let c = xh + 1; c < CYL; c++) put(hub, ox + c, "=");
const lines: string[] = [];
for (let r = 0; r < rows; r++) lines.push(out.slice(r * cols, (r + 1) * cols).join(""));
return lines.join("\n");
};
}