dividers

[github]

rules and ornaments for between sections, plain to fancy

ui 63×25 still 2.9 kB

use it

html

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

<ascii-art piece="dividers" options='{"width":59}'></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 { dividers } from "ascii.rest/pieces";

<Ascii piece={dividers} options={{"width":59}} />

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

astro

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

<Ascii piece="dividers" options={{"width":59}} />

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

const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, dividers, {"width":59});

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.

dividers.ts

[js] [github]
/*
 * dividers: a sheet of rules to set between sections of a page, from a plain
 * line through short centred rules down to ornaments, chains and waves.
 */
import type { Frame, Meta } from "../types.ts";

export interface DividersOptions {
  [key: string]: unknown;
  width: number;
}

export const meta = {
  name: "dividers",
  category: "ui",
  note: "rules and ornaments for between sections, plain to fancy",
  cols: 63,
  rows: 25,
  fps: 0,
  options: { width: 59 },
} satisfies Meta<DividersOptions>;

// `unit` repeated to n characters.
const rep = (unit: string, n: number) => unit.repeat(Math.max(0, Math.ceil(n / unit.length))).slice(0, Math.max(0, n));
// The same string seen in a mirror, so its slanted and pointed glyphs turn too.
const FLIP: Record<string, string> = { "<": ">", ">": "<", "╱": "╲", "╲": "╱", "╶": "╴", "╴": "╶", "╡": "╞", "╞": "╡" };
const mirror = (s: string) => [...s].reverse().map((c) => FLIP[c] || c).join("");
// A left half, an optional middle cell and the half again in the mirror.
const sym = (half: string, mid = "") => half + mid + mirror(half);

// A centrepiece with a rule run out to either side, n wide.
const around = (mid: string, fill: string, n: number) => {
  const side = Math.floor((n - [...mid].length) / 2);
  return sym(rep(fill, side), [...mid].length + 2 * side < n ? fill + mid : mid);
};

// Thin at the ends, heavy in the middle: dots, then dashes, then rules.
const tapered = (n: number) => {
  const h = Math.floor(n / 2);
  if (h < 12) return rep("─", n);
  const run = h - 8, thin = Math.ceil(run * 0.45);
  return sym("· · ─ ─ " + rep("─", thin) + rep("━", run - thin), n % 2 ? "━" : "");
};

export default function dividers({ width = 59 }: Partial<DividersOptions> = {}): Frame {
  const { cols, rows } = meta;
  const W = Math.max(13, Math.min(cols, Math.floor(width) || 59));
  // The ornaments run a fraction of the measure, so they shrink with it.
  const part = (f: number) => Math.max(5, Math.round(W * f));
  const odd = (n: number) => n - (n % 2 ? 0 : 1);

  const SHEET = [
    rep("─", W),
    rep("═", W),
    rep("· ", odd(W)),
    rep("━", odd(part(0.25))), // a short rule, centred
    tapered(W),
    around(" · ° · ", "─", W),
    "*     *     *",
    sym("╶" + rep("─", (odd(part(0.5)) - 5) / 2) + " ", "°"),
    around("╡ ° ╞", "═", odd(part(0.7))),
    sym(rep("─<>", Math.floor((part(0.73) - 1) / 6) * 3), "─"),
    rep("╱╲", part(0.55) - (part(0.55) % 2)), // even, so it ends on the half it began with, mirrored
    rep("_.·°·.", Math.floor((part(0.73) - 1) / 6) * 6) + "_",
  ];

  const lines: string[] = [];
  for (let r = 0; r < rows; r++) {
    const s = r % 2 === 1 ? SHEET[(r - 1) / 2] || "" : "";
    const left = Math.floor((cols - [...s].length) / 2);
    lines.push((" ".repeat(Math.max(0, left)) + s).padEnd(cols).slice(0, cols));
  }
  const picture = lines.join("\n");
  return () => picture;
}