HaikuDepot icons for oxy and oh (HVIF and PNGs) and the script that draws them (refs #157) 16aa9e03d85b
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key e762beeaa0bb
- When
- · 2026-10-29 17:58 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
16aa9e03d85bcc1285bb49bf98460ad57077aadefdfdb6fd3cc85b13e0965a2c- Depends on
- 045340293dc7 8fd78eb52ad2 a42f9fb47e37 ed931d1d1102
- Depended on by
- 135bdcadec4a 2f0b4b0651ea c397a8c36a9f 6f4226bfb87f 91334b7fa574
- In bookmarks
- main
- In tags
- v0.10.0 v0.11.0
- Edits
- 13 atoms, 12 tree operations, 13 files
Browse the tree at this change
added packaging/haikuports/icons/oh-16.png +0 −0
No line changes.
added packaging/haikuports/icons/oh-32.png +0 −0
No line changes.
added packaging/haikuports/icons/oh-64.png +0 −0
No line changes.
added packaging/haikuports/icons/oh.hvif +0 −0
No line changes.
added packaging/haikuports/icons/oh.svg +17 −0
| 1 | + | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64"> |
|
| 2 | + | <defs> |
|
| 3 | + | <linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-22" y1="-11" x2="22" y2="11"> |
|
| 4 | + | <stop offset="0%" stop-color="#00F0FF"/> |
|
| 5 | + | <stop offset="100%" stop-color="#0072FF"/> |
|
| 6 | + | </linearGradient> |
|
| 7 | + | </defs> |
|
| 8 | + | <rect width="64" height="64" rx="14" fill="#070c18"/> |
|
| 9 | + | <g transform="translate(32 32)"> |
|
| 10 | + | <path d="M -11 -6 L 11 -6" stroke="url(#g)" stroke-width="3" stroke-linecap="round"/> |
|
| 11 | + | <path d="M -11 6 L 11 6" stroke="url(#g)" stroke-width="3" stroke-linecap="round"/> |
|
| 12 | + | <circle cx="-11" cy="0" r="10" fill="none" stroke="url(#g)" stroke-width="3"/> |
|
| 13 | + | <circle cx="-11" cy="0" r="3.5" fill="#00F0FF"/> |
|
| 14 | + | <circle cx="11" cy="0" r="10" fill="none" stroke="url(#g)" stroke-width="3"/> |
|
| 15 | + | <circle cx="11" cy="0" r="3.5" fill="#FFFFFF"/> |
|
| 16 | + | </g> |
|
| 17 | + | </svg> |
added packaging/haikuports/icons/oxy-16.png +0 −0
No line changes.
added packaging/haikuports/icons/oxy-32.png +0 −0
No line changes.
added packaging/haikuports/icons/oxy-64.png +0 −0
No line changes.
added packaging/haikuports/icons/oxy.hvif +0 −0
No line changes.
added packaging/haikuports/icons/oxy.svg +17 −0
| 1 | + | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64"> |
|
| 2 | + | <defs> |
|
| 3 | + | <linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-22" y1="-11" x2="22" y2="11"> |
|
| 4 | + | <stop offset="0%" stop-color="#00F0FF"/> |
|
| 5 | + | <stop offset="100%" stop-color="#0072FF"/> |
|
| 6 | + | </linearGradient> |
|
| 7 | + | </defs> |
|
| 8 | + | <rect width="64" height="64" rx="14" fill="#C9DCF5"/><rect x="1.5" y="1.5" width="61" height="61" rx="12.5" fill="#F2F7FF"/> |
|
| 9 | + | <g transform="translate(32 32)"> |
|
| 10 | + | <path d="M -11 -6 L 11 -6" stroke="url(#g)" stroke-width="3" stroke-linecap="round"/> |
|
| 11 | + | <path d="M -11 6 L 11 6" stroke="url(#g)" stroke-width="3" stroke-linecap="round"/> |
|
| 12 | + | <circle cx="-11" cy="0" r="10" fill="none" stroke="url(#g)" stroke-width="3"/> |
|
| 13 | + | <circle cx="-11" cy="0" r="3.5" fill="#00F0FF"/> |
|
| 14 | + | <circle cx="11" cy="0" r="10" fill="none" stroke="url(#g)" stroke-width="3"/> |
|
| 15 | + | <circle cx="11" cy="0" r="3.5" fill="#0072FF"/> |
|
| 16 | + | </g> |
|
| 17 | + | </svg> |
added packaging/haikuports/make-icons.py +163 −0
| 1 | + | #!/usr/bin/env python3 |
|
| 2 | + | """HaikuDepot icons for oxy and oh (oxygen#157): the Oxygen mark as Haiku's |
|
| 3 | + | vector icon format (HVIF), and as SVG to render the 16, 32 and 64 px PNGs. |
|
| 4 | + | ||
| 5 | + | A family of two (Andy, 2026-10-29): Oxygen (oxy, the Oxygen VCS extension) |
|
| 6 | + | is the mark on a pale rounded tile with a blue right dot; OxygenHub (oh, the |
|
| 7 | + | OxygenHub extension) is the mark on the hub's dark tile with a white one, as |
|
| 8 | + | oxygenhub/assets/icons/oxygenhub.svg. Each keeps its edge on light and dark |
|
| 9 | + | backgrounds alike. |
|
| 10 | + | ||
| 11 | + | make-icons.py OUTDIR writes oxy.hvif, oh.hvif, oxy.svg, oh.svg |
|
| 12 | + | ||
| 13 | + | HVIF as Haiku's FlatIconImporter reads it: "ncif", then styles, paths and |
|
| 14 | + | shapes, each a count byte and its items; coordinates in a 64-unit canvas. |
|
| 15 | + | """ |
|
| 16 | + | import math |
|
| 17 | + | import struct |
|
| 18 | + | import sys |
|
| 19 | + | ||
| 20 | + | CYAN, BLUE, WHITE, TILE = (0x00, 0xF0, 0xFF), (0x00, 0x72, 0xFF), (0xFF, 0xFF, 0xFF), (0x07, 0x0C, 0x18) |
|
| 21 | + | LIGHT, EDGE = (0xF2, 0xF7, 0xFF), (0xC9, 0xDC, 0xF5) |
|
| 22 | + | K = 0.5522847498 # a circle's quarter as one cubic Bézier |
|
| 23 | + | ||
| 24 | + | ||
| 25 | + | def coord(v): |
|
| 26 | + | if v == int(v) and -32 <= v <= 95: |
|
| 27 | + | return bytes([int(v) + 32]) |
|
| 28 | + | n = int(round((v + 128.0) * 102.0)) |
|
| 29 | + | return bytes([(n >> 8) | 0x80, n & 0xFF]) |
|
| 30 | + | ||
| 31 | + | ||
| 32 | + | def float24(v): |
|
| 33 | + | if v == 0: |
|
| 34 | + | return b"\0\0\0" |
|
| 35 | + | bits = struct.unpack(">I", struct.pack(">f", v))[0] |
|
| 36 | + | sign, exp, man = bits >> 31, ((bits >> 23) & 0xFF) - 127, bits & 0x7FFFFF |
|
| 37 | + | if not -32 <= exp < 32: |
|
| 38 | + | raise ValueError(f"{v} does not fit a float24") |
|
| 39 | + | n = (sign << 23) | ((exp + 32) << 17) | (man >> 6) |
|
| 40 | + | return n.to_bytes(3, "big") |
|
| 41 | + | ||
| 42 | + | ||
| 43 | + | def solid(rgb): |
|
| 44 | + | return bytes([3, *rgb]) # STYLE_TYPE_SOLID_COLOR_NO_ALPHA |
|
| 45 | + | ||
| 46 | + | ||
| 47 | + | def linear(p1, p2, stops): |
|
| 48 | + | """A linear gradient from p1 to p2: gradient space runs x from -64 to 64.""" |
|
| 49 | + | dx, dy = (p2[0] - p1[0]) / 128.0, (p2[1] - p1[1]) / 128.0 |
|
| 50 | + | mx, my = (p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2 |
|
| 51 | + | out = bytes([2, 0, (1 << 1) | (1 << 2), len(stops)]) # gradient, linear, transform | no alpha |
|
| 52 | + | for v in (dx, dy, -dy, dx, mx, my): |
|
| 53 | + | out += float24(v) |
|
| 54 | + | for off, rgb in stops: |
|
| 55 | + | out += bytes([off, *rgb]) |
|
| 56 | + | return out |
|
| 57 | + | ||
| 58 | + | ||
| 59 | + | def line(a, b): |
|
| 60 | + | return bytes([1 << 3, 2]) + coord(a[0]) + coord(a[1]) + coord(b[0]) + coord(b[1]) # no curves, open |
|
| 61 | + | ||
| 62 | + | ||
| 63 | + | def curve_path(points, closed=True): |
|
| 64 | + | """points: (x, y, in_x, in_y, out_x, out_y).""" |
|
| 65 | + | out = bytes([(1 << 1) if closed else 0, len(points)]) |
|
| 66 | + | for p in points: |
|
| 67 | + | for v in p: |
|
| 68 | + | out += coord(v) |
|
| 69 | + | return out |
|
| 70 | + | ||
| 71 | + | ||
| 72 | + | def circle(cx, cy, r): |
|
| 73 | + | k = r * K |
|
| 74 | + | pts = [(cx, cy - r, cx - k, cy - r, cx + k, cy - r), (cx + r, cy, cx + r, cy - k, cx + r, cy + k), |
|
| 75 | + | (cx, cy + r, cx + k, cy + r, cx - k, cy + r), (cx - r, cy, cx - r, cy + k, cx - r, cy - k)] |
|
| 76 | + | return curve_path(pts) |
|
| 77 | + | ||
| 78 | + | ||
| 79 | + | def rounded(x, y, w, h, r): |
|
| 80 | + | k = r * (1 - K) |
|
| 81 | + | x2, y2 = x + w, y + h |
|
| 82 | + | pts = [ |
|
| 83 | + | (x + r, y, x + k, y, x + r, y), (x2 - r, y, x2 - r, y, x2 - k, y), |
|
| 84 | + | (x2, y + r, x2, y + k, x2, y + r), (x2, y2 - r, x2, y2 - r, x2, y2 - k), |
|
| 85 | + | (x2 - r, y2, x2 - k, y2, x2 - r, y2), (x + r, y2, x + r, y2, x + k, y2), |
|
| 86 | + | (x, y2 - r, x, y2 - k, x, y2 - r), (x, y + r, x, y + r, x, y + k), |
|
| 87 | + | ] |
|
| 88 | + | return curve_path(pts) |
|
| 89 | + | ||
| 90 | + | ||
| 91 | + | def shape(style, paths, stroke=None, cap=0): |
|
| 92 | + | out = bytes([10, style, len(paths), *paths]) |
|
| 93 | + | if stroke is None: |
|
| 94 | + | return out + bytes([0]) |
|
| 95 | + | # has transformers: one stroke (width + 128, cap << 4 | round join, miter limit) |
|
| 96 | + | return out + bytes([1 << 4, 1, 23, int(round(stroke)) + 128, (cap << 4) | 2, 4]) |
|
| 97 | + | ||
| 98 | + | ||
| 99 | + | def mark(scale, ox, oy): |
|
| 100 | + | """The mark's geometry, scaled about its centre and moved to (ox, oy).""" |
|
| 101 | + | p = lambda x, y: (ox + x * scale, oy + y * scale) |
|
| 102 | + | return { |
|
| 103 | + | "top": (p(-11, -6), p(11, -6)), "bottom": (p(-11, 6), p(11, 6)), |
|
| 104 | + | "left": (p(-11, 0), 10 * scale), "right": (p(11, 0), 10 * scale), |
|
| 105 | + | "dot": 3.5 * scale, "stroke": 3 * scale, "grad": (p(-22, -11), p(22, 11)), |
|
| 106 | + | } |
|
| 107 | + | ||
| 108 | + | ||
| 109 | + | def hvif(dark): |
|
| 110 | + | m = mark(1.0, 32, 32) |
|
| 111 | + | styles = [linear(*m["grad"], [(0, CYAN), (255, BLUE)]), solid(CYAN), solid(WHITE if dark else BLUE)] |
|
| 112 | + | paths = [line(*m["top"]), line(*m["bottom"]), circle(*m["left"][0], m["left"][1]), |
|
| 113 | + | circle(*m["right"][0], m["right"][1]), circle(*m["left"][0], m["dot"]), circle(*m["right"][0], m["dot"])] |
|
| 114 | + | if dark: |
|
| 115 | + | styles.append(solid(TILE)) |
|
| 116 | + | paths.append(rounded(0, 0, 64, 64, 14)) |
|
| 117 | + | tile = [shape(3, [6])] |
|
| 118 | + | else: |
|
| 119 | + | # A pale tile with a thin edge: the edge's colour, then the tile inset by it. |
|
| 120 | + | styles += [solid(EDGE), solid(LIGHT)] |
|
| 121 | + | paths += [rounded(0, 0, 64, 64, 14), rounded(1.5, 1.5, 61, 61, 12.5)] |
|
| 122 | + | tile = [shape(3, [6]), shape(4, [7])] |
|
| 123 | + | shapes = tile + [ |
|
| 124 | + | shape(0, [0], m["stroke"], cap=2), shape(0, [1], m["stroke"], cap=2), |
|
| 125 | + | shape(0, [2], m["stroke"]), shape(0, [3], m["stroke"]), |
|
| 126 | + | shape(1, [4]), shape(2, [5]), |
|
| 127 | + | ] |
|
| 128 | + | out = b"ncif" |
|
| 129 | + | for section in (styles, paths, shapes): |
|
| 130 | + | out += bytes([len(section)]) + b"".join(section) |
|
| 131 | + | return out |
|
| 132 | + | ||
| 133 | + | ||
| 134 | + | def svg(dark): |
|
| 135 | + | if dark: |
|
| 136 | + | bg, dot = '<rect width="64" height="64" rx="14" fill="#070c18"/>', "#FFFFFF" |
|
| 137 | + | else: |
|
| 138 | + | bg = '<rect width="64" height="64" rx="14" fill="#C9DCF5"/><rect x="1.5" y="1.5" width="61" height="61" rx="12.5" fill="#F2F7FF"/>' |
|
| 139 | + | dot = "#0072FF" |
|
| 140 | + | return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64"> |
|
| 141 | + | <defs> |
|
| 142 | + | <linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-22" y1="-11" x2="22" y2="11"> |
|
| 143 | + | <stop offset="0%" stop-color="#00F0FF"/> |
|
| 144 | + | <stop offset="100%" stop-color="#0072FF"/> |
|
| 145 | + | </linearGradient> |
|
| 146 | + | </defs> |
|
| 147 | + | {bg} |
|
| 148 | + | <g transform="translate(32 32)"> |
|
| 149 | + | <path d="M -11 -6 L 11 -6" stroke="url(#g)" stroke-width="3" stroke-linecap="round"/> |
|
| 150 | + | <path d="M -11 6 L 11 6" stroke="url(#g)" stroke-width="3" stroke-linecap="round"/> |
|
| 151 | + | <circle cx="-11" cy="0" r="10" fill="none" stroke="url(#g)" stroke-width="3"/> |
|
| 152 | + | <circle cx="-11" cy="0" r="3.5" fill="#00F0FF"/> |
|
| 153 | + | <circle cx="11" cy="0" r="10" fill="none" stroke="url(#g)" stroke-width="3"/> |
|
| 154 | + | <circle cx="11" cy="0" r="3.5" fill="{dot}"/> |
|
| 155 | + | </g> |
|
| 156 | + | </svg> |
|
| 157 | + | ''' |
|
| 158 | + | ||
| 159 | + | ||
| 160 | + | out = sys.argv[1] |
|
| 161 | + | for name, dark in (("oxy", False), ("oh", True)): |
|
| 162 | + | open(f"{out}/{name}.hvif", "wb").write(hvif(dark)) |
|
| 163 | + | open(f"{out}/{name}.svg", "w").write(svg(dark)) |
modified CHANGELOG.md +4 −0
| 1 | 1 | # Changelog |
|
| 2 | 2 | ||
| 3 | + | ## Unreleased |
|
| 4 | + | ||
| 5 | + | - **Icons for HaikuDepot (#157).** `packaging/haikuports/icons/` has oxy's and oh's icons as HVIF (Haiku's vector format) with 16, 32 and 64 px PNGs, written by `make-icons.py`: the Oxygen mark on a pale tile for oxy, on OxygenHub's dark tile for oh, the same family as the VS Code extensions. |
|
| 6 | + | ||
| 3 | 7 | ## 0.9.0 (2026-10-29) |
|
| 4 | 8 | ||
| 5 | 9 | - **Releases carry their source (#157).** Each release now has `oxy-VERSION-src.tar.gz`, the source as released, reproducible (sorted, fixed times and owners, gzip without a timestamp), listed in SHA256SUMS, for packagers to fetch at a fixed checksum. `packaging/haikuports/make-recipe.py oxy|oh TARBALL VERSION` writes the haikuports recipe from it (oh's from OxygenHub's source tarball); the 0.8.0 recipe, which used the on-demand archive, is gone. |
|
| … | 304 unchanged lines |
modified packaging/haikuports/README.md +10 −0
| … | 27 unchanged lines |
||
| 28 | 28 | zstd's hidden symbols break the link), which the recipe sets. oh's tests |
|
| 29 | 29 | pull in the hub, whose pipeline parser needs a newer Rust than HaikuPorts' |
|
| 30 | 30 | 1.94, so its recipe tests `hub-api` only. |
|
| 31 | + | ||
| 32 | + | ## Icons |
|
| 33 | + | ||
| 34 | + | HaikuDepot takes a package's icon from HaikuDepotServer, not from the |
|
| 35 | + | recipe. `icons/` holds them, written by `make-icons.py icons/` (the PNGs |
|
| 36 | + | rendered from the SVGs): Oxygen (`oxy`) is the mark on a pale tile, |
|
| 37 | + | OxygenHub (`oh`) the mark on the hub's dark tile, as the VS Code |
|
| 38 | + | extensions' icons are. Once the packages are listed on |
|
| 39 | + | depot.haiku-os.org, upload `NAME.hvif` with `NAME-16.png`, `-32` and `-64` |
|
| 40 | + | as each package's icon. |