ferris wheel
[github]a wheel turning on its a-frame, cabins hanging upright
objects 64×26 10 fps 4.9 kB
...*--o---... ..-''' ╭─┴─╮ ''*-.. .-' ╰───╯ '-. .o' · 'o. .╭─┴─╮· · ·╭─┴─╮. ./╰───╯··· · ···╰───╯\. .* ·· · ·· \. | · · · | | ·· · ·· | | ·· · ·· * o ················ (@) ················ o ╭─┴─╮ /··/ \··\ ╭─┴─╮ ╰───╯ /· / · \ ·\ ╰───╯ | /──/ · \──\ | '\ ··/ / · \ \· *' '\ ··· / / · \ \ ··· /' '\. ··· /─/ · \─\ ··· ./' 'o. / / · \ \ .o' ╭─┴─╮./ / · \ \.╭─┴─╮ ╰───╯/──/.. · ..\──\╰───╯ / / '''---o--*''' \ \ / / ╭─┴─╮ \ \ /──/ ╰───╯ \──\ / / ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ \ \ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ...*--o---... ..-''' ╭─┴─╮ ''*-.. .-' ╰───╯ '-. .o' · 'o. .╭─┴─╮· · ·╭─┴─╮. ./╰───╯··· · ···╰───╯\. .* ·· · ·· \. | · · · | | ·· · ·· | | ·· · ·· * o ················ (@) ················ o ╭─┴─╮ /··/ \··\ ╭─┴─╮ ╰───╯ /· / · \ ·\ ╰───╯ | /──/ · \──\ | '\ ··/ / · \ \· *' '\ ··· / / · \ \ ··· /' '\. ··· /─/ · \─\ ··· ./' 'o. / / · \ \ .o' ╭─┴─╮./ / · \ \.╭─┴─╮ ╰───╯/──/.. · ..\──\╰───╯ / / '''---o--*''' \ \ / / ╭─┴─╮ \ \ /──/ ╰───╯ \──\ / / ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ \ \ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
use it
html
<script type="module" src="https://ascii.rest/ascii.js"></script> <ascii-art piece="ferris-wheel"></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 { ferrisWheel } from "ascii.rest/pieces";
<Ascii piece={ferrisWheel} /> A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="ferris-wheel", to fetch the piece only when it mounts.
astro
--- // npm install github:bas3line/ascii import Ascii from "ascii.rest/astro"; --- <Ascii piece="ferris-wheel" />
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 { ferrisWheel } from "ascii.rest/pieces";
const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, ferrisWheel); 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.
/*
* ferris wheel: a wheel of eight spokes turning slowly on a braced A-frame,
* its cabins hanging upright from the rim and passing the boarding deck.
*/
import type { Frame, Meta } from "../types.ts";
export const meta = {
name: "ferris wheel",
category: "objects",
note: "a wheel turning on its a-frame, cabins hanging upright",
cols: 64,
rows: 26,
fps: 10,
} satisfies Meta;
const N = 8; // spokes, one cabin at the end of each
const R = 10; // rim radius, in rows
const TURN = 30; // seconds per revolution
const BULBS = 24; // lights round the rim
export default function ferrisWheel(): Frame {
const { cols, rows } = meta;
const cx = 32.5, cy = 11.5; // the hub, in cells
const ground = rows - 1;
const out: string[] = new Array(cols * rows);
const put = (c: number, r: number, g: string) => {
c = Math.floor(c), r = Math.floor(r);
if (c >= 0 && c < cols && r >= 0 && r < rows) out[r * cols + c] = g;
};
const text = (c: number, r: number, s: string) => [...s].forEach((g, i) => g !== " " && put(c + i, r, g));
// The rim, traced once: each cell it crosses gets the glyph for the
// slope and height of the arc inside it, and its upright sides take one
// cell a row.
const acc = new Map<number, number[]>();
const side = R * 0.3;
for (let r = Math.ceil(cy - side - 0.5); r + 0.5 < cy + side; r++) {
const w = 2 * Math.sqrt(R * R - (r + 0.5 - cy) ** 2);
for (const c of [cx - w, cx + w]) acc.set(r * cols + Math.floor(c), [1, 0.5, 1, 0]);
}
for (let i = 0; i < 2000; i++) {
const p = (i / 2000) * 2 * Math.PI;
const x = cx + 2 * R * Math.cos(p), y = cy + R * Math.sin(p);
if (Math.abs(Math.floor(y) + 0.5 - cy) < side) continue;
const k = Math.floor(y) * cols + Math.floor(x);
const e = acc.get(k) || [0, 0, 0, 0];
(e[0] += 1), (e[1] += y - Math.floor(y)), (e[2] += Math.abs(Math.cos(p))), (e[3] += Math.sin(p) * Math.cos(p) < 0 ? 1 : 0);
acc.set(k, e);
}
// Only the fullest slanted cell on each row and side keeps its slant.
const most = new Map<number, number>();
const phiOf = ([n, , c]: number[]): number => (Math.atan2(c / n, Math.sqrt(Math.max(0, 1 - (c / n) ** 2))) * 180) / Math.PI;
const id = (k: number): number => Math.floor(k / cols) * 2 + (k % cols < cx ? 0 : 1);
// A row and side not in most yet looks up undefined, which acc never holds.
for (const [k, e] of acc) if (phiOf(e) > 50 && phiOf(e) <= 72 && e[0] > (acc.get(most.get(id(k)) as number)?.[0] ?? 0)) most.set(id(k), k);
const rim = [...acc].map(([k, e]): [number, string] => {
const [n, fy, , fall] = e, phi = phiOf(e);
return [k, phi > 72 ? "|" : most.get(id(k)) === k ? (fall / n > 0.5 ? "\\" : "/") : fy / n < 0.38 ? "'" : fy / n > 0.62 ? (phi > 10 ? "." : "_") : "-"];
});
// The A-frame: a pair of legs a side running from the axle down to the
// ground, one cell a row, with rungs between each pair.
const frame = new Map<number, string>();
const top = Math.ceil(cy), slope = 0.9;
for (const s of [-1, 1]) {
for (let r = top; r < ground; r++) {
const d = 1.2 + slope * (r - top);
const a = Math.floor(cx + s * d), b = Math.floor(cx + s * (d + 2.6));
frame.set(r * cols + a, s < 0 ? "/" : "\\");
frame.set(r * cols + b, s < 0 ? "/" : "\\");
if ((r - top) % 3 === 2) for (let c = Math.min(a, b) + 1; c < Math.max(a, b); c++) frame.set(r * cols + c, "─");
}
}
return (t) => {
const a = (2 * Math.PI * t) / TURN;
out.fill(" ");
const ends: [x: number, y: number][] = [];
for (let k = 0; k < N; k++) {
const th = a + (k * 2 * Math.PI) / N;
const x = cx + 2 * R * Math.cos(th), y = cy + R * Math.sin(th);
// Spokes, dotted from the hub to just short of the rim.
for (let i = 3; i < 2 * R - 1; i++) put(cx + i * Math.cos(th), cy + (i / 2) * Math.sin(th), "·");
ends.push([x, y]);
}
for (const [k, g] of rim) out[k] = g;
// Bulbs on the rim between the cabins, every other one lit in turn.
const blink = Math.floor(t / 0.6) % 2;
for (let k = 0; k < BULBS; k++) {
if (k % (BULBS / N) === 0) continue;
const th = a + (k * 2 * Math.PI) / BULBS + Math.PI / BULBS;
if ((k + blink) % 2) put(cx + 2 * R * Math.cos(th), cy + R * Math.sin(th), "*");
}
for (const [k, g] of frame) out[k] = g;
text(cx - 1.5, cy, "(@)");
// Cabins hang from each spoke end, always upright.
for (const [x, y] of ends) {
const c = Math.floor(x), r = Math.floor(y);
put(c, r, "o");
text(c - 2, r + 1, "╭─┴─╮");
text(c - 2, r + 2, "╰───╯");
}
// The boarding deck under the lowest cabin, and the ground.
text(cx - 7.5, ground - 1, "▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄");
for (let c = 0; c < cols; c++) put(c, ground, "▀");
const lines: string[] = [];
for (let r = 0; r < rows; r++) lines.push(out.slice(r * cols, (r + 1) * cols).join(""));
return lines.join("\n");
};
}