From 2cc880fcf29605d90b099aab2bb0d5175ff68403 Mon Sep 17 00:00:00 2001 From: nstarman Date: Sat, 3 Oct 2026 21:27:23 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20assets:=20add=20a=20logo,=20a=20?= =?UTF-8?q?frozen=20dict?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xmmutablemap had no logo. It now has a snowflake between a dict's braces: a mapping that cannot change, in GalacticDynamics' purple and teal. docs/_static/make_logo.py writes it as an SVG (943 bytes), and as a PNG at any size on request: uv run docs/_static/make_logo.py --size 2048 big.png Co-Authored-By: Claude Opus 5.5 --- docs/_static/favicon.svg | 7 +++ docs/_static/make_logo.py | 115 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 docs/_static/favicon.svg create mode 100644 docs/_static/make_logo.py diff --git a/docs/_static/favicon.svg b/docs/_static/favicon.svg new file mode 100644 index 0000000..7c4dccb --- /dev/null +++ b/docs/_static/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/_static/make_logo.py b/docs/_static/make_logo.py new file mode 100644 index 0000000..8488631 --- /dev/null +++ b/docs/_static/make_logo.py @@ -0,0 +1,115 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = ["resvg-py"] +# /// +"""Draw the xmmutablemap logo: a frozen dict. + +A snowflake between a dict's braces: a mapping that cannot change, in +GalacticDynamics' purple and teal. The shapes are vector, so the logo is written +as an SVG, sharp at any size; for a bitmap, name a .png and give its size:: + + uv run docs/_static/make_logo.py # favicon.svg + uv run docs/_static/make_logo.py --size 2048 big.png +""" + +import argparse +import math +from pathlib import Path + +TEAL, PURPLE = "#66a19a", "#7738eb" # GalacticDynamics' colours + +# In a 64-unit square. Each brace: its ends' x, its top and bottom, its depth. +BRACES = (15, 49, 10, 54, 3.4) +# The snowflake: its centre, its arms' half-length, and where along each arm +# its pair of side branches grows, and how long they are. +CENTRE, ARM, BRANCH_AT, BRANCH = 32, 13, 8, 4.5 + +SVG = """\ + + + + + + + +""" + + +def brace(x: float, sign: int) -> str: + """Return one brace as an SVG path: ``sign`` 1 opens right, -1 opens left.""" + _, _, top, bottom, depth = BRACES + h, mid = bottom - top, (top + bottom) / 2 + # The brace's back, its point, and where its curves pull in to the point. + back, tip, neck = x - sign * depth, x - sign * 2.2 * depth, x - sign * 1.8 * depth + return ( + f"M{x:g} {top:g}" + f"C{back:g} {top:g} {back:g} {top + 0.08 * h:g} {back:g} {top + 0.2 * h:g}" + f"V{mid - 0.12 * h:g}" + f"C{back:g} {mid - 0.03 * h:g} {neck:g} {mid:g} {tip:g} {mid:g}" + f"C{neck:g} {mid:g} {back:g} {mid + 0.03 * h:g} {back:g} {mid + 0.12 * h:g}" + f"V{bottom - 0.2 * h:g}" + f"C{back:g} {bottom - 0.08 * h:g} {back:g} {bottom:g} {x:g} {bottom:g}" + ) + + +def snowflake() -> tuple[str, str]: + """Return the snowflake's three arms, and its side branches, as SVG paths.""" + arms, branches = [], [] + for k in range(3): + angle = math.radians(90 + 60 * k) + dx, dy = ARM * math.cos(angle), ARM * math.sin(angle) + arms.append( + f"M{CENTRE - dx:.2f} {CENTRE - dy:.2f}L{CENTRE + dx:.2f} {CENTRE + dy:.2f}" + ) + for end in (0, math.pi): # both ends of the arm + out = angle + end + bx = CENTRE + BRANCH_AT * math.cos(out) + by = CENTRE + BRANCH_AT * math.sin(out) + for spread in (30, -30): + a = out + math.radians(spread) + branches.append( + f"M{bx:.2f} {by:.2f}" + f"L{bx + BRANCH * math.cos(a):.2f} {by + BRANCH * math.sin(a):.2f}" + ) + return "".join(arms), "".join(branches) + + +def svg() -> str: + """Return the logo as SVG text.""" + left, right, *_ = BRACES + arms, branches = snowflake() + return SVG.format( + braces=brace(left, 1) + brace(right, -1), + arms=arms, + branches=branches, + purple=PURPLE, + teal=TEAL, + ) + + +def main() -> None: + """Parse the command line and save the logo.""" + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument( + "out", + nargs="?", + type=Path, + default=Path(__file__).with_name("favicon.svg"), + help="output file, SVG or PNG by its extension (default: favicon.svg)", + ) + parser.add_argument( + "--size", type=int, default=512, help="pixels per side, for a PNG" + ) + args = parser.parse_args() + + if args.out.suffix == ".svg": + args.out.write_text(svg()) + else: + import resvg_py # noqa: PLC0415 # only a PNG needs a renderer + + png = resvg_py.svg_to_bytes(svg_string=svg(), width=args.size) + args.out.write_bytes(bytes(png)) + + +if __name__ == "__main__": + main()