#!/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'''
'''
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))