import sharp from "sharp"; const hex = (rgb) => `#${rgb.map((value) => Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0")).join("").toUpperCase()}`; const mix = (a, b, ratio) => a.map((value, index) => value * (1 - ratio) + b[index] * ratio); export async function compileDesignTokens(productBuffer) { const { data, info } = await sharp(productBuffer).resize({ width: 160 }).ensureAlpha().raw().toBuffer({ resolveWithObject: true }); const buckets = new Map(); for (let index = 0; index < data.length; index += info.channels) { if (data[index + 3] < 180) continue; const rgb = [data[index], data[index + 1], data[index + 2]]; const max = Math.max(...rgb); const min = Math.min(...rgb); if (max > 242 && max - min < 12) continue; const key = rgb.map((value) => Math.floor(value / 32) * 32).join(","); buckets.set(key, (buckets.get(key) || 0) + 1); } const ranked = [...buckets.entries()].map(([key, count]) => { const rgb = key.split(",").map(Number).map((value) => value + 16); const saturation = Math.max(...rgb) - Math.min(...rgb); const luminance = rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722; return { rgb, count, saturation, luminance, score: count * (1 + saturation / 55) * (luminance < 210 ? 1.25 : 0.5) }; }).sort((a, b) => b.score - a.score); const primary = ranked.find((item) => item.saturation >= 18 && item.luminance < 190)?.rgb || ranked[0]?.rgb || [38, 49, 38]; const accentCandidate = ranked.find((item) => ( item.saturation > 24 && item.rgb[0] > 120 && item.rgb[0] >= item.rgb[1] * 0.95 && item.rgb[1] > item.rgb[2] * 1.12 )); const accent = accentCandidate?.rgb || mix(primary, [218, 160, 72], 0.58); return { primary: hex(primary), primarySoft: hex(mix(primary, [255, 255, 255], 0.72)), primaryDark: hex(mix(primary, [15, 20, 16], 0.42)), surface: hex(mix(primary, [250, 247, 239], 0.88)), accent: hex(accent), ink: "#1D2922", lightText: "#FAF7EF", }; }