import type { CanvasState } from "@dada/shared-contracts"; import { fontOption, type FontOption } from "./text-assets.js"; import type { DynamicTemplateId } from "./dynamic-provider.js"; type CanvasElement = CanvasState["elements"][number]; export type DynamicTextValue = | "city" | "city_en" | "day" | "hour" | "latitude_dms" | "longitude_dms" | "meridiem" | "minute" | "month" | "month_en" | "nickname" | "time" | "title" | "year_short"; export interface DynamicImageLayer { assetId: string; height: number; width: number; x: number; y: number; } export interface DynamicTextLayer { align: "center" | "left"; color: string; fontId: string; fontSize: number; value: DynamicTextValue; x: number; y: number; } export interface DynamicRenderModel { halfSize: { height: number; width: number }; imageLayers: readonly DynamicImageLayer[]; sourceCandidateId: string; textLayers: readonly DynamicTextLayer[]; } export const DYNAMIC_RESOURCE_VERSION = "wp4-dynamic-source-v1"; const dynamicFont = (fontId: string): FontOption => ({ displayName: fontId, fontId, url: `/api/v1/assets/public/${DYNAMIC_RESOURCE_VERSION}/${fontId}`, }); export const DYNAMIC_FONT_OPTIONS: readonly FontOption[] = [ dynamicFont("15974853bc3294ef68e7e6d58fe74fd7"), dynamicFont("46f8336813e4c48d06a1aef294fdccf6"), dynamicFont("53ca6b704728520da50c145eabb2e635"), dynamicFont("cca5efc0e02fb1bf62349bd68ef30fc1"), dynamicFont("dd25b35dcb7ba4476cbaa9a9592e39e2"), dynamicFont("e4210c9872f0c279b35273f230809821"), dynamicFont("f4bfd4132df2d6be97ceabadf3853505"), ] as const; export const DYNAMIC_RENDER_MODELS: Readonly> = { DYN001: { halfSize: { height: 42, width: 130 }, imageLayers: [{ assetId: "DYN001-image28", height: 67, width: 219, x: -17.562, y: 2.203 }], sourceCandidateId: "l_POI01", textLayers: [{ align: "center", color: "#FFFFFF", fontId: "dd25b35dcb7ba4476cbaa9a9592e39e2", fontSize: 21, value: "title", x: 0, y: 0 }], }, DYN002: { halfSize: { height: 48, width: 215 }, imageLayers: [{ assetId: "DYN002-image29", height: 77, width: 232, x: 94.5, y: 0.5 }], sourceCandidateId: "l_POI02", textLayers: [ { align: "left", color: "#FFFFFF", fontId: "15974853bc3294ef68e7e6d58fe74fd7", fontSize: 19, value: "title", x: -94.5, y: 10.5 }, { align: "left", color: "#FFFFFF", fontId: "15974853bc3294ef68e7e6d58fe74fd7", fontSize: 10, value: "city_en", x: 0, y: 36 }, ], }, DYN003: { halfSize: { height: 54, width: 215 }, imageLayers: [{ assetId: "DYN003-image30", height: 85, width: 235, x: -93.857, y: -23.635 }], sourceCandidateId: "l_POI03", textLayers: [ { align: "left", color: "#FFFFFF", fontId: "dd25b35dcb7ba4476cbaa9a9592e39e2", fontSize: 18, value: "title", x: -54, y: 9 }, { align: "left", color: "#FFFFFF", fontId: "dd25b35dcb7ba4476cbaa9a9592e39e2", fontSize: 24, value: "city", x: -53, y: -16 }, ], }, DYN004: { halfSize: { height: 48, width: 180 }, imageLayers: [{ assetId: "DYN004-image32", height: 76, width: 39, x: -21.067, y: -1 }], sourceCandidateId: "l_POI04", textLayers: [ { align: "left", color: "#FFFFFF", fontId: "dd25b35dcb7ba4476cbaa9a9592e39e2", fontSize: 26, value: "city", x: -28, y: 0 }, { align: "left", color: "#FFFFFF", fontId: "15974853bc3294ef68e7e6d58fe74fd7", fontSize: 14, value: "latitude_dms", x: -29.844, y: 36 }, { align: "left", color: "#FFFFFF", fontId: "15974853bc3294ef68e7e6d58fe74fd7", fontSize: 14, value: "longitude_dms", x: 71.523, y: 36 }, ], }, DYN007: { halfSize: { height: 34, width: 190 }, imageLayers: [], sourceCandidateId: "diaoyu", textLayers: [{ align: "center", color: "#295E8B", fontId: "53ca6b704728520da50c145eabb2e635", fontSize: 50, value: "nickname", x: 0, y: 0 }], }, DYN008: { halfSize: { height: 180, width: 140 }, imageLayers: [{ assetId: "DYN008-backendui0", height: 185, width: 5, x: -125, y: 0 }], sourceCandidateId: "l_shijian2", textLayers: [ { align: "left", color: "#FFFFFF", fontId: "f4bfd4132df2d6be97ceabadf3853505", fontSize: 70, value: "month_en", x: -112, y: -32 }, { align: "left", color: "#FFFFFF", fontId: "f4bfd4132df2d6be97ceabadf3853505", fontSize: 130, value: "time", x: -114.024, y: 97 }, ], }, DYN011: { halfSize: { height: 40, width: 130 }, imageLayers: [{ assetId: "DYN011-backendui0", height: 12, width: 8, x: -120, y: -19 }], sourceCandidateId: "l_shijian6", textLayers: [ { align: "center", color: "#FF6D2F", fontId: "e4210c9872f0c279b35273f230809821", fontSize: 64, value: "year_short", x: -85, y: 0 }, { align: "center", color: "#FF6D2F", fontId: "e4210c9872f0c279b35273f230809821", fontSize: 64, value: "month", x: 0, y: 0 }, { align: "center", color: "#FF6D2F", fontId: "e4210c9872f0c279b35273f230809821", fontSize: 64, value: "day", x: 85, y: 0 }, ], }, DYN012: { halfSize: { height: 78, width: 118 }, imageLayers: [], sourceCandidateId: "l_shijian7", textLayers: [], }, DYN015: { halfSize: { height: 42, width: 300 }, imageLayers: [{ assetId: "DYN015-imager2", height: 60, width: 60, x: -140, y: 0 }], sourceCandidateId: "0721userna", textLayers: [{ align: "left", color: "#666666", fontId: "cca5efc0e02fb1bf62349bd68ef30fc1", fontSize: 40, value: "nickname", x: -100, y: 0 }], }, DYN016: { halfSize: { height: 48, width: 160 }, imageLayers: [{ assetId: "DYN016-image21", height: 59, width: 186, x: -61.842, y: 16.483 }], sourceCandidateId: "l_username00", textLayers: [{ align: "left", color: "#FFFFFF", fontId: "46f8336813e4c48d06a1aef294fdccf6", fontSize: 27, value: "nickname", x: 0, y: 0 }], }, }; export function dynamicFontOptionsFor(templateId: string) { if (templateId === "DYN012") { const replacement = fontOption("FONT081"); return replacement ? [replacement] : []; } const model = DYNAMIC_RENDER_MODELS[templateId as DynamicTemplateId]; if (!model) return []; const ids = new Set(model.textLayers.map((layer) => layer.fontId)); return DYNAMIC_FONT_OPTIONS.filter((option) => ids.has(option.fontId)); } export function dynamicImageUrl(resourceVersion: string, assetId: string) { return `/api/v1/assets/public/${encodeURIComponent(resourceVersion)}/${encodeURIComponent(assetId)}`; } function numericField(element: CanvasElement, key: string) { const value = element.dynamic_fields?.[key]; return typeof value === "number" ? value : Number(value); } function coordinate(value: number, positive: string, negative: string) { if (!Number.isFinite(value)) return ""; const absolute = Math.abs(value); const degrees = Math.floor(absolute); const minutesFloat = (absolute - degrees) * 60; const minutes = Math.floor(minutesFloat); const seconds = (minutesFloat - minutes) * 60; return `${degrees}°${String(minutes).padStart(2, "0")}'${seconds.toFixed(2).padStart(5, "0")}\"${value >= 0 ? positive : negative}`; } export function dynamicTextValue(element: CanvasElement, value: DynamicTextValue) { const fields = element.dynamic_fields ?? {}; const text = (key: string) => String(fields[key] ?? ""); const override = fields.display_override; if (typeof override === "string" && (value === "nickname" || value === "title")) return element.formatted_value ?? override; if (value === "time") return `${text("hour")}:${text("minute")}`; if (value === "month_en") { const month = Math.max(1, Math.min(12, numericField(element, "month"))); return `${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][month - 1]}.`; } if (value === "year_short") return text("year").slice(-2); if (value === "latitude_dms") return coordinate(numericField(element, "latitude"), "N", "S"); if (value === "longitude_dms") return coordinate(numericField(element, "longitude"), "E", "W"); if (value === "meridiem") { const hour = numericField(element, "hour"); const minute = numericField(element, "minute"); return hour > 12 || (hour === 12 && minute > 0) ? "PM" : "AM"; } return text(value); }