fountain
[github]arcs of water from a nozzle falling as drops into a basin
physics 57×22 30 fps 6.7 kB
· | |· | ···\\\ | ///-·· · \\ | // ·· · ·____ \|||/ ___···· ·· · -\\\|/// · · · : \|/ : · ·· : _/^\_ : · · : : | | : : : : | | : : : : | | : : . . .: . | | : . . . : : | | . : ~-^--~---^--~-~--~--~_/ \_--~~-------^^---~-^~- ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ \_____________________________________________/ | | ___|_____________|___ · | |· | ···\\\ | ///-·· · \\ | // ·· · ·____ \|||/ ___···· ·· · -\\\|/// · · · : \|/ : · ·· : _/^\_ : · · : : | | : : : : | | : : : : | | : : . . .: . | | : . . . : : | | . : ~-^--~---^--~-~--~--~_/ \_--~~-------^^---~-^~- ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ \_____________________________________________/ | | ___|_____________|___
use it
html
<script type="module" src="https://ascii.rest/ascii.js"></script> <ascii-art piece="fountain"></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 { fountain } from "ascii.rest/pieces";
<Ascii piece={fountain} /> A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="fountain", to fetch the piece only when it mounts.
astro
--- // npm install github:bas3line/ascii import Ascii from "ascii.rest/astro"; --- <Ascii piece="fountain" />
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 { fountain } from "ascii.rest/pieces";
const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, fountain); 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.
/*
* fountain: a tall jet and two pairs of arcs leave a nozzle, break into drops
* on the way down and splash into a brim-full basin, rippling its surface.
*/
import type { Frame, Meta } from "../types.ts";
export const meta = {
name: "fountain",
category: "physics",
note: "arcs of water from a nozzle falling as drops into a basin",
cols: 57,
rows: 22,
fps: 30,
} satisfies Meta;
const G = 30; // gravity, rows a second squared
const RATE = 80; // drops leaving each jet a second
const WATER = 17; // the row of the water line
const NOZ = 6.5; // the nozzle's mouth above the water, rows
const T0 = 8; // seconds in, so every arc is already full
// Each jet: angle from upright in degrees, where its middle meets the water
// in rows from the post (0 for the upright jet, which is aimed by height),
// how much its drops wander in angle and speed, and one drop in how many
// stays in sight once it breaks up on the way down.
const JETS: [number, number, number, number, number][] = [
[0, 0, 0.004, 0.035, 3],
[-17, -7, 0.006, 0.008, 4],
[17, 7, 0.006, 0.008, 4],
[-42, -10.4, 0.005, 0.006, 5],
[42, 10.4, 0.005, 0.006, 5],
];
const TOP = 8.4; // how far the upright jet climbs above the nozzle
const BASIN = [
"▀".repeat(49),
"\\" + "_".repeat(45) + "/",
"|" + " ".repeat(13) + "|",
"___|" + "_".repeat(13) + "|___",
];
// "#" is the post's hollow middle: blank, and drops pass behind it.
const POST = ["_/^\\_", " |#| ", " |#| ", " |#| ", " |#| ", " |#| ", "_/#\\_"];
const hash = (i: number, s: number) => {
let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b) ^ Math.imul(s + 1, 0xc2b2ae35);
h = Math.imul(h ^ (h >>> 15), 0x2c1b3c6d);
h = Math.imul(h ^ (h >>> 12), 0x297a2d39);
return ((h ^ (h >>> 15)) >>> 0) / 4294967296;
};
// Seconds from leaving the nozzle with upward speed vy until meeting the water.
const flight = (vy: number) => (vy + Math.sqrt(vy * vy + 2 * G * NOZ)) / G;
export default function fountain(): Frame {
const { cols, rows } = meta;
const CX = Math.floor(cols / 2) + 0.5; // the middle of the nozzle's cell
// The launch speed that lands each arc where it should, by bisection.
const jets = JETS.map(([deg, land, wa, wv, keep], j) => {
const a = (deg * Math.PI) / 180;
let v = Math.sqrt(2 * G * TOP);
if (deg) {
let lo = 1, hi = 60;
for (let k = 0; k < 40; k++) {
v = (lo + hi) / 2;
Math.abs(v * Math.sin(a) * flight(v * Math.cos(a))) < Math.abs(land) ? (lo = v) : (hi = v);
}
}
return { a, v, wa, wv, keep, j };
});
jets.push(jets.shift()!); // the upright jet is drawn last, so it runs whole down to the nozzle
// Drop i of a jet: when it left, its velocity, and how long it flies. The
// upright jet's pressure swells and eases every five seconds.
const drop = (jet: (typeof jets)[number], i: number) => {
const s = jet.j * 7 + 1;
const pulse = jet.j ? 1 : 0.95 + 0.05 * Math.sin((i / RATE) * 1.2566);
const a = jet.a + (hash(i, s) - 0.5) * 2 * jet.wa * Math.PI;
const v = jet.v * pulse * (1 + (hash(i, s + 1) - 0.5) * 2 * jet.wv);
const vx = v * Math.sin(a), vy = v * Math.cos(a);
return { t0: (i + hash(i, s + 2) * 0.6) / RATE, vx, vy, end: flight(vy) };
};
return (time) => {
const t = T0 + time;
const g = Array.from({ length: rows }, () => new Array<string>(cols).fill(" "));
const solid = Array.from({ length: rows }, () => new Array<boolean>(cols).fill(false));
const put = (c: number, r: number, ch: string) => {
if (r >= 0 && r < rows && c >= 0 && c < cols && !solid[r][c]) g[r][c] = ch;
};
const stamp = (art: string[], r0: number) =>
art.forEach((line, k) => {
const c0 = Math.floor(CX) - (line.length - 1) / 2; // every line is odd and centred on the post
for (let c = 0; c < line.length; c++) {
if (line[c] === " ") continue;
g[r0 + k][c0 + c] = line[c] === "#" ? " " : line[c];
solid[r0 + k][c0 + c] = true;
}
});
stamp(BASIN, WATER + 1);
stamp(POST, WATER - POST.length + 1);
const rings: [number, number][] = [], hits: number[] = [], crowns: [number, number, number, number][] = [];
for (const jet of jets) {
for (let i = Math.floor((t - 3.2) * RATE); i <= Math.ceil(t * RATE); i++) {
const d = drop(jet, i), age = t - d.t0;
if (age < 0) continue;
if (age >= d.end) {
const k = age - d.end, x = d.vx * d.end;
if (k < 1.4 && hash(i, jet.j + 60) < 0.018) rings.push([x, k]);
if (k < 0.07 && i % jet.keep === 0) hits.push(x);
if (k < 0.4 && hash(i, jet.j + 70) < 0.03) crowns.push([x, k, i, jet.j]);
continue;
}
const vy = d.vy - G * age;
// The stream rises whole and breaks into drops past the top, a few in sight.
if (vy < 0 && i % jet.keep) continue;
const row = WATER - NOZ - d.vy * age + (G * age * age) / 2;
if (vy < 0 && !jet.j && row > WATER - NOZ - 1) continue; // fallen back into the nozzle's throat
const speed = Math.hypot(d.vx, vy), ang = (Math.atan2(vy, d.vx) * 180) / Math.PI;
// Rising, the stream's glyph runs along it, and the upright one stays
// upright, beading where it slows at the top. Falling, a drop is a dot,
// drawn out once it is fast.
let ch: string;
if (vy < 0) ch = speed > 17 ? ":" : "·";
else if (!jet.j) ch = speed > 7 ? "|" : speed > 3 ? "·" : "°";
else if (Math.abs(ang - 90) < 22) ch = "|";
else if (ang < 28 || ang > 152) ch = row % 1 < 0.5 ? "-" : "_";
else ch = ang < 90 ? "/" : "\\";
put(Math.floor(CX + 2 * d.vx * age), Math.floor(row), ch);
}
}
// The water line: calm, with a wavelet running out each way from a landing.
const lo = Math.floor(CX) - 23, hi = Math.floor(CX) + 23;
for (let c = lo; c <= hi; c++) {
if (solid[WATER][c]) continue;
const x = (c + 0.5 - CX) / 2;
let h = 0;
for (const [x0, k] of rings) {
const d = Math.abs(Math.abs(x - x0) - 0.5 - 2.6 * k);
if (d < 0.4) h += (1 - d * 2.5) * (1 - k / 1.4);
}
g[WATER][c] = h > 0.3 ? "~" : "-";
}
// Where a drop strikes, the surface jumps; a crown of two drops is thrown
// up either side and falls back in.
for (const x of hits) {
const c = Math.floor(CX + 2 * x);
if (c >= lo && c <= hi && !solid[WATER][c]) g[WATER][c] = "^";
}
for (const [x, k, i, j] of crowns) {
for (const side of [-1, 1]) {
const vx = side * (1.2 + hash(i, j + 40) * 1.4), vy = 7 + hash(i, j + 50 + side) * 4;
const y = vy * k - (G * k * k) / 2;
if (y <= 0.2) continue;
put(Math.floor(CX + 2 * (x + vx * k)), Math.floor(WATER - y), vy - G * k > 0 ? "'" : ".");
}
}
return g.map((row) => row.join("")).join("\n");
};
}