An amber screen spreadsheet where the cell pointer moves to a figure, a new value is typed on the entry line, the totals recalculate and the sheet flips to a bar graph.
C5: (V) 1,950 READY ▄▄▄▄▄▄▄▄A▄▄▄▄▄▄▄▄▄B▄▄▄▄▄▄▄▄▄C▄▄▄▄▄▄▄▄▄D▄▄▄▄▄▄▄▄▄E▄▄▄▄▄▄▄▄▄F▄▄▄▄▄▄▄▄▄G▄▄▄▄▄▄▄▄▄▄▄▄▄ 1 HOUSEHOLD BUDGET 1987 2 3 Q1 Q2 Q3 Q4 TOTAL 4 --------- --------- --------- --------- --------- --------- 5 Rent 1,950 ▒▒▒▒1,950 1,950 1,950 7,800 6 Groceries 820 790 860 910 3,380 7 Heating 410 180 120 395 1,105 8 Telephone 96 102 99 110 407 9 Car 340 515 280 300 1,435 10 Savings 300 300 300 600 1,500 11 ========= ========= ========= ========= ========= ========= 12 TOTAL 3,916 3,837 3,609 4,265 15,627 13 14 Monthly 1,302 15 Heat share 7.1% 16 17 18 19 20
Paste two lines into any page, play it in a terminal, or put a GIF in a README. The code follows the look you pick above.
<script type="module" src="https://www.ascii-magic.com/ascii/ascii.js"></script>
<ascii-art piece="amber-ledger-spreadsheet"></ascii-art>// copy amber-ledger-spreadsheet.js and kit.js into your project, then:
import { useEffect, useRef } from "react";
import create from "./amber-ledger-spreadsheet.js";
export function Piece() {
const ref = useRef(null);
useEffect(() => {
const draw = create(), t0 = performance.now();
let raf;
const tick = () => {
const f = draw((performance.now() - t0) / 1000);
ref.current.textContent = f.text ?? f;
raf = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(raf);
}, []);
return <pre ref={ref} style={{ lineHeight: 1, fontFamily: "Menlo, Consolas, monospace" }} />;
}npx --yes https://www.ascii-magic.com/ascii/cli.tgz amber-ledger-spreadsheet<!-- Download GIF below, commit it, then: -->
<p align="center">
<img src="assets/amber-ledger-spreadsheet.gif" alt="Amber Ledger Spreadsheet, animated ASCII art" width="720">
</p>Pause on the frame you like, pick a look, then download it at an exact size.
MIT licensed. It imports one small helper file, kit.js.
/* amber-ledger-spreadsheet: an amber monochrome spreadsheet where figures are typed in, totals recalculate and a bar graph is drawn. */
import { Grid, palette } from "../lib/kit.js";
export const meta = {
slug: "amber-ledger-spreadsheet",
name: "Amber Ledger Spreadsheet",
category: "retro",
note: "An amber screen spreadsheet where the cell pointer moves to a figure, a new value is typed on the entry line, the totals recalculate and the sheet flips to a bar graph.",
tags: ["retro", "spreadsheet", "amber", "finance", "data", "terminal"],
cols: 82, rows: 25, fps: 20,
palette: palette(["#ffc24a", "#e09a20", "#9a6410", "#4a300a", "#ffe6a8", "#2a1a06"]),
};
const ITEMS = ["Rent", "Groceries", "Heating", "Telephone", "Car", "Savings"];
const BASE = [[1950, 1950, 1950, 1950], [820, 790, 860, 910], [410, 180, 120, 395], [96, 102, 99, 110], [340, 515, 280, 300], [300, 300, 300, 600]];
const EDITS = [
{ at: 1.0, r: 1, c: 1, v: 905, s: "905" },
{ at: 5.0, r: 2, c: 3, v: 520, s: "520" },
{ at: 9.0, r: 5, c: 2, v: 450, s: "450" },
];
const GRAPH = 13.5, PERIOD = 19;
const fmt = (n) => n.toLocaleString("en-US");
export default function create() {
const W = meta.cols, H = meta.rows;
const g = new Grid(W, H);
const CW = 10, X0 = 4;
const colX = (c) => X0 + c * CW;
return (t) => {
g.clear();
const lt = t % PERIOD;
const v = BASE.map((r) => r.slice());
let cur = { r: 0, c: 1 }, entry = "", typingEdit = null, lastCommit = -9;
for (const e of EDITS) {
const moveT = e.at, typeT = e.at + 1.0, commitT = typeT + e.s.length * 0.25 + 0.4;
if (lt >= moveT) {
const k = Math.min(1, (lt - moveT) / 0.9);
cur = { r: Math.round(cur.r + (e.r - cur.r) * k), c: Math.round(cur.c + (e.c - cur.c) * k) };
}
if (lt >= typeT && lt < commitT) { typingEdit = e; entry = e.s.slice(0, Math.floor((lt - typeT) / 0.25) + 1); }
if (lt >= commitT) { v[e.r][e.c] = e.v; lastCommit = commitT; }
}
const recalc = lt - lastCommit < 0.7;
const rowT = v.map((r) => r.reduce((a, b) => a + b, 0));
const colT = [0, 1, 2, 3].map((c) => v.reduce((a, r) => a + r[c], 0));
const grand = rowT.reduce((a, b) => a + b, 0);
// top lines
const cellName = "ABCDEFG"[cur.c + 1] + (cur.r + 5);
if (lt < GRAPH) {
g.text(0, 0, cellName + ": (V) " + fmt(v[cur.r][cur.c]), "0");
g.text(0, 1, typingEdit ? entry : "", "4");
if (typingEdit && Math.floor(lt * 4) % 2 === 0) g.set(entry.length, 1, "█", "0");
} else g.text(0, 0, "GRAPH: BAR RANGE B5..E10", "0");
g.text(W - 8, 0, typingEdit ? "VALUE" : recalc ? "CALC " : "READY", recalc ? "4" : "1");
// column header
g.fill(0, 2, W, 1, "▄", "3");
for (let c = 0; c < 7; c++) g.text(colX(c) + 4, 2, "ABCDEFG"[c], "0");
for (let r = 0; r < 20; r++) g.text(0, 3 + r, String(r + 1).padStart(3), "2");
if (lt < GRAPH) {
const put = (r, c, s, co, right = true) => g.text(colX(c) + (right ? CW - 1 - s.length : 0), 3 + r, s, co);
put(0, 0, "HOUSEHOLD BUDGET 1987", "0", false);
["Q1", "Q2", "Q3", "Q4", "TOTAL"].forEach((h, i) => put(2, i + 1, h, "0"));
for (let c = 0; c < 6; c++) put(3, c, "-".repeat(CW - 1), "3", false);
ITEMS.forEach((name, r) => {
put(4 + r, 0, name, "1", false);
for (let c = 0; c < 4; c++) put(4 + r, c + 1, fmt(v[r][c]), "1");
put(4 + r, 5, fmt(rowT[r]), recalc ? "4" : "0");
});
for (let c = 0; c < 6; c++) put(10, c, "=".repeat(CW - 1), "3", false);
put(11, 0, "TOTAL", "0", false);
for (let c = 0; c < 4; c++) put(11, c + 1, fmt(colT[c]), recalc ? "4" : "0");
put(11, 5, fmt(grand), recalc ? "4" : "0");
put(13, 0, "Monthly", "1", false);
put(13, 5, fmt(Math.round(grand / 12)), recalc ? "4" : "1");
put(14, 0, "Heat share", "1", false);
put(14, 5, ((rowT[2] / grand) * 100).toFixed(1) + "%", recalc ? "4" : "1");
// the cell pointer
const px = colX(cur.c + 1), py = 3 + 4 + cur.r;
for (let x = px; x < px + CW - 1; x++) if (g.get(x, py) === " ") g.set(x, py, "▒", "2");
for (let x = px; x < px + CW - 1; x++) if (g.get(x, py) !== "▒") g.set(x, py, g.get(x, py), "4");
} else {
// bar graph of quarter totals, each item stacked as its own shade
const k = Math.min(1, (lt - GRAPH) / 1.5);
const SH = ["█", "▓", "▒", "░", "▓", "█"], CO = ["0", "1", "0", "1", "2", "4"];
const maxT = Math.max(...colT);
const base = 20;
for (let q = 0; q < 4; q++) {
let y = base;
const x = 12 + q * 15;
for (let r = 0; r < 6; r++) {
const hgt = (v[r][q] / maxT) * 15 * k;
const y1 = base - ((v.slice(0, r + 1).reduce((a, rr) => a + rr[q], 0)) / maxT) * 15 * k;
for (let yy = Math.round(y1); yy < Math.round(y); yy++) g.text(x, 3 + yy - 3, SH[r].repeat(8), CO[r]);
y = y1; void hgt;
}
g.text(x + 3, 21, "Q" + (q + 1), "0");
if (k >= 1) g.text(x + 1, Math.round(y) - 1, fmt(colT[q]).padStart(6), "4");
}
g.hline(10, 72, 20, "─", "2");
ITEMS.forEach((name, r) => g.text(74, 6 + r * 2, SH[r] + SH[r] + " " + name.slice(0, 4), CO[r]));
}
return g.frame();
};
}
/YOUR OWN
The pieces here are hand-built animations. To turn your own photo, logo or video into animated ASCII, use the ASCII Magic editor.