mirror of
https://github.com/piabrota/mu.git
synced 2026-11-07 06:15:59 -10:00
- mu is piabrota's company chart, not an umbrella, and the README header is centred. - products/ holds pirsig, alerts, ihero, gramatico, nix-os, dojo, rax and agents. The yggdrasil and asgard submodules go; our forks of them stay in the index. - PIABROTA.md follows the org as it is now: 24 public repos. - The logo is the eye that piabrota.art shows. む is drawn in pure Nix, with nix-os' pen, and checked by the flake. - .claude/skills/adr-verbatim is copied unchanged from davidondrej/cloudroom, under Apache-2.0, with credit in NOTICE.md. - The map shows the eight products, and the portfolio figure goes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
334 lines
12 KiB
Nix
334 lines
12 KiB
Nix
# A pen for Desmos line art, in pure Nix: pirsig's pen (crates/pirsig-drawings, at 450e4da)
|
||
# Copied from piabrota/nix-os (drawings/pen.nix at 6d636be), MPL-2.0.
|
||
# ported from Rust.
|
||
#
|
||
# A stroke is a list of points. The pen runs a Catmull-Rom spline through them, so the line is
|
||
# smooth and passes through every point. Each piece of the spline is a cubic Bézier, and each
|
||
# Bézier is one Desmos parametric expression, `(a+bt+ct^2+dt^3, …)` for `0 ≤ t ≤ 1`.
|
||
#
|
||
# The SVG has y down and Desmos has y up: a Desmos y is `H - y` of the SVG.
|
||
# Nix has no fused multiply-add and no square root: `fma` stands in for Rust's `mul_add`, so the
|
||
# numbers come out as the Rust pen's did, and the check compares squared distances.
|
||
{ lib }:
|
||
let
|
||
inherit (builtins) concatStringsSep elemAt floor genList length;
|
||
|
||
# The ink of the drawing, light and dark, as in picasso's drawings.
|
||
ink = "#1f2328";
|
||
inkDark = "#e6edf3";
|
||
# The blue of the check render, the same as in piabrota/alerts, and the orange of its Desmos layer.
|
||
blue = "#1682D4";
|
||
orange = "#FF4F00";
|
||
|
||
x = p: elemAt p 0;
|
||
y = p: elemAt p 1;
|
||
|
||
# Rust's f64::round, half away from zero, as an integer.
|
||
round = v: if v < 0 then -(floor (0.5 - v)) else floor (v + 0.5);
|
||
abs = v: if v < 0 then -v else v;
|
||
|
||
# a * b + c as one rounding, as Rust's f64::mul_add: the exact product as a sum of two floats
|
||
# (Dekker's split), then a compensated sum (Ogita, Rump and Oishi, 2005). Rounding a * b first
|
||
# moves some numbers of the drawing by 0.001.
|
||
fma = a: b: c:
|
||
let
|
||
split = v:
|
||
let t = 134217729.0 * v; hi = t - (t - v);
|
||
in [ hi (v - hi) ];
|
||
sa = split a;
|
||
sb = split b;
|
||
p = a * b;
|
||
e = elemAt sa 0 * elemAt sb 0 - p + elemAt sa 0 * elemAt sb 1 + elemAt sa 1 * elemAt sb 0
|
||
+ elemAt sa 1 * elemAt sb 1;
|
||
s = p + c;
|
||
z = s - p;
|
||
err = p - (s - z) + (c - z);
|
||
in
|
||
s + (err + e);
|
||
|
||
# The integer `a` with its last `d` digits as decimals, after a minus sign when `neg`.
|
||
decimals = d: neg: a:
|
||
let p = lib.foldl' (acc: _: acc * 10) 1 (genList (_: null) d);
|
||
in "${lib.optionalString neg "-"}${toString (a / p)}.${lib.fixedWidthNumber d (a - a / p * p)}";
|
||
|
||
# `v` with one decimal, as Rust's `{:.1}`: the nearest of the exact value, a tie to even.
|
||
tenths = v:
|
||
let
|
||
f = floor (v * 10.0);
|
||
r = fma v 10.0 (-f * 1.0);
|
||
n = if r > 0.5 || (r == 0.5 && lib.mod f 2 != 0) then f + 1 else f;
|
||
in
|
||
decimals 1 (v < 0) (abs n);
|
||
|
||
# A number with at most three decimals, without trailing zeros: `12`, `-3.5`, `0.125`.
|
||
num = v:
|
||
let
|
||
n = round (v * 1000.0);
|
||
m = builtins.match "(-?[0-9]+)\\.([0-9]*[1-9])?0*" (decimals 3 (n < 0) (abs n));
|
||
in
|
||
if elemAt m 1 == null then elemAt m 0 else "${elemAt m 0}.${elemAt m 1}";
|
||
|
||
# `a+bt+ct^{2}+dt^{3}`, skipping the terms that round to zero.
|
||
poly = c:
|
||
num (elemAt c 0) + concatStringsSep "" (genList
|
||
(i:
|
||
let
|
||
k = i + 1;
|
||
s = num (elemAt c k);
|
||
pow = if k == 1 then "t" else "t^{${toString k}}";
|
||
in
|
||
lib.optionalString (s != "0") "${lib.optionalString (!lib.hasPrefix "-" s) "+"}${s}${pow}")
|
||
3);
|
||
|
||
# The power form `a + bt + ct² + dt³` of each axis of a Bézier: [[a b c d] [a b c d]].
|
||
coeffs = bz:
|
||
let
|
||
f = axis:
|
||
let
|
||
a = axis (elemAt bz 0);
|
||
b = axis (elemAt bz 1);
|
||
c = axis (elemAt bz 2);
|
||
d = axis (elemAt bz 3);
|
||
in
|
||
[ a (3.0 * (b - a)) (3.0 * (fma 2.0 (-b) a + c)) (fma 3.0 (b - c) (d - a)) ];
|
||
in
|
||
[ (f x) (f y) ];
|
||
|
||
# The point of a Bézier at `t`.
|
||
at = bz: t:
|
||
let
|
||
u = 1.0 - t;
|
||
f = axis:
|
||
fma (u * u * u) (axis (elemAt bz 0)) (fma (3.0 * u * u * t) (axis (elemAt bz 1))
|
||
(fma (3.0 * u * t * t) (axis (elemAt bz 2)) (t * t * t * axis (elemAt bz 3))));
|
||
in
|
||
[ (f x) (f y) ];
|
||
|
||
# The same curve between `t0` and `t1`, as a Bézier of its own.
|
||
part = bz: t0: t1:
|
||
let
|
||
h = t1 - t0;
|
||
# derivative at t, for the control points of the piece
|
||
deriv = t:
|
||
let
|
||
u = 1.0 - t;
|
||
g = axis:
|
||
let
|
||
a = axis (elemAt bz 0);
|
||
b = axis (elemAt bz 1);
|
||
c = axis (elemAt bz 2);
|
||
e = axis (elemAt bz 3);
|
||
in
|
||
3.0 * fma (u * u) (b - a) (fma (2.0 * u * t) (c - b) (t * t * (e - c)));
|
||
in
|
||
[ (g x) (g y) ];
|
||
a = at bz t0;
|
||
b = at bz t1;
|
||
da = deriv t0;
|
||
db = deriv t1;
|
||
in
|
||
[
|
||
a
|
||
[ (fma (x da) (h / 3.0) (x a)) (fma (y da) (h / 3.0) (y a)) ]
|
||
[ (fma (x db) (-h / 3.0) (x b)) (fma (y db) (-h / 3.0) (y b)) ]
|
||
b
|
||
];
|
||
|
||
# The Béziers of the open Catmull-Rom spline through `pts`, held at its ends.
|
||
spline = pts:
|
||
let
|
||
n = length pts;
|
||
get = i: elemAt pts (lib.max 0 (lib.min (n - 1) i));
|
||
in
|
||
genList
|
||
(i:
|
||
let
|
||
p0 = get (i - 1);
|
||
p1 = get i;
|
||
p2 = get (i + 1);
|
||
p3 = get (i + 2);
|
||
in
|
||
[
|
||
p1
|
||
[ (x p1 + (x p2 - x p0) / 6.0) (y p1 + (y p2 - y p0) / 6.0) ]
|
||
[ (x p2 - (x p3 - x p1) / 6.0) (y p2 - (y p3 - y p1) / 6.0) ]
|
||
p2
|
||
])
|
||
(n - 1);
|
||
|
||
# Pieces each segment of a brush stroke is cut into, so its width changes smoothly.
|
||
pieces = 4;
|
||
|
||
# A brush stroke through `pts`: the smooth line of `spline`, whose width follows `widths` (one
|
||
# per point) as a brush presses down and lifts. Each segment is cut into `pieces` Béziers, each
|
||
# a mark { bezier; width; } drawn at the width of its middle.
|
||
brush = pts: widths:
|
||
assert length pts == length widths;
|
||
lib.concatLists (lib.imap0
|
||
(i: bz: genList
|
||
(j:
|
||
let
|
||
t0 = j * 1.0 / pieces;
|
||
t1 = (j + 1) * 1.0 / pieces;
|
||
mid = (t0 + t1) / 2.0;
|
||
w0 = elemAt widths i;
|
||
in
|
||
{
|
||
bezier = part bz t0 t1;
|
||
width = fma (elemAt widths (i + 1) - w0) mid w0;
|
||
})
|
||
pieces)
|
||
(spline pts));
|
||
|
||
# Pretty JSON as serde_json writes it: two spaces, sorted keys, a float with its `.0`.
|
||
json = indent: v:
|
||
let next = indent + " ";
|
||
in
|
||
if builtins.isAttrs v then
|
||
"{\n${concatStringsSep ",\n" (map (k: "${next}${builtins.toJSON k}: ${json next v.${k}}") (builtins.attrNames v))}\n${indent}}"
|
||
else if builtins.isList v then
|
||
"[\n${concatStringsSep ",\n" (map (e: next + json next e) v)}\n${indent}]"
|
||
else if builtins.isFloat v then
|
||
(if floor v == v then "${toString (floor v)}.0" else throw "json: ${toString v} is not whole")
|
||
else builtins.toJSON v;
|
||
|
||
# The two coordinates of `\left(x,y\right)`, each as polynomial coefficients by power of t.
|
||
parsePair = latex:
|
||
let
|
||
m = builtins.match "\\\\left\\(([^,]*),([^,]*)\\\\right\\)" latex;
|
||
in
|
||
if m == null then throw "cannot read ${latex}" else map parsePoly m;
|
||
|
||
# `12.5-3t+0.25t^{2}` → [12.5 -3 0.25 0].
|
||
parsePoly = s:
|
||
let
|
||
parts = builtins.split "([+-]?[^+-]+)" s;
|
||
read = term:
|
||
let m = builtins.match "\\+?(-?[0-9.]+)(t(\\^\\{([0-9])})?)?" term;
|
||
in
|
||
if m == null then throw "cannot read the term ${term} of ${s}"
|
||
else {
|
||
c = builtins.fromJSON (elemAt m 0) * 1.0;
|
||
k = if elemAt m 1 == null then 0 else if elemAt m 3 == null then 1 else lib.toInt (elemAt m 3);
|
||
};
|
||
terms = map (m: read (elemAt m 0)) (builtins.filter builtins.isList parts);
|
||
in
|
||
assert builtins.all (p: builtins.isList p || p == "") parts;
|
||
genList (k: lib.foldl' (acc: r: if r.k == k then acc + r.c else acc) 0.0 terms) 4;
|
||
|
||
evalPoly = c: t: lib.foldr (v: acc: fma acc t v) 0.0 c;
|
||
in
|
||
{
|
||
inherit fma num poly coeffs at part evalPoly parsePoly brush tenths;
|
||
|
||
# A finished drawing: its canvas `w` × `h`, its label and description, and its marks.
|
||
drawing = { w, h, label, desc, marks }:
|
||
let
|
||
expressions = lib.imap1
|
||
(i: mark:
|
||
let
|
||
c = coeffs mark.bezier;
|
||
cx = elemAt c 0;
|
||
cy = elemAt c 1;
|
||
px = poly cx;
|
||
py = poly ([ (h - elemAt cy 0) ] ++ map (v: -v) (lib.drop 1 cy));
|
||
in
|
||
{
|
||
type = "expression";
|
||
id = toString i;
|
||
color = blue;
|
||
latex = "\\left(${px},${py}\\right)";
|
||
parametricDomain = { min = "0"; max = "1"; };
|
||
lineWidth = num mark.width;
|
||
})
|
||
marks;
|
||
|
||
# The geometry as SVG paths, one per mark.
|
||
shapes = map
|
||
(mark:
|
||
let
|
||
b = mark.bezier;
|
||
pt = p: "${num (x p)} ${num (y p)}";
|
||
in
|
||
"<path d=\"M${pt (elemAt b 0)} C${pt (elemAt b 1)} ${pt (elemAt b 2)} ${pt (elemAt b 3)}\" stroke-width=\"${num mark.width}\"/>")
|
||
marks;
|
||
|
||
sw = num w;
|
||
sh = num h;
|
||
in
|
||
{
|
||
# The drawing as a Desmos graph state (`version` 11).
|
||
desmos = json ""
|
||
{
|
||
version = 11;
|
||
graph = {
|
||
viewport = { xmin = 0; xmax = w; ymin = 0; ymax = h; };
|
||
showGrid = false;
|
||
showXAxis = false;
|
||
showYAxis = false;
|
||
};
|
||
expressions.list = expressions;
|
||
} + "\n";
|
||
|
||
# The drawing as SVG, in ink, as picasso draws: near black on light pages, near
|
||
# white on dark ones (`prefers-color-scheme`).
|
||
svg = ''
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${sw} ${sh}" width="${sw}" height="${sh}" role="img" aria-label="${label}">
|
||
<title>${label}</title>
|
||
<desc>${desc}</desc>
|
||
<style>.l{fill:none;stroke:${ink};stroke-linecap:round;stroke-linejoin:round}.d{fill:${ink};stroke:none}@media (prefers-color-scheme:dark){.l{stroke:${inkDark}}.d{fill:${inkDark}}}</style>
|
||
<g class="l">
|
||
${concatStringsSep "\n " shapes}
|
||
</g>
|
||
</svg>
|
||
'';
|
||
|
||
# The check render: the geometry in blue and, on top, each Desmos expression read back from
|
||
# its LaTeX and sampled in orange. Where the two agree, the orange line runs inside the blue.
|
||
checkSvg =
|
||
let
|
||
orangeLines = map
|
||
(e:
|
||
let
|
||
f = parsePair e.latex;
|
||
pts = genList
|
||
(i:
|
||
let t = i / 24.0;
|
||
in "${tenths (evalPoly (elemAt f 0) t)},${tenths (h - evalPoly (elemAt f 1) t)}")
|
||
25;
|
||
lw = builtins.fromJSON e.lineWidth * 1.0;
|
||
in
|
||
"<polyline points=\"${concatStringsSep " " pts}\" stroke-width=\"${num (lib.min (0.4 * lw) 0.8)}\"/>")
|
||
expressions;
|
||
in
|
||
''
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${sw} ${sh}" width="${sw}" height="${sh}" role="img" aria-label="${label}, with its Desmos expressions over it">
|
||
<g fill="none" stroke="${blue}" stroke-linecap="round" stroke-linejoin="round">
|
||
${concatStringsSep "\n " shapes}
|
||
</g>
|
||
<g fill="none" stroke="${orange}" stroke-linecap="round" stroke-linejoin="round">
|
||
${concatStringsSep "\n " orangeLines}
|
||
</g>
|
||
</svg>
|
||
'';
|
||
|
||
# The square of how far, at most, any Desmos expression strays from the line it stands for:
|
||
# each one is read back from its LaTeX and compared with its Bézier at eleven points.
|
||
worstSquared = lib.foldl' lib.max 0.0 (lib.zipListsWith
|
||
(mark: e:
|
||
let f = parsePair e.latex;
|
||
in lib.foldl' lib.max 0.0 (genList
|
||
(i:
|
||
let
|
||
t = i / 10.0;
|
||
p = at mark.bezier t;
|
||
dx = evalPoly (elemAt f 0) t - x p;
|
||
dy = h - evalPoly (elemAt f 1) t - y p;
|
||
in
|
||
dx * dx + dy * dy)
|
||
11))
|
||
marks
|
||
expressions);
|
||
};
|
||
}
|