From d5e883f050979d1547157b87075d579883470ed9 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Wed, 17 Jun 2026 11:12:10 -0700 Subject: [PATCH] =?UTF-8?q?sandbox:=20braille=20is=20the=20working=20inlin?= =?UTF-8?q?e=20'draw=20anything'=20=E2=80=94=20drop=20dead=20math=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified: MathJax \rule is text-mode (unsupported on GitHub), \colorbox broken since 2023 — so inline math cannot draw bitmaps/paths. Replace with art-to-braille.mjs: parametric shapes (smiley/flower) + PNG->braille (zlib decode, sips normalize), packed into U+2800 glyphs in a code fence. Renders everywhere, no files. Record the support verdict in the README. --- sandbox/md-graphics-demo/README.md | 242 ++++++-------------- sandbox/md-graphics-demo/art-to-braille.mjs | 122 ++++++++++ sandbox/md-graphics-demo/path-to-math.mjs | 65 ------ 3 files changed, 192 insertions(+), 237 deletions(-) create mode 100644 sandbox/md-graphics-demo/art-to-braille.mjs delete mode 100644 sandbox/md-graphics-demo/path-to-math.mjs diff --git a/sandbox/md-graphics-demo/README.md b/sandbox/md-graphics-demo/README.md index 69f8751405..bf61866961 100644 --- a/sandbox/md-graphics-demo/README.md +++ b/sandbox/md-graphics-demo/README.md @@ -4,8 +4,7 @@ Graphics that render on **github.com** from **text alone** — nothing committed this `.md`. No SVG, no PNG, no STL. Companion to `HAND.md` and `papers/VOICE.md`. > [!NOTE] -> This page is the live render test. On github.com every block should paint; a -> ❌ below marks a form GitHub's MathJax **rejected** ("unable to render"). +> This page is the live render test. On github.com every block below paints. --- @@ -14,8 +13,9 @@ this `.md`. No SVG, no PNG, no STL. Companion to `HAND.md` and `papers/VOICE.md` | Method | Draws | Cost | |---|---|---| | ` ```mermaid ` | diagrams **and charts** (15+ types) | text | -| `$…$` / ` ```math ` | formulas — **and bitmaps/paths** (the hacks below) | text | +| `$…$` / ` ```math ` | formulas + colored symbols | text | | ` ```geojson ` | interactive maps | text | +| **braille / block art in a code fence** | **arbitrary low-res raster** | text | | prose primitives | tables, task lists, `
`, alerts, footnotes | text | --- @@ -58,187 +58,84 @@ $\Delta\varphi = 2\pi f / f_s$ --- -## Drawing with math — what actually renders on GitHub +## "Draw anything" inline — the verdict, then the one method that works -We probe each primitive in isolation so the support matrix is empirical, not -assumed. (GitHub's MathJax autoloads the `color` extension.) +I tried to draw bitmaps/paths with the math block (`\rule` + `\textcolor`, then a +`\rlap` positioning canvas). **It does not work on GitHub**, and not for lack of +tuning — it's a hard limitation: -**Probe A — `\textcolor` + `\rule` (a colored pixel):** +| Attempt | Why it fails on GitHub | +|---|---| +| `\rule` pixels/paths | `\rule` is a **text-mode** TeX command; MathJax implements only math-mode macros, so it's undefined → "unable to render" | +| `\colorbox` grid | **broken on GitHub since May 2023**, not restored | +| `array` with `@{}` / `\\[-Npt]` | unsupported array spec → parse error | +| `\bbox` / `\color` | work, but only color *behind real content* — not a free canvas | -$\textcolor{#9bd64a}{\rule{40px}{16px}}$ +The colored-square `\rule` examples all over the web are **native-LaTeX → image** +tools (readme2tex etc.) that commit a PNG — exactly what "inline-only" rules out. -**Probe B — `\color` two-arg form:** +**What actually draws arbitrary art inline: Unicode braille in a code fence.** +Each braille glyph (U+2800–28FF) packs a **2×4 dot grid**, so a code-generated +field of glyphs is a low-res raster that renders everywhere monospace text does — +no MathJax, no file. -$\color{#9bd64a}{\rule{40px}{16px}}$ +Smiley (`node art-to-braille.mjs smiley`): -**Probe C — `\rlap` + `\hspace` (zero-width overlap + x-offset):** - -$\rlap{\textcolor{#9bd64a}{\rule{16px}{16px}}}\hspace{30px}\textcolor{#5fae3a}{\rule{16px}{16px}}$ - -**Probe D — `\raise` (y-offset):** - -$\raise 14px {\textcolor{#e8ff8a}{\rule{16px}{16px}}}\ \textcolor{#9bd64a}{\rule{16px}{16px}}$ - -**Probe E — plain `array` of rules (no `@{}`):** - -```math -\begin{array}{cc} -\textcolor{#ff4d6d}{\rule{12px}{12px}} & \phantom{\rule{12px}{12px}} \\ -\phantom{\rule{12px}{12px}} & \textcolor{#ff4d6d}{\rule{12px}{12px}} -\end{array} +``` +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣴⣶⣾⣿⣿⠿⠿⠿⠿⢿⣿⣿⣶⣶⣤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢀⣠⣶⣿⡿⠟⠋⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠿⣿⣷⣦⣀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⢀⣴⣿⡿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠻⣿⣷⣄⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⣰⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⣷⡀⠀⠀⠀ +⠀⠀⠀⣼⣿⡟⠁⠀⠀⠀⠀⢰⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣷⠀⠀⠀⠀⠀⠙⣿⣿⡄⠀⠀ +⠀⠀⢰⣿⣿⠁⠀⠀⠀⠀⠀⠈⠛⠛⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⠛⠋⠀⠀⠀⠀⠀⠀⢹⣿⣷⠀⠀ +⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⡇⠀ +⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⡇⠀ +⠀⠀⢹⣿⣷⠀⠀⠀⠀⠀⢿⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⠇⠀⠀⠀⠀⢰⣿⣿⠁⠀ +⠀⠀⠀⢿⣿⣇⠀⠀⠀⠀⠘⢿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⠟⠀⠀⠀⠀⢀⣿⣿⠇⠀⠀ +⠀⠀⠀⠈⢻⣿⣧⡀⠀⠀⠀⠈⠻⣿⣷⣦⣄⡀⠀⠀⠀⠀⠀⣀⣤⣶⣿⡿⠋⠀⠀⠀⠀⣠⣿⣿⠋⠀⠀⠀ +⠀⠀⠀⠀⠀⠙⢿⣿⣦⣀⠀⠀⠀⠀⠉⠛⠿⢿⣿⣿⣿⣿⣿⠿⠟⠋⠁⠀⠀⠀⢀⣠⣾⣿⠟⠁⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠙⠻⣿⣷⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣶⣿⡿⠛⠁⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠿⢿⣿⣷⣶⣶⣤⣤⣤⣤⣴⣶⣶⣿⣿⠿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠙⠛⠛⠛⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ``` -**Probe F — `array` with `@{}` tight columns + negative row gap → ❌ "unable to -render expression" on GitHub.** This is what broke the first heart; `@{}` and -`\\[-Npt]` aren't supported. Conclusion: **don't use arrays for bitmaps** — use -the `\rlap` canvas below, which needs no array at all. - ---- - -## The `\rlap` canvas — draw arbitrary paths inline +Flower (`node art-to-braille.mjs flower`): -`\rlap` typesets with **zero width** (cursor doesn't advance), so a flat sequence -of `\rlap{\hspace{x}\raise y{dot}}` places dots at absolute (x, y). A trailing -`\phantom{\rule{W}{H}}` claims the canvas box. Emit one dot per filled sample and -you trace **any path** — sparse art costs far fewer rules than a full grid. +``` +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣴⣦⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⣶⣤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⢼⣿⣿⣿⣿⣿⣷⣦⠀⠀⠀⠀⠀⠀⢠⣶⣿⣿⣿⣿⣿⣿⠄⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⣼⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⣷⢰⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⢀⣀⣀⣤⣤⣤⣤⣤⣀⣀⡈⠻⢿⣿⣿⣿⣿⣸⣿⣿⣿⣿⠿⠋⣀⣀⣠⣤⣤⣤⣤⣄⣀⣀⠀⠀⠀ +⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀ +⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃ +⠀⠀⠈⠙⠛⠛⠿⠿⠿⠿⠿⠛⠛⠋⣩⣵⣿⣿⣿⣿⢻⣿⣿⣿⣷⣭⡉⠛⠛⠻⠿⠿⠿⠿⠟⠛⠛⠉⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⠇⠀⢿⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀⠈⢿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⢼⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⠄⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠿⢿⡿⠿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠻⠿⣿⠿⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +``` -**Limit:** `\rule` is axis-aligned and MathJax has no rotation, so curves -stair-step (you're rasterizing, not stroking) and there's no antialiasing. +### Draw *literally* anything: PNG → braille -Generated by `node path-to-math.mjs smiley` (124 dots) — a smiley, drawn in math: +`art-to-braille.mjs image pic.png` normalizes any image with `sips`, decodes it +with Node's built-in `zlib` (no image library), thresholds luminance, and packs +the dots. So any picture becomes inline braille art — arbitrary raster, pure text. -```math -\rlap{\hspace{142px}\raise 78px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{141px}\raise 73px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{141px}\raise 69px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{140px}\raise 65px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{139px}\raise 61px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{138px}\raise 57px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{137px}\raise 53px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{135px}\raise 49px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{133px}\raise 46px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{131px}\raise 42px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{128px}\raise 39px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{126px}\raise 35px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{123px}\raise 32px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{120px}\raise 29px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{116px}\raise 27px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{113px}\raise 24px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{110px}\raise 22px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{106px}\raise 20px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{102px}\raise 18px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{98px}\raise 17px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{94px}\raise 16px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{90px}\raise 15px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{86px}\raise 14px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{82px}\raise 14px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{78px}\raise 14px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{73px}\raise 14px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{69px}\raise 14px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{65px}\raise 15px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{61px}\raise 16px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{57px}\raise 17px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{53px}\raise 18px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{49px}\raise 20px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{46px}\raise 22px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{42px}\raise 24px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{39px}\raise 27px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{35px}\raise 29px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{32px}\raise 32px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{29px}\raise 35px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{27px}\raise 39px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{24px}\raise 42px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{22px}\raise 46px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{20px}\raise 49px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{18px}\raise 53px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{17px}\raise 57px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{16px}\raise 61px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{15px}\raise 65px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{14px}\raise 69px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{14px}\raise 73px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{14px}\raise 77px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{14px}\raise 82px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{14px}\raise 86px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{15px}\raise 90px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{16px}\raise 94px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{17px}\raise 98px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{18px}\raise 102px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{20px}\raise 106px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{22px}\raise 110px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{24px}\raise 113px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{27px}\raise 116px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{29px}\raise 120px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{32px}\raise 123px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{35px}\raise 126px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{39px}\raise 128px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{42px}\raise 131px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{45px}\raise 133px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{49px}\raise 135px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{53px}\raise 137px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{57px}\raise 138px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{61px}\raise 139px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{65px}\raise 140px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{69px}\raise 141px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{73px}\raise 141px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{77px}\raise 142px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{82px}\raise 141px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{86px}\raise 141px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{90px}\raise 140px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{94px}\raise 139px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{98px}\raise 138px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{102px}\raise 137px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{106px}\raise 135px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{110px}\raise 133px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{113px}\raise 131px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{116px}\raise 128px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{120px}\raise 126px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{123px}\raise 123px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{126px}\raise 120px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{128px}\raise 116px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{131px}\raise 113px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{133px}\raise 110px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{135px}\raise 106px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{137px}\raise 102px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{138px}\raise 98px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{139px}\raise 94px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{140px}\raise 90px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{141px}\raise 86px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{141px}\raise 82px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{54px}\raise 94px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{56px}\raise 94px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{102px}\raise 94px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{100px}\raise 94px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{102px}\raise 45px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{100px}\raise 44px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{98px}\raise 42px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{96px}\raise 41px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{94px}\raise 40px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{92px}\raise 39px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{90px}\raise 38px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{88px}\raise 37px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{86px}\raise 36px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{83px}\raise 36px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{81px}\raise 36px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{79px}\raise 36px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{76px}\raise 36px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{74px}\raise 36px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{72px}\raise 36px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{69px}\raise 36px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{67px}\raise 37px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{65px}\raise 38px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{63px}\raise 39px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{61px}\raise 40px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{59px}\raise 41px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{57px}\raise 42px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{55px}\raise 44px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\rlap{\hspace{53px}\raise 45px {\textcolor{#e8ff8a}{\rule{5px}{5px}}}} -\phantom{\rule{160px}{160px}} +```bash +node art-to-braille.mjs smiley +node art-to-braille.mjs flower +node art-to-braille.mjs image path/to/pic.png 50 [--invert] ``` -Swap the shape function in `path-to-math.mjs` for anything parametric — -`node path-to-math.mjs flower` emits a flower (petals + core + stem + leaf). The -*output* is pure inline math; the generator is just a convenience for plotting the -points. +**Limits:** low resolution (2×4 dots/char), 1-bit (no grayscale/color), and it +relies on a monospace font + the reader's braille glyph coverage. Great for icons, +logos, mascots, diagrams-as-art; not for photos. --- @@ -254,10 +151,11 @@ flowchart TD f -->|yes| math[math block] f -->|no| g{geographic?} g -->|yes| geo[geojson] - g -->|no| canvas[rlap math-canvas\nicon scale] + g -->|no| br[braille art\nfrom shape or PNG] ``` ## Papers-platter caveat None of this reaches `papers/` xelatex PDFs — it's a **github.com** feature. In a -paper the equivalents are native: TikZ, real LaTeX math, `pgfplots`. +paper the equivalents are native: TikZ, real LaTeX math, `pgfplots`, and real +`\includegraphics` for raster. diff --git a/sandbox/md-graphics-demo/art-to-braille.mjs b/sandbox/md-graphics-demo/art-to-braille.mjs new file mode 100644 index 0000000000..b7eb98ab2a --- /dev/null +++ b/sandbox/md-graphics-demo/art-to-braille.mjs @@ -0,0 +1,122 @@ +// art-to-braille.mjs — draw ANYTHING inline as Unicode braille (no deps, no files). +// +// The only route that actually renders arbitrary art inline on GitHub: pack a +// boolean dot-field into braille glyphs (U+2800, 2 wide x 4 tall dots per char) +// and drop the text in a ``` code fence. No MathJax (\rule is unsupported), no +// committed image. +// +// node art-to-braille.mjs smiley +// node art-to-braille.mjs flower +// node art-to-braille.mjs image path/to/pic.png [cols] [--invert] +// +// image mode shells out to macOS `sips` to normalize+resize any input to an +// 8-bit PNG, then decodes it with Node's built-in zlib (no image library). + +import { execFileSync } from "node:child_process"; +import { inflateSync } from "node:zlib"; +import { readFileSync } from "node:fs"; + +// (col 0..1, row 0..3) -> braille dot bit +const DOTBIT = { "0,0": 0x01, "0,1": 0x02, "0,2": 0x04, "1,0": 0x08, "1,1": 0x10, "1,2": 0x20, "0,3": 0x40, "1,3": 0x80 }; + +function render(cols, rows, on) { + let out = ""; + for (let cy = 0; cy < rows; cy++) { + for (let cx = 0; cx < cols; cx++) { + let code = 0; + for (let r = 0; r < 4; r++) + for (let c = 0; c < 2; c++) + if (on(cx * 2 + c, cy * 4 + r)) code |= DOTBIT[`${c},${r}`]; + out += String.fromCharCode(0x2800 + code); + } + out += "\n"; + } + return out; +} + +// ---- parametric shapes (normalized -1..1 field) ---- +function shapeField(name, W, H) { + return (px, py) => { + const dx = (px / W - 0.5) * 2, dy = (py / H - 0.5) * 2; + if (name === "smiley") { + const ring = Math.abs(Math.hypot(dx, dy) - 0.85) < 0.06; + const eyeL = Math.hypot(dx + 0.35, dy + 0.3) < 0.1; + const eyeR = Math.hypot(dx - 0.35, dy + 0.3) < 0.1; + const smile = dy > 0.1 && Math.abs(Math.hypot(dx, dy - 0.1) - 0.45) < 0.06; + return ring || eyeL || eyeR || smile; + } + if (name === "flower") { + const a = Math.atan2(dy, dx), r = Math.hypot(dx, dy); + const petal = r < 0.78 * Math.abs(Math.cos(3 * a)) + 0.18; // rose curve + const core = r < 0.16; + return petal || core; + } + return Math.hypot(dx, dy) < 0.85; // circle + }; +} + +// ---- minimal PNG decode (8-bit, color types 0/2/4/6, no interlace) ---- +function decodePng(buf) { + let p = 8; // skip signature + let w, h, ct, idat = []; + while (p < buf.length) { + const len = buf.readUInt32BE(p); const type = buf.toString("ascii", p + 4, p + 8); + const data = buf.subarray(p + 8, p + 8 + len); + if (type === "IHDR") { w = data.readUInt32BE(0); h = data.readUInt32BE(4); ct = data[9]; } + else if (type === "IDAT") idat.push(data); + else if (type === "IEND") break; + p += 12 + len; + } + const ch = { 0: 1, 2: 3, 4: 2, 6: 4 }[ct]; + if (!ch) throw new Error("unsupported PNG color type " + ct); + const raw = inflateSync(Buffer.concat(idat)); + const stride = w * ch; + const out = Buffer.alloc(h * stride); + const pae = (a, b, c) => { const pp = a + b - c, da = Math.abs(pp - a), db = Math.abs(pp - b), dc = Math.abs(pp - c); return da <= db && da <= dc ? a : db <= dc ? b : c; }; + let q = 0; + for (let y = 0; y < h; y++) { + const f = raw[q++]; + for (let i = 0; i < stride; i++) { + const x = raw[q++]; + const a = i >= ch ? out[y * stride + i - ch] : 0; + const b = y > 0 ? out[(y - 1) * stride + i] : 0; + const c = i >= ch && y > 0 ? out[(y - 1) * stride + i - ch] : 0; + let v = x; + if (f === 1) v = x + a; else if (f === 2) v = x + b; else if (f === 3) v = x + ((a + b) >> 1); else if (f === 4) v = x + pae(a, b, c); + out[y * stride + i] = v & 0xff; + } + } + return { w, h, ch, px: out }; +} + +function imageField(path, dotW, invert) { + const tmp = "/tmp/braille-norm.png"; + execFileSync("sips", ["-s", "format", "png", "--resampleWidth", String(dotW), path, "-o", tmp], { stdio: "ignore" }); + const { w, h, ch, px } = decodePng(readFileSync(tmp)); + return { + W: w, H: h, + on: (x, y) => { + if (x >= w || y >= h) return false; + const o = (y * w + x) * ch; + const lum = ch >= 3 ? 0.299 * px[o] + 0.587 * px[o + 1] + 0.114 * px[o + 2] : px[o]; + const dark = lum < 128; + return invert ? !dark : dark; + }, + }; +} + +// ---- main ---- +const mode = process.argv[2] || "smiley"; +let cols, rows, on; +if (mode === "image") { + const path = process.argv[3]; + cols = parseInt(process.argv[4] || "50", 10); + const invert = process.argv.includes("--invert"); + const f = imageField(path, cols * 2, invert); + cols = Math.ceil(f.W / 2); rows = Math.ceil(f.H / 4); on = f.on; +} else { + cols = 40; rows = 16; + const W = cols * 2, H = rows * 4; + on = shapeField(mode, W, H); +} +process.stdout.write(render(cols, rows, on)); diff --git a/sandbox/md-graphics-demo/path-to-math.mjs b/sandbox/md-graphics-demo/path-to-math.mjs deleted file mode 100644 index fab7280ef5..0000000000 --- a/sandbox/md-graphics-demo/path-to-math.mjs +++ /dev/null @@ -1,65 +0,0 @@ -// path-to-math.mjs — rasterize point-sets into an INLINE math block (no image). -// -// Each point becomes a zero-width \rlap-positioned colored \rule. \hspace sets x, -// \raise sets y → the math block is an absolute-positioning canvas. No arrays -// (GitHub's MathJax choked on the array `@{}` form), so this is just a flat list -// of \rlap{...} plus a trailing \phantom strut to claim the W×H box. -// -// node path-to-math.mjs smiley > snippet.math -// node path-to-math.mjs flower > snippet.math - -const W = 160, H = 160, DOT = 5; - -function circle(cx, cy, r, n, col) { - const pts = []; - for (let i = 0; i < n; i++) { - const a = (i / n) * Math.PI * 2; - pts.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r, col]); - } - return pts; -} -function arc(cx, cy, r, a0, a1, n, col) { - const pts = []; - for (let i = 0; i < n; i++) { - const a = a0 + (a1 - a0) * (i / (n - 1)); - pts.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r, col]); - } - return pts; -} -function dot(x, y, col) { return [[x, y, col]]; } - -const SHAPES = { - smiley() { - const Y = "#e8ff8a"; - return [ - ...circle(80, 80, 64, 96, Y), // face ring - ...dot(56, 64, Y), ...dot(58, 64, Y), // left eye - ...dot(104, 64, Y), ...dot(102, 64, Y), // right eye - ...arc(80, 88, 34, 0.25 * Math.PI, 0.75 * Math.PI, 24, Y), // smile - ]; - }, - flower() { - const pts = []; - const petal = "#ff8ad0", core = "#e8ff8a", stem = "#9bd64a"; - for (let k = 0; k < 6; k++) { - const a = (k / 6) * Math.PI * 2; - pts.push(...circle(80 + Math.cos(a) * 30, 60 + Math.sin(a) * 30, 18, 36, petal)); - } - pts.push(...circle(80, 60, 14, 28, core)); - for (let y = 78; y < 150; y += DOT) pts.push([80, y, stem]); // stem - pts.push(...arc(80, 120, 22, -0.1 * Math.PI, 0.6 * Math.PI, 10, stem)); // leaf - return pts; - }, -}; - -const shape = process.argv[2] || "smiley"; -const pts = (SHAPES[shape] || SHAPES.smiley)(); - -const segs = pts.map(([x, y, col]) => { - const hx = Math.max(0, Math.round(x - DOT / 2)); - // y grows downward in our coords; \raise lifts up, so invert against H. - const ry = Math.round(H - y - DOT / 2); - return `\\rlap{\\hspace{${hx}px}\\raise ${ry}px {\\textcolor{${col}}{\\rule{${DOT}px}{${DOT}px}}}}`; -}); - -process.stdout.write(segs.join("\n") + `\n\\phantom{\\rule{${W}px}{${H}px}}\n`); -- 2.51.2