jellyfish
[github]a jellyfish pulsing upward, its tentacles trailing late
creatures 44×26 24 fps 7.1 kB
· . . . · . · ._------_. _-::::::::::::-_ -=:#*#+::::::+#*#:=- . ./=*::::=+::::+=::::*=\. .+:*:::::++::::++:::::*:+. (::**==*+::::::::+*==**::) . =::::::::........::::::::= ''~'~'~'~'~'~'~'~'~'~'~'~' | | | ; ; | | | ( .| | ; ; ) | | | | | |; :| | | ·| | | | :| ;) | | | . | | | (;| ;) | | | | | |(;||: | |./ | | |:) |; .| )/ . ( ;) (; | /: . \ /: (: :/ . : /: \. : . .( : . . : . ·.. : . . · .. · . . . · . · ._------_. _-::::::::::::-_ -=:#*#+::::::+#*#:=- . ./=*::::=+::::+=::::*=\. .+:*:::::++::::++:::::*:+. (::**==*+::::::::+*==**::) . =::::::::........::::::::= ''~'~'~'~'~'~'~'~'~'~'~'~' | | | ; ; | | | ( .| | ; ; ) | | | | | |; :| | | ·| | | | :| ;) | | | . | | | (;| ;) | | | | | |(;||: | |./ | | |:) |; .| )/ . ( ;) (; | /: . \ /: (: :/ . : /: \. : . .( : . . : . ·.. : . . · ..
use it
html
<script type="module" src="https://ascii.rest/ascii.js"></script> <ascii-art piece="jellyfish"></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 { jellyfish } from "ascii.rest/pieces";
<Ascii piece={jellyfish} /> A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="jellyfish", to fetch the piece only when it mounts.
astro
--- // npm install github:bas3line/ascii import Ascii from "ascii.rest/astro"; --- <Ascii piece="jellyfish" />
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 { jellyfish } from "ascii.rest/pieces";
const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, jellyfish); 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.
/*
* jellyfish: a moon jelly swimming up through dark water. The bell squeezes
* and relaxes, the tentacles follow the rim a beat late, and specks drift by.
*/
import type { Frame, Meta } from "../types.ts";
export const meta = {
name: "jellyfish",
category: "creatures",
note: "a jellyfish pulsing upward, its tentacles trailing late",
cols: 44,
rows: 26,
fps: 24,
} satisfies Meta;
const RAMP = " .::=+*#%@"; // no "-" inside, so only the outline uses it
const P = 2.2; // seconds per stroke
const SQ = 0.28; // share of the stroke spent squeezing
const ease = (x: number) => x * x * (3 - 2 * x);
const phase = (t: number) => (((t % P) + P) % P) / P;
// 0 relaxed, 1 fully squeezed.
const squeeze = (t: number) => {
const p = phase(t);
return p < SQ ? ease(p / SQ) : 1 - ease((p - SQ) / (1 - SQ));
};
// How far the stroke has carried the body up: it lurches with the squeeze and sinks back.
const lift = (t: number) => {
const p = phase(t);
return p < 0.4 ? ease(p / 0.4) : 1 - ease((p - 0.4) / 0.6);
};
// Where the rim sits and how wide it is, given how squeezed the bell is.
const rimAt = (t: number) => {
const c = squeeze(t);
const x = meta.cols / 2 + 1.5 * Math.sin((2 * Math.PI * t) / (5 * P));
return { c, x, y: 11 - 0.8 * c - 1.4 * lift(t), half: (13 - 2.6 * c) * (1 - 0.3 * c) };
};
export default function jellyfish(): Frame {
const { cols, rows } = meta;
let seed = 7;
const rnd = () => {
seed = (seed + 0x6d2b79f5) | 0;
let z = Math.imul(seed ^ (seed >>> 15), 1 | seed);
z = (z + Math.imul(z ^ (z >>> 7), 61 | z)) ^ z;
return ((z ^ (z >>> 14)) >>> 0) / 4294967296;
};
const specks = Array.from({ length: 30 }, () => ({ x: rnd() * cols, y: rnd() * rows, z: 0.4 + rnd() * 0.6, w: rnd() * 6.3 }));
const grid: string[] = new Array(cols * rows);
const put = (x: number, y: number, ch: string) => {
x = Math.round(x);
y = Math.round(y);
if (x >= 0 && x < cols && y >= 0 && y < rows) grid[y * cols + x] = ch;
};
// How far the water has moved past: faster while the bell pushes.
const travel = (t: number) => {
const k = Math.floor(t / P), p = phase(t);
return 0.9 * t + 2.4 * (k + (p < SQ ? 0.8 * ease(p / SQ) : 0.8 + (0.2 * (p - SQ)) / (1 - SQ)));
};
return (t) => {
grid.fill(" ");
const D = travel(t);
for (const s of specks) {
const y = Math.floor((((s.y + D * s.z) % rows) + rows) % rows);
put(s.x + 0.6 * Math.sin(t * 0.5 + s.w), y, s.z > 0.8 ? "·" : ".");
}
const now = rimAt(t);
const { c } = now;
const a = 13 - 2.6 * c, b = 13 + 2 * c;
const rim = Math.floor(now.y);
// Each point down a tentacle hangs from where the rim was a moment ago.
const trail = (d: number, lagPer: number) => {
const was = rimAt(t - d * lagPer);
return { x: was.x, y: rim + d + Math.max(0, was.y - now.y) * 0.8, half: was.half };
};
const cx = now.x;
// A strand traced down from the rim, one mark per row: how far it leans
// since the last row, and how much that lean changed, which is its bow.
const strand = (
len: number,
lag: number,
xAt: (d: number, at: ReturnType<typeof trail>) => number,
mark: (x: number, y: number, dx: number, bow: number, d: number, f: number) => void,
) => {
let d = 1, px = xAt(1, trail(1, lag)), pdx = 0;
for (let y = rim + 1; y < rows; y++) {
while (d < len && trail(d, lag).y < y) d += 0.25;
if (d >= len) break;
const x = xAt(d, trail(d, lag)), dx = x - px;
mark(x, y, dx, dx - pdx, d, d / len);
(px = x), (pdx = dx);
}
};
// Eight threads, the outer ones shorter, so the tips stagger. A thread is
// a plain stroke where it hangs straight and bows only where it bends.
for (let i = 0; i < 8; i++) {
const u = -0.92 + (1.84 * i) / 7;
const wob = (d: number) => Math.sin(d * 0.5 - t * 3 + i * 1.9);
const len = 8.5 + 3.2 * (1 - Math.abs(u)) + ((i * 3) % 5) * 0.8;
strand(len, 0.06, (d, at) => at.x + u * at.half * (1 - 0.01 * d) + (0.05 + d * 0.013) * d * wob(d), (x, y, dx, bow, d, f) => {
const ch = f > 0.9 ? "." : f > 0.8 ? ":" : dx > 0.6 ? "\\" : dx < -0.6 ? "/" : bow > 0.22 ? "(" : bow < -0.22 ? ")" : "|";
put(x, y, ch);
});
}
// Two frilled mouth arms, each one ribbon a cell wide that widens to two
// where it turns face on.
for (let i = 0; i < 2; i++) {
const s = i ? 1 : -1;
strand(10, 0.09, (d, at) => at.x + s * 2 * (1 - d * 0.06) * (at.half / 10) + 0.9 * Math.sin(d * 0.6 - t * 2.2 + i * 2.5), (x, y, dx, bow, d, f) => {
const tw = Math.cos(d * 1.1 + i * 1.3 - t * 1.6);
if (f > 0.8) return put(x, y, f > 0.9 ? "." : ":");
if (Math.abs(tw) < 0.35) return put(x, y, ":");
put(x, y, ";");
if (tw > 0.5) put(x + s, y, s > 0 ? ")" : "(");
});
}
// The bell: a half dome, glassy in the middle and thick at the edges. Ink
// stands for how much jelly the eye looks through, so it reads the same
// on paper and the ramp is not flipped.
const width = (s: number) => a * Math.sqrt(1 - s * s) * (1 - 0.3 * c * (1 - s) ** 2);
const inside = (X: number, yy: number) => {
const s = ((now.y - yy) * 2) / b;
return s >= 0 && s < 1 && Math.abs(X) < width(s);
};
for (let x = 0; x < cols; x++) {
const u = (x + 0.5 - cx) / width(0);
if (Math.abs(u) < 1) grid[rim * cols + x] = Math.abs(u) > 0.93 ? "'" : x & 1 ? "~" : "'";
}
for (let r = 0; r < rim; r++) {
for (let x = 0; x < cols; x++) {
let n = 0, sx = 0, sy = 0;
for (let j = 0; j < 4; j++)
for (let i = 0; i < 3; i++)
if (inside(x + (i + 0.5) / 3 - cx, r + (j + 0.5) / 4)) (n++, (sx += i - 1), (sy += (j - 1.5) / 1.5));
if (!n) continue;
const k = r * cols + x;
if (n < 10) {
// An edge cell: draw the outline facing the way the shape lies.
const ux = sx / n, uy = sy / n;
if (uy > Math.abs(ux) * 1.2) grid[k] = n < 5 ? "_" : "-";
else if (Math.abs(ux) > uy * 1.5) grid[k] = ux > 0 ? "(" : ")";
else grid[k] = n < 4 ? "." : ux > 0 ? "/" : "\\";
continue;
}
const s = Math.max(0, ((now.y - r - 0.5) * 2) / b);
const u = Math.max(-0.999, Math.min(0.999, (x + 0.5 - cx) / width(s)));
const nz = Math.sqrt(1 - u * u) * Math.sqrt(1 - s * s);
// Four pale horseshoes show through the top, two of them side on.
const ph = Math.asin(u);
const g = Math.min(Math.abs(ph - 0.6), Math.abs(ph + 0.6));
const ring = Math.max(0, 1 - Math.abs(Math.hypot(g / 0.36, (s - 0.45) / 0.26) - 1) * 3.5);
// The crown is the thinnest, glassiest part, so the shade fades out up there.
const crown = 1 - 0.6 * ease(Math.max(0, (s - 0.62) / 0.38));
const v = Math.min(1, (0.16 + 0.07 * s + 0.6 * (1 - nz) ** 2) * crown + 0.55 * ring);
grid[k] = RAMP[Math.max(1, Math.round(v * (RAMP.length - 1)))];
}
}
const lines: string[] = [];
for (let r = 0; r < rows; r++) lines.push(grid.slice(r * cols, (r + 1) * cols).join(""));
return lines.join("\n");
};
}