donut

[github]

a lit torus turning on two axes, after donut.c

shapes 40×22 30 fps 2.4 kB

use it

html

<script type="module" src="https://ascii.rest/ascii.js"></script>

<ascii-art piece="donut"></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 { donut } from "ascii.rest/pieces";

<Ascii piece={donut} />

A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="donut", to fetch the piece only when it mounts.

astro

---
// npm install github:bas3line/ascii
import Ascii from "ascii.rest/astro";
---

<Ascii piece="donut" />

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 { donut } from "ascii.rest/pieces";

const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, donut);

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.

donut.ts

[js] [github]
/*
 * donut: a torus turning on two axes, lit from the upper left. After Andy
 * Sloane's donut.c, with the ring sized so it never clips at any angle.
 */
import type { Frame, Meta } from "../types.ts";

export const meta = {
  name: "donut",
  category: "shapes",
  note: "a lit torus turning on two axes, after donut.c",
  cols: 40,
  rows: 22,
  fps: 30,
} satisfies Meta;

const RAMP = ".,-~:;=!*#$@";

export default function donut(): Frame {
  const { cols, rows } = meta;
  const R1 = 1; // tube radius
  const R2 = 2; // ring radius
  const K2 = 6; // eye to centre
  const ASPECT = 0.5; // a cell is about twice as tall as it is wide
  const K1 = (cols - 2) / 2 / ((R1 + R2) / Math.sqrt(K2 * K2 - (R1 + R2) ** 2));
  const m = Math.hypot(-0.4, 1, -1);
  const [lx, ly, lz] = [-0.4 / m, 1 / m, -1 / m];
  const out: string[] = new Array(cols * rows);
  const depth = new Float32Array(cols * rows);

  return (t, { paper = false } = {}) => {
    const A = 1 + t * 0.8;
    const B = 1 + t * 0.35;
    out.fill(" ");
    depth.fill(0);
    const cA = Math.cos(A), sA = Math.sin(A), cB = Math.cos(B), sB = Math.sin(B);
    for (let th = 0; th < 6.283; th += 0.07) {
      const ct = Math.cos(th), st = Math.sin(th);
      for (let ph = 0; ph < 6.283; ph += 0.03) {
        const cp = Math.cos(ph), sp = Math.sin(ph);
        const h = R2 + R1 * ct;
        const x = h * (cB * cp + sA * sB * sp) - R1 * st * cA * sB;
        const y = h * (sB * cp - sA * cB * sp) + R1 * st * cA * cB;
        const ooz = 1 / (K2 + cA * h * sp + R1 * st * sA);
        const col = Math.floor(cols / 2 + K1 * ooz * x);
        const row = Math.floor(rows / 2 - K1 * ASPECT * ooz * y);
        if (col < 0 || col >= cols || row < 0 || row >= rows) continue;
        const k = col + row * cols;
        if (ooz <= depth[k]) continue;
        depth[k] = ooz;
        // The surface normal is the same rotation applied to the tube's circle.
        const nx = ct * (cB * cp + sA * sB * sp) - st * cA * sB;
        const ny = ct * (sB * cp - sA * cB * sp) + st * cA * cB;
        const nz = cA * ct * sp + st * sA;
        const i = Math.round(Math.max(0, nx * lx + ny * ly + nz * lz) * (RAMP.length - 1));
        out[k] = RAMP[paper ? RAMP.length - 1 - i : i];
      }
    }
    const lines: string[] = [];
    for (let r = 0; r < rows; r++) lines.push(out.slice(r * cols, (r + 1) * cols).join(""));
    return lines.join("\n");
  };
}