spider
[github]a spider lowering itself on silk, then climbing back up
creatures 28×24 15 fps 5.1 kB
/ \ | / \ / \__|__/ \ / /\ | /\ \ / / \|/ \ \ _____/____*____\_____\_ \ /|\ / / \ \ | / / \ | .-|-. / / \ (#####) / ___\_ `---' _/___ / \(@@@)/ | ___/ `V' \___ | / \ \ / | / \ | / \ / \__|__/ \ / /\ | /\ \ / / \|/ \ \ _____/____*____\_____\_ \ /|\ / / \ \ | / / \ | .-|-. / / \ (#####) / ___\_ `---' _/___ / \(@@@)/ | ___/ `V' \___ | / \ \ / |
use it
html
<script type="module" src="https://ascii.rest/ascii.js"></script> <ascii-art piece="spider"></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 { spider } from "ascii.rest/pieces";
<Ascii piece={spider} /> A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="spider", to fetch the piece only when it mounts.
astro
--- // npm install github:bas3line/ascii import Ascii from "ascii.rest/astro"; --- <Ascii piece="spider" />
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 { spider } from "ascii.rest/pieces";
const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, spider); 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.
/*
* spider: a spider drops from the hub of its web in the corner on a line of
* silk, bounces at the end of it with its legs going, then climbs back up.
*/
import type { Frame, Meta } from "../types.ts";
export const meta = {
name: "spider",
category: "creatures",
note: "a spider lowering itself on silk, then climbing back up",
cols: 28,
rows: 24,
fps: 15,
} satisfies Meta;
const LOOP = 10;
const HUB = [10, 4]; // the middle of the web, in columns and rows; the walls cut its outer ring
const RINGS = [3, 6]; // hexagonal rings, by how many rows they sit from the hub
const TOP = 3, LOW = 15.6; // rows the sprite's top hangs at, on the web and at the end of the line
// The left half of the spider, head down, in two leg poses; the right half
// is the mirror. A side in one pose and the other side in the other is a
// step, so both halves keep the same reach.
const POSES = [
[" \\ .-", " \\ (##", " ___\\_ `-", " | \\(@", " ___/ `", " / / ", " | "],
[" | .-", " \\ (##", " ___\\_ `-", "/ \\(@", " ___/ `", " | / ", " / "],
];
const MID = "|#-@V ";
const FLIP: Record<string, string> = { "/": "\\", "\\": "/", "(": ")", ")": "(", "`": "'" };
const sprite = (l: number, r: number) => POSES[l].map((s, j) => s + MID[j] + [...POSES[r][j]].reverse().map((c) => FLIP[c] || c).join(""));
const SPRITES = [sprite(0, 0), sprite(0, 1), sprite(1, 0)];
const W = SPRITES[0][0].length, H = SPRITES[0].length;
const ease = (x: number) => (x <= 0 ? 0 : x >= 1 ? 1 : x * x * (3 - 2 * x));
export default function spider(): Frame {
const { cols, rows } = meta;
const out: string[] = new Array(cols * rows);
const set = (c: number, r: number, ch: string) => {
if (c >= 0 && c < cols && r >= 0 && r < rows) out[r * cols + c] = ch;
};
// The web never changes, so it is drawn once and copied under each frame:
// hexagonal rings, then eight spokes over them, out to the ceiling and the
// wall above and to the left and just past the last ring elsewhere.
out.fill(" ");
const [hc, hr] = HUB, K = RINGS[RINGS.length - 1];
for (const k of RINGS) {
for (let c = hc - k + 1; c < hc + k; c++) set(c, hr - k, "_"), set(c, hr + k - 1, "_");
for (const s of [-1, 1]) {
for (let i = 1; i <= k; i++) set(hc + s * (k + i - 1), hr - k + i, s > 0 ? "\\" : "/");
for (let i = 1; i < k; i++) set(hc + s * (2 * k - i), hr + i, s > 0 ? "/" : "\\");
}
}
for (let j = 1; j < cols; j++) {
set(hc, hr - j, "|");
if (j <= K) set(hc, hr + j, "|");
for (const s of [-1, 1]) {
set(hc + s * j, hr - j, s > 0 ? "/" : "\\");
if (j <= K) set(hc + s * j, hr + j, s > 0 ? "\\" : "/");
const k = hr * cols + hc + s * j;
if ((s < 0 ? hc - j >= 0 : j <= 2 * K) && out[k] === " ") out[k] = "_";
}
}
set(hc, hr, "*");
const web = out.slice();
// A straight thread from the hub to the spinnerets: one glyph a row.
const silk = (x0: number, y0: number, x1: number, y1: number) => {
const k = (x1 - x0) / (y1 - y0);
const g = Math.abs(k) < 0.3 ? "|" : k > 0 ? "\\" : "/";
for (let r = Math.round(y0); r < Math.round(y1); r++) set(Math.floor(x0 + k * (r + 0.5 - y0)), r, g);
};
return (t) => {
const u = (((t + 2.4) % LOOP) + LOOP) % LOOP;
// Where the sprite's top row is, and how fast the legs go.
let y = TOP, rate = 0, climb = -1;
if (u >= 1 && u < 3.4) (y = TOP + (LOW - TOP) * ((u - 1) / 2.4) ** 2), (rate = 3);
else if (u >= 3.4 && u < 6) {
// The line stops paying out and the silk springs back: it leaves at the speed it fell.
const s = u - 3.4, w = 6, v = (2 * (LOW - TOP)) / 2.4;
(y = LOW + (v / w) * Math.exp(-s * 1.3) * Math.sin(s * w)), (rate = s < 1.8 ? 6 : 0);
} else if (u >= 6 && u < 9.4) {
// Hand over hand: a lurch up with each pull, then a pause.
const pull = ((u - 6) / 3.4) * 8;
y = LOW - (LOW - TOP) * ((Math.floor(pull) + ease((pull % 1) * 1.6)) / 8);
climb = Math.floor(pull) & 1;
}
const sway = u > 3 && u < 7 ? 0.8 * Math.sin((u - 3) * 2.1) * Math.exp(-(u - 3) * 0.6) : 0;
const r0 = Math.round(y), c0 = hc - (W >> 1) + Math.round(sway);
// Legs step between poses, the two sides out of turn with each other.
const pose = climb >= 0 ? 1 + climb : rate ? 1 + (Math.floor(u * rate) & 1) : u > 0.3 && u < 0.55 ? 1 : 0;
const spr = SPRITES[pose];
for (let i = 0; i < out.length; i++) out[i] = web[i];
// Clear a cell's width of web round every mark, so no thread touches a leg.
for (let j = 0; j < H; j++)
for (let i = 0; i < W; i++)
if (spr[j][i] !== " ") for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) set(c0 + i + di, r0 + j + dj, " ");
// Off the hub, the line hangs where the lower spoke ran.
if (r0 > hr) {
for (let j = 1; j <= K; j++) if (out[(hr + j) * cols + hc] === "|") out[(hr + j) * cols + hc] = " ";
silk(hc + 0.5, hr + 1, c0 + (W >> 1) + 0.5, r0);
}
for (let j = 0; j < H; j++) for (let i = 0; i < W; i++) if (spr[j][i] !== " ") set(c0 + i, r0 + j, spr[j][i]);
const lines: string[] = [];
for (let i = 0; i < rows; i++) lines.push(out.slice(i * cols, (i + 1) * cols).join(""));
return lines.join("\n");
};
}