The previous icon was a bare mountain silhouette that didn't read as a bookmark app at first glance. The new design uses the universal bookmark ribbon glyph (vertical rectangle with a V-cut at the bottom) as the primary silhouette and embeds the mountain peak inside it as a contrast fill. The ribbon shape carries the *function* signal; the inner peak ties to the Trailhead theme.
122 lines
3.8 KiB
JavaScript
122 lines
3.8 KiB
JavaScript
// Generates assets/tray-icon.png — Trailhead branding: a bookmark ribbon
|
|
// (vertical rectangle with a V-cut at the bottom — the universal "save"
|
|
// glyph) carrying a mountain peak motif inside it. The ribbon silhouette
|
|
// communicates the app's *function* (bookmarking); the inner peak ties to
|
|
// the Trailhead theme.
|
|
// Strokes are kept >=2 source pixels wide so the silhouette survives the
|
|
// downscale to 16-24px Windows tray sizes.
|
|
// No external image deps; raw PNG bytes via zlib.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const zlib = require('zlib');
|
|
|
|
const SIZE = 32;
|
|
const OUT = path.join(__dirname, '..', 'assets', 'tray-icon.png');
|
|
|
|
const ORANGE = [217, 119, 87, 255]; // #d97757 — ribbon fill
|
|
const ORANGE_DARK = [165, 82, 56, 255]; // shadow on right edge
|
|
const CREAM = [250, 230, 215, 255]; // mountain motif inside the ribbon
|
|
const TRANSPARENT = [0, 0, 0, 0];
|
|
|
|
// Ribbon spans 14px wide (x=9..22) and 25px tall (y=3..27). The bottom 5
|
|
// rows form a V-cut, narrowing the ribbon into two prongs.
|
|
function inRibbon(x, y) {
|
|
if (x < 9 || x > 22) return false;
|
|
if (y < 3 || y > 27) return false;
|
|
if (y >= 23) {
|
|
const depth = y - 23; // 0..4
|
|
const cutLeft = 15 - depth;
|
|
const cutRight = 16 + depth;
|
|
if (x >= cutLeft && x <= cutRight) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Mountain peak motif inside the upper half of the ribbon. Peak at y=7,
|
|
// base at y=14, max half-width 4 (so base spans 8 source pixels).
|
|
function inMountain(x, y) {
|
|
if (y < 7 || y > 14) return false;
|
|
const halfW = Math.floor((y - 7) / 2) + 1;
|
|
return x >= 15 - halfW + 1 && x <= 15 + halfW;
|
|
}
|
|
|
|
function colorAt(x, y) {
|
|
if (!inRibbon(x, y)) return TRANSPARENT;
|
|
|
|
// Inner mountain silhouette painted in cream so it pops against the
|
|
// orange ribbon at any size.
|
|
if (inMountain(x, y)) return CREAM;
|
|
|
|
// Right-edge shadow on the ribbon body (skip the V-cut zone so the cut
|
|
// edges look clean rather than half-shaded).
|
|
if (x === 22 && y >= 3 && y < 23) return ORANGE_DARK;
|
|
|
|
return ORANGE;
|
|
}
|
|
|
|
function buildRawImage() {
|
|
const rowLen = SIZE * 4 + 1;
|
|
const raw = Buffer.alloc(rowLen * SIZE);
|
|
for (let y = 0; y < SIZE; y++) {
|
|
raw[y * rowLen] = 0;
|
|
for (let x = 0; x < SIZE; x++) {
|
|
const [r, g, b, a] = colorAt(x, y);
|
|
const off = y * rowLen + 1 + x * 4;
|
|
raw[off] = r;
|
|
raw[off + 1] = g;
|
|
raw[off + 2] = b;
|
|
raw[off + 3] = a;
|
|
}
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
function crc32(buf) {
|
|
let c;
|
|
const table = crc32.table || (crc32.table = (() => {
|
|
const t = new Uint32Array(256);
|
|
for (let n = 0; n < 256; n++) {
|
|
let v = n;
|
|
for (let k = 0; k < 8; k++) v = (v & 1) ? (0xedb88320 ^ (v >>> 1)) : (v >>> 1);
|
|
t[n] = v >>> 0;
|
|
}
|
|
return t;
|
|
})());
|
|
c = 0xffffffff;
|
|
for (let i = 0; i < buf.length; i++) c = table[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
return (c ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
function chunk(type, data) {
|
|
const len = Buffer.alloc(4);
|
|
len.writeUInt32BE(data.length, 0);
|
|
const typeBuf = Buffer.from(type, 'ascii');
|
|
const crcBuf = Buffer.alloc(4);
|
|
crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
|
|
return Buffer.concat([len, typeBuf, data, crcBuf]);
|
|
}
|
|
|
|
function buildPng() {
|
|
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
const ihdr = Buffer.alloc(13);
|
|
ihdr.writeUInt32BE(SIZE, 0);
|
|
ihdr.writeUInt32BE(SIZE, 4);
|
|
ihdr[8] = 8; // bit depth
|
|
ihdr[9] = 6; // color type RGBA
|
|
ihdr[10] = 0; // compression
|
|
ihdr[11] = 0; // filter
|
|
ihdr[12] = 0; // interlace
|
|
const idat = zlib.deflateSync(buildRawImage());
|
|
return Buffer.concat([
|
|
sig,
|
|
chunk('IHDR', ihdr),
|
|
chunk('IDAT', idat),
|
|
chunk('IEND', Buffer.alloc(0)),
|
|
]);
|
|
}
|
|
|
|
fs.mkdirSync(path.dirname(OUT), { recursive: true });
|
|
fs.writeFileSync(OUT, buildPng());
|
|
console.log(`Wrote ${OUT}`);
|