import type { CanvasState } from "@dada/shared-contracts"; import { P0A_DYNAMIC_STICKER_IDS } from "@dada/template-registry"; import { DYNAMIC_RESOURCE_VERSION } from "./dynamic-render-models.js"; import type { CanvasElementIdentity } from "./editor-elements.js"; import complexAssetCatalog from "./generated/complex-assets.json"; type CanvasElement = CanvasState["elements"][number]; export type DynamicCategory = "identity" | "location" | "other" | "time"; export type DynamicTemplateId = typeof P0A_DYNAMIC_STICKER_IDS[number]; export interface DynamicStickerDefinition { category: DynamicCategory; displayName: string; requiresLocationConsent: boolean; templateId: DynamicTemplateId; } export interface DynamicProviderContext { location?: { formattedValue: string; latitude?: number; longitude?: number }; now: Date; profile: { creatorName: string; socialId: string }; } // Declarative conversion of l_shijian7.prefab. The browser never executes the Android Prefab or Lua. export const DYN012_RENDER_LAYOUT = { background: "transparent", divider: { color: "#FFFFFF", height: 42, width: 7, x: 0, y: 0 }, hour: { color: "#FFFFFF", fontId: "FONT081", fontSize: 100, x: -60, y: 0 }, meridiem: { color: "#FFFFFF", fontId: "FONT081", fontSize: 20, x: 94, y: -65 }, minute: { color: "#FFFFFF", fontId: "FONT081", fontSize: 100, x: 60, y: 0 }, } as const; export function dyn012DisplayParts(element: CanvasElement) { if (element.type !== "dynamic_sticker" || element.template_or_asset_id !== "DYN012") throw new Error("dyn012_element_required"); const formatted = element.formatted_value?.match(/^(\d{1,2}):(\d{2})$/); const hour = formatted?.[1]?.padStart(2, "0") ?? String(element.dynamic_fields?.hour ?? "00").padStart(2, "0"); const minute = formatted?.[2] ?? String(element.dynamic_fields?.minute ?? "00").padStart(2, "0"); const hourNumber = Number(hour); const minuteNumber = Number(minute); return { hour, meridiem: hourNumber > 12 || (hourNumber === 12 && minuteNumber > 0) ? "PM" : "AM", minute, } as const; } const dynamicCatalogById = new Map(complexAssetCatalog.dynamic_stickers.map((item) => [item.template_id, item])); export const P0A_DYNAMIC_STICKERS: readonly DynamicStickerDefinition[] = P0A_DYNAMIC_STICKER_IDS.map((templateId) => { const item = dynamicCatalogById.get(templateId); if (!item) throw new Error(`missing dynamic sticker definition ${templateId}`); return { category: item.category as DynamicCategory, displayName: item.display_name, requiresLocationConsent: item.requires_location_consent, templateId, }; }); function twoDigits(value: number) { return String(value).padStart(2, "0"); } export function normalizeSocialId(value: string) { return `@${value.trim().replace(/^@+/, "")}`; } function snapshotFor(templateId: DynamicTemplateId, context: DynamicProviderContext) { const year = String(context.now.getFullYear()); const month = twoDigits(context.now.getMonth() + 1); const day = twoDigits(context.now.getDate()); const hour = twoDigits(context.now.getHours()); const minute = twoDigits(context.now.getMinutes()); if (templateId === "DYN001") return { fields: { title: context.location?.formattedValue ?? "输入地点" }, value: context.location?.formattedValue ?? "输入地点" }; if (templateId === "DYN002") return { fields: { city_en: context.location?.formattedValue ?? "LOCATION", title: context.location?.formattedValue ?? "输入地点" }, value: (context.location?.formattedValue ?? "LOCATION").toUpperCase() }; if (templateId === "DYN003") return { fields: { city: context.location?.formattedValue ?? "城市", title: context.location?.formattedValue ?? "输入地点" }, value: context.location?.formattedValue ?? "城市 · 输入地点" }; if (templateId === "DYN004") { if (context.location?.latitude === undefined || context.location.longitude === undefined) throw new Error("dynamic_location_consent_required"); return { fields: { city: context.location.formattedValue, latitude: context.location.latitude, longitude: context.location.longitude }, value: `${context.location.formattedValue}\n${context.location.latitude.toFixed(4)}, ${context.location.longitude.toFixed(4)}`, }; } if (templateId === "DYN007") { const nickname = normalizeSocialId(context.profile.socialId); return { fields: { nickname }, value: nickname }; } if (templateId === "DYN008") return { fields: { hour, minute, month }, value: `${month}月 · ${hour}:${minute}` }; if (templateId === "DYN011") return { fields: { day, month, year }, value: `${year}.${month}.${day}` }; if (templateId === "DYN012") return { fields: { font_substitution: "FONT081", hour, minute }, value: `${hour}:${minute}` }; if (templateId === "DYN015") return { fields: { nickname: context.profile.creatorName }, value: context.profile.creatorName }; if (templateId === "DYN016") return { fields: { nickname: normalizeSocialId(context.profile.socialId) }, value: normalizeSocialId(context.profile.socialId) }; const definition = dynamicCatalogById.get(templateId); if (!definition) throw new Error("dynamic_template_unavailable"); const location = context.location?.formattedValue ?? "输入地点"; const values: Record = { city: location, city_en: location.toUpperCase(), day, hour, latitude: context.location?.latitude ?? 0, longitude: context.location?.longitude ?? 0, minute, month, nickname: normalizeSocialId(context.profile.socialId), title: location, year, }; const fields = Object.fromEntries(definition.required_fields.map((field) => [field, values[field] ?? ""])); if (definition.category === "identity") return { fields, value: normalizeSocialId(context.profile.socialId) }; if (definition.category === "location") return { fields, value: location }; if (definition.category === "time") return { fields, value: `${year}.${month}.${day} ${hour}:${minute}` }; return { fields, value: context.profile.creatorName }; } export function createDynamicStickerElement( templateId: DynamicTemplateId, context: DynamicProviderContext, identity: CanvasElementIdentity, zIndex: number, ): CanvasElement { if (!P0A_DYNAMIC_STICKERS.some((item) => item.templateId === templateId)) throw new Error("dynamic_template_unavailable"); const snapshot = snapshotFor(templateId, context); return { created_at: identity.createdAt, dynamic_fields: snapshot.fields, element_id: identity.elementId, ...(templateId === "DYN004" && context.location?.latitude !== undefined && context.location.longitude !== undefined ? { coordinates: { latitude: context.location.latitude, longitude: context.location.longitude } } : {}), ...(templateId === "DYN012" ? { font_override: "FONT081" } : {}), formatted_value: snapshot.value, opacity: 1, position: { x: 0.5, y: 0.5 }, resource_version: DYNAMIC_RESOURCE_VERSION, rotation: 0, scale: { x: 1, y: 1 }, style_parameters: { provider: "typescript-declarative-v1", ...(templateId === "DYN012" ? { font_substitution_disclosure: "DIN_MediumAlternate.otf missing; FONT081 Lexend Deca substitute" } : {}), }, template_or_asset_id: templateId, type: "dynamic_sticker", z_index: zIndex, }; } export function overrideDynamicStickerValue(element: CanvasElement, formattedValue: string): CanvasElement { if (element.type !== "dynamic_sticker") throw new Error("dynamic_element_required"); const value = formattedValue.trim(); if (!value) throw new Error("dynamic_value_required"); return { ...structuredClone(element), dynamic_fields: { ...structuredClone(element.dynamic_fields ?? {}), display_override: value }, formatted_value: value, }; } export interface LocationResult { formattedValue: string; latitude: number; longitude: number; } export class LocationConsentGate { constructor(private readonly adapter: { geolocate: () => Promise<{ latitude: number; longitude: number }>; reverseGeocode: (coordinates: { latitude: number; longitude: number }) => Promise; }) {} reject() { return undefined; } async confirm(): Promise { const coordinates = await this.adapter.geolocate(); if (!Number.isFinite(coordinates.latitude) || coordinates.latitude < -90 || coordinates.latitude > 90 || !Number.isFinite(coordinates.longitude) || coordinates.longitude < -180 || coordinates.longitude > 180) { throw new Error("dynamic_location_invalid"); } const formattedValue = (await this.adapter.reverseGeocode(coordinates)).trim(); if (!formattedValue) throw new Error("dynamic_location_unavailable"); return { ...coordinates, formattedValue }; } } export function browserGeolocate() { return new Promise<{ latitude: number; longitude: number }>((resolve, reject) => { if (!navigator.geolocation) { reject(new Error("dynamic_location_unsupported")); return; } navigator.geolocation.getCurrentPosition( (position) => resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude }), () => reject(new Error("dynamic_location_denied")), { enableHighAccuracy: false, maximumAge: 0, timeout: 10_000 }, ); }); }