feat: complete TASK-WP4-04 color and dynamic stickers
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
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>;
|
||||
}
|
||||
Reference in New Issue
Block a user