import { MMCQ } from "@vibrant/quantizer-mmcq"; import type { CanvasState } from "@dada/shared-contracts"; import { P0A_COLOR_CARD_DEFINITIONS, type ColorCardDefinition } from "@dada/asset-renderer"; import type { CanvasElementIdentity } from "./editor-elements.js"; type CanvasElement = CanvasState["elements"][number]; export type { ColorCardDefinition } from "@dada/asset-renderer"; export interface PaletteColor { hex: string; population: number; rgbValue: number; } export const P0A_COLOR_CARDS: readonly ColorCardDefinition[] = P0A_COLOR_CARD_DEFINITIONS; // Coordinates are translated from the archived 320x320 renderer around its 160px center. export const COLOR_CARD_SOURCE_GEOMETRY = { style_01: { bounds: { bottom: 76, left: -26, right: 26, top: -74 }, palette: { bottom: 37, left: -23, right: 23, top: -71 }, }, style_02: { bounds: { bottom: 69, left: -18, right: 18, top: -77 } }, style_08: { bounds: { bottom: 10, left: -73, right: 73, top: -9 } }, style_16: { bounds: { bottom: 9, left: -78, right: 78, top: -9 } }, } as const; function normalizedHex(value: string) { return value.toUpperCase(); } export function extractMmcqPalette(pixels: Uint8ClampedArray): PaletteColor[] { const swatches = MMCQ(pixels, { colorCount: 10 }); if (swatches.length < 5) throw new Error("palette_five_colors_required"); return swatches .map((swatch) => ({ hex: normalizedHex(swatch.hex), population: swatch.population, rgbValue: (Math.round(swatch.r) << 16) + (Math.round(swatch.g) << 8) + Math.round(swatch.b), })) .sort((left, right) => right.population - left.population || left.rgbValue - right.rgbValue) .slice(0, 5); } export function extractPaletteFromImage(image: CanvasImageSource & { height: number; width: number }) { const longestEdge = Math.max(image.width, image.height); if (longestEdge <= 0) throw new Error("palette_image_invalid"); const scale = Math.min(1, 256 / longestEdge); const width = Math.max(1, Math.round(image.width * scale)); const height = Math.max(1, Math.round(image.height * scale)); const surface = document.createElement("canvas"); surface.width = width; surface.height = height; const context = surface.getContext("2d", { willReadFrequently: true }); if (!context) throw new Error("palette_canvas_unavailable"); context.drawImage(image, 0, 0, width, height); return extractMmcqPalette(context.getImageData(0, 0, width, height).data).map((entry) => entry.hex); } function validatePalette(palette: readonly string[]) { if (palette.length !== 5 || palette.some((color) => !/^#[0-9A-Fa-f]{6}$/.test(color))) throw new Error("canvas_palette_invalid"); return palette.map(normalizedHex); } export function createColorCardElement( definition: ColorCardDefinition, palette: readonly string[], identity: CanvasElementIdentity, zIndex: number, ): CanvasElement { return { colors: validatePalette(palette), created_at: identity.createdAt, element_id: identity.elementId, opacity: 1, position: { x: 0.5, y: 0.5 }, resource_version: "wp4-provider-v1", rotation: 0, scale: { x: 1, y: 1 }, style_id: definition.styleId, style_parameters: { mapping_status: definition.mappingStatus, palette_algorithm_version: "mmcq-v1", renderer_name: definition.rendererName, }, template_or_asset_id: definition.cardId, type: "color_card", z_index: zIndex, }; } export function refreshColorCards(state: CanvasState, palette: readonly string[]): CanvasState { const colors = validatePalette(palette); const next = structuredClone(state); next.elements = next.elements.map((element) => element.type === "color_card" ? { ...element, colors: [...colors] } : element); return next; } export function drawColorCard(context: CanvasRenderingContext2D, element: CanvasElement) { const colors = element.colors ?? ["#111111", "#333333", "#555555", "#777777", "#999999"]; if (element.style_id === "style_01") { context.fillStyle = "#ffffff"; context.fillRect(-26, -74, 52, 145); colors.forEach((color, index) => { const top = -71 + index * 21.6; context.fillStyle = color; context.fillRect(-23, top, 46, index === colors.length - 1 ? 37 - top : 21.6); }); context.fillStyle = "#222222"; context.font = "700 5px Arial, sans-serif"; context.textAlign = "left"; context.textBaseline = "top"; context.fillText("COLOR", -20, 44); context.fillStyle = "#979797"; context.fillRect(-20, 54, 24, 1); context.fillStyle = "#ffffff"; for (let index = 0; index < 8; index += 1) { const left = -26 + index * 6.5; context.beginPath(); context.moveTo(left, 71); context.lineTo(left + 3.25, 76); context.lineTo(left + 6.5, 71); context.closePath(); context.fill(); } return; } if (element.style_id === "style_02") { context.font = "4px Arial, sans-serif"; context.textAlign = "left"; context.textBaseline = "top"; context.fillStyle = "#222222"; context.fillText("C O L O R", -17, -77); colors.forEach((color, index) => { context.fillStyle = color; context.fillRect(-18, -71 + index * 28, 36, 27); }); return; } if (element.style_id === "style_08") { colors.forEach((color, index) => { const left = -73 + index * 29.2; const center = -59 + index * 29; context.fillStyle = color; context.fillRect(left, -2, index === colors.length - 1 ? 73 - left : 29.2, 4); context.beginPath(); context.arc(center, -7, 2, 0, Math.PI * 2); context.fill(); context.strokeStyle = "#ffffff"; context.lineWidth = 1; context.stroke(); context.fillStyle = "#222222"; context.font = "4px Arial, sans-serif"; context.textAlign = "center"; context.textBaseline = "middle"; context.fillText(`0${index + 1}`, center, 8); }); return; } context.fillStyle = "#ffffff"; context.beginPath(); context.moveTo(-78, -9); for (let index = 0; index < 4; index += 1) { const y = -9 + index * 4.5; context.lineTo(-74, y + 2.25); context.lineTo(-78, y + 4.5); } context.lineTo(-70, 9); context.lineTo(-70, -9); context.closePath(); context.fill(); context.fillStyle = "#222222"; context.fillRect(-72, -9, 17, 18); context.fillStyle = "#ffffff"; context.font = "700 5px Arial, sans-serif"; context.textAlign = "center"; context.textBaseline = "middle"; context.fillText("C", -63.5, 0); const stripColors = [...colors, "#f2f2f2"]; stripColors.forEach((color, index) => { const left = -55 + index * (133 / stripColors.length); context.fillStyle = color; context.fillRect(left, -9, index === stripColors.length - 1 ? 78 - left : 133 / stripColors.length, 18); }); }