SPB Git

spb/chat-spboucher Public

Private universal chat interface over the OpenRouter ecosystem — 400+ models, branching, streaming, usage tracking. Next.js 16 + SQLite, PWA, deployed on m4m64a at chat.spboucher.ai

TypeScript 78.8% CSS 15.1% JavaScript 4.9% Shell 1.2%
3.5 KB · 107 lines javascript
Raw Blame History
1// Author: Simon-Pierre Boucher2// Contact: contact@spboucher.ai3// Project: chat.spboucher.ai45// Dependency-free PNG icon generator: rasterizes the ▮▯▮ instrument mark6// (three rounded bars, signal teal on ink) at the sizes the PWA needs.78import { deflateSync } from "node:zlib";9import { writeFileSync, mkdirSync } from "node:fs";1011const INK = [0x0e, 0x11, 0x16, 255];12const TEAL = [0x3f, 0xb6, 0xa8, 255];1314function crc32(buf) {15  let table = crc32.table;16  if (!table) {17    table = crc32.table = new Int32Array(256);18    for (let n = 0; n < 256; n++) {19      let c = n;20      for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;21      table[n] = c;22    }23  }24  let c = 0xffffffff;25  for (let i = 0; i < buf.length; i++) c = table[(c ^ buf[i]) & 0xff] ^ (c >>> 8);26  return (c ^ 0xffffffff) >>> 0;27}2829function chunk(type, data) {30  const len = Buffer.alloc(4);31  len.writeUInt32BE(data.length);32  const body = Buffer.concat([Buffer.from(type, "ascii"), data]);33  const crc = Buffer.alloc(4);34  crc.writeUInt32BE(crc32(body));35  return Buffer.concat([len, body, crc]);36}3738function encodePng(size, pixels) {39  const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);40  const ihdr = Buffer.alloc(13);41  ihdr.writeUInt32BE(size, 0);42  ihdr.writeUInt32BE(size, 4);43  ihdr[8] = 8; // bit depth44  ihdr[9] = 6; // RGBA45  const raw = Buffer.alloc((size * 4 + 1) * size);46  for (let y = 0; y < size; y++) {47    raw[y * (size * 4 + 1)] = 0; // filter none48    pixels.copy(raw, y * (size * 4 + 1) + 1, y * size * 4, (y + 1) * size * 4);49  }50  return Buffer.concat([51    sig,52    chunk("IHDR", ihdr),53    chunk("IDAT", deflateSync(raw, { level: 9 })),54    chunk("IEND", Buffer.alloc(0)),55  ]);56}5758function inRoundedRect(x, y, rx, ry, w, h, r) {59  if (x < rx || x >= rx + w || y < ry || y >= ry + h) return false;60  const cx = Math.max(rx + r, Math.min(x, rx + w - r));61  const cy = Math.max(ry + r, Math.min(y, ry + h - r));62  return (x - cx) ** 2 + (y - cy) ** 2 <= r * r || (x >= rx + r && x < rx + w - r) || (y >= ry + r && y < ry + h - r);63}6465function drawIcon(size, { maskable = false } = {}) {66  const px = Buffer.alloc(size * size * 4);67  const s = size / 512;68  const bgRadius = maskable ? 0 : 112 * s;69  const bars = [70    { x: 138, filled: true },71    { x: 228, filled: false },72    { x: 318, filled: true },73  ];74  const stroke = 14 * s;75  for (let y = 0; y < size; y++) {76    for (let x = 0; x < size; x++) {77      let color = [0, 0, 0, 0];78      if (maskable || inRoundedRect(x, y, 0, 0, size, size, bgRadius)) color = INK;79      for (const b of bars) {80        const bx = b.x * s;81        const by = 156 * s;82        const bw = 56 * s;83        const bh = 200 * s;84        const br = 12 * s;85        if (inRoundedRect(x, y, bx, by, bw, bh, br)) {86          if (b.filled || !inRoundedRect(x, y, bx + stroke, by + stroke, bw - 2 * stroke, bh - 2 * stroke, Math.max(br - stroke, 0))) {87            color = TEAL;88          }89        }90      }91      const i = (y * size + x) * 4;92      px[i] = color[0];93      px[i + 1] = color[1];94      px[i + 2] = color[2];95      px[i + 3] = color[3];96    }97  }98  return encodePng(size, px);99}100101mkdirSync("public/icons", { recursive: true });102writeFileSync("public/icons/icon-192.png", drawIcon(192));103writeFileSync("public/icons/icon-512.png", drawIcon(512));104writeFileSync("public/icons/icon-maskable-512.png", drawIcon(512, { maskable: true }));105writeFileSync("public/icons/apple-touch-icon.png", drawIcon(180, { maskable: true }));106console.log("icons written");107