aquarium
[github]fish swimming and turning in a tank, bubbles and weed
creatures 60×20 15 fps 6.0 kB
┌──────────────────────────────────────────────────────────┐ │~~~~~~-----~~~~~--------~~~~-----~~~~~~--------~~--------~│ │ │ │ O │ │ ><(((°> O │ │ │ │ | │ │ | ><> o │ │ ><> / /\ │ │ \ | ><> ( <° )>< │ │ \ | ( \/ \ │ │ ) | ( /_.-._ |( ) │ │ )|( (° )>< |( . ) | │ │ )|( |( `-.-' ) |( / ) | │ │ )( |( ) / | / . ( │ │ ( | |/ |/ .--._ | │ │ | | <°)>< ( ( ( . `) | │ │...·,,.....,·,...,···,.....,,,·.,...,.··,....·...···..,·.·│ │ .O. .o O O. o o . o. ° oOo.O oo . o o o│ └──────────────────────────────────────────────────────────┘┌──────────────────────────────────────────────────────────┐ │~~~~~~-----~~~~~--------~~~~-----~~~~~~--------~~--------~│ │ │ │ O │ │ ><(((°> O │ │ │ │ | │ │ | ><> o │ │ ><> / /\ │ │ \ | ><> ( <° )>< │ │ \ | ( \/ \ │ │ ) | ( /_.-._ |( ) │ │ )|( (° )>< |( . ) | │ │ )|( |( `-.-' ) |( / ) | │ │ )( |( ) / | / . ( │ │ ( | |/ |/ .--._ | │ │ | | <°)>< ( ( ( . `) | │ │...·,,.....,·,...,···,.....,,,·.,...,.··,....·...···..,·.·│ │ .O. .o O O. o o . o. ° oOo.O oo . o o o│ └──────────────────────────────────────────────────────────┘
use it
html
<script type="module" src="https://ascii.rest/ascii.js"></script> <ascii-art piece="aquarium"></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 { aquarium } from "ascii.rest/pieces";
<Ascii piece={aquarium} /> A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="aquarium", to fetch the piece only when it mounts.
astro
--- // npm install github:bas3line/ascii import Ascii from "ascii.rest/astro"; --- <Ascii piece="aquarium" />
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 { aquarium } from "ascii.rest/pieces";
const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, aquarium); 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.
/*
* aquarium: a fish tank. Fish of a few kinds swim its length, drift between
* depths and turn about, bubbles stream up from a stone, and weed sways.
*/
import type { Frame, Meta } from "../types.ts";
export const meta = {
name: "aquarium",
category: "creatures",
note: "fish swimming and turning in a tank, bubbles and weed",
cols: 60,
rows: 20,
fps: 15,
} satisfies Meta;
const LOOP = 48; // seconds; every period below divides it
const FLIP: Record<string, string> = { "<": ">", ">": "<", "(": ")", ")": "(", "/": "\\", "\\": "/", "`": "'", "'": "`", "{": "}", "}": "{" };
// Fish, facing right, with the face-on view shown while they turn, farthest
// first: [art, face-on art, row, from column, to column, seconds there and
// back, phase, rows it drifts up and down over the loop, drift phase].
const FISH: [art: string[], turn: string[], row: number, from: number, to: number, period: number, phase: number, drift: number, dp: number][] = [
[["><>"], ["<o>"], 5.2, 6, 54, 12, 0.8, 1.5, 1],
[["><>"], ["<o>"], 6.6, 3, 51, 12, 0.775, 1.5, 1.1],
[["><>"], ["<o>"], 8, 8, 56, 12, 0.79, 1.5, 0.9],
[["><(((°>"], ["<(°o°)>"], 3.3, 3, 50, 24, 0.1, 1.2, 1],
[["><(°>"], ["(°o°)"], 15.6, 4, 40, 48, 0.55, 0.4, 3],
[[" /\\", " ><( °>", " \\/"], ["/^\\", "(°o°)", "\\v/"], 9.8, 10, 51, 24, 0.35, 0.9, 4],
[[" _.-._", "><( °)", " `-.-'"], [".-^-.", "(°o°)", "`-v-'"], 12.8, 2, 46, 16, 0.6, 0.9, 4],
];
// Clumps of weed, [root column, [height, lean] for each blade, phase].
const WEEDS: [root: number, blades: [height: number, lean: number][], phase: number][] = [
[6, [[11, 0.6], [8, -1.6], [6, 2]], 0],
[13, [[6, 1.2], [4, -1.2]], 1.7],
[25, [[4, -1], [3, 1]], 0.9],
[37, [[9, 1.2], [6, -1.4], [4, 2.2]], 2.4],
[55, [[7, -1.2], [5, 0.8]], 0.4],
];
const STONE: [row: number, col: number, art: string][] = [[15, 43, " .--._"], [16, 42, "( . `)"]];
const NOZZLE = 46; // where the bubbles leave the stone
const hash = (a: number, b: number): number => {
let h = Math.imul(a, 0x27d4eb2d) ^ Math.imul(b, 0x165667b1);
h = Math.imul(h ^ (h >>> 15), 0x85ebca6b);
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
};
const mirror = (art: string[]): string[] => {
const w = Math.max(...art.map((l) => l.length));
return art.map((l) => [...l.padEnd(w)].reverse().map((c) => FLIP[c] || c).join(""));
};
export default function aquarium(): Frame {
const { cols, rows } = meta;
const TAU = Math.PI * 2;
const SURFACE = 1, FLOOR = 17;
const fish = FISH.map(([art, turn, row, from, to, period, phase, drift, dp]) => ({ art, back: mirror(art), turn, row, from, to, period, phase, drift, dp }));
// Gravel: a fine bed with coarser stones lying in it here and there.
const gravel = [0, 1].map((k) => Array.from({ length: cols - 2 }, (_, c) => {
const n = hash(c, k + 3);
return k ? (n < 0.14 ? "o" : n < 0.2 ? "O" : n < 0.27 ? "°" : n < 0.4 ? "." : " ") : n < 0.55 ? "." : n < 0.75 ? "," : "·";
}));
return (t) => {
const u = ((t % LOOP) + LOOP) % LOOP;
const g = Array.from({ length: rows }, () => new Array<string>(cols).fill(" "));
// Art is opaque from its first mark to its last on each line.
const put = (r: number, c: number, s: string) => {
if (r <= SURFACE || r >= rows - 1) return;
for (let i = Math.max(0, s.search(/\S/)); i < s.trimEnd().length; i++) if (c + i > 0 && c + i < cols - 1) g[r][c + i] = s[i];
};
// The tank: glass, rim, and the water's surface rippling under it.
g[0] = ["┌", ..."─".repeat(cols - 2), "┐"];
g[rows - 1] = ["└", ..."─".repeat(cols - 2), "┘"];
for (let r = 1; r < rows - 1; r++) g[r][0] = g[r][cols - 1] = "│";
for (let c = 1; c < cols - 1; c++) g[SURFACE][c] = Math.sin(c * 0.55 - (u * TAU) / 4) + 0.5 * Math.sin(c * 0.23 + (u * TAU) / 6) > 0.3 ? "~" : "-";
for (let k = 0; k < 2; k++) for (let c = 1; c < cols - 1; c++) g[FLOOR + k][c] = gravel[k][c - 1];
// Weed: blades that rise straight from the root and bend further toward
// the tip, the tip swinging two or three columns as the water moves.
const weed = ([c0, blades, ph]: (typeof WEEDS)[number]) => {
for (const [h, lean] of blades) {
let px = c0 + 0.5;
for (let k = 1; k <= h; k++) {
const f = k / h, sway = 1.4 * Math.sin((u * TAU) / 6 + ph - f * 0.7 + lean * 0.2);
const x = c0 + 0.5 + lean * f ** 1.5 + sway * f * f, d = x - px;
put(FLOOR - k, Math.floor(x), d > 0.5 ? "/" : d > 0.17 ? "(" : d < -0.5 ? "\\" : d < -0.17 ? ")" : "|");
px = x;
}
}
};
WEEDS.slice(0, -1).forEach(weed);
STONE.forEach(([r, c, s]) => put(r, c, s));
// Each fish swims there and back, easing into each turn, and drifts up
// and down over the loop; for a moment mid-turn it faces out of the glass.
for (const f of fish) {
const th = TAU * (u / f.period + f.phase);
const p = Math.asin(0.995 * Math.sin(th)) / (Math.PI / 2);
const x = Math.round(f.from + ((p + 1) / 2) * (f.to - f.from));
const y = Math.round(f.row + f.drift * Math.sin((TAU * u) / LOOP + f.dp) + 0.35 * Math.sin((TAU * u) / 6 + f.dp * 3));
const art = Math.abs(Math.cos(th)) < 0.09 ? f.turn : Math.cos(th) > 0 ? f.art : f.back;
const w = Math.max(...f.art.map((l) => l.length)), tw = Math.max(...art.map((l) => l.length));
art.forEach((line, i) => put(y + i - (art.length >> 1), x + ((w - tw) >> 1), line));
}
// Bubbles leave the stone in a stream, wobbling and swelling as they rise,
// and break at the surface.
for (let k = 0; k < 80; k++) {
const age = (u - k * (LOOP / 80) + LOOP) % LOOP;
const y = 14.6 - age * (2.6 + hash(k, 1));
if (y < SURFACE + 1) continue;
const x = NOZZLE + Math.round(0.7 * Math.sin(age * 3.5 + k) + (hash(k, 2) - 0.5));
const r = Math.floor(y);
if (g[r][x] === " ") g[r][x] = y > 11 ? "." : y > 6 ? "o" : "O";
}
weed(WEEDS[WEEDS.length - 1]); // one in front of the fish
return g.map((row) => row.join("")).join("\n");
};
}