// DOME-style icon generator for the CAIIC OBS fork. // Renders a blue observatory/planetarium dome with a white three-blade // aperture (nod to the OBS logo) wrapped by three CAIIC-colored arcs // (orange top, green lower-left, yellow lower-right). // Usage: node dome-icon-gen.js const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); const DEG = Math.PI / 180; // ---------- CRC32 / PNG ---------- const crcTable = (() => { const t = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c >>> 0; } return t; })(); function crc32(buf) { let c = 0xffffffff; for (let i = 0; i < buf.length; i++) c = crcTable[(c ^ buf[i]) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } function chunk(type, data) { const out = Buffer.alloc(8 + data.length + 4); out.writeUInt32BE(data.length, 0); out.write(type, 4, 'ascii'); data.copy(out, 8); out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length); return out; } function pngEncode(width, height, rgba) { const stride = width * 4; const raw = Buffer.alloc((stride + 1) * height); for (let y = 0; y < height; y++) { raw[y * (stride + 1)] = 0; rgba.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride); } const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4); ihdr[8] = 8; // bit depth ihdr[9] = 6; // RGBA return Buffer.concat([ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw, { level: 9 })), chunk('IEND', Buffer.alloc(0)), ]); } function icoEncode(images) { // images: [{size, png}] const header = Buffer.alloc(6); header.writeUInt16LE(1, 2); header.writeUInt16LE(images.length, 4); let offset = 6 + images.length * 16; const entries = []; for (const img of images) { const e = Buffer.alloc(16); e[0] = img.size >= 256 ? 0 : img.size; e[1] = img.size >= 256 ? 0 : img.size; e[4] = 1; // planes e.writeUInt16LE(32, 6); e.writeUInt32LE(img.png.length, 8); e.writeUInt32LE(offset, 12); offset += img.png.length; entries.push(e); } return Buffer.concat([header, ...entries, ...images.map((i) => i.png)]); } // ---------- geometry ---------- function clamp(x, a, b) { return Math.min(Math.max(x, a), b); } function mix(a, b, t) { return a + (b - a) * t; } function mix3(c1, c2, t) { return [mix(c1[0], c2[0], t), mix(c1[1], c2[1], t), mix(c1[2], c2[2], t)]; } // signed distance to a circular arc stroke (rounded caps), angles in radians function arcSDF(px, py, cx, cy, r, a0, a1, w) { const dx = px - cx, dy = py - cy; const d = Math.hypot(dx, dy); let ang = Math.atan2(dy, dx); // normalize ang into [a0, a0 + 2PI) const TWO_PI = Math.PI * 2; while (ang < a0) ang += TWO_PI; while (ang >= a0 + TWO_PI) ang -= TWO_PI; let dist; if (ang <= a1) { dist = Math.abs(d - r); } else { const e0x = cx + r * Math.cos(a0), e0y = cy + r * Math.sin(a0); const e1x = cx + r * Math.cos(a1), e1y = cy + r * Math.sin(a1); dist = Math.min(Math.hypot(px - e0x, py - e0y), Math.hypot(px - e1x, py - e1y)); } return dist - w / 2; } // rounded rect sdf function rrSDF(px, py, cx, cy, hw, hh, r) { const qx = Math.abs(px - cx) - (hw - r); const qy = Math.abs(py - cy) - (hh - r); const ax = Math.max(qx, 0), ay = Math.max(qy, 0); return Math.hypot(ax, ay) + Math.min(Math.max(qx, qy), 0) - r; } // ---------- design ---------- const DOME_C = [0.5, 0.56]; // dome circle center const DOME_R = 0.295; // dome radius const BLADE_C = [0.5, 0.475]; // aperture center const BLADE_R = 0.155; const BLADE_W = 0.048; const ARC_C = [0.5, 0.53]; const ARC_R = 0.4; const ARC_W = 0.036; const NAVY = [10, 52, 128]; const BLUE_HI = [110, 196, 250]; const BLUE_LO = [9, 68, 152]; const WHITE = [248, 252, 255]; const ORANGE = [240, 128, 60]; const GREEN = [110, 190, 74]; const YELLOW = [240, 204, 60]; function domeColor(px, py) { const t = Math.pow(clamp(Math.hypot(px - 0.34, py - 0.36) / 0.5, 0, 1), 0.8); let c = mix3(BLUE_HI, BLUE_LO, t); // darken slightly toward the base for depth const base = clamp((py - 0.42) / 0.16, 0, 1) * 0.18; return [c[0] * (1 - base), c[1] * (1 - base), c[2] * (1 - base)]; } function bladeSDF(px, py) { let best = 1e9; const span = 100 * DEG; for (let i = 0; i < 3; i++) { const a0 = (-90 + i * 120) * DEG; best = Math.min(best, arcSDF(px, py, BLADE_C[0], BLADE_C[1], BLADE_R, a0, a0 + span, BLADE_W)); } return best; } function arcsSDF(px, py) { return [ { sdf: arcSDF(px, py, ARC_C[0], ARC_C[1], ARC_R, -160 * DEG, -20 * DEG, ARC_W), color: ORANGE }, { sdf: arcSDF(px, py, ARC_C[0], ARC_C[1], ARC_R, 105 * DEG, 170 * DEG, ARC_W), color: GREEN }, { sdf: arcSDF(px, py, ARC_C[0], ARC_C[1], ARC_R, 10 * DEG, 75 * DEG, ARC_W), color: YELLOW }, ]; } function render(size) { const SS = 4; const R = size * SS; const out = Buffer.alloc(size * size * 4); const px = 1 / R; for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { let r = 0, g = 0, b = 0, a = 0; for (let sy = 0; sy < SS; sy++) { for (let sx = 0; sx < SS; sx++) { const nx = (x * SS + sx + 0.5) / R; const ny = (y * SS + sy + 0.5) / R; const aa = (sdf) => clamp(0.5 - sdf / px, 0, 1); let cr = 0, cg = 0, cb = 0, ca = 0; const over = (sdf, col, opacity) => { const cov = aa(sdf) * opacity; cr = cr * (1 - cov) + col[0] * cov; cg = cg * (1 - cov) + col[1] * cov; cb = cb * (1 - cov) + col[2] * cov; ca = ca * (1 - cov) + cov; }; // base bar over(rrSDF(nx, ny, 0.5, DOME_C[1] + 0.022, DOME_R + 0.045, 0.028, 0.026), NAVY, 1); // dome (upper half of circle) const dd = Math.max(Math.hypot(nx - DOME_C[0], ny - DOME_C[1]) - DOME_R, ny - DOME_C[1]); over(dd, domeColor(nx, ny), 1); // aperture blades over(bladeSDF(nx, ny), WHITE, 0.95); // colored arcs for (const arc of arcsSDF(nx, ny)) over(arc.sdf, arc.color, 1); r += cr; g += cg; b += cb; a += ca; } } const n = SS * SS; const o = (y * size + x) * 4; out[o] = Math.round(clamp(r / n, 0, 255)); out[o + 1] = Math.round(clamp(g / n, 0, 255)); out[o + 2] = Math.round(clamp(b / n, 0, 255)); out[o + 3] = Math.round(clamp(a / n, 0, 1) * 255); } } return out; } // ---------- main ---------- const outDir = process.argv[2] || '.'; const sizes = [16, 24, 32, 48, 64, 128, 256]; const images = sizes.map((s) => ({ size: s, png: pngEncode(s, s, render(s)) })); fs.writeFileSync(path.join(outDir, 'obs.png'), images[images.length - 1].png); fs.writeFileSync(path.join(outDir, 'obs-studio.ico'), icoEncode(images)); console.log('wrote obs.png (256x256) and obs-studio.ico (' + sizes.join(',') + ') to ' + outDir);