big text
[github]a block-letter banner with a line shadow and a passing glint
type 66×8 24 fps 4.6 kB
▓▓╗ ▓▓╗ ▓▓▓▓▓▓▓▓╗ ▓▓╗ ▓▓╗ ▓▓▓▓╗ ▓▓║ ▓▓║ ▓▓╔═════╝ ▓▓║ ▓▓║ ▓▓╔═══▓▓╗ ▓▓▓▓▓▓▓▓║ ▓▓▓▓▓▓╗ ▓▓║ ▓▓║ ▓▓║ ▓▓║ ▓▓╔═══▓▓║ ▓▓╔═══╝ ▓▓║ ▓▓║ ▓▓║ ▓▓║ ▓▓║ ▓▓║ ▓▓▓▓▓▓▓▓╗ ▓▓▓▓▓▓▓▓╗ ▓▓▓▓▓▓▓▓╗ ╚═▓▓▓▓╔═╝ ╚═╝ ╚═╝ ╚═══════╝ ╚═══════╝ ╚═══════╝ ╚═══╝ ██╗ ██╗ ████████╗ ██╗ ██╗ ████╗ ██║ ██║ ██╔═════╝ ██║ ██║ ██╔═══██╗ ████████║ ██████╗ ██║ ██║ ██║ ██║ ██╔═══██║ ██╔═══╝ ██║ ██║ ██║ ██║ ██║ ██║ ████████╗ ████████╗ ████████╗ ╚═████╔═╝ ╚═╝ ╚═╝ ╚═══════╝ ╚═══════╝ ╚═══════╝ ╚═══╝
use it
html
<script type="module" src="https://ascii.rest/ascii.js"></script>
<ascii-art piece="big-text" options='{"text":"hello"}'></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 { bigText } from "ascii.rest/pieces";
<Ascii piece={bigText} options={{"text":"hello"}} /> A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="big-text", to fetch the piece only when it mounts.
astro
---
// npm install github:bas3line/ascii
import Ascii from "ascii.rest/astro";
---
<Ascii piece="big-text" options={{"text":"hello"}} /> 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 { bigText } from "ascii.rest/pieces";
const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, bigText, {"text":"hello"}); 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.
/*
* big-text: a banner in a hand-made five-row block font with a drop shadow
* drawn in double lines, and a glint that sweeps across now and then.
*/
import type { Frame, Meta } from "../types.ts";
export interface BigTextOptions {
[key: string]: unknown;
text: string;
}
export const meta = {
name: "big text",
category: "type",
note: "a block-letter banner with a line shadow and a passing glint",
cols: 66,
rows: 8,
fps: 24,
options: { text: "hello" },
} satisfies Meta<BigTextOptions>;
// Five rows each, # is ink. Widths vary, so I is one pixel and M is five.
const FONT: Record<string, string> = {
A: ".##.|#..#|####|#..#|#..#", B: "###.|#..#|###.|#..#|###.", C: ".###|#...|#...|#...|.###",
D: "###.|#..#|#..#|#..#|###.", E: "####|#...|###.|#...|####", F: "####|#...|###.|#...|#...",
G: ".###|#...|#.##|#..#|.###", H: "#..#|#..#|####|#..#|#..#", I: "#|#|#|#|#",
J: "...#|...#|...#|#..#|.##.", K: "#..#|#.#.|##..|#.#.|#..#", L: "#...|#...|#...|#...|####",
M: "#...#|##.##|#.#.#|#...#|#...#", N: "#...#|##..#|#.#.#|#..##|#...#", O: ".##.|#..#|#..#|#..#|.##.",
P: "###.|#..#|###.|#...|#...", Q: ".##.|#..#|#..#|#.#.|.#.#", R: "###.|#..#|###.|#.#.|#..#",
S: ".###|#...|.##.|...#|###.", T: "#####|..#..|..#..|..#..|..#..", U: "#..#|#..#|#..#|#..#|.##.",
V: "#...#|#...#|#...#|.#.#.|..#..", W: "#...#|#...#|#.#.#|##.##|#...#", X: "#...#|.#.#.|..#..|.#.#.|#...#",
Y: "#...#|.#.#.|..#..|..#..|..#..", Z: "####|...#|..#.|.#..|####",
0: "###|#.#|#.#|#.#|###", 1: ".#.|##.|.#.|.#.|###", 2: "###|..#|###|#..|###", 3: "###|..#|.##|..#|###",
4: "#.#|#.#|###|..#|..#", 5: "###|#..|###|..#|###", 6: "###|#..|###|#.#|###", 7: "###|..#|..#|..#|..#",
8: "###|#.#|###|#.#|###", 9: "###|#.#|###|..#|###",
".": ".|.|.|.|#", ",": ".|.|.|#|#", "!": "#|#|#|.|#", "?": "###|..#|.##|...|.#.", "'": "#|#|.|.|.",
":": ".|#|.|#|.", "-": "...|...|###|...|...", "+": "...|.#.|###|.#.|...", "=": "...|###|...|###|...",
"/": "..#|..#|.#.|#..|#..", _: "...|...|...|...|###", " ": "..",
};
// Shadow cells by which neighbours they join: up 1, down 2, left 4, right 8.
const JOIN = " ║║║═╝╗╣═╚╔╠═╩╦╬";
const U = 1, D = 2, L = 4, R = 8;
export default function bigText({ text = meta.options.text }: Partial<BigTextOptions> = {}): Frame {
const { cols, rows } = meta;
const glyphs = [...String(text).toUpperCase()].map((ch) => FONT[ch]).filter(Boolean).map((g) => g.split("|"));
// Each pixel is two columns by one row, so it is square; one column narrower if it would not fit.
const span = (s: number) => glyphs.reduce((w, g) => w + g[0].length * s + 2, -1);
const s = span(2) <= cols - 2 ? 2 : 1;
const x0 = Math.max(0, Math.floor((cols - span(s)) / 2));
const y0 = Math.floor((rows - 6) / 2);
const ink = new Uint8Array(cols * rows);
let x = x0;
for (const g of glyphs) {
g.forEach((line, y) => {
for (let i = 0; i < line.length; i++)
if (line[i] === "#") for (let k = 0; k < s; k++) if (x + i * s + k < cols) ink[(y0 + y) * cols + x + i * s + k] = 1;
});
x += g[0].length * s + 2;
}
const at = (c: number, r: number) => (c >= 0 && c < cols && r >= 0 && r < rows ? ink[r * cols + c] : 0);
// The shadow is the letters' outline moved half a cell right and down, so
// each empty cell draws the outline edges that meet at its top left corner.
const grid: (string | null)[] = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (at(c, r)) {
grid.push(null);
continue;
}
const a = at(c - 1, r - 1), b = at(c, r - 1), d = at(c - 1, r);
const edges = (a !== b ? U : 0) | (d ? D : 0) | (a !== d ? L : 0) | (b ? R : 0);
// Where two letters touch only at that corner, the shadow turns down and right.
grid.push(JOIN[edges === 15 ? D | R : edges]);
}
}
const SWEEP = 2.4, PERIOD = 3.2;
const left = x0 - 2, right = x0 + span(s) + 9;
return (t, { paper = false } = {}) => {
// The glint crosses at an even pace, then rests out of sight. Frame 0 is at rest.
const u = (((t - 0.5) % PERIOD) + PERIOD) % PERIOD / SWEEP;
const p = u < 1 ? left + (right - left) * u : -99;
const lines: string[] = [];
for (let r = 0; r < rows; r++) {
let line = "";
for (let c = 0; c < cols; c++) {
const g = grid[r * cols + c];
if (g !== null) {
line += g;
continue;
}
const d = Math.abs(c + 1.2 * r - p);
// Ink is a dense shade on a dark page so the glint can be brighter, and solid on paper.
if (paper) line += d < 1.2 ? "▒" : d < 2.4 ? "▓" : "█";
else line += d < 2.4 ? "█" : "▓";
}
lines.push(line);
}
return lines.join("\n");
};
}