arnaucube
arnaucube/study-labPublic Self-hosted repository
study-lab/scripts/generate-icons.js
scripts/generate-icons.jsRaw
  1#!/usr/bin/env node
  2
  3const fs = require('node:fs');
  4const path = require('node:path');
  5const zlib = require('node:zlib');
  6
  7const root = path.resolve(__dirname, '..');
  8
  9function crc32(buffer) {
 10  let crc = 0xffffffff;
 11  for (const byte of buffer) {
 12    crc ^= byte;
 13    for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
 14  }
 15  return (crc ^ 0xffffffff) >>> 0;
 16}
 17
 18function chunk(type, data) {
 19  const name = Buffer.from(type);
 20  const length = Buffer.alloc(4);
 21  const checksum = Buffer.alloc(4);
 22  length.writeUInt32BE(data.length);
 23  checksum.writeUInt32BE(crc32(Buffer.concat([name, data])));
 24  return Buffer.concat([length, name, data, checksum]);
 25}
 26
 27function distanceToSegment(px, py, ax, ay, bx, by) {
 28  const dx = bx - ax;
 29  const dy = by - ay;
 30  const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)));
 31  return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
 32}
 33
 34function onStroke(x, y, points, radius) {
 35  for (let index = 1; index < points.length; index += 1) {
 36    if (distanceToSegment(x, y, ...points[index - 1], ...points[index]) <= radius) return true;
 37  }
 38  return false;
 39}
 40
 41function sample(x, y, maskable) {
 42  const paper = [248, 247, 242];
 43  const sage = [79, 109, 96];
 44  const ink = [24, 32, 29];
 45  const orange = [233, 133, 87];
 46  let color = maskable ? sage : paper;
 47  const blob = ((x - 256) / 162) ** 2 + ((y - 258) / 181) ** 2 <= 1;
 48  if (blob) color = ink;
 49
 50  const sPath = [
 51    [330, 171], [309, 150], [270, 143], [228, 151], [201, 175], [195, 204],
 52    [205, 229], [235, 246], [278, 260], [310, 277], [320, 304], [314, 335],
 53    [287, 361], [245, 369], [207, 358], [184, 335]
 54  ];
 55  if (blob && onStroke(x, y, sPath, 24)) color = paper;
 56  if ((x - 379) ** 2 + (y - 137) ** 2 <= 30 ** 2) color = orange;
 57  return color;
 58}
 59
 60function render(size, maskable = false) {
 61  const rows = [];
 62  const samples = 2;
 63  for (let py = 0; py < size; py += 1) {
 64    const row = Buffer.alloc(1 + size * 4);
 65    for (let px = 0; px < size; px += 1) {
 66      const total = [0, 0, 0];
 67      for (let sy = 0; sy < samples; sy += 1) {
 68        for (let sx = 0; sx < samples; sx += 1) {
 69          const color = sample((px + (sx + 0.5) / samples) * 512 / size, (py + (sy + 0.5) / samples) * 512 / size, maskable);
 70          total[0] += color[0]; total[1] += color[1]; total[2] += color[2];
 71        }
 72      }
 73      const offset = 1 + px * 4;
 74      row[offset] = Math.round(total[0] / (samples * samples));
 75      row[offset + 1] = Math.round(total[1] / (samples * samples));
 76      row[offset + 2] = Math.round(total[2] / (samples * samples));
 77      row[offset + 3] = 255;
 78    }
 79    rows.push(row);
 80  }
 81
 82  const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
 83  const header = Buffer.alloc(13);
 84  header.writeUInt32BE(size, 0);
 85  header.writeUInt32BE(size, 4);
 86  header[8] = 8;
 87  header[9] = 6;
 88  const png = Buffer.concat([
 89    signature,
 90    chunk('IHDR', header),
 91    chunk('IDAT', zlib.deflateSync(Buffer.concat(rows), { level: 9 })),
 92    chunk('IEND', Buffer.alloc(0))
 93  ]);
 94  return png;
 95}
 96
 97const outputs = [
 98  ['icon-192.png', 192, false],
 99  ['icon-512.png', 512, false],
100  ['icon-maskable-512.png', 512, true],
101  ['apple-touch-icon.png', 180, false]
102];
103
104for (const [filename, size, maskable] of outputs) {
105  fs.writeFileSync(path.join(root, 'icons', filename), render(size, maskable));
106}