solar system
[github]four planets on dotted orbits, periods by kepler's third law
space 61×29 20 fps 4.3 kB
. . . . . . . . . . . . . . . . . .. .. . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . .. //· .. .. . . . O ° . . . . .. . . . . . .. .. . . . . . . . . . . . . . . . +++ o\ . . . . . . . **%%%** |· . . . . . . . +*%@@@%*+ . . . . . . . . **%%%** . . . . . . . . +++ . . . . . . . . . . . . . . . .. .. . . . . . .. . . . . .. . -(O)- . .. .. .. .. ·/ . . . . . . . . . . . . .·\ . . . . (@) .. . . . . . . . . . . . . . .. .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. .. . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . .. //· .. .. . . . O ° . . . . .. . . . . . .. .. . . . . . . . . . . . . . . . +++ o\ . . . . . . . **%%%** |· . . . . . . . +*%@@@%*+ . . . . . . . . **%%%** . . . . . . . . +++ . . . . . . . . . . . . . . . .. .. . . . . . .. . . . . .. . -(O)- . .. .. .. .. ·/ . . . . . . . . . . . . .·\ . . . . (@) .. . . . . . . . . . . . . . .. .. . . . . . . . . . . . . . . . . .
use it
html
<script type="module" src="https://ascii.rest/ascii.js"></script>
<ascii-art piece="solar-system" options='{"speed":1}'></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 { solarSystem } from "ascii.rest/pieces";
<Ascii piece={solarSystem} options={{"speed":1}} /> A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="solar-system", to fetch the piece only when it mounts.
astro
---
// npm install github:bas3line/ascii
import Ascii from "ascii.rest/astro";
---
<Ascii piece="solar-system" options={{"speed":1}} /> 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 { solarSystem } from "ascii.rest/pieces";
const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, solarSystem, {"speed":1}); 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.
/*
* solar-system: planets seen from above on dotted circular orbits. Each
* period goes as the orbit's radius to the power 1.5, Kepler's third law.
*/
import type { Frame, Meta } from "../types.ts";
export interface SolarSystemOptions {
[key: string]: unknown;
/** How fast the planets go round: 1 is the innermost orbit in three seconds. */
speed: number;
}
export const meta = {
name: "solar system",
category: "space",
note: "four planets on dotted orbits, periods by kepler's third law",
cols: 61,
rows: 29,
fps: 20,
options: { speed: 1 },
} satisfies Meta<SolarSystemOptions>;
const TAU = Math.PI * 2;
interface Planet {
a: number;
glyph: string;
at: number;
moon?: boolean;
}
// Orbit radius in columns, glyph, starting angle. Orbits open out with
// distance, as they do; the second planet has a moon.
const PLANETS: Planet[] = [
{ a: 9.6, glyph: "o", at: 0.5 },
{ a: 14.5, glyph: "O", at: 2.3, moon: true },
{ a: 21, glyph: "(@)", at: 4.1 },
{ a: 28, glyph: "-(O)-", at: 5.9 },
];
const T1 = 3; // seconds for the innermost orbit at speed 1
const TRAIL = 3.2; // trail cells behind the innermost planet; slower ones leave less
export default function solarSystem({ speed = 1 }: Partial<SolarSystemOptions> = {}): Frame {
const { cols, rows } = meta;
const cx = (cols - 1) / 2, cy = (rows - 1) / 2;
const grid = new Array<string>(cols * rows);
// Rounds away from the sun, so each orbit comes out symmetric.
const sym = (v: number, o: number) => o + Math.sign(v - o) * Math.round(Math.abs(v - o));
const cell = (x: number, y: number) => {
const c = sym(x, cx), r = sym(y, cy);
return c >= 0 && c < cols && r >= 0 && r < rows ? c + r * cols : -1;
};
const at = (p: Planet, th: number): [number, number] => [cx + p.a * Math.cos(th), cy - (p.a / 2) * Math.sin(th)];
// A dot every two columns of arc, rows counting double, so each orbit is
// an even dotted curve.
const rings = new Set<number>();
for (const p of PLANETS) {
const n = Math.round((TAU * p.a) / 2);
for (let k = 0; k < n; k++) rings.add(cell(...at(p, (k / n) * TAU)));
}
// The sun, shaded brighter toward its middle.
const sun: [number, string][] = [];
for (let r = -2; r <= 2; r++)
for (let c = -5; c <= 5; c++) {
const d = Math.hypot(c, r * 2) / 4.4;
if (d <= 1) sun.push([cell(cx + c, cy + r), d < 0.35 ? "@" : d < 0.6 ? "%" : d < 0.82 ? "*" : "+"]);
}
return (t) => {
grid.fill(" ");
for (const k of rings) if (k >= 0) grid[k] = ".";
for (const [k, ch] of sun) grid[k] = ch;
for (const p of PLANETS) {
const th = p.at + (TAU * t * speed) / (T1 * (p.a / PLANETS[0].a) ** 1.5);
const [x, y] = at(p, th);
const x0 = Math.round(x - (p.glyph.length - 1) / 2), r0 = Math.round(y);
const mine = (k: number) => k >= 0 && Math.floor(k / cols) === r0 && k % cols >= x0 && k % cols < x0 + p.glyph.length;
// The trail walks back along the orbit, one cell at a time: a stroke
// the way the planet is heading, then a fading dot. Orbital speed goes
// as one over the root of the radius, so outer trails are shorter.
const len = Math.round(TRAIL * Math.sqrt(PLANETS[0].a / p.a));
let last = cell(x, y), drawn = 0;
for (let u = th; drawn < len && u > th - 1; u -= 0.01) {
const k = cell(...at(p, u));
if (k === last || mine(k)) continue;
last = k;
if (k < 0) break;
const s = Math.abs(Math.cos(u)) / Math.max(1e-6, Math.abs(Math.sin(u))); // slope, rows counted double
const g = s < 0.6 ? "-" : s > 2.4 ? "|" : Math.sin(u) * Math.cos(u) > 0 ? "\\" : "/";
grid[k] = drawn < len - 1 ? g : "·";
drawn++;
}
// The planet, with the orbit dots either side cleared so it stands out.
for (let i = -1; i <= p.glyph.length; i++) {
const k = cell(x0 + i, r0);
if (k < 0) continue;
if (p.glyph[i]) grid[k] = p.glyph[i];
else if (grid[k] === ".") grid[k] = " ";
}
if (p.moon) {
const m = (TAU * t * speed) / 1.1;
const k = cell(x + 2.4 * Math.cos(m), y - 1.2 * Math.sin(m));
if (k >= 0) grid[k] = "°";
}
}
const lines: string[] = [];
for (let r = 0; r < rows; r++) lines.push(grid.slice(r * cols, (r + 1) * cols).join(""));
return lines.join("\n");
};
}