#!/usr/bin/env python3 """HaikuDepot icons for oxy and oh (oxygen#157): the Oxygen mark as Haiku's vector icon format (HVIF), and as SVG to render the 16, 32 and 64 px PNGs. A family of two (Andy, 2026-10-29): Oxygen (oxy, the Oxygen VCS extension) is the mark on a pale rounded tile with a blue right dot; OxygenHub (oh, the OxygenHub extension) is the mark on the hub's dark tile with a white one, as oxygenhub/assets/icons/oxygenhub.svg. Each keeps its edge on light and dark backgrounds alike. make-icons.py OUTDIR writes oxy.hvif, oh.hvif, oxy.svg, oh.svg HVIF as Haiku's FlatIconImporter reads it: "ncif", then styles, paths and shapes, each a count byte and its items; coordinates in a 64-unit canvas. """ import math import struct import sys CYAN, BLUE, WHITE, TILE = (0x00, 0xF0, 0xFF), (0x00, 0x72, 0xFF), (0xFF, 0xFF, 0xFF), (0x07, 0x0C, 0x18) LIGHT, EDGE = (0xF2, 0xF7, 0xFF), (0xC9, 0xDC, 0xF5) K = 0.5522847498 # a circle's quarter as one cubic Bézier def coord(v): if v == int(v) and -32 <= v <= 95: return bytes([int(v) + 32]) n = int(round((v + 128.0) * 102.0)) return bytes([(n >> 8) | 0x80, n & 0xFF]) def float24(v): if v == 0: return b"\0\0\0" bits = struct.unpack(">I", struct.pack(">f", v))[0] sign, exp, man = bits >> 31, ((bits >> 23) & 0xFF) - 127, bits & 0x7FFFFF if not -32 <= exp < 32: raise ValueError(f"{v} does not fit a float24") n = (sign << 23) | ((exp + 32) << 17) | (man >> 6) return n.to_bytes(3, "big") def solid(rgb): return bytes([3, *rgb]) # STYLE_TYPE_SOLID_COLOR_NO_ALPHA def linear(p1, p2, stops): """A linear gradient from p1 to p2: gradient space runs x from -64 to 64.""" dx, dy = (p2[0] - p1[0]) / 128.0, (p2[1] - p1[1]) / 128.0 mx, my = (p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2 out = bytes([2, 0, (1 << 1) | (1 << 2), len(stops)]) # gradient, linear, transform | no alpha for v in (dx, dy, -dy, dx, mx, my): out += float24(v) for off, rgb in stops: out += bytes([off, *rgb]) return out def line(a, b): return bytes([1 << 3, 2]) + coord(a[0]) + coord(a[1]) + coord(b[0]) + coord(b[1]) # no curves, open def curve_path(points, closed=True): """points: (x, y, in_x, in_y, out_x, out_y).""" out = bytes([(1 << 1) if closed else 0, len(points)]) for p in points: for v in p: out += coord(v) return out def circle(cx, cy, r): k = r * K pts = [(cx, cy - r, cx - k, cy - r, cx + k, cy - r), (cx + r, cy, cx + r, cy - k, cx + r, cy + k), (cx, cy + r, cx + k, cy + r, cx - k, cy + r), (cx - r, cy, cx - r, cy + k, cx - r, cy - k)] return curve_path(pts) def rounded(x, y, w, h, r): k = r * (1 - K) x2, y2 = x + w, y + h pts = [ (x + r, y, x + k, y, x + r, y), (x2 - r, y, x2 - r, y, x2 - k, y), (x2, y + r, x2, y + k, x2, y + r), (x2, y2 - r, x2, y2 - r, x2, y2 - k), (x2 - r, y2, x2 - k, y2, x2 - r, y2), (x + r, y2, x + r, y2, x + k, y2), (x, y2 - r, x, y2 - k, x, y2 - r), (x, y + r, x, y + r, x, y + k), ] return curve_path(pts) def shape(style, paths, stroke=None, cap=0): out = bytes([10, style, len(paths), *paths]) if stroke is None: return out + bytes([0]) # has transformers: one stroke (width + 128, cap << 4 | round join, miter limit) return out + bytes([1 << 4, 1, 23, int(round(stroke)) + 128, (cap << 4) | 2, 4]) def mark(scale, ox, oy): """The mark's geometry, scaled about its centre and moved to (ox, oy).""" p = lambda x, y: (ox + x * scale, oy + y * scale) return { "top": (p(-11, -6), p(11, -6)), "bottom": (p(-11, 6), p(11, 6)), "left": (p(-11, 0), 10 * scale), "right": (p(11, 0), 10 * scale), "dot": 3.5 * scale, "stroke": 3 * scale, "grad": (p(-22, -11), p(22, 11)), } def hvif(dark): m = mark(1.0, 32, 32) styles = [linear(*m["grad"], [(0, CYAN), (255, BLUE)]), solid(CYAN), solid(WHITE if dark else BLUE)] paths = [line(*m["top"]), line(*m["bottom"]), circle(*m["left"][0], m["left"][1]), circle(*m["right"][0], m["right"][1]), circle(*m["left"][0], m["dot"]), circle(*m["right"][0], m["dot"])] if dark: styles.append(solid(TILE)) paths.append(rounded(0, 0, 64, 64, 14)) tile = [shape(3, [6])] else: # A pale tile with a thin edge: the edge's colour, then the tile inset by it. styles += [solid(EDGE), solid(LIGHT)] paths += [rounded(0, 0, 64, 64, 14), rounded(1.5, 1.5, 61, 61, 12.5)] tile = [shape(3, [6]), shape(4, [7])] shapes = tile + [ shape(0, [0], m["stroke"], cap=2), shape(0, [1], m["stroke"], cap=2), shape(0, [2], m["stroke"]), shape(0, [3], m["stroke"]), shape(1, [4]), shape(2, [5]), ] out = b"ncif" for section in (styles, paths, shapes): out += bytes([len(section)]) + b"".join(section) return out def svg(dark): if dark: bg, dot = '', "#FFFFFF" else: bg = '' dot = "#0072FF" return f''' {bg} ''' out = sys.argv[1] for name, dark in (("oxy", False), ("oh", True)): open(f"{out}/{name}.hvif", "wb").write(hvif(dark)) open(f"{out}/{name}.svg", "w").write(svg(dark))