commit f7a5610d8d89dcc1c1a459053b4e4ed31aa8214c
parent 9623a00bbb5df4758d382877a3ef9cea0ce5daf2
Author: Michael Camilleri <[email protected]>
Date: Wed, 17 Jun 2026 06:14:43 +0900
Reposition share link composite image
Diffstat:
1 file changed, 56 insertions(+), 71 deletions(-)
diff --git a/Workers/link-worker.js b/Workers/link-worker.js
@@ -112,7 +112,7 @@ export default {
headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=86400" }
});
}
- return new Response(renderSilhouettePNG(grid.side, grid.blocks), {
+ return new Response(await renderSilhouettePNG(grid.side, grid.blocks), {
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=86400, immutable"
@@ -316,61 +316,40 @@ function base64urlToBytes(s) {
}
// Render constants: each cell is CELL px with a MARGIN-px quiet border, so the
-// image side is a pure function of the grid side (35 → 798px is the largest).
+// image side is a pure function of the grid side (35 → 842px is the largest).
const CELL = 22;
-const MARGIN = 14;
-const BG = 0; // empty square / background
-const LINE = 1; // grid line gray
-const BLOCK = 2; // block square / black logo line
+const MARGIN = 36;
+const BG = [255, 255, 255]; // empty square / background
+const LINE = [205, 205, 205]; // grid line gray
+const BLOCK = [0, 0, 0]; // block square / black logo line
const DYNAMIC_IMAGE_MIN_SIDE = 10;
-const PALETTE = new Uint8Array([
- 255, 255, 255, // background / white logo square
- 205, 205, 205, // grid line gray
- 0, 0, 0, // block square / black logo line
- 42, 42, 42, // logo dark
- 255, 135, 132, // logo coral
- 109, 173, 240, // logo blue
- 207, 226, 246, // logo pale blue
- 142, 142, 142, // shadow ramp
- 154, 154, 154,
- 166, 166, 166,
- 178, 178, 178,
- 190, 190, 190,
- 202, 202, 202,
- 214, 214, 214,
- 226, 226, 226,
- 238, 238, 238,
- 246, 246, 246
-]);
-
const LOGO_SOURCE_SIZE = 512;
const LOGO_MIN_SIZE = 64;
const LOGO_MAX_SIZE = 152;
const LOGO_SCALE = 0.22;
const LOGO_PADDING_SCALE = 0.035;
const LOGO_MIN_PADDING = 10;
-const LOGO_DARK = 3;
-const LOGO_CORAL = 4;
-const LOGO_BLUE = 5;
-const LOGO_PALE_BLUE = 6;
+const LOGO_DARK = [42, 42, 42];
+const LOGO_CORAL = [255, 135, 132];
+const LOGO_BLUE = [109, 173, 240];
+const LOGO_PALE_BLUE = [207, 226, 246];
const LOGO_WHITE = BG;
const LOGO_BLACK = BLOCK;
-const SHADOW_START = 7;
-const SHADOW_LEVELS = 10;
const SHADOW_OFFSET_X = 0.035;
const SHADOW_OFFSET_Y = 0.055;
const SHADOW_BLUR = 0.150;
+const SHADOW_ALPHA = 0.26;
function silhouettePixelSize(side) {
return MARGIN * 2 + side * CELL;
}
-// Paints the grid and a scaled Crossmate icon into an indexed-color PNG.
-function renderSilhouettePNG(side, blocks) {
+// Paints the grid and a scaled Crossmate icon into a truecolor PNG.
+async function renderSilhouettePNG(side, blocks) {
const dim = silhouettePixelSize(side);
- const pixels = new Uint8Array(dim * dim).fill(BG);
- const set = (x, y, v) => { pixels[y * dim + x] = v; };
+ const pixels = new Uint8Array(dim * dim * 3).fill(255);
+ const set = (x, y, color) => setRGB(pixels, dim, x, y, color);
// Grid lines bounding every cell.
const extent = side * CELL;
@@ -394,23 +373,30 @@ function renderSilhouettePNG(side, blocks) {
}
drawCrossmateIcon(pixels, dim, side);
- return encodeIndexedPNG(dim, dim, pixels);
+ return await encodeTruecolorPNG(dim, dim, pixels);
+}
+
+function setRGB(pixels, width, x, y, color) {
+ const offset = (y * width + x) * 3;
+ pixels[offset] = color[0];
+ pixels[offset + 1] = color[1];
+ pixels[offset + 2] = color[2];
}
function drawCrossmateIcon(pixels, dim, side) {
const size = clamp(Math.round(dim * LOGO_SCALE), LOGO_MIN_SIZE, LOGO_MAX_SIZE);
const padding = Math.max(LOGO_MIN_PADDING, Math.round(dim * LOGO_PADDING_SCALE));
- const gridMax = MARGIN + side * CELL;
- const origin = gridMax - padding - size;
+ const origin = dim - padding - size;
const radius = Math.round(size * 0.135);
drawRoundedShadow(pixels, dim, origin, size, radius);
for (let y = 0; y < size; y++) {
const sourceY = Math.floor((y * LOGO_SOURCE_SIZE) / size);
for (let x = 0; x < size; x++) {
- if (!isInsideRoundedRect(x, y, size, radius)) continue;
+ const coverage = roundedRectCoverage(x, y, size, radius);
+ if (coverage <= 0) continue;
const sourceX = Math.floor((x * LOGO_SOURCE_SIZE) / size);
- pixels[(origin + y) * dim + origin + x] = crossmateIconColor(sourceX, sourceY);
+ blendPixel(pixels, dim, origin + x, origin + y, crossmateIconColor(sourceX, sourceY), coverage);
}
}
}
@@ -433,22 +419,17 @@ function drawRoundedShadow(pixels, dim, iconOrigin, size, radius) {
if (distance > blur) continue;
const strength = Math.pow(1 - Math.max(distance, 0) / blur, 1.85);
- const level = SHADOW_LEVELS - 1 - Math.round(strength * (SHADOW_LEVELS - 1));
- blendShadowPixel(pixels, py * dim + px, SHADOW_START + level);
+ blendPixel(pixels, dim, px, py, BLOCK, strength * SHADOW_ALPHA);
}
}
}
-function blendShadowPixel(pixels, offset, color) {
- const current = pixels[offset];
- if (current === BLOCK) return;
- if (current >= SHADOW_START) {
- pixels[offset] = Math.min(current, color);
- return;
- }
- if (current === BG || color <= SHADOW_START + 5) {
- pixels[offset] = color;
- }
+function blendPixel(pixels, width, x, y, color, alpha) {
+ const offset = (y * width + x) * 3;
+ const keep = 1 - alpha;
+ pixels[offset] = Math.round(pixels[offset] * keep + color[0] * alpha);
+ pixels[offset + 1] = Math.round(pixels[offset + 1] * keep + color[1] * alpha);
+ pixels[offset + 2] = Math.round(pixels[offset + 2] * keep + color[2] * alpha);
}
function distanceToRoundedRect(x, y, size, radius) {
@@ -460,16 +441,8 @@ function distanceToRoundedRect(x, y, size, radius) {
return Math.hypot(outsideX, outsideY) + Math.min(Math.max(qx, qy), 0) - radius;
}
-function isInsideRoundedRect(x, y, size, radius) {
- const left = radius;
- const right = size - 1 - radius;
- const top = radius;
- const bottom = size - 1 - radius;
- const cx = x < left ? left : x > right ? right : x;
- const cy = y < top ? top : y > bottom ? bottom : y;
- const dx = x - cx;
- const dy = y - cy;
- return dx * dx + dy * dy <= radius * radius;
+function roundedRectCoverage(x, y, size, radius) {
+ return clamp(0.5 - distanceToRoundedRect(x, y, size, radius), 0, 1);
}
function crossmateIconColor(x, y) {
@@ -487,14 +460,15 @@ function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
-// MARK: - Minimal PNG encoder (8-bit indexed color, stored DEFLATE)
+// MARK: - Minimal PNG encoder (8-bit truecolor, stored DEFLATE)
-function encodeIndexedPNG(width, height, pixels) {
+async function encodeTruecolorPNG(width, height, pixels) {
// Each scanline is prefixed with a filter-type byte (0 = None).
- const raw = new Uint8Array(height * (width + 1));
+ const stride = width * 3;
+ const raw = new Uint8Array(height * (stride + 1));
for (let y = 0; y < height; y++) {
- raw[y * (width + 1)] = 0;
- raw.set(pixels.subarray(y * width, (y + 1) * width), y * (width + 1) + 1);
+ raw[y * (stride + 1)] = 0;
+ raw.set(pixels.subarray(y * stride, (y + 1) * stride), y * (stride + 1) + 1);
}
const ihdr = new Uint8Array(13);
@@ -502,19 +476,30 @@ function encodeIndexedPNG(width, height, pixels) {
dv.setUint32(0, width);
dv.setUint32(4, height);
ihdr[8] = 8; // bit depth
- ihdr[9] = 3; // color type: indexed color
+ ihdr[9] = 2; // color type: truecolor
// [10..12] compression/filter/interlace = 0
const signature = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
return concatBytes([
signature,
pngChunk("IHDR", ihdr),
- pngChunk("PLTE", PALETTE),
- pngChunk("IDAT", zlibStore(raw)),
+ pngChunk("IDAT", await zlibDeflate(raw)),
pngChunk("IEND", new Uint8Array(0))
]);
}
+async function zlibDeflate(raw) {
+ if (typeof CompressionStream === "undefined") {
+ return zlibStore(raw);
+ }
+ try {
+ const stream = new Blob([raw]).stream().pipeThrough(new CompressionStream("deflate"));
+ return new Uint8Array(await new Response(stream).arrayBuffer());
+ } catch {
+ return zlibStore(raw);
+ }
+}
+
function pngChunk(type, data) {
const out = new Uint8Array(12 + data.length);
const dv = new DataView(out.buffer);