Files
tyx_AI_xhs/apps/web/src/dynamic-sticker-panel.tsx
T

68 lines
3.7 KiB
TypeScript

import { useEffect, useRef } from "react";
import { createDynamicStickerElement, P0A_DYNAMIC_STICKERS, type DynamicTemplateId } from "./dynamic-provider.js";
import { DYNAMIC_RENDER_MODELS, dynamicImageUrl } from "./dynamic-render-models.js";
import { drawDynamicSticker } from "./editor-stage.js";
import type { ArchivedFontStatus } from "./text-font-loader.js";
const categoryLabels = { identity: "身份", location: "地点", other: "综合", time: "时间" } as const;
function loadImage(url: string) {
return new Promise<HTMLImageElement | undefined>((resolve) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => resolve(undefined);
image.src = url;
});
}
function DynamicPreview(props: { fontStatuses: Readonly<Record<string, ArchivedFontStatus>>; templateId: DynamicTemplateId }) {
const ref = useRef<HTMLCanvasElement>(null);
const statusKey = Object.entries(props.fontStatuses).map(([id, status]) => `${id}:${status}`).join("|");
useEffect(() => {
let active = true;
const model = DYNAMIC_RENDER_MODELS[props.templateId];
const element = createDynamicStickerElement(props.templateId, {
location: { formattedValue: "温州", latitude: 27.9943, longitude: 120.6994 },
now: new Date("2026-08-03T09:07:00+08:00"), profile: { creatorName: "Dada Creator", socialId: "@dada" },
}, { createdAt: "2026-08-03T00:00:00.000Z", elementId: "00000000-0000-4000-8000-000000000002" }, 0);
void Promise.all(model.imageLayers.map(async (layer) => [layer.assetId, await loadImage(dynamicImageUrl(element.resource_version, layer.assetId))] as const)).then((entries) => {
if (!active) return;
const canvas = ref.current;
const context = canvas?.getContext("2d");
if (!canvas || !context) return;
context.setTransform(1, 0, 0, 1, 0, 0);
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = "#30343b";
context.fillRect(0, 0, canvas.width, canvas.height);
const scale = Math.min(1, 146 / (model.halfSize.width * 2), 62 / (model.halfSize.height * 2));
context.translate(canvas.width / 2, canvas.height / 2);
context.scale(scale, scale);
const images = Object.fromEntries(entries.filter((entry): entry is readonly [string, HTMLImageElement] => entry[1] !== undefined));
drawDynamicSticker(context, element, props.fontStatuses, images);
});
return () => { active = false; };
}, [props.templateId, statusKey]);
return <canvas aria-hidden="true" className="editor-source-preview-canvas" height={72} ref={ref} width={160} />;
}
export function DynamicStickerPanel(props: { canAdd: boolean; fontStatuses: Readonly<Record<string, ArchivedFontStatus>>; onAdd: (templateId: DynamicTemplateId) => void }) {
return <section className="editor-provider-panel">
<h2>动态贴纸</h2>
<div className="editor-provider-groups">
{(["location", "time", "identity", "other"] as const).map((category) => <section key={category}>
<h3>{categoryLabels[category]}</h3>
<div className="editor-provider-grid">
{P0A_DYNAMIC_STICKERS.filter((item) => item.category === category).map((item) => <button
aria-label={`添加动态贴纸 ${item.templateId} ${item.displayName}`}
disabled={!props.canAdd}
key={item.templateId}
onClick={() => props.onAdd(item.templateId)}
type="button"
><DynamicPreview fontStatuses={props.fontStatuses} templateId={item.templateId} /><strong>{item.templateId}</strong><span>{item.displayName}</span>{item.templateId === "DYN012" ? <small>FONT081 替代字体</small> : null}</button>)}
</div>
</section>)}
</div>
</section>;
}