Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

build-dxt-assets.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

build-dxt-assets.tsBlame302 lines · 1 contributor
cd4f63bTest User1#!/usr/bin/env bun
2/**
3 * BLOCK Q1 — placeholder asset generator for the Claude Desktop .dxt bundle.
4 *
5 * Generates two PNGs into extension/gluecron.dxt/:
6 * - icon.png 256x256 — dark-bg "g" mark with accent gradient
7 * - screenshot-1.png 1280x800 — dark-bg wordmark placeholder
8 *
9 * These are intentionally simple: hand-rolled PNGs (no Canvas / sharp / etc.)
10 * so the build works on any system with `bun` + system `zip`. The intent is
11 * for a designer to replace them later; both files are committed as
12 * placeholders so the .dxt is shippable today.
13 *
14 * Follow-up: replace icon.png with the actual brand "g" SVG-rasterised at
15 * 256x256, and screenshot-1.png with a real product screenshot of Claude
16 * opening a PR on Gluecron.
17 *
18 * Usage:
19 * bun run scripts/build-dxt-assets.ts
20 *
21 * Idempotent — re-running overwrites the files in place.
22 */
23
24import { writeFileSync, mkdirSync } from "node:fs";
25import { join, dirname } from "node:path";
26import { deflateSync } from "node:zlib";
27
28const OUT_DIR = join(import.meta.dir, "..", "extension", "gluecron.dxt");
29
30// ---------------------------------------------------------------------------
31// Minimal PNG encoder (RGB, no alpha, no filtering beyond filter=0).
32// Hand-rolled to avoid pulling in a dependency just for placeholder assets.
33// ---------------------------------------------------------------------------
34
35const CRC_TABLE = (() => {
36 const t = new Uint32Array(256);
37 for (let n = 0; n < 256; n++) {
38 let c = n;
39 for (let k = 0; k < 8; k++) {
40 c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
41 }
42 t[n] = c >>> 0;
43 }
44 return t;
45})();
46
47function crc32(buf: Uint8Array): number {
48 let c = 0xffffffff;
49 for (let i = 0; i < buf.length; i++) {
50 c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
51 }
52 return (c ^ 0xffffffff) >>> 0;
53}
54
55function u32be(n: number): Uint8Array {
56 const b = new Uint8Array(4);
57 b[0] = (n >>> 24) & 0xff;
58 b[1] = (n >>> 16) & 0xff;
59 b[2] = (n >>> 8) & 0xff;
60 b[3] = n & 0xff;
61 return b;
62}
63
64function chunk(type: string, data: Uint8Array): Uint8Array {
65 const typeBytes = new TextEncoder().encode(type);
66 const len = u32be(data.length);
67 const crcInput = new Uint8Array(typeBytes.length + data.length);
68 crcInput.set(typeBytes, 0);
69 crcInput.set(data, typeBytes.length);
70 const crc = u32be(crc32(crcInput));
71 const out = new Uint8Array(4 + 4 + data.length + 4);
72 out.set(len, 0);
73 out.set(typeBytes, 4);
74 out.set(data, 8);
75 out.set(crc, 8 + data.length);
76 return out;
77}
78
79function encodePng(
80 width: number,
81 height: number,
82 pixels: Uint8Array // RGB, length = width*height*3
83): Uint8Array {
84 if (pixels.length !== width * height * 3) {
85 throw new Error("pixel buffer size mismatch");
86 }
87 // IHDR
88 const ihdr = new Uint8Array(13);
89 ihdr.set(u32be(width), 0);
90 ihdr.set(u32be(height), 4);
91 ihdr[8] = 8; // bit depth
92 ihdr[9] = 2; // color type: RGB
93 ihdr[10] = 0; // compression
94 ihdr[11] = 0; // filter
95 ihdr[12] = 0; // interlace
96
97 // IDAT — prepend filter byte (0 = None) to every scanline.
98 const raw = new Uint8Array(height * (1 + width * 3));
99 for (let y = 0; y < height; y++) {
100 const rowStart = y * (1 + width * 3);
101 raw[rowStart] = 0; // filter type
102 raw.set(pixels.subarray(y * width * 3, (y + 1) * width * 3), rowStart + 1);
103 }
104 const idat = deflateSync(Buffer.from(raw));
105
106 const SIG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
107 const ihdrChunk = chunk("IHDR", ihdr);
108 const idatChunk = chunk("IDAT", new Uint8Array(idat));
109 const iendChunk = chunk("IEND", new Uint8Array(0));
110
111 const out = new Uint8Array(
112 SIG.length + ihdrChunk.length + idatChunk.length + iendChunk.length
113 );
114 let off = 0;
115 out.set(SIG, off);
116 off += SIG.length;
117 out.set(ihdrChunk, off);
118 off += ihdrChunk.length;
119 out.set(idatChunk, off);
120 off += idatChunk.length;
121 out.set(iendChunk, off);
122 return out;
123}
124
125// ---------------------------------------------------------------------------
126// Drawing helpers
127// ---------------------------------------------------------------------------
128
129type Rgb = [number, number, number];
130
131const BG: Rgb = [0x0d, 0x11, 0x17]; // #0d1117
132const ACCENT_START: Rgb = [0x8c, 0x6d, 0xff]; // #8c6dff (purple)
133const ACCENT_END: Rgb = [0x36, 0xc5, 0xd6]; // #36c5d6 (teal)
134const FG: Rgb = [0xe6, 0xed, 0xf3]; // soft white
135
136function lerp(a: number, b: number, t: number): number {
137 return Math.round(a + (b - a) * t);
138}
139
140function lerpRgb(a: Rgb, b: Rgb, t: number): Rgb {
141 return [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)];
142}
143
144function makeBuf(w: number, h: number, fill: Rgb): Uint8Array {
145 const buf = new Uint8Array(w * h * 3);
146 for (let i = 0; i < w * h; i++) {
147 buf[i * 3] = fill[0];
148 buf[i * 3 + 1] = fill[1];
149 buf[i * 3 + 2] = fill[2];
150 }
151 return buf;
152}
153
154function setPixel(buf: Uint8Array, w: number, x: number, y: number, c: Rgb) {
155 const i = (y * w + x) * 3;
156 buf[i] = c[0];
157 buf[i + 1] = c[1];
158 buf[i + 2] = c[2];
159}
160
161function fillRect(
162 buf: Uint8Array,
163 w: number,
164 x0: number,
165 y0: number,
166 x1: number,
167 y1: number,
168 c: Rgb
169) {
170 for (let y = y0; y < y1; y++) {
171 for (let x = x0; x < x1; x++) {
172 setPixel(buf, w, x, y, c);
173 }
174 }
175}
176
177function fillCircle(
178 buf: Uint8Array,
179 w: number,
180 cx: number,
181 cy: number,
182 r: number,
183 c: Rgb
184) {
185 const r2 = r * r;
186 for (let y = cy - r; y <= cy + r; y++) {
187 for (let x = cx - r; x <= cx + r; x++) {
188 const dx = x - cx;
189 const dy = y - cy;
190 if (dx * dx + dy * dy <= r2) {
191 setPixel(buf, w, x, y, c);
192 }
193 }
194 }
195}
196
197function ringMask(cx: number, cy: number, rOuter: number, rInner: number) {
198 const rO2 = rOuter * rOuter;
199 const rI2 = rInner * rInner;
200 return (x: number, y: number) => {
201 const dx = x - cx;
202 const dy = y - cy;
203 const d2 = dx * dx + dy * dy;
204 return d2 <= rO2 && d2 >= rI2;
205 };
206}
207
208// ---------------------------------------------------------------------------
209// 1. icon.png — 256x256 "g" mark on dark bg, gradient ring
210// ---------------------------------------------------------------------------
211
212function drawIcon(): Uint8Array {
213 const W = 256;
214 const H = 256;
215 const buf = makeBuf(W, H, BG);
216
217 // Gradient ring: outer radius 110, inner radius 86.
218 const cx = W / 2;
219 const cy = H / 2;
220 const inRing = ringMask(cx, cy, 110, 86);
221 for (let y = 0; y < H; y++) {
222 for (let x = 0; x < W; x++) {
223 if (inRing(x, y)) {
224 // gradient along x (left = start, right = end).
225 const t = x / W;
226 setPixel(buf, W, x, y, lerpRgb(ACCENT_START, ACCENT_END, t));
227 }
228 }
229 }
230
231 // Stylised "g" — a filled disc with a notch cut on the right side and a
232 // descender bar. Placeholder, not the real wordmark.
233 fillCircle(buf, W, cx, cy, 60, FG);
234 // Cut the right notch (rectangle in BG colour).
235 fillRect(buf, W, cx + 20, cy - 18, cx + 65, cy + 8, BG);
236 // Descender bar.
237 fillRect(buf, W, cx + 18, cy + 8, cx + 60, cy + 24, FG);
238
239 return encodePng(W, H, buf);
240}
241
242// ---------------------------------------------------------------------------
243// 2. screenshot-1.png — 1280x800 dark mockup with wordmark band
244// ---------------------------------------------------------------------------
245
246function drawScreenshot(): Uint8Array {
247 const W = 1280;
248 const H = 800;
249 const buf = makeBuf(W, H, BG);
250
251 // Top accent gradient bar (the wordmark band).
252 for (let y = 60; y < 80; y++) {
253 for (let x = 0; x < W; x++) {
254 const t = x / W;
255 setPixel(buf, W, x, y, lerpRgb(ACCENT_START, ACCENT_END, t));
256 }
257 }
258
259 // Mock "browser chrome" — three traffic-light circles.
260 fillCircle(buf, W, 30, 30, 8, [0xff, 0x5f, 0x57]);
261 fillCircle(buf, W, 52, 30, 8, [0xfe, 0xbc, 0x2e]);
262 fillCircle(buf, W, 74, 30, 8, [0x27, 0xc9, 0x3f]);
263
264 // Mock "card" — a centered panel that hints at a PR view.
265 const px = 200;
266 const py = 200;
267 const pw = W - 400;
268 const ph = H - 400;
269 fillRect(buf, W, px, py, px + pw, py + ph, [0x16, 0x1b, 0x22]); // panel bg
270
271 // Header strip on the panel
272 fillRect(buf, W, px, py, px + pw, py + 60, [0x21, 0x26, 0x2d]);
273
274 // "PR opened" accent dot (green).
275 fillCircle(buf, W, px + 30, py + 30, 10, [0x3f, 0xb9, 0x50]);
276
277 // Two faux content rows.
278 fillRect(buf, W, px + 30, py + 100, px + pw - 30, py + 110, [0x30, 0x36, 0x3d]);
279 fillRect(buf, W, px + 30, py + 140, px + pw - 100, py + 150, [0x30, 0x36, 0x3d]);
280 fillRect(buf, W, px + 30, py + 180, px + pw - 200, py + 190, [0x30, 0x36, 0x3d]);
281
282 return encodePng(W, H, buf);
283}
284
285// ---------------------------------------------------------------------------
286// Main
287// ---------------------------------------------------------------------------
288
289function main() {
290 mkdirSync(OUT_DIR, { recursive: true });
291
292 const iconPath = join(OUT_DIR, "icon.png");
293 const screenshotPath = join(OUT_DIR, "screenshot-1.png");
294
295 writeFileSync(iconPath, drawIcon());
296 console.log(`wrote ${iconPath}`);
297
298 writeFileSync(screenshotPath, drawScreenshot());
299 console.log(`wrote ${screenshotPath}`);
300}
301
302main();