Add Rippr source snapshot and full-history bundle

Two copies for two jobs. rippr-src/ is a browsable git archive export of
the tracked tree at 2f76983 - no build outputs, no local.properties, no
nested .git - which is convenient to read in gitea but carries no history
and will drift.

rippr-full-history.bundle is the real backup: all 18 commits, verified as
"records a complete history" and test-cloned before committing. This
matters because ~/dojo/rippr has no git remote and otherwise exists only
on one machine.

rippr-src/SNAPSHOT.md explains the difference and how to restore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 08:30:25 -05:00
parent 4debdfa518
commit 280fd7f988
106 changed files with 10377 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""
Generates the Rippr logo concept set as standalone SVGs, then rasterises them.
Everything is authored on Android's adaptive-icon canvas (108x108) with the key
shapes kept inside the 66-unit safe circle centred at (54,54), so a chosen concept
converts to res/drawable/ic_launcher_foreground.xml without re-drawing anything.
The figurative geometry is Google's Material Symbols (Apache-2.0, vendored under
design/material/) rather than hand-approximated curves. Only the framing — gauge
arcs, badge grounds, rings — is authored here, because arcs and circles are the
shapes that can be computed exactly instead of eyeballed.
"""
import math
import pathlib
import re
import subprocess
HERE = pathlib.Path(__file__).parent
OUT = HERE / "logos"
GLYPHS = HERE / "material"
PNG = HERE / "preview"
CX = CY = 54.0
INK = "#F2F5F8"
ACCENT = "#FF5722"
DEEP = "#101418"
DIM = "#66727E"
# Material glyphs are drawn in a 960 box spanning y = -960..0.
GLYPH_BOX = 960.0
def glyph_path(name: str) -> str:
"""Extract the single path's `d` attribute from a vendored Material Symbol."""
svg = (GLYPHS / f"{name}.svg").read_text()
m = re.search(r'<path[^>]*\sd="([^"]+)"', svg)
if not m:
raise ValueError(f"no path found in {name}.svg")
return m.group(1)
def glyph(name: str, size: float, cx: float = CX, cy: float = CY, fill: str = INK,
rotate: float = 0.0) -> str:
"""Place a Material glyph centred on (cx, cy), scaled to `size` units square."""
s = size / GLYPH_BOX
# Map x: 0..960 -> centred; y: -960..0 -> centred.
tx = cx - size / 2
ty = cy + size / 2
rot = f' transform="rotate({rotate} {cx} {cy})"' if rotate else ""
return (
f'<g{rot}><path d="{glyph_path(name)}" fill="{fill}" '
f'transform="translate({tx:.3f},{ty:.3f}) scale({s:.6f})"/></g>'
)
def polar(cx, cy, r, deg):
a = math.radians(deg)
return cx + r * math.cos(a), cy - r * math.sin(a)
def arc(cx, cy, r, start_deg, end_deg):
x0, y0 = polar(cx, cy, r, start_deg)
x1, y1 = polar(cx, cy, r, end_deg)
large = 1 if abs(end_deg - start_deg) > 180 else 0
sweep = 0 if end_deg > start_deg else 1
return f"M{x0:.3f},{y0:.3f} A{r:.3f},{r:.3f} 0 {large} {sweep} {x1:.3f},{y1:.3f}"
# --------------------------------------------------------------------------
# A — Apex. Material's `speed` gauge inside a computed tachometer sweep, with the
# last quarter of the sweep in accent as a redline.
# --------------------------------------------------------------------------
def concept_apex():
parts = [
f'<path d="{arc(CX, CY, 44, 215, -35)}" fill="none" stroke="{DIM}" '
f'stroke-width="5" stroke-linecap="round" opacity="0.55"/>',
f'<path d="{arc(CX, CY, 44, 25, -35)}" fill="none" stroke="{ACCENT}" '
f'stroke-width="5" stroke-linecap="round"/>',
]
for i in range(11):
deg = 215 - i * 25
major = i % 5 == 0
r0 = 34 if major else 37
x0, y0 = polar(CX, CY, r0, deg)
x1, y1 = polar(CX, CY, 39.5, deg)
parts.append(
f'<line x1="{x0:.2f}" y1="{y0:.2f}" x2="{x1:.2f}" y2="{y1:.2f}" '
f'stroke="{INK}" stroke-width="{2.6 if major else 1.6}" '
f'stroke-linecap="round" opacity="{0.85 if major else 0.45}"/>'
)
parts.append(glyph("speed", 46))
return "".join(parts)
# --------------------------------------------------------------------------
# D — Route. Material's `route` glyph ringed by a thin border, reading as a map
# trace rather than an instrument.
# --------------------------------------------------------------------------
def concept_route():
return (
f'<circle cx="{CX}" cy="{CY}" r="43" fill="none" stroke="{DIM}" '
f'stroke-width="3.5" opacity="0.6"/>'
+ glyph("route", 54, fill=INK)
+ f'<circle cx="{CX}" cy="{CY}" r="43" fill="none" stroke="{ACCENT}" '
f'stroke-width="3.5" stroke-linecap="round" '
f'stroke-dasharray="34 236" stroke-dashoffset="-100"/>'
)
# --------------------------------------------------------------------------
# E — Heading. `navigation` is the cleanest silhouette Material offers and the
# only one that stays unambiguous as a flat 24px status-bar icon.
# --------------------------------------------------------------------------
def concept_heading():
heading = 20 # degrees clockwise from north
# The accent segment marks where the arrow points, so the two read as one object
# rather than a ring with an arrow dropped inside it.
centre = 90 - heading
parts = [
f'<circle cx="{CX}" cy="{CY}" r="42" fill="none" stroke="{DIM}" '
f'stroke-width="2.5" opacity="0.7"/>',
f'<path d="{arc(CX, CY, 42, centre + 26, centre - 26)}" fill="none" '
f'stroke="{ACCENT}" stroke-width="5.5" stroke-linecap="round"/>',
# The glyph's ink sits below its box centre, so nudge up to centre it optically.
glyph("navigation", 48, cy=CY - 2, rotate=heading),
]
return "".join(parts)
CONCEPTS = {
"apex": ("Apex", "Tachometer sweep with redline", concept_apex),
"route": ("Route", "Recorded trace inside a ring", concept_route),
"heading": ("Heading", "Navigation arrow with heading arc", concept_heading),
}
def svg_doc(body, bg=None):
bg_rect = f'<rect width="108" height="108" fill="{bg}"/>' if bg else ""
return (
'<svg xmlns="http://www.w3.org/2000/svg" width="108" height="108" '
f'viewBox="0 0 108 108">{bg_rect}{body}</svg>'
)
def rasterise():
"""Render each concept large so the output can actually be reviewed."""
PNG.mkdir(parents=True, exist_ok=True)
for key in CONCEPTS:
src = OUT / f"rippr-{key}-onbg.svg"
dst = PNG / f"{key}.png"
subprocess.run(
["rsvg-convert", "-w", "512", "-h", "512", str(src), "-o", str(dst)],
check=True,
)
# Strips for review. `montage` needs a font for its auto-labels and fails without
# one, so compose with +append instead — no text involved.
def strip(px: int, name: str):
subprocess.run(
["magick"]
+ [str(PNG / f"{k}.png") for k in CONCEPTS]
+ ["-resize", f"{px}x{px}", "-background", "#05070A", "-splice", "8x0",
"+append", str(PNG / name)],
check=True,
)
strip(512, "contact-sheet.png")
# True launcher and status-bar sizes: where detail either survives or turns to mush.
strip(48, "small-48.png")
strip(24, "small-24.png")
def main():
OUT.mkdir(parents=True, exist_ok=True)
for key, (_name, _desc, fn) in CONCEPTS.items():
body = fn()
(OUT / f"rippr-{key}.svg").write_text(svg_doc(body))
(OUT / f"rippr-{key}-onbg.svg").write_text(svg_doc(body, bg=DEEP))
print(f"wrote rippr-{key}.svg")
rasterise()
print(f"rendered PNGs -> {PNG}")
if __name__ == "__main__":
main()