pendulum wave

[github]

a row of pendulums drifting in and out of step, from above

physics 67×19 30 fps 2.2 kB

use it

html

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

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

<Ascii piece={pendulumWave} />

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

astro

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

<Ascii piece="pendulum-wave" />

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

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

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.

pendulum-wave.ts

[js] [github]
/*
 * pendulum-wave: a row of pendulums seen from above, hung from one rail. Bob
 * n swings 60 + n times every 150 seconds, so the row drifts from one wave
 * into two, three, a braid and a zigzag, then lines up again once a cycle.
 */
import type { Frame, Meta } from "../types.ts";

export const meta = {
  name: "pendulum wave",
  category: "physics",
  note: "a row of pendulums drifting in and out of step, from above",
  cols: 67,
  rows: 19,
  fps: 30,
} satisfies Meta;

const N = 21; // bobs
const GAP = 3; // columns from one bob to the next
const CYCLE = 150; // seconds until every bob lines up again
const BASE = 60; // swings the slowest bob makes in one cycle
const BLUR = 0.07; // seconds of motion blur behind each bob
const T0 = (1.5 * CYCLE) / (N - 1); // a wave and a half across the row

export default function pendulumWave(): Frame {
  const { cols, rows } = meta;
  const left = Math.floor((cols - (N - 1) * GAP - 1) / 2);
  const mid = (rows - 1) / 2; // the rail, straight above every bob at rest
  // The rail runs the length of the row, a little past each end.
  const right = left + (N - 1) * GAP;
  const rail = new Array<string>(cols).fill(" ");
  for (let c = left - 1; c <= right + 1; c++) rail[c] = "─";
  rail[left - 1] = "╶";
  rail[right + 1] = "╴";
  const out: string[] = new Array(cols * rows);
  // The swing spans every row but the first and last.
  const row = (n: number, t: number) => 1 + ((1 - Math.cos((2 * Math.PI * (BASE + n) * t) / CYCLE)) * (rows - 3)) / 2;

  return (time) => {
    const t = time + T0;
    out.fill(" ");
    for (let c = 0; c < cols; c++) out[mid * cols + c] = rail[c];
    for (let n = 0; n < N; n++) {
      const c = left + n * GAP;
      const now = Math.round(row(n, t));
      // The cells the bob crossed in the last instant smear into a faint
      // streak; the rail shows through it.
      const then = Math.round(row(n, t - BLUR));
      const [a, b] = now < then ? [now + 1, then] : [then, now - 1];
      for (let r = a; r <= b; r++) if (r !== mid) out[r * cols + c] = Math.abs(r - now) === 1 ? ":" : "·";
      out[now * cols + c] = "o";
    }
    const lines: string[] = [];
    for (let r = 0; r < rows; r++) lines.push(out.slice(r * cols, (r + 1) * cols).join(""));
    return lines.join("\n");
  };
}