mandelbrot
[github]a slow zoom into seahorse valley and back out again
generative 64×26 15 fps 3.2 kB
..:=*%@#+++-:. ..:-=+#%@%@%+-:.. ...::-=*##%@@%#*=-::.. ..::----=*%@@@ @@@%+=--::::. ..:-++**++**#@@ @@#**+==+*+-:. ..::-+%@@@%%@@@@@ @@@@@%###@%*=:. ...:.......:::-=+#%@ @@ @@@@@@@#+-:. ..-=++=--------=+#%@@@ @@#*+-:. .::=*%@#***%*++=+*%@@ @%@#+-: ..::-=+#%@@%%@@%%#*#@@ @@#+-: .....::::-=++*#%@@@@ @@@%%@ @@*=-: .::---==--==+*@%%%@ @@ @@#+-:. .:=+***#%#*####%%@@@@ @@#+=-:. .-+####%@%#%%%%@@@ @@ @%*+=-:. .::--==+=====+*%@%%@ @@ @#+-:. ....:::::--+*+*%%@@ @@@%@ @@*=-: ...:--=+#%@@@%@@@@%*#@@ @%#+-: ..:-=*%@##*#@#*+++*%@@ @@@%+-: .:-+#*+=====---=+#@@@@ @@@#*+=:. .::::::::::::-=+*#%@ @@ @@ @@*=-:. ...:-=+%@@@@%@@@@@@ @@@@@@%#%%@@#=-. .:-=*###***##@@ @@#*#*+++#*=-. .::----==*%@@ @@%+=-----::. ...::--+*%%@@@%#*+-::... .::-=+*%@@@%+=::. ..:=+%@%***=:.. ..:=*%@#+++-:. ..:-=+#%@%@%+-:.. ...::-=*##%@@%#*=-::.. ..::----=*%@@@@@@@%+=--::::. ..:-++**++**#@@@@@@@#**+==+*+-:. ..::-+%@@@%%@@@@@@@@@@@@@%###@%*=:. ...:.......:::-=+#%@@@@@@@@@@@@@@@@@@@@@@@#+-:. ..-=++=--------=+#%@@@@@@@@@@@@@@@@@@@@@@@@#*+-:. .::=*%@#***%*++=+*%@@@@@@@@@@@@@@@@@@@@@@@@@%@#+-: ..::-=+#%@@%%@@%%#*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-: .....::::-=++*#%@@@@@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@*=-: .::---==--==+*@%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-:. .:=+***#%#*####%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+=-:. .-+####%@%#%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*+=-:. .::--==+=====+*%@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-:. ....:::::--+*+*%%@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@*=-: ...:--=+#%@@@%@@@@%*#@@@@@@@@@@@@@@@@@@@@@@@@@@@%#+-: ..:-=*%@##*#@#*+++*%@@@@@@@@@@@@@@@@@@@@@@@@@@@%+-: .:-+#*+=====---=+#@@@@@@@@@@@@@@@@@@@@@@@@@#*+=:. .::::::::::::-=+*#%@@@@@@@@@@@@@@@@@@@@@@@*=-:. ...:-=+%@@@@%@@@@@@@@@@@@@%#%%@@#=-. .:-=*###***##@@@@@@@#*#*+++#*=-. .::----==*%@@@@@@@%+=-----::. ...::--+*%%@@@%#*+-::... .::-=+*%@@@%+=::. ..:=+%@%***=:..
use it
html
<script type="module" src="https://ascii.rest/ascii.js"></script> <ascii-art piece="mandelbrot"></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 { mandelbrot } from "ascii.rest/pieces";
<Ascii piece={mandelbrot} /> A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="mandelbrot", to fetch the piece only when it mounts.
astro
--- // npm install github:bas3line/ascii import Ascii from "ascii.rest/astro"; --- <Ascii piece="mandelbrot" />
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 { mandelbrot } from "ascii.rest/pieces";
const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, mandelbrot); 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.
/*
* mandelbrot: the Mandelbrot set, zooming slowly into seahorse valley, the
* crack between the main cardioid and the disc to its left, then back out.
*/
import type { Frame, Meta } from "../types.ts";
export const meta = {
name: "mandelbrot",
category: "generative",
note: "a slow zoom into seahorse valley and back out again",
cols: 64,
rows: 26,
fps: 15,
} satisfies Meta;
const RAMP = " .:-=+*#%@";
const P = 48; // seconds to zoom in and back out
const T0 = 1.5; // start just after the turn, so the zoom is already under way
const DEEP = Math.log(6000); // the deepest zoom: one seahorse's curled tail
const TX = -0.74435, TY = 0.12119; // where the zoom heads: the centre of that curl
const CX = -0.75, SPAN = 3; // the centre and width of the whole set
const SS = 2; // samples per cell each way, so fine filaments blur rather than flicker
const WIDE = 3, NARROW = 1.2; // octaves of cells out from the set over which the glow fades
export default function mandelbrot(): Frame {
const { cols, rows } = meta;
const N = RAMP.length;
const S = SS * SS;
return (t, { paper = false } = {}) => {
const z = Math.exp((DEEP * (1 - Math.cos((2 * Math.PI * (t + T0)) / P))) / 2);
// Moving the centre with the zoom keeps the target still on screen.
const cx = TX + (CX - TX) / z, cy = TY - TY / z;
const dx = SPAN / z / cols, dy = dx * 2; // a cell is twice as tall as it is wide
const max = Math.round(120 + 60 * Math.log2(z));
const edge = WIDE + ((NARROW - WIDE) * Math.log(z)) / DEEP;
const lines: string[] = [];
for (let r = 0; r < rows; r++) {
let s = "";
for (let c = 0; c < cols; c++) {
let ink = 0, inside = 0;
for (let j = 0; j < S; j++) {
const x0 = cx + (c + ((j % SS) + 0.5) / SS - cols / 2) * dx;
const y0 = cy - (r + (Math.floor(j / SS) + 0.5) / SS - rows / 2) * dy;
// Inside the main cardioid or the disc beside it: no need to iterate.
const q = (x0 - 0.25) ** 2 + y0 * y0;
if (q * (q + x0 - 0.25) < 0.25 * y0 * y0 || (x0 + 1) ** 2 + y0 * y0 < 0.0625) {
inside++;
continue;
}
// Iterate z, and its derivative for the distance to the set.
let x = 0, y = 0, u = 0, v = 0, n = 0, m = 0;
for (; n < max; n++) {
const nu = 2 * (x * u - y * v) + 1;
v = 2 * (x * v + y * u);
u = nu;
const xx = x * x, yy = y * y;
y = 2 * x * y + y0;
x = xx - yy + x0;
m = x * x + y * y;
if (m > 1e6) break;
}
if (n === max) {
inside++;
continue;
}
// A glow that hugs the boundary, measured in cells: wide on the
// whole set, narrower deep down so the curls stay apart.
const d = (0.5 * Math.sqrt(m) * Math.log(m)) / Math.hypot(u, v) / dx;
ink += Math.max(0, 1 - Math.log2(1 + d) / edge);
}
// A cell the boundary runs through is at full glow. The set itself
// is dark: left bare on a dark page, inked on paper.
s += inside === S ? (paper ? "@" : " ") : RAMP[Math.min(N - 1, Math.floor(((ink + inside) / S) * N))];
}
lines.push(s);
}
return lines.join("\n");
};
}