From 4a6bfd599ab890db67856ecff1298c4f8a205516 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Mon, 14 Sep 2026 18:54:15 +0700 Subject: [PATCH] feat : dreamgineer ask me try to use this and now im ran out of token sad [no ci] --- .gitignore | 3 + Dockerfile | 3 +- bun.lock | 26 + components/admin/guide-overview-form.tsx | 20 +- components/admin/overview-image-generator.tsx | 88 +-- components/public/artifact-image-export.tsx | 37 +- components/public/guide-image-export.tsx | 12 +- components/public/structured-guide.tsx | 36 +- components/public/takumi-image-export.tsx | 139 ++++ components/public/team-search.tsx | 55 +- components/public/weapon-image-export.tsx | 103 +-- lib/guides/image-export.test.tsx | 11 +- lib/image-export/README.md | 7 + lib/image-export/assets.ts | 33 + lib/image-export/client.test.ts | 73 ++ lib/image-export/client.ts | 52 ++ lib/image-export/export.worker.ts | 71 ++ lib/image-export/fixtures.ts | 247 +++++++ lib/image-export/render.test.tsx | 205 ++++++ lib/image-export/render.ts | 67 ++ lib/image-export/styles.ts | 19 + lib/image-export/templates/artifact.tsx | 668 ++++++++++++++++++ .../image-export/templates/constellation.tsx | 18 +- lib/image-export/templates/index.tsx | 110 +++ lib/image-export/templates/materials.tsx | 307 ++++++++ lib/image-export/templates/overview.tsx | 393 +++++++++++ lib/image-export/templates/primitives.tsx | 161 +++++ lib/image-export/templates/team.tsx | 279 ++++++++ lib/image-export/templates/weapon.tsx | 107 +++ lib/image-export/types.ts | 52 ++ package.json | 12 +- public/Anuphan-LICENSE.txt | 93 +++ public/Anuphan.ttf | Bin 0 -> 231712 bytes scripts/prepare-image-export.mjs | 47 ++ 34 files changed, 3222 insertions(+), 332 deletions(-) create mode 100644 components/public/takumi-image-export.tsx create mode 100644 lib/image-export/README.md create mode 100644 lib/image-export/assets.ts create mode 100644 lib/image-export/client.test.ts create mode 100644 lib/image-export/client.ts create mode 100644 lib/image-export/export.worker.ts create mode 100644 lib/image-export/fixtures.ts create mode 100644 lib/image-export/render.test.tsx create mode 100644 lib/image-export/render.ts create mode 100644 lib/image-export/styles.ts create mode 100644 lib/image-export/templates/artifact.tsx rename components/public/constellation-image.tsx => lib/image-export/templates/constellation.tsx (82%) create mode 100644 lib/image-export/templates/index.tsx create mode 100644 lib/image-export/templates/materials.tsx create mode 100644 lib/image-export/templates/overview.tsx create mode 100644 lib/image-export/templates/primitives.tsx create mode 100644 lib/image-export/templates/team.tsx create mode 100644 lib/image-export/templates/weapon.tsx create mode 100644 lib/image-export/types.ts create mode 100644 public/Anuphan-LICENSE.txt create mode 100644 public/Anuphan.ttf create mode 100644 scripts/prepare-image-export.mjs diff --git a/.gitignore b/.gitignore index 7b8da95..159dd40 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +/public/takumi.wasm +/public/takumi.css diff --git a/Dockerfile b/Dockerfile index 918e1f8..f98d012 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,8 @@ COPY --from=dependencies /app/node_modules ./node_modules COPY . . # Bun 1.3.14 can crash after a successful Next build on arm64. The application # still runs on Bun; Node is used only for the deterministic compiler process. -RUN node node_modules/next/dist/bin/next build +RUN node scripts/prepare-image-export.mjs && \ + node node_modules/next/dist/bin/next build FROM dependencies AS migration WORKDIR /app diff --git a/bun.lock b/bun.lock index 4271f31..a912397 100644 --- a/bun.lock +++ b/bun.lock @@ -42,10 +42,12 @@ "sharp": "^0.35.4", "sonner": "^2.0.8", "tailwind-merge": "^3.6.0", + "takumi-js": "2.13.7", "tw-animate-css": "^1.4.0", "zod": "^4.4.3", }, "devDependencies": { + "@tailwindcss/node": "4.3.3", "@tailwindcss/postcss": "^4", "@types/bun": "^1.4.0", "@types/node": "^20", @@ -500,6 +502,28 @@ "@tailwindcss/typography": ["@tailwindcss/typography@0.5.20", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders" } }, "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw=="], + "@takumi-rs/core": ["@takumi-rs/core@2.13.7", "", { "dependencies": { "@takumi-rs/helpers": "2.13.7" }, "optionalDependencies": { "@takumi-rs/core-darwin-arm64": "2.13.7", "@takumi-rs/core-darwin-x64": "2.13.7", "@takumi-rs/core-linux-arm64-gnu": "2.13.7", "@takumi-rs/core-linux-arm64-musl": "2.13.7", "@takumi-rs/core-linux-x64-gnu": "2.13.7", "@takumi-rs/core-linux-x64-musl": "2.13.7", "@takumi-rs/core-win32-arm64-msvc": "2.13.7", "@takumi-rs/core-win32-x64-msvc": "2.13.7" }, "peerDependencies": { "csstype": "*" }, "optionalPeers": ["csstype"] }, "sha512-FBkglt8kGBZwT2CHr+kppN+FH72Oy8gLIZz1BIbD9vNcocLrvtLsh1Y0nB18mOPmaROKGUVUXLPnIuWo49CRng=="], + + "@takumi-rs/core-darwin-arm64": ["@takumi-rs/core-darwin-arm64@2.13.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Jbe1ik4bcVOOHDpa5RCjm5fe+LfJjjyebvvPBMekhEKXg6hlTbIEFmhr/GCxMOKUfaNsvb4amZ/bVCo0NDSCjA=="], + + "@takumi-rs/core-darwin-x64": ["@takumi-rs/core-darwin-x64@2.13.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-PvmSmhjbSosM3XaHZ9VhFm1x7Xc9oZ5GqXlskiwZblKIi5S4EI43buhUd28xzcBlKkB/vC3c15Cj417XjnUk4g=="], + + "@takumi-rs/core-linux-arm64-gnu": ["@takumi-rs/core-linux-arm64-gnu@2.13.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-TGvDidIF3dtb7TMoJRF+0BET/v6eAuQxV8AXo/57GqO3TM9wjLt3AbkrFkzxSsaIRXlBQxCfb+GsS0YdGnpo4w=="], + + "@takumi-rs/core-linux-arm64-musl": ["@takumi-rs/core-linux-arm64-musl@2.13.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-CL92+JeFbqlPfWI2SSQqLtcjWpxEsPYQ+wNhaqUWLnlTMfZfj+Fl3ERFLbmK+lYnGs7bzpPXLisd3GJQ37rTYw=="], + + "@takumi-rs/core-linux-x64-gnu": ["@takumi-rs/core-linux-x64-gnu@2.13.7", "", { "os": "linux", "cpu": "x64" }, "sha512-ZxoapyUDlInik93WIA9+Sk/ByOb3QGO7GaL9PA7OOdIlK5ovK9v9q88tZb1cvfxtIVzdE9HPBUhb5quNMcuXWg=="], + + "@takumi-rs/core-linux-x64-musl": ["@takumi-rs/core-linux-x64-musl@2.13.7", "", { "os": "linux", "cpu": "x64" }, "sha512-OqoVTd7mrlIwjKYYFizUwz+3zpnVsbrpLFPXFC+BmUxdFYNZwKa0R+Pk56EslyBZS3CuBy+RnBusHCvNC7+09g=="], + + "@takumi-rs/core-win32-arm64-msvc": ["@takumi-rs/core-win32-arm64-msvc@2.13.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-evSyi/y3uV03U+PJfLeG2V3aRsmmWJ6elrFpkLtHebWwi49kXazTL5TYsCkPj5hA4i+rNs6z/t/BygLj1Krp8Q=="], + + "@takumi-rs/core-win32-x64-msvc": ["@takumi-rs/core-win32-x64-msvc@2.13.7", "", { "os": "win32", "cpu": "x64" }, "sha512-GawtljFxuJemQynlF5wryxtSLv18XNuKSjMt52xf66WMaBKt4eN6/b7yIZp9QqKPof8ip8uqFOt7fWVSUN+gHQ=="], + + "@takumi-rs/helpers": ["@takumi-rs/helpers@2.13.7", "", { "peerDependencies": { "preact": "^10.0.0", "react": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["preact", "react"] }, "sha512-6AKk/ixuvoCEZD9/LPAz3kxTcwWHeZh15quhHIHYiVxFRl4x2LyKN8j6eZngiepYYsORXAuJ5YlTGA7uIRfeMQ=="], + + "@takumi-rs/wasm": ["@takumi-rs/wasm@2.13.7", "", { "dependencies": { "@takumi-rs/helpers": "2.13.7" }, "peerDependencies": { "csstype": "*" }, "optionalPeers": ["csstype"] }, "sha512-tc2IFETJ9HtZpS94cL3qSTiecJhhR4RUTXbb3E8cyz6WKdyoZSFkL2r67xnidVtcv22RKTyfVJ2gxV7eWvF+mA=="], + "@tiptap/core": ["@tiptap/core@3.31.3", "", { "peerDependencies": { "@tiptap/pm": "3.31.3" } }, "sha512-Cz50pvciQrxdSxgTkHOVz0uD0Yl/8Xt0QatGD6ILm47jW8EzyHR9RkUGs/D5IqzXKuVPntfw1ttaT926vXfiRg=="], "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.31.3", "", { "peerDependencies": { "@tiptap/core": "3.31.3", "@tiptap/pm": "3.31.3" } }, "sha512-fyY2XMbyDDDfOTQ1Qdrnqa1qwC9DWE4n7AfE0EKQI0G8MfLV8RaDlLDcOZDJ9JbMPY7/Gx7EjyK4NKxSW9n2hQ=="], @@ -1674,6 +1698,8 @@ "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], + "takumi-js": ["takumi-js@2.13.7", "", { "dependencies": { "@takumi-rs/core": "2.13.7", "@takumi-rs/helpers": "2.13.7", "@takumi-rs/wasm": "2.13.7" } }, "sha512-6CuATUv1O6X/aD3T0OQIkv0nyesefJibamskfQxcQqKVl2CCk6kIX9E0e+glMTILTJC2I1IcrcFEPlmDlcexgQ=="], + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], diff --git a/components/admin/guide-overview-form.tsx b/components/admin/guide-overview-form.tsx index ebddd8c..782308d 100644 --- a/components/admin/guide-overview-form.tsx +++ b/components/admin/guide-overview-form.tsx @@ -11,7 +11,7 @@ import { saveGuideOverview } from "@/app/admin/actions"; import { AutosaveStatus } from "@/components/admin/autosave-status"; import { MediaUploader } from "@/components/admin/media-uploader"; import { MaterialPoster, type PosterBlendMode } from "@/components/public/material-poster"; -import { GuideImageExport } from "@/components/public/guide-image-export"; +import { TakumiImageExport } from "@/components/public/takumi-image-export"; import { Button } from "@/components/ui/button"; import { Card, @@ -458,26 +458,16 @@ export function GuideOverviewForm({ ลบภาพตัวละคร ) : null} - - - + preview + template={{ kind: "materials", data: { character: guide.character, characterArtUrl: posterArt.url, artworkOffsetX: posterArtOffset.x, artworkOffsetY: posterArtOffset.y, artworkScale: posterArtScale, topColors: posterTopColors, bottomColors: posterBottomColors, bleeding } }} + />
diff --git a/components/admin/overview-image-generator.tsx b/components/admin/overview-image-generator.tsx index a8dff70..fa40d33 100644 --- a/components/admin/overview-image-generator.tsx +++ b/components/admin/overview-image-generator.tsx @@ -21,7 +21,6 @@ import { import { CSS } from "@dnd-kit/utilities"; import { CropIcon, - DownloadIcon, GripVerticalIcon, ImageIcon, PlusIcon, @@ -41,6 +40,7 @@ import { import { toast } from "sonner"; import { Button } from "@/components/ui/button"; +import { TakumiImageExport } from "@/components/public/takumi-image-export"; import { Card, CardContent, @@ -80,7 +80,6 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { Spinner } from "@/components/ui/spinner"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { elementIconFilter } from "@/lib/catalog/element"; import { rarityGradientClass } from "@/lib/catalog/rarity"; @@ -699,7 +698,6 @@ export function OverviewImageGenerator({ characters: Character[]; elementIconUrls: Record; }) { - const captureRef = useRef(null); const [character, setCharacter] = useState(null); const [artworkUrl, setArtworkUrl] = useState(null); const [artworkError, setArtworkError] = useState(null); @@ -735,7 +733,6 @@ export function OverviewImageGenerator({ coordinateGetter: sortableKeyboardCoordinates, }), ); - const [exporting, setExporting] = useState(false); const [backgroundUrl, setBackgroundUrl] = useState(null); const [fadeColor, setFadeColor] = useState("#0E0200"); const [fadeTint, setFadeTint] = useState(100); @@ -998,49 +995,6 @@ export function OverviewImageGenerator({ } } - async function download() { - const node = captureRef.current; - if ( - !node || - !character || - !artworkUrl || - !normalizeHex(topColorStart) || - !normalizeHex(topColorEnd) || - !normalizeHex(bottomColorStart) || - !normalizeHex(bottomColorEnd) || - !normalizeHex(elementColorStart) || - !normalizeHex(elementColorEnd) || - !normalizeHex(fadeColor) - ) - return; - - setExporting(true); - try { - const { domToBlob, waitUntilLoad } = await import("modern-screenshot"); - await Promise.all([document.fonts.ready, waitUntilLoad(node)]); - const blob = await domToBlob(node, { - backgroundColor: "#000000", - type: "image/png", - width: 1000, - height: 1000, - scale: 3, - }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = overviewFileName(character.key, displayRole); - document.body.append(link); - link.click(); - link.remove(); - window.setTimeout(() => URL.revokeObjectURL(url), 0); - toast.success("ดาวน์โหลดภาพ Overview แล้ว"); - } catch { - toast.error("ส่งออกภาพไม่สำเร็จ กรุณาลองอีกครั้ง"); - } finally { - setExporting(false); - } - } - const ready = Boolean( character && artworkUrl && @@ -1335,18 +1289,12 @@ export function OverviewImageGenerator({ - +
@@ -1469,28 +1417,6 @@ export function OverviewImageGenerator({ - - { diff --git a/components/public/artifact-image-export.tsx b/components/public/artifact-image-export.tsx index fa200b0..fde0475 100644 --- a/components/public/artifact-image-export.tsx +++ b/components/public/artifact-image-export.tsx @@ -1,9 +1,10 @@ "use client"; -import { useState, type ReactNode } from "react"; +import { useState } from "react"; import { DownloadIcon } from "lucide-react"; -import { GuideImageExport } from "@/components/public/guide-image-export"; +import { TakumiImageExport } from "@/components/public/takumi-image-export"; +import type { PublicGuide } from "@/lib/guides/queries"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { @@ -27,13 +28,13 @@ const sectionOptions = [ type ArtifactExportSection = (typeof sectionOptions)[number][0]; export function ArtifactImageExport({ - children, + guide, fileName, hasNote, targetConditionLabels, recommendedConditionLabels, }: { - children: ReactNode; + guide: PublicGuide; fileName: string; hasNote: boolean; targetConditionLabels: readonly string[]; @@ -47,15 +48,6 @@ export function ArtifactImageExport({ const [targetCondition, setTargetCondition] = useState("0"); const showNote = hasNote && includeNote; const entireSection = sections.size === sectionOptions.length; - const hiddenSections = sectionOptions - .filter(([value]) => !sections.has(value)) - .map(([value]) => `[data-artifact-export-section="${value}"]{display:none!important}`) - .join(""); - const targetRule = `[data-target-condition]:not([data-target-condition="${targetCondition}"]){display:none!important}`; - const recommendedRule = `[data-recommended-condition]:not([data-recommended-condition="${recommendedCondition}"]){display:none!important}`; - const noteRule = showNote - ? "" - : '[data-artifact-export-section="note"]{display:none!important}'; const onlySection = sections.size === 1 ? [...sections][0] : null; const exportWidth = onlySection === "best-stats" ? 700 @@ -63,16 +55,6 @@ export function ArtifactImageExport({ ? 800 : 1200; const exportHeight = onlySection === "best-stats" ? 700 : undefined; - const hideEmptyRightColumn = !showNote && !sections.has("target-stats") && !sections.has("talents") - ? ".artifact-export-right{display:none!important}" - : ""; - const layoutRules = onlySection === "tierlist" - ? ".artifact-export-grid{grid-template-columns:1fr!important}.artifact-export-tierlist{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr));gap:1.25rem}" - : onlySection === "best-stats" - ? ".artifact-export-root{height:100%!important}.artifact-export-grid{grid-template-columns:1fr!important}.artifact-export-left{display:flex!important;flex-direction:column!important}.artifact-export-main-stats,.artifact-export-substats{width:480px!important;max-width:100%!important;margin-inline:auto!important}.artifact-export-main-stats{order:1!important}.artifact-export-substats{order:2!important}.artifact-export-right{width:100%!important}.guide-export-watermark{top:auto!important;bottom:16px!important}" - : onlySection === "target-stats" || onlySection === "talents" - ? ".artifact-export-grid{display:block!important}.artifact-export-right{width:560px!important;max-width:100%;margin-left:auto!important;margin-right:auto!important;justify-self:center!important}" - : ""; return ( @@ -170,16 +152,13 @@ export function ArtifactImageExport({
) : null} - - - {children} - + template={{ kind: "artifact", data: { guide, sections: [...sections], recommendedCondition: Number(recommendedCondition), targetCondition: Number(targetCondition), includeNote: showNote } }} + /> ); diff --git a/components/public/guide-image-export.tsx b/components/public/guide-image-export.tsx index 78db04d..67dfe0d 100644 --- a/components/public/guide-image-export.tsx +++ b/components/public/guide-image-export.tsx @@ -62,17 +62,7 @@ export function GuideImageExport({ node.querySelectorAll("[data-export-note]").forEach((note) => { note.style.display = "none"; }); - // A note can be the only remaining content in the artifact right column. - const right = node.querySelector(".artifact-export-right"); - if (right) { - const sections = right.querySelectorAll( - "[data-artifact-export-section]", - ); - const hasVisibleSection = Array.from(sections).some( - (section) => getComputedStyle(section).display !== "none", - ); - if (!hasVisibleSection) right.style.display = "none"; - } + } const { domToBlob, waitUntilLoad } = await import("modern-screenshot"); diff --git a/components/public/structured-guide.tsx b/components/public/structured-guide.tsx index 82ded9f..83e550f 100644 --- a/components/public/structured-guide.tsx +++ b/components/public/structured-guide.tsx @@ -31,7 +31,7 @@ import { WeaponChart } from "@/components/public/weapon-chart"; import { WeaponDisplayMode, WeaponDisplayModeProvider } from "@/components/public/weapon-display-mode"; import { ArtifactImageExport } from "@/components/public/artifact-image-export"; import { GuideExportBranding } from "@/components/public/guide-export-branding"; -import { ConstellationImage } from "@/components/public/constellation-image"; +import { TakumiImageExport } from "@/components/public/takumi-image-export"; import { ConditionSwitcher } from "@/components/public/condition-switcher"; import { MaterialPoster } from "@/components/public/material-poster"; import { RichTextContent } from "@/components/rich-text-content"; @@ -242,25 +242,15 @@ function GuideOverview({ bleeding={guide.posterBleeding} linkMaterials /> - - - + preview + template={{ kind: "materials", data: { character: guide.character, characterArtUrl: guide.posterArtUrl, artworkOffsetX: guide.posterArtOffsetX, artworkOffsetY: guide.posterArtOffsetY, artworkScale: guide.posterArtScale, topColors: guide.posterTopColors, bottomColors: guide.posterBottomColors, bleeding: guide.posterBleeding } }} + /> ) : null} {youtubeVideos.length ? ( @@ -615,13 +605,12 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) { guide={guide} action={ group.name || `เงื่อนไข ${index + 1}`)} targetConditionLabels={targetConditions.map((group, index) => group.name || `เงื่อนไข ${index + 1}`)} - > - - + /> } /> ); @@ -1156,17 +1145,12 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) { {guide.constellationGroups.filter((group) => group.rows.length > 0).map((group, groupIndex, groups) => (
- 1 ? `-${group.name || `Condition-${groupIndex + 1}`}` : ""}.png`} width={1500} height={1500} - captureClassName="p-0" - > - - + template={{ kind: "constellation", data: { guide, group } }} + />
{groups.length > 1 ?

{group.name || `เงื่อนไข ${groupIndex + 1}`}

: null} {group.note ?

{group.note}

: null} diff --git a/components/public/takumi-image-export.tsx b/components/public/takumi-image-export.tsx new file mode 100644 index 0000000..1dd02dc --- /dev/null +++ b/components/public/takumi-image-export.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { DownloadIcon, EyeIcon } from "lucide-react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/spinner"; +import type { ExportRequest, ExportTemplate } from "@/lib/image-export/types"; +import { downloadImage, renderImage } from "@/lib/image-export/client"; + +export function TakumiImageExport({ + template, + fileName, + width = 1200, + height, + format = "png", + disabled = false, + allowWithoutNotes = true, + preview = false, + buttonLabel = "ส่งออกภาพ", +}: { + template: ExportTemplate; + fileName: string; + width?: number; + height?: number; + format?: "png" | "webp"; + disabled?: boolean; + allowWithoutNotes?: boolean; + preview?: boolean; + buttonLabel?: string; +}) { + const [mode, setMode] = useState< + "with-notes" | "without-notes" | "preview" | null + >(null); + const [image, setImage] = useState<{ url: string; key: string } | null>(null); + const mounted = useRef(false); + const previewUrl = useRef(null); + const active = useRef(false); + const request = { ...template, width, height, format } as ExportRequest; + const key = preview ? JSON.stringify(request) : ""; + useEffect(() => { + mounted.current = true; + return () => { + mounted.current = false; + if (previewUrl.current) URL.revokeObjectURL(previewUrl.current); + }; + }, []); + async function generate(nextMode: NonNullable) { + if (active.current) return; + active.current = true; + setMode(nextMode); + try { + const blob = await renderImage({ + ...request, + withoutNotes: nextMode === "without-notes", + }); + if (!mounted.current) return; + if (nextMode === "preview") { + if (previewUrl.current) URL.revokeObjectURL(previewUrl.current); + previewUrl.current = URL.createObjectURL(blob); + setImage({ url: previewUrl.current, key }); + } else { + const name = + nextMode === "without-notes" + ? /\.[^.]+$/u.test(fileName) + ? fileName.replace(/(\.[^.]+)$/u, "-without-notes$1") + : `${fileName}-without-notes` + : fileName; + downloadImage(blob, name); + } + } catch { + if (mounted.current) toast.error("ส่งออกภาพไม่สำเร็จ กรุณาลองอีกครั้ง"); + } finally { + active.current = false; + if (mounted.current) setMode(null); + } + } + return ( +
+
+ {(["with-notes", "without-notes"] as const) + .filter((value) => allowWithoutNotes || value === "with-notes") + .map((value) => ( + + ))} + {preview ? ( + + ) : null} +
+ {image ? ( +
+ {image.key !== key ? ( +

+ ตัวอย่างยังไม่รวมการเปลี่ยนแปลงล่าสุด กดดูตัวอย่างภาพอีกครั้ง +

+ ) : null} + {/* Encoded local preview must bypass Next.js image optimization. */} + {/* eslint-disable-next-line @next/next/no-img-element */} + ตัวอย่างภาพส่งออก +
+ ) : null} +
+ ); +} diff --git a/components/public/team-search.tsx b/components/public/team-search.tsx index 49bc660..f32b7f9 100644 --- a/components/public/team-search.tsx +++ b/components/public/team-search.tsx @@ -6,7 +6,7 @@ import Link from "next/link"; import { ArrowUpRightIcon, SearchIcon, XIcon } from "lucide-react"; import { useMemo, useState, type ReactNode } from "react"; -import { GuideImageExport } from "@/components/public/guide-image-export"; +import { TakumiImageExport } from "@/components/public/takumi-image-export"; import { ScrollableNav } from "@/components/public/scrollable-nav"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -161,55 +161,6 @@ export function TeamArtifactIcon({ ); } -function TeamExportImage({ team }: { team: Team }) { - const total = team.members.reduce((sum, member) => sum + Number(member.damage), 0); - const seconds = Number(team.rotationSeconds); - return ( -
-
-

{team.title}

-

{`Rotation duration: ${seconds.toLocaleString("en-US", { maximumFractionDigits: 2 })} seconds`}

-
-
- {team.members.map((member) => { - const damage = Number(member.damage); - const percent = total > 0 ? damage / total * 100 : 0; - return ( -
-
- -
-
- {member.weapon ?
: null} - -
-
-
- - {member.character.name} - C{member.constellation}R{member.refinement} - - {damage.toLocaleString("en-US")} -
-
{percent.toFixed(1)}%
-
-
- ); - })} -
-

{team.combo}

-
DPS: {seconds > 0 ? (total / seconds).toLocaleString("en-US", { maximumFractionDigits: 0 }) : "0"}
-
By YT Genshin Isn't Mobile Game
-
- ); -} - function TeamCard({ team, action, @@ -463,9 +414,7 @@ export function TeamSearch({ groups }: { groups: PublicGuide["teamGroups"] }) { key={team.id} team={team} action={ - - - + } /> ))} diff --git a/components/public/weapon-image-export.tsx b/components/public/weapon-image-export.tsx index 4af5fb4..d730986 100644 --- a/components/public/weapon-image-export.tsx +++ b/components/public/weapon-image-export.tsx @@ -1,10 +1,10 @@ "use client"; -import Image from "next/image"; + import { DownloadIcon } from "lucide-react"; -import { GuideExportBranding } from "@/components/public/guide-export-branding"; -import { GuideImageExport } from "@/components/public/guide-image-export"; + +import { TakumiImageExport } from "@/components/public/takumi-image-export"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -17,90 +17,6 @@ import { import type { PublicGuide } from "@/lib/guides/queries"; type WeaponGroup = PublicGuide["weaponGroups"][number]; -type DamageMetric = "overallPercent" | "personalPercent"; - -function formattedPercent(value: string | null): string { - if (value === null) return "-%"; - const numeric = Number(value); - if (numeric === 0) return "-%"; - return `${numeric.toLocaleString("en-US", { maximumFractionDigits: 2 })}%`; -} - -function WeaponComparisonImage({ - guide, - group, - metric, - conditionLabel, -}: { - guide: PublicGuide; - group: WeaponGroup; - metric: DamageMetric; - conditionLabel: string; -}) { - const values = group.rows.map((row) => Number(row[metric]) || 0); - const maximum = Math.max(100, ...values); - - return ( -
- -
-

- {`${guide.character.name}’s Weapon Comparison`} -

-

- {metric === "overallPercent" ? "Overall Damage" : "Personal Damage"} -

-

{conditionLabel}

-
- -
- {group.rows.map((row) => { - const numeric = Number(row[metric]) || 0; - const width = row[metric] === null - ? 28 - : numeric <= 100 - ? Math.max(28, Math.max(0, numeric) * 0.5) - : Math.min(90, 50 + ((numeric - 100) / Math.max(1, maximum - 100)) * 40); - return ( -
-
- -
-
-
- - {row.weapon.name} R{row.refinement} - -
-
- {formattedPercent(row[metric])} - {row.note ? {row.note} : null} -
-
-
- ); - })} -
- - {group.note ? ( -

- {group.note} -

- ) : null} -
- ); -} - export function WeaponImageExport({ guide, group, @@ -134,20 +50,13 @@ export function WeaponImageExport({
{metricOptions.map(([metric, label]) => ( - - - + template={{ kind: "weapon", data: { guide, group, metric, conditionLabel } }} + /> ))}
diff --git a/lib/guides/image-export.test.tsx b/lib/guides/image-export.test.tsx index 1ec2e17..41d42f3 100644 --- a/lib/guides/image-export.test.tsx +++ b/lib/guides/image-export.test.tsx @@ -2,28 +2,29 @@ import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; -import { GuideImageExport } from "@/components/public/guide-image-export"; +import { TakumiImageExport } from "@/components/public/takumi-image-export"; +import { exportFixtures } from "@/lib/image-export/fixtures"; import { MaterialPoster } from "@/components/public/material-poster"; -import { ConstellationImage } from "@/components/public/constellation-image"; +import { ConstellationImage } from "@/lib/image-export/templates/constellation"; import type { PublicGuide } from "@/lib/guides/queries"; describe("image export controls", () => { it("always offers both export modes even without notes", () => { - const html = renderToStaticMarkup(No notes); + const html = renderToStaticMarkup(); expect(html).toContain("ส่งออกภาพ"); expect(html).toContain("ส่งออกภาพโดยไม่รวมโน้ต"); expect(html.match(/ { - const html = renderToStaticMarkup(Content); + const html = renderToStaticMarkup(); expect(html).toContain("ส่งออกภาพ"); expect(html).not.toContain("ส่งออกภาพโดยไม่รวมโน้ต"); expect(html.match(/ { - const html = renderToStaticMarkup(No sections); + const html = renderToStaticMarkup(); expect(html.match(/\sdisabled=""/gu)).toHaveLength(2); }); diff --git a/lib/image-export/README.md b/lib/image-export/README.md new file mode 100644 index 0000000..028356b --- /dev/null +++ b/lib/image-export/README.md @@ -0,0 +1,7 @@ +Custom image exports render from serializable data in a lazily created Web Worker using Takumi WASM. Templates use export primitives and shared calculation helpers; they do not mount webpage components. `GuideImageExport` remains the screenshot path for extra webpage sections. + +`prepare-image-export.mjs` copies the pinned WASM binary and compiles export CSS using the application's design tokens. Dev, build, and test scripts run it automatically; Docker runs it with Node before Next builds. The generated `public/takumi.wasm` and `public/takumi.css` are ignored by Git. Restart dev after changing export utility classes or theme tokens to regenerate the CSS. + +Load fonts from local assets. Original artwork and mask URLs are fetched in the worker, including blob URLs for unsaved uploads. Remote assets need CORS permission. Fetch failures reject the export; they do not fall back to a screenshot. + +Keep intrinsically sized artifact columns in Flexbox: nested auto-height grids can measure as zero in the current WASM renderer. Regression tests render actual WASM output, check sizes and constellation bar pixels, and cover artifact selections, notes, WebP, failed assets, and worker retries. diff --git a/lib/image-export/assets.ts b/lib/image-export/assets.ts new file mode 100644 index 0000000..6e1ee54 --- /dev/null +++ b/lib/image-export/assets.ts @@ -0,0 +1,33 @@ +import type { Node } from "takumi-js"; + +export function imageSources(node: Node): string[] { + const sources = new Set(); + function visit(current: Node) { + if (current.type === "image" && typeof current.src === "string") + sources.add(current.src); + for (const value of Object.values(current.style ?? {})) { + if (typeof value !== "string") continue; + for (const match of value.matchAll(/url\(\s*["']?([^"')]+)["']?\s*\)/g)) + sources.add(match[1]); + } + if (current.type === "container") current.children?.forEach(visit); + } + visit(node); + return [...sources]; +} +export async function loadImageSources(node: Node, origin: string) { + return Promise.all( + imageSources(node).map(async (src) => { + const response = await fetch(new URL(src, origin), { + signal: AbortSignal.timeout(15000), + }); + if (!response.ok) + throw new Error(`Image load failed: ${response.status}`); + return { + src, + data: new Uint8Array(await response.arrayBuffer()), + cache: src.startsWith("blob:") ? ("none" as const) : ("auto" as const), + }; + }), + ); +} diff --git a/lib/image-export/client.test.ts b/lib/image-export/client.test.ts new file mode 100644 index 0000000..d35ba58 --- /dev/null +++ b/lib/image-export/client.test.ts @@ -0,0 +1,73 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { exportFixtures } from "./fixtures"; +import type { WorkerRequest, WorkerResult } from "./types"; + +class TestWorker { + static instances: TestWorker[] = []; + requests: WorkerRequest[] = []; + onmessage?: (event: { data: WorkerResult }) => void; + onerror?: () => void; + terminate = vi.fn(); + constructor() { + TestWorker.instances.push(this); + } + postMessage(request: WorkerRequest) { + this.requests.push(request); + } +} +beforeEach(() => { + vi.resetModules(); + TestWorker.instances = []; + vi.stubGlobal("Worker", TestWorker); + vi.stubGlobal("window", { location: { origin: "http://export.test" } }); +}); +afterEach(() => vi.unstubAllGlobals()); + +describe("export worker client", () => { + it("reuses one lazy worker and matches replies to concurrent requests", async () => { + const { renderImage } = await import("./client"); + expect(TestWorker.instances).toHaveLength(0); + const first = renderImage(exportFixtures[0]); + const second = renderImage({ ...exportFixtures[0], format: "webp" }); + expect(TestWorker.instances).toHaveLength(1); + const worker = TestWorker.instances[0]; + const [a, b] = worker.requests; + worker.onmessage!({ + data: { id: b.id, bytes: new Uint8Array([2]).buffer }, + }); + worker.onmessage!({ + data: { id: a.id, bytes: new Uint8Array([1]).buffer }, + }); + expect((await first).type).toBe("image/png"); + expect((await second).type).toBe("image/webp"); + }); + it("rejects a failed job and permits retry", async () => { + const { renderImage } = await import("./client"); + const failed = renderImage(exportFixtures[0]); + const worker = TestWorker.instances[0]; + const rejection = expect(failed).rejects.toThrow("Missing artwork"); + worker.onmessage!({ + data: { id: worker.requests[0].id, error: "Missing artwork" }, + }); + await rejection; + const retried = renderImage(exportFixtures[0]); + worker.onmessage!({ + data: { id: worker.requests[1].id, bytes: new ArrayBuffer(1) }, + }); + expect(await retried).toBeInstanceOf(Blob); + }); + it("clears pending jobs and recreates a crashed worker", async () => { + const { renderImage } = await import("./client"); + const failed = renderImage(exportFixtures[0]); + const rejection = expect(failed).rejects.toThrow("Image renderer failed"); + TestWorker.instances[0].onerror!(); + await rejection; + expect(TestWorker.instances[0].terminate).toHaveBeenCalledOnce(); + const retried = renderImage(exportFixtures[0]); + const worker = TestWorker.instances[1]; + worker.onmessage!({ + data: { id: worker.requests[0].id, bytes: new ArrayBuffer(1) }, + }); + expect(await retried).toBeInstanceOf(Blob); + }); +}); diff --git a/lib/image-export/client.ts b/lib/image-export/client.ts new file mode 100644 index 0000000..48b8a9c --- /dev/null +++ b/lib/image-export/client.ts @@ -0,0 +1,52 @@ +import type { ExportRequest, WorkerResult } from "./types"; +let worker: Worker | undefined; +let nextId = 0; +const pending = new Map< + number, + { + resolve: (blob: Blob) => void; + reject: (error: Error) => void; + format: string; + } +>(); +export function renderImage(request: ExportRequest): Promise { + if (!worker) { + worker = new Worker(new URL("./export.worker.ts", import.meta.url), { + type: "module", + }); + worker.onmessage = ({ data }: MessageEvent) => { + const job = pending.get(data.id); + if (!job) return; + pending.delete(data.id); + if (data.error !== undefined) job.reject(new Error(data.error)); + else job.resolve(new Blob([data.bytes], { type: `image/${job.format}` })); + }; + worker.onerror = () => { + worker?.terminate(); + worker = undefined; + for (const job of pending.values()) + job.reject(new Error("Image renderer failed")); + pending.clear(); + }; + } + return new Promise((resolve, reject) => { + const id = ++nextId; + pending.set(id, { resolve, reject, format: request.format }); + try { + worker!.postMessage({ id, request, origin: window.location.origin }); + } catch (error) { + pending.delete(id); + reject(error); + } + }); +} +export function downloadImage(blob: Blob, fileName: string) { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.download = fileName.replace(/[\\/:*?"<>|]/gu, "-"); + link.href = url; + document.body.append(link); + link.click(); + link.remove(); + window.setTimeout(() => URL.revokeObjectURL(url), 1000); +} diff --git a/lib/image-export/export.worker.ts b/lib/image-export/export.worker.ts new file mode 100644 index 0000000..2190405 --- /dev/null +++ b/lib/image-export/export.worker.ts @@ -0,0 +1,71 @@ +import { init, Renderer } from "takumi-js/wasm/no-init"; +import type { WorkerRequest, WorkerResult } from "./types"; +import { renderExport } from "./render"; + +let rendererPromise: Promise | undefined; +async function getRenderer(origin: string) { + rendererPromise ??= (async () => { + const response = await fetch(new URL("/takumi.wasm", origin), { + signal: AbortSignal.timeout(15000), + }); + if (!response.ok) throw new Error("Unable to load image renderer"); + await init({ module_or_path: await response.arrayBuffer() }); + const renderer = new Renderer(); + try { + const fonts = await Promise.all( + [ + { name: "Anuphan", path: "/Anuphan.ttf" }, + { name: "Genshin Overview", path: "/zh-cn.ttf" }, + { + name: "Kanit Overview", + path: "/Kanit-SemiBold.ttf", + weight: 600, + style: "normal" as const, + }, + { + name: "Kanit Poster", + path: "/Kanit-ExtraBoldItalic.ttf", + weight: 800, + style: "italic" as const, + }, + ].map(async ({ path, ...descriptor }) => { + const font = await fetch(new URL(path, origin), { + signal: AbortSignal.timeout(15000), + }); + if (!font.ok) throw new Error("Unable to load export fonts"); + return { + ...descriptor, + data: new Uint8Array(await font.arrayBuffer()), + }; + }), + ); + for (const font of fonts) await renderer.registerFont(font); + return renderer; + } catch (error) { + renderer.free(); + throw error; + } + })().catch((error) => { + rendererPromise = undefined; + throw error; + }); + return rendererPromise; +} +let queue = Promise.resolve(); +self.onmessage = ({ data }: MessageEvent) => { + queue = queue.then(async () => { + try { + const renderer = await getRenderer(data.origin); + const image = await renderExport(renderer, data.request, data.origin); + const bytes = image.buffer as ArrayBuffer; + self.postMessage({ id: data.id, bytes } satisfies WorkerResult, { + transfer: [bytes], + }); + } catch (error) { + self.postMessage({ + id: data.id, + error: error instanceof Error ? error.message : "Image export failed", + } satisfies WorkerResult); + } + }); +}; diff --git a/lib/image-export/fixtures.ts b/lib/image-export/fixtures.ts new file mode 100644 index 0000000..7edcdce --- /dev/null +++ b/lib/image-export/fixtures.ts @@ -0,0 +1,247 @@ +// Representative data for export renderer regression tests. +import type { PublicGuide } from "@/lib/guides/queries"; +import type { ExportRequest } from "./types"; +const character = { + key: "raiden", + name: "Raiden Shogun", + element: "Electro", + rarity: 5, + weaponType: "Polearm", + imageUrl: "/test-art.png", + splashImageUrl: "/test-art.png", + nameCardImageUrl: null, + baseStats: [ + { key: "hp", value: 12907 }, + { key: "atk", value: 337 }, + ], + ascension: [ + { + items: [ + { + key: "material", + name: "Ascension Material", + amount: 168, + imageUrl: "/test-art.png", + }, + ], + }, + ], + leveling: [], + talents: [ + { type: "E", name: "Skill", iconUrl: null }, + { type: "Q", name: "Burst", iconUrl: null }, + ], +}; +export const exportGuide = { + name: "Raiden", + character, + coverUrl: "/test-art.png", + elementIconUrls: {}, + artifactGroups: [ + { + id: "a", + name: "Late Game", + rows: [ + { + id: "r", + first: { + key: "emblem", + name: "Emblem", + imageUrl: "/test-art.png", + rarity: 5, + }, + second: null, + percent: "100", + note: "อันดับภาษาไทย", + }, + ], + }, + ], + artifactProfile: { + note: "โน้ตโปรไฟล์", + recommendedConditions: [ + { + name: "First", + note: "โน้ตออฟ", + sands: ["ATK%"], + goblet: ["Electro DMG%"], + circlet: ["CRIT RATE"], + substats: ["CRIT RATE", "ER"], + }, + { + name: "Second", + note: null, + sands: ["ER"], + goblet: ["ATK%"], + circlet: ["CRIT DAMAGE"], + substats: ["ER"], + }, + ], + targetConditions: [ + { + name: "First", + note: "เป้าหมาย", + rows: [{ stat: "ER", value: "250%" }], + }, + { name: "Second", note: null, rows: [{ stat: "ATK", value: "2000" }] }, + ], + talents: [ + { type: "Q", level: 9, relationToNext: "gt" }, + { type: "E", level: 9, relationToNext: "gt" }, + ], + }, +} as unknown as PublicGuide; +export const exportFixtures: ExportRequest[] = [ + { + kind: "constellation", + width: 1500, + height: 1500, + format: "png", + data: { + guide: exportGuide, + group: { + id: "c", + name: "Team", + note: "โน้ตภาษาไทย", + rows: [ + { level: 1, cumulativePercent: "115.5", isGreat: true }, + { level: 6, cumulativePercent: "200", isGreat: false }, + ], + } as unknown as PublicGuide["constellationGroups"][number], + }, + }, + { + kind: "weapon", + width: 1200, + format: "png", + data: { + guide: exportGuide, + metric: "overallPercent", + conditionLabel: "Condition 1", + group: { + id: "w", + name: "Weapons", + note: "โน้ตอาวุธ", + rows: [ + { + id: "1", + weapon: { name: "Engulfing Lightning", imageUrl: "/test-art.png" }, + refinement: 1, + overallPercent: "115", + personalPercent: "120", + note: "คำอธิบายภาษาไทย", + }, + { + id: "2", + weapon: { name: "The Catch", imageUrl: "/test-art.png" }, + refinement: 5, + overallPercent: null, + personalPercent: "100", + note: null, + }, + ], + } as unknown as PublicGuide["weaponGroups"][number], + }, + }, + { + kind: "team", + width: 1080, + format: "png", + data: { + team: { + title: "Raiden National", + rotationSeconds: "20", + combo: "E → Q\nตัวอย่างคอมโบ", + members: [1, 2, 3, 4].map((position) => ({ + position, + character, + weapon: + position === 1 + ? { + name: "Engulfing Lightning", + imageUrl: "/test-art.png", + rarity: 5, + } + : null, + artifact: + position === 1 + ? { + key: "emblem", + name: "Emblem", + imageUrl: "/test-art.png", + rarity: 5, + } + : null, + secondArtifact: + position === 1 + ? { + key: "noblesse", + name: "Noblesse", + imageUrl: "/test-art.png", + rarity: 5, + } + : null, + constellation: 0, + refinement: 1, + damage: "100000", + })), + } as unknown as PublicGuide["teams"][number], + }, + }, + { + kind: "materials", + width: 1600, + height: 2000, + format: "png", + data: { + character: exportGuide.character, + characterArtUrl: "/test-art.png", + artworkOffsetX: 10, + artworkOffsetY: -5, + artworkScale: 120, + topColors: ["#ffbb00", "#112233"], + bottomColors: ["#112233", "#ffbb00"], + bleeding: [{ id: "1", imageUrl: null, amount: 20, blendMode: "screen" }], + }, + }, + { + kind: "overview", + width: 3000, + height: 3000, + format: "png", + data: { + character: character as unknown as PublicGuide["character"], + artworkUrl: "/test-art.png", + role: "Main DPS", + topColors: ["#ffbb00", "#112233"], + bottomColors: ["#112233", "#ffbb00"], + elementColors: ["#aa00ff", "#ff00aa"], + elementGradientAngle: 90, + blendLayers: [{ id: 1, amount: 20, mode: "screen" }], + nameCardCrop: null, + nameCardSize: null, + elementIconUrls: { Electro: "/test-art.png" }, + composition: { + backgroundUrl: null, + fadeColor: "#0e0200", + fadeTint: 20, + auraUpOffset: 0, + characterOffset: { x: 5, y: 0 }, + fadeOffset: { x: 0, y: 0 }, + }, + disclaimerFontSize: null, + }, + }, + { + kind: "artifact", + width: 1200, + format: "png", + data: { + guide: exportGuide, + sections: ["tierlist", "best-stats", "target-stats", "talents"], + recommendedCondition: 0, + targetCondition: 0, + includeNote: true, + }, + }, +]; diff --git a/lib/image-export/render.test.tsx b/lib/image-export/render.test.tsx new file mode 100644 index 0000000..2b87783 --- /dev/null +++ b/lib/image-export/render.test.tsx @@ -0,0 +1,205 @@ +import { readFile } from "node:fs/promises"; +import { renderToStaticMarkup } from "react-dom/server"; +import sharp from "sharp"; +import { beforeAll, afterAll, describe, expect, it, vi } from "vitest"; +import { init, Renderer } from "takumi-js/wasm/no-init"; +import { exportFixtures } from "./fixtures"; +import { renderExport } from "./render"; +import { exportTemplate } from "./templates"; +import { imageSources, loadImageSources } from "./assets"; + +let renderer: Renderer; +beforeAll(async () => { + await init({ module_or_path: await readFile("public/takumi.wasm") }); + renderer = new Renderer(); + for (const [name, path] of [ + ["Anuphan", "Anuphan.ttf"], + ["Genshin Overview", "zh-cn.ttf"], + ["Kanit Overview", "Kanit-SemiBold.ttf"], + ["Kanit Poster", "Kanit-ExtraBoldItalic.ttf"], + ]) { + await renderer.registerFont({ + name, + data: await readFile(`public/${path}`), + }); + } +}); +afterAll(() => renderer.free()); + +describe("browser WASM export templates", () => { + it.each(exportFixtures.map((request) => [request.kind, request] as const))( + "renders %s with intended dimensions and visible content", + async (_, request) => { + const art = await sharp({ + create: { width: 500, height: 700, channels: 4, background: "#786a9c" }, + }) + .png() + .toBuffer(); + const fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(async (input) => { + const path = new URL(String(input)).pathname; + const bytes = + path === "/test-art.png" ? art : await readFile(`public${path}`); + return new Response(bytes); + }); + try { + const bytes = await renderExport( + renderer, + request, + "http://export.test", + ); + const metadata = await sharp(bytes).metadata(); + expect(metadata.width).toBe(request.width); + if (request.height) expect(metadata.height).toBe(request.height); + else expect(metadata.height).toBeGreaterThan(200); + expect(bytes.length).toBeGreaterThan(10000); + const { stdev } = (await sharp(bytes).stats()).channels[0]; + expect(stdev).toBeGreaterThan(5); + if (request.kind === "constellation") { + const topBar = await sharp(bytes) + .extract({ left: 200, top: 250, width: 1, height: 1 }) + .removeAlpha() + .raw() + .toBuffer(); + expect([...topBar]).toEqual([255, 255, 255]); + const highlightedBar = await sharp(bytes) + .extract({ left: 200, top: 750, width: 1, height: 1 }) + .removeAlpha() + .raw() + .toBuffer(); + expect(highlightedBar[0]).toBeGreaterThan(highlightedBar[1]); + expect(highlightedBar[1]).toBeGreaterThan(highlightedBar[2]); + } + } finally { + fetchMock.mockRestore(); + } + }, + 30000, + ); + it.each(["tierlist", "best-stats", "target-stats", "talents"] as const)( + "renders artifact %s selection", + async (section) => { + const fixture = exportFixtures.find( + (request) => request.kind === "artifact", + )!; + if (fixture.kind !== "artifact") + throw new Error("Missing artifact fixture"); + const art = await sharp({ + create: { width: 50, height: 50, channels: 4, background: "#786a9c" }, + }) + .png() + .toBuffer(); + const fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(async (input) => { + const path = new URL(String(input)).pathname; + return new Response( + path === "/test-art.png" ? art : await readFile(`public${path}`), + ); + }); + try { + const width = + section === "best-stats" ? 700 : section === "tierlist" ? 1200 : 800; + const height = section === "best-stats" ? 700 : undefined; + const bytes = await renderExport( + renderer, + { + ...fixture, + width, + height, + withoutNotes: true, + data: { ...fixture.data, sections: [section], includeNote: false }, + }, + "http://export.test", + ); + const metadata = await sharp(bytes).metadata(); + expect(metadata.width).toBe(width); + if (height) expect(metadata.height).toBe(height); + expect(metadata.height).toBeGreaterThan(150); + } finally { + fetchMock.mockRestore(); + } + }, + 30000, + ); + it("encodes a WebP export", async () => { + const fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(async (input) => { + const path = new URL(String(input)).pathname; + return new Response( + await readFile( + path === "/test-art.png" + ? "public/icon/Watermark.png" + : `public${path}`, + ), + ); + }); + try { + const bytes = await renderExport( + renderer, + { ...exportFixtures[0], format: "webp", withoutNotes: true }, + "http://export.test", + ); + expect((await sharp(bytes).metadata()).format).toBe("webp"); + } finally { + fetchMock.mockRestore(); + } + }, 30000); + it("omits Thai notes before layout", () => { + const request = exportFixtures[0]; + expect(renderToStaticMarkup(exportTemplate(request))).toContain( + "โน้ตภาษาไทย", + ); + expect( + renderToStaticMarkup(exportTemplate({ ...request, withoutNotes: true })), + ).not.toContain("โน้ตภาษาไทย"); + }); + it("filters artifact sections and conditions before layout", () => { + const request = exportFixtures.find((item) => item.kind === "artifact")!; + if (request.kind !== "artifact") + throw new Error("Missing artifact fixture"); + const html = renderToStaticMarkup( + exportTemplate({ + ...request, + data: { + ...request.data, + sections: ["best-stats"], + recommendedCondition: 1, + includeNote: false, + }, + }), + ); + expect(html).toContain("Second"); + expect(html).not.toContain("First"); + expect(html).not.toContain("Late Game"); + expect(html).not.toContain("Talent Priority"); + expect(html).not.toContain("artifact-export-right"); + expect(html).not.toContain("โน้ตโปรไฟล์"); + }); + it("collects mask sources and deduplicates artwork", () => { + expect( + imageSources({ + type: "container", + style: { maskImage: "url('/mask.png')" }, + children: [{ type: "image", src: "/mask.png" }], + }), + ).toEqual(["/mask.png"]); + }); + it("rejects missing artwork instead of producing an incomplete image", async () => { + const fetchMock = vi + .spyOn(globalThis, "fetch") + .mockResolvedValue(new Response(null, { status: 404 })); + try { + await expect( + loadImageSources( + { type: "image", src: "/missing.png" }, + "http://export.test", + ), + ).rejects.toThrow("Image load failed"); + } finally { + fetchMock.mockRestore(); + } + }); +}); diff --git a/lib/image-export/render.ts b/lib/image-export/render.ts new file mode 100644 index 0000000..43202cb --- /dev/null +++ b/lib/image-export/render.ts @@ -0,0 +1,67 @@ +import { fromJsx } from "takumi-js/helpers/jsx"; +import type { Renderer } from "takumi-js/wasm/no-init"; +import type { ExportRequest } from "./types"; +import { exportTemplate } from "./templates"; +import { exportStyles } from "./styles"; +import { loadImageSources } from "./assets"; + +const stylesheets = new Map>(); +function stylesheet(origin: string) { + let css = stylesheets.get(origin); + if (!css) { + css = fetch(new URL("/takumi.css", origin), { + signal: AbortSignal.timeout(15000), + }) + .then((response) => { + if (!response.ok) throw new Error("Unable to load export styles"); + return response.text(); + }) + .catch((error) => { + stylesheets.delete(origin); + throw error; + }); + stylesheets.set(origin, css); + } + return css; +} +export async function renderExport( + renderer: Renderer, + request: ExportRequest, + origin: string, +) { + const { node, css } = await fromJsx(exportTemplate(request), { + defaultStyles: { + div: { display: "block" }, + section: { display: "block" }, + header: { display: "block" }, + h1: { display: "block" }, + h2: { display: "block" }, + p: { display: "block" }, + img: { display: "block" }, + }, + }); + const images = await loadImageSources(node, origin); + const options = { + width: request.width, + height: request.height, + format: request.format, + fontFamilies: [ + "Anuphan", + "Kanit Overview", + "Kanit Poster", + "Genshin Overview", + ], + css: [ + await stylesheet(origin), + ...css, + ".export-root{font-family:Anuphan;font-size:16px;line-height:1.5;background:var(--background);color:var(--foreground)}", + exportStyles(request), + ], + images, + }; + const measured = await renderer.measure(node, options); + return renderer.render(node, { + ...options, + height: request.height ?? Math.ceil(measured.height), + }); +} diff --git a/lib/image-export/styles.ts b/lib/image-export/styles.ts new file mode 100644 index 0000000..4b6bcb3 --- /dev/null +++ b/lib/image-export/styles.ts @@ -0,0 +1,19 @@ +import type { ExportRequest } from "./types"; + +export function exportStyles(request: ExportRequest): string { + if (request.kind !== "artifact") return ""; + const only = + request.data.sections.length === 1 ? request.data.sections[0] : null; + const rules = + only === "tierlist" + ? ".artifact-export-grid{display:block}.artifact-export-tierlist{display:flex;flex-direction:row;flex-wrap:wrap;gap:20px}.artifact-export-tierlist>div{width:calc(50% - 10px)}" + : only === "best-stats" + ? ".artifact-export-root{height:100%}.artifact-export-grid{display:block}.artifact-export-main-stats,.artifact-export-substats{width:480px;max-width:100%;margin-inline:auto}.guide-export-watermark{top:auto;bottom:16px}" + : only === "target-stats" || only === "talents" + ? ".artifact-export-grid{display:block}.artifact-export-right{width:560px;max-width:100%;margin-inline:auto}" + : ""; + return ( + ".artifact-export-grid{display:flex;align-items:flex-start}.artifact-export-left{flex:1.2;min-width:0}.artifact-export-right{flex:1;min-width:0}.artifact-export-main-stats>div:last-child{display:flex}.artifact-export-main-stats>div:last-child>div{width:33.333333%;flex-shrink:0}" + + rules + ); +} diff --git a/lib/image-export/templates/artifact.tsx b/lib/image-export/templates/artifact.tsx new file mode 100644 index 0000000..f3ddafc --- /dev/null +++ b/lib/image-export/templates/artifact.tsx @@ -0,0 +1,668 @@ +import type { ReactNode } from "react"; +import { + Image, + GuideExportBranding, + Card, + CardHeader, + CardTitle, + CardContent, + Badge, + Separator, + ConditionSwitcher, +} from "./primitives"; +import { TeamArtifactIcon } from "./team"; +import type { PublicGuide } from "@/lib/guides/queries"; +import { cn } from "@/lib/utils"; +import { artifactTierCount } from "@/lib/guides/artifacts"; +import { elementIconFilter } from "@/lib/catalog/element"; +import { + normalizeTargetStat, + recommendedStatElement, + recommendedStatIconName, + statPriorityColorClass, + targetStatIconName, +} from "@/lib/guides/stats"; +import { + normalizeTalentPriorities, + talentRelationSymbol, +} from "@/lib/guides/talents"; +function metric(value: string | null, locale?: string) { + return value === null + ? null + : `${Number(value).toLocaleString(locale, { maximumFractionDigits: 2 })}%`; +} +export function ArtifactSectionContent({ + guide, + action, + exporting = true, +}: { + guide: PublicGuide; + action?: ReactNode; + exporting?: boolean; +}) { + const profile = guide.artifactProfile; + const recommendedConditions = profile?.recommendedConditions ?? []; + const recommendedCards = recommendedConditions.map( + (condition, conditionIndex) => ( +
+ {exporting && recommendedConditions.length > 1 ? ( +

+ {condition.name || `Condition ${conditionIndex + 1}`} +

+ ) : null} + {condition.note ? ( +

+ {condition.note} +

+ ) : null} + {profile ? ( + + + + {exporting ? "Best Main Stats" : "ออฟหลักที่ดีที่สุด"} + + + + + + + + + ) : null} + {condition.substats.length ? ( + + + + {exporting ? "Best Substats" : "ออฟรองที่ดีที่สุด"} + +
+ {condition.substats.map((stat, index) => ( + + ))} +
+
+
+ ) : null} +
+ ), + ); + + return ( +
+ {exporting ? ( + + ) : null} +
+
+

+ {exporting + ? "Recommended Artifacts & Stats" + : "อาร์ติแฟกต์และสแตทที่แนะนำ"} +

+
+ +

+ {exporting ? "Artifacts & Stats" : "อาร์ติแฟกต์และค่าสถานะ"} +

+
+ {action} +
+
+
+ {" "} +
+ {exporting ? ( + recommendedCards + ) : ( + condition.name || `เงื่อนไข ${index + 1}`, + )} + > + {recommendedCards} + + )} +
+ {guide.artifactGroups.map((group) => ( + + + + {group.name === "Late Game" + ? exporting + ? "Late Game" + : "เลทเกม" + : group.name === "Early Game" + ? exporting + ? "Early Game" + : "ต้นเกม" + : group.name} + + + + {group.rows.map((row, index) => ( +
+ +
+

+ {artifactTierCount(row.second?.name ?? null)}{" "} + {row.first.name} + {row.second ? ` + 2 ${row.second.name}` : ""} +

+ {row.note ? ( +

+ {row.note} +

+ ) : null} +
+

+ {Number(row.percent) === 0 + ? "-%" + : metric( + row.percent, + exporting ? "en-US" : undefined, + )} +

+
+ ))} +
+
+ ))} +
+
+ {profile ? ( +
+ + + + {profile.targetConditions.some((group) => group.rows.length) ? ( + exporting ? ( + profile.targetConditions + .filter((group) => group.rows.length) + .map((group, groupIndex) => ( +
+

+ {exporting + ? "Recommended Minimum Stats" + : "ค่าสถานะต่ำๆที่ควรจะปั้นไว้"} +

+ {group.note ? ( +

+ {group.note} +

+ ) : null} +
+ {group.rows.map((target, index) => ( +
+ + {targetStatIconName(target.stat) ? ( + + ) : null} + + {statLabel( + normalizeTargetStat(target.stat) ?? + target.stat, + exporting, + )} + + + + {target.value} + +
+ ))} +
+
+ )) + ) : ( + group.rows.length) + .map( + (group, index) => + group.name || `เงื่อนไข ${index + 1}`, + )} + > + {profile.targetConditions + .filter((group) => group.rows.length) + .map((group, groupIndex) => ( +
+

+ {exporting + ? "Recommended Minimum Stats" + : "ค่าสถานะต่ำๆที่ควรจะปั้นไว้"} +

+ {group.note ? ( +

+ {group.note} +

+ ) : null} +
+ {group.rows.map((target, index) => ( +
+ + {targetStatIconName(target.stat) ? ( + + ) : null} + + {statLabel( + normalizeTargetStat(target.stat) ?? + target.stat, + )} + + + + {target.value} + +
+ ))} +
+
+ ))} +
+ ) + ) : null} + {profile.talents.length ? ( +
+

+ {exporting ? "Talent Priority" : "ลำดับการอัพพรสวรรค์"} +

+
+ {normalizeTalentPriorities(profile.talents).map( + (talent, index, talents) => { + const catalogTalent = guide.character.talents.find( + (item) => item.type === talent.type, + ); + const talentName = catalogTalent?.name ?? talent.type; + return ( +
+
+
+ {catalogTalent?.iconUrl ? ( + {talentName} + ) : ( + + {talent.type} + + )} +
+ + {talent.level + ? exporting + ? `Level ${talent.level}+` + : `เลเวล ${talent.level}+` + : "-"} + +

+ {talentName} +

+

+ {talent.type === "E" + ? exporting + ? "Elemental Skill" + : "สกิลธาตุ" + : talent.type === "Q" + ? exporting + ? "Elemental Burst" + : "ท่าไม้ตาย" + : exporting + ? "Normal Attack" + : "ตีปกติ"} +

+
+ {index < talents.length - 1 ? ( + + {talentRelationSymbol(talent.relationToNext)} + + ) : null} +
+ ); + }, + )} +
+
+ ) : null} + {profile.note ? ( +
+

+ {exporting ? "Notes" : "โน็ต"} +

+

+ {profile.note} +

+
+ ) : null} +
+
+
+ ) : null} +
+
+ ); +} + +const thaiStats: Record = { + HP: "พลังชีวิต", + ATK: "พลังโจมตี", + DEF: "พลังป้องกัน", + EM: "ความชำนาญธาตุ", + ER: "ฟื้นฟูพลังงาน", + "CRIT RATE": "อัตราคริ", + "CRIT DAMAGE": "ความแรงคริ", + "ATK%": "พลังโจมตี%", + "HP%": "พลังชีวิต%", + "DEF%": "พลังป้องกัน%", + "CRIT Rate%/CRIT DMG%": "อัตราคริ/ความแรงคริ", + "Pyro DMG%": "ความเสียหายธาตุไฟ%", + "Hydro DMG%": "ความเสียหายธาตุน้ำ%", + "Cryo DMG%": "ความเสียหายธาตุน้ำแข็ง%", + "Electro DMG%": "ความเสียหายธาตุไฟฟ้า%", + "Anemo DMG%": "ความเสียหายธาตุลม%", + "Geo DMG%": "ความเสียหายธาตุดิน%", + "Dendro DMG%": "ความเสียหายธาตุไม้%", + "Physical DMG%": "ความเสียหายกายภาพ%", + "Healing Bonus%": "โบนัสการรักษา%", +}; + +const englishStats: Record = { + HP: "HP", + ATK: "ATK", + DEF: "DEF", + EM: "Elemental Mastery", + ER: "Energy Recharge", + "CRIT RATE": "CRIT Rate", + "CRIT DAMAGE": "CRIT DMG", + "CRIT Rate%/CRIT DMG%": "CRIT Rate / CRIT DMG", +}; + +function statLabel(value: string, exporting = true): string { + return (exporting ? englishStats : thaiStats)[value] ?? value; +} + +function ArtifactStat({ + iconSrc, + label, + values, + exporting = true, + elementIconUrls, +}: { + iconSrc: string; + label: string; + values: string[]; + exporting?: boolean; + elementIconUrls: Record; +}) { + return ( +
+ +

{label}

+ +
+ {values.length ? ( + values.map((value, index) => ( + + )) + ) : ( + - + )} +
+
+ ); +} + +function ArtifactStatValue({ + value, + index, + exporting = true, + elementIconUrls, +}: { + value: string; + index: number; + exporting?: boolean; + elementIconUrls: Record; +}) { + const iconName = recommendedStatIconName(value); + const element = recommendedStatElement(value); + const elementIconUrl = element ? elementIconUrls[element] : null; + const hasIcon = Boolean(iconName) && iconName !== "elemental-bonus"; + return ( + + {elementIconUrl ? ( + + ) : hasIcon ? ( + + ) : null} + {statLabel(value, exporting)} + + ); +} diff --git a/components/public/constellation-image.tsx b/lib/image-export/templates/constellation.tsx similarity index 82% rename from components/public/constellation-image.tsx rename to lib/image-export/templates/constellation.tsx index 653ed5f..21081de 100644 --- a/components/public/constellation-image.tsx +++ b/lib/image-export/templates/constellation.tsx @@ -1,6 +1,5 @@ -import Image from "next/image"; +import { Image, GuideExportBranding } from "./primitives"; -import { GuideExportBranding } from "@/components/public/guide-export-branding"; import type { PublicGuide } from "@/lib/guides/queries"; import { constellationChartRows } from "@/lib/guides/export-image"; import { cn } from "@/lib/utils"; @@ -37,16 +36,14 @@ export function ConstellationImage({

16 - ? "text-[60px]" - : "text-[76px]", + guide.character.name.length > 16 ? "text-[60px]" : "text-[76px]", )} > {`${guide.character.name}’s Constellations`}

-

- Team Damage -

+

+ Team Damage +

@@ -82,7 +79,10 @@ export function ConstellationImage({ {footer ? ( -

+

{footer}

) : null} diff --git a/lib/image-export/templates/index.tsx b/lib/image-export/templates/index.tsx new file mode 100644 index 0000000..6004c2f --- /dev/null +++ b/lib/image-export/templates/index.tsx @@ -0,0 +1,110 @@ +import { + createElement, + isValidElement, + type ReactNode, + type ReactElement, +} from "react"; +import type { ExportRequest } from "../types"; +import { ConstellationImage } from "./constellation"; +import { WeaponComparisonImage } from "./weapon"; +import { TeamExportImage } from "./team"; +import { MaterialPoster } from "./materials"; +import { OverviewPoster } from "./overview"; +import { ArtifactSectionContent } from "./artifact"; + +type Props = { + children?: ReactNode; + className?: string; + [key: string]: unknown; +}; +// Resolve only our pure functions. Filter sections and notes before layout, without DOM mutation. +export function resolveTemplate( + node: ReactNode, + request: ExportRequest, +): ReactNode { + if (Array.isArray(node)) + return node.map((child) => resolveTemplate(child, request)); + if (!isValidElement(node)) return node; + if (typeof node.type === "function") { + return resolveTemplate( + (node.type as (props: Props) => ReactNode)(node.props), + request, + ); + } + const props = { ...node.props }; + if (request.withoutNotes && "data-export-note" in props) return null; + if (request.kind === "artifact") { + const { sections, recommendedCondition, targetCondition, includeNote } = + request.data; + const section = props["data-artifact-export-section"]; + if ( + section && + (section === "note" + ? !includeNote + : !sections.includes(section as (typeof sections)[number])) + ) + return null; + if ( + props["data-recommended-condition"] !== undefined && + Number(props["data-recommended-condition"]) !== recommendedCondition + ) + return null; + if ( + props["data-target-condition"] !== undefined && + Number(props["data-target-condition"]) !== targetCondition + ) + return null; + const profile = request.data.guide.artifactProfile; + const hasNote = + includeNote && !request.withoutNotes && Boolean(profile?.note); + const hasTargets = + sections.includes("target-stats") && + Boolean( + profile?.targetConditions.filter((condition) => condition.rows.length)[ + targetCondition + ], + ); + const hasTalents = + sections.includes("talents") && Boolean(profile?.talents.length); + if ( + props.className?.includes("artifact-export-right") && + !hasNote && + !hasTargets && + !hasTalents + ) + return null; + } + props.children = resolveTemplate(props.children, request); + return createElement(node.type, { ...props, key: node.key }); +} +export function exportTemplate(request: ExportRequest): ReactElement { + let content: ReactNode; + switch (request.kind) { + case "constellation": + content = ; + break; + case "weapon": + content = ; + break; + case "team": + content = ; + break; + case "materials": + content = ; + break; + case "overview": + content = ; + break; + case "artifact": + content = ; + break; + } + return ( +
+ {resolveTemplate(content, request)} +
+ ); +} diff --git a/lib/image-export/templates/materials.tsx b/lib/image-export/templates/materials.tsx new file mode 100644 index 0000000..5dc15f2 --- /dev/null +++ b/lib/image-export/templates/materials.tsx @@ -0,0 +1,307 @@ +import { Image, Link } from "./primitives"; + +import type { + CatalogAscensionGroup, + CatalogBaseStat, +} from "@/lib/catalog/lunaris"; +import { + mergePosterMaterials, + type PosterMaterial, +} from "@/lib/material-poster"; +import { accentGradient } from "@/lib/overview-image"; +import { cn } from "@/lib/utils"; + +export type PosterBlendMode = + | "normal" + | "multiply" + | "screen" + | "overlay" + | "darken" + | "lighten" + | "color-dodge" + | "color-burn" + | "hard-light" + | "soft-light" + | "difference" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + +type PosterCharacter = { + name: string; + nameCardImageUrl: string | null; + baseStats: CatalogBaseStat[]; + ascension: Array< + Omit & { items: PosterMaterial[] } + >; + leveling: Array< + Omit & { items: PosterMaterial[] } + >; +}; + +const POSTER_EXPORT_WIDTH = 1600; +const POSTER_IMAGE_QUALITY = 100; +const MAX_OPTIMIZED_IMAGE_WIDTH = 3840; + +const statLabels: Record = { + hp: "HP", + atk: "ATK", + def: "DEF", + em: "EM", +}; + +function statLabel(key: string): string { + return statLabels[key.toLowerCase()] ?? key; +} + +function statIconName(key: string): string | null { + const normalized = key.toLowerCase().replace(/[^a-z]/gu, ""); + if (["hp", "atk", "def", "em", "er"].includes(normalized)) return normalized; + if (["critrate", "crit", "criticalrate"].includes(normalized)) + return "critrate"; + if (["critdmg", "critdamage", "criticaldamage"].includes(normalized)) + return "critdmg"; + return null; +} + +function statValue(key: string, value: number | string): string { + const formatted = + typeof value === "number" + ? value.toLocaleString("en-US", { maximumFractionDigits: 2 }) + : value; + return key.includes("%") ? `${formatted}%` : formatted; +} + +export function MaterialPoster({ + character, + characterArtUrl, + artworkOffsetX, + artworkOffsetY, + artworkScale, + topColors, + bottomColors, + interactive = false, + linkMaterials = false, + className, + bleeding = [], +}: { + character: PosterCharacter; + characterArtUrl: string | null; + artworkOffsetX: number; + artworkOffsetY: number; + topColors: [string, string]; + bottomColors: [string, string]; + interactive?: boolean; + linkMaterials?: boolean; + onArtworkMove?: (offset: { x: number; y: number }) => void; + className?: string; + artworkScale: number; + bleeding?: Array<{ + id: string; + imageUrl: string | null; + amount: number; + blendMode: PosterBlendMode; + }>; +}) { + const stats = character.baseStats.slice(0, 4); + const materials = mergePosterMaterials( + [...character.ascension, ...character.leveling].flatMap( + (group) => group.items, + ), + ); + const artworkImageSize = Math.min( + MAX_OPTIMIZED_IMAGE_WIDTH, + Math.max( + POSTER_EXPORT_WIDTH, + Math.ceil((POSTER_EXPORT_WIDTH * artworkScale) / 100), + ), + ); + const background = `linear-gradient(180deg, ${topColors[0]} 0%, ${topColors[1]} 22%, rgba(0,0,0,0) 42%, rgba(0,0,0,0) 62%, ${bottomColors[0]} 88%, ${bottomColors[1]} 100%)`; + + return ( +
+ {character.nameCardImageUrl ? ( + + ) : null} +
+ {characterArtUrl ? ( +
+ {character.name} + {[...bleeding].reverse().map((layer) => ( + + ))} +
+ ) : null} + +
+
+
+ +
+

+ BASE STATS +

+
+ {Array.from({ length: 4 }, (_, index) => { + const stat = stats[index]; + const iconName = stat ? statIconName(stat.key) : null; + return ( +
+ + {iconName ? ( + + ) : null} + {stat ? statLabel(stat.key) : "—"} + + + {stat ? statValue(stat.key, stat.value) : "—"} + +
+ ); + })} +
+
+ +
+

+ MATERIALS +

+

LV.90

+
+ {materials.map((material, index) => { + const materialCard = ( + <> +
+ {material.imageUrl ? ( + + ) : null} +
+
+ + ×{material.amount.toLocaleString("en-US")} + +
+ + ); + const className = + "aspect-[0.72] overflow-hidden rounded-[1.3cqw] bg-black/95 ring-1 ring-white/10"; + + return linkMaterials ? ( + + {materialCard} + + ) : ( +
+ {materialCard} +
+ ); + })} +
+
+
+ ); +} diff --git a/lib/image-export/templates/overview.tsx b/lib/image-export/templates/overview.tsx new file mode 100644 index 0000000..1586b36 --- /dev/null +++ b/lib/image-export/templates/overview.tsx @@ -0,0 +1,393 @@ +import type { CSSProperties, ReactNode } from "react"; +import { Image, ImageIcon, getImageProps } from "./primitives"; +import { cn } from "@/lib/utils"; +import { elementIconFilter } from "@/lib/catalog/element"; +import { + accentGradient, + elementOrder, + elementGlowColor, + normalizeHex, + weaponTypeIconPath, + type OverviewBlendMode, +} from "@/lib/overview-image"; +type Character = { + key: string; + name: string; + aliases: string[]; + element: string | null; + rarity: number | null; + weaponType: string | null; + imageUrl: string; + nameCardImageUrl: string | null; +}; + +type Composition = { + backgroundUrl: string | null; + fadeColor: string; + fadeTint: number; + auraUpOffset: number; + characterOffset: Position; + fadeOffset: Position; +}; + +type BlendLayer = { + id: number; + mode: OverviewBlendMode; + amount: number; +}; + +type CropArea = { x: number; y: number; width: number; height: number }; +type Position = { x: number; y: number }; +type PosterLayer = "character" | "fade"; + +const DISCLAIMER = + "ไม่สามารถเอาไปเปรียบเทียบกับการคำนวณของ TC ท่านอื่นๆได้\nเพราะแต่ละคนใช้สแตทในการคำนวณที่ไม่เหมือนกัน\nและตารางที่เห็นอาจมีการเปลี่ยนแปลงได้ในอนาคต"; +function OverviewLayer({ + offset, + className, + style, + children, +}: { + id: PosterLayer; + offset: Position; + interactive: boolean; + className?: string; + style?: CSSProperties; + children: ReactNode; +}) { + return ( +
+ {children} +
+ ); +} +export function OverviewPoster({ + artworkUrl, + character, + role, + topColors, + bottomColors, + elementColors, + elementGradientAngle, + blendLayers, + nameCardCrop, + nameCardSize, + elementIconUrls, + composition, + disclaimerFontSize = null, + interactive = false, + activeLayer = undefined, +}: { + artworkUrl: string | null; + character: Character | null; + role: string; + topColors: [string, string]; + bottomColors: [string, string]; + elementColors: [string, string]; + elementGradientAngle: number; + blendLayers: BlendLayer[]; + nameCardCrop: CropArea | null; + nameCardSize: { width: number; height: number } | null; + elementIconUrls: Record; + composition: Composition; + disclaimerFontSize: number | null; + interactive?: boolean; + activeLayer?: PosterLayer; + onLayerMove?: (layer: PosterLayer, delta: Position) => void; +}) { + const weaponIcon = weaponTypeIconPath(character?.weaponType ?? null); + const z = { + character: 1, + fade: 2, + aura: 3, + badge: 4, + text: 5, + } as const; + + return ( +
+ {composition.backgroundUrl ? ( + + ) : character?.nameCardImageUrl ? ( + nameCardCrop && nameCardSize ? ( + + ) : ( + + ) + ) : null} + {artworkUrl ? ( + + + {[...blendLayers].reverse().map((layer) => ( + + ))} + + ) : ( +
+
+ )} + + + +
+ + +
+
+
+ +
+
+
+ +
+ +
+ {weaponIcon ? ( + + ) : null} + {role} +
+
+ + {character ? ( +
+

+ {character.name.toLocaleUpperCase("en")} +

+
+ {elementOrder.map((element) => { + const url = elementIconUrls[element]; + const active = element === character.element; + if (!url) return null; + if (active) { + const { props: maskImageProps } = getImageProps({ + src: url, + alt: "", + width: 72, + height: 72, + }); + return ( + + + + + ); + } + return ( + + ); + })} +
+

+ {DISCLAIMER} +

+
+ ) : null} +
+ ); +} diff --git a/lib/image-export/templates/primitives.tsx b/lib/image-export/templates/primitives.tsx new file mode 100644 index 0000000..c144459 --- /dev/null +++ b/lib/image-export/templates/primitives.tsx @@ -0,0 +1,161 @@ +/* eslint-disable @next/next/no-img-element -- Pure image-renderer templates never mount in the webpage. */ +import type { ComponentProps, CSSProperties, ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +type ImageProps = Omit, "width" | "height"> & { + width?: number; + height?: number; + fill?: boolean; + sizes?: string; + unoptimized?: boolean; + priority?: boolean; + quality?: number; +}; +// The renderer receives original image sources, not Next.js optimized image URLs. +export function Image({ + src, + alt = "", + width, + height, + fill, + className, + style, +}: ImageProps) { + return ( + {alt} + ); +} +export function getImageProps({ src }: ImageProps) { + return { props: { src } }; +} +export function Link({ + children, + className, +}: { + children: ReactNode; + className?: string; + [key: string]: unknown; +}) { + return
{children}
; +} +export function Kbd({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return {children}; +} +export function ArrowUpRightIcon() { + return ↗; +} +export function ImageIcon({ + className, +}: { + className?: string; + [key: string]: unknown; +}) { + return ◇; +} +export function ConditionSwitcher({ + children, +}: { + children: ReactNode; + label: string; + labels: string[]; +}) { + return <>{children}; +} +export function Separator() { + return
; +} +export function Badge({ children }: { children: ReactNode; variant?: string }) { + return ( + + {children} + + ); +} +type BoxProps = ComponentProps<"div">; +export function Card({ className, children, ...props }: BoxProps) { + return ( +
+ {children} +
+ ); +} +export function CardHeader({ className, ...props }: BoxProps) { + return ( +
+ ); +} +export function CardTitle({ className, ...props }: BoxProps) { + return ( +
+ ); +} +export function CardContent({ className, ...props }: BoxProps) { + return
; +} +export function GuideExportBranding({ + backgroundUrl, + variant = "standard", +}: { + backgroundUrl: string; + variant?: "standard" | "constellation"; +}) { + const constellation = variant === "constellation"; + return ( + <> + +
+ + + ); +} diff --git a/lib/image-export/templates/team.tsx b/lib/image-export/templates/team.tsx new file mode 100644 index 0000000..6661787 --- /dev/null +++ b/lib/image-export/templates/team.tsx @@ -0,0 +1,279 @@ +import { Image, Link, Kbd, ArrowUpRightIcon } from "./primitives"; +import { cn } from "@/lib/utils"; +import { rarityGradientClass } from "@/lib/catalog/rarity"; +import { elementBackgroundColor } from "@/lib/catalog/element"; +import { isCustomTravelerKey } from "@/lib/catalog/travelers"; +import type { PublicGuide } from "@/lib/guides/queries"; +type Team = PublicGuide["teams"][number]; +type TeamArtifact = { + key: string; + name: string; + imageUrl: string; + rarity: number | null; +}; + +function ArtifactHalf({ + artifact, + side, + eager, + linked, + sizes, +}: { + artifact: TeamArtifact; + side: "left" | "right"; + eager: boolean; + linked: boolean; + sizes: string; +}) { + const content = ( + + {linked + + ); + const className = cn( + "absolute inset-0 overflow-hidden", + side === "left" + ? "[clip-path:polygon(0_0,0_100%,100%_100%)]" + : "[clip-path:polygon(0_0,100%_0,100%_100%)]", + rarityGradientClass(artifact.rarity), + ); + + return linked ? ( + + {content} + + ) : ( + {content} + ); +} + +export function TeamArtifactIcon({ + first, + second, + eager = false, + linked = false, + className, + sizes, +}: { + first: TeamArtifact | null; + second: TeamArtifact | null; + eager?: boolean; + linked?: boolean; + className?: string; + sizes: string; +}) { + const primary = first ?? second; + const secondary = first ? second : null; + + if (!primary) { + return ( +
+ ); + } + + if (!secondary) { + const content = ( + <> + {linked + {linked ? ( + + + ) : null} + + ); + const artifactClassName = cn( + "relative aspect-square scale-110 overflow-hidden rounded-md bg-muted", + rarityGradientClass(primary.rarity), + className, + ); + + return linked ? ( + + {content} + + ) : ( +
{content}
+ ); + } + + return ( +
+ + +
+ ); +} + +export function TeamExportImage({ team }: { team: Team }) { + const total = team.members.reduce( + (sum, member) => sum + Number(member.damage), + 0, + ); + const seconds = Number(team.rotationSeconds); + return ( +
+
+

{team.title}

+

{`Rotation duration: ${seconds.toLocaleString("en-US", { maximumFractionDigits: 2 })} seconds`}

+
+
+ {team.members.map((member) => { + const damage = Number(member.damage); + const percent = total > 0 ? (damage / total) * 100 : 0; + return ( +
+
+ +
+
+ {member.weapon ? ( +
+ +
+ ) : null} + +
+
+
+ + {member.character.name} + + C{member.constellation}R{member.refinement} + + + + {damage.toLocaleString("en-US")} + +
+
+
+
+
+ + {percent.toFixed(1)}% + +
+
+
+ ); + })} +
+
+

{team.combo}

+
+
+ DPS:{" "} + {seconds > 0 + ? (total / seconds).toLocaleString("en-US", { + maximumFractionDigits: 0, + }) + : "0"} +
+
+ By YT Genshin Isn't Mobile Game +
+
+ ); +} diff --git a/lib/image-export/templates/weapon.tsx b/lib/image-export/templates/weapon.tsx new file mode 100644 index 0000000..8f657d9 --- /dev/null +++ b/lib/image-export/templates/weapon.tsx @@ -0,0 +1,107 @@ +import { Image, GuideExportBranding } from "./primitives"; +import type { PublicGuide } from "@/lib/guides/queries"; +type WeaponGroup = PublicGuide["weaponGroups"][number]; +type DamageMetric = "overallPercent" | "personalPercent"; + +function formattedPercent(value: string | null): string { + if (value === null) return "-%"; + const numeric = Number(value); + if (numeric === 0) return "-%"; + return `${numeric.toLocaleString("en-US", { maximumFractionDigits: 2 })}%`; +} + +export function WeaponComparisonImage({ + guide, + group, + metric, + conditionLabel, +}: { + guide: PublicGuide; + group: WeaponGroup; + metric: DamageMetric; + conditionLabel: string; +}) { + const values = group.rows.map((row) => Number(row[metric]) || 0); + const maximum = Math.max(100, ...values); + + return ( +
+ +
+

+ {`${guide.character.name}’s Weapon Comparison`} +

+

+ {metric === "overallPercent" ? "Overall Damage" : "Personal Damage"} +

+

{conditionLabel}

+
+ +
+ {group.rows.map((row) => { + const numeric = Number(row[metric]) || 0; + const width = + row[metric] === null + ? 28 + : numeric <= 100 + ? Math.max(28, Math.max(0, numeric) * 0.5) + : Math.min( + 90, + 50 + ((numeric - 100) / Math.max(1, maximum - 100)) * 40, + ); + return ( +
+
+ +
+
+
+ + {row.weapon.name} R{row.refinement} + +
+
+ + {formattedPercent(row[metric])} + + {row.note ? ( + + {row.note} + + ) : null} +
+
+
+ ); + })} +
+ + {group.note ? ( +

+ {group.note} +

+ ) : null} +
+ ); +} diff --git a/lib/image-export/types.ts b/lib/image-export/types.ts new file mode 100644 index 0000000..280a78c --- /dev/null +++ b/lib/image-export/types.ts @@ -0,0 +1,52 @@ +import type { ComponentProps } from "react"; +import type { ConstellationImage } from "./templates/constellation"; +import type { WeaponComparisonImage } from "./templates/weapon"; +import type { TeamExportImage } from "./templates/team"; +import type { MaterialPoster } from "./templates/materials"; +import type { OverviewPoster } from "./templates/overview"; +import type { PublicGuide } from "@/lib/guides/queries"; + +export type ArtifactExportSection = + "tierlist" | "best-stats" | "target-stats" | "talents"; +export type ExportTemplate = + | { kind: "constellation"; data: ComponentProps } + | { kind: "weapon"; data: ComponentProps } + | { kind: "team"; data: ComponentProps } + | { + kind: "materials"; + data: Omit< + ComponentProps, + "onArtworkMove" | "interactive" | "linkMaterials" + >; + } + | { + kind: "overview"; + data: Omit< + ComponentProps, + "onLayerMove" | "interactive" | "activeLayer" + >; + } + | { + kind: "artifact"; + data: { + guide: PublicGuide; + sections: ArtifactExportSection[]; + recommendedCondition: number; + targetCondition: number; + includeNote: boolean; + }; + }; +export type ExportRequest = ExportTemplate & { + width: number; + height?: number; + format: "png" | "webp"; + withoutNotes?: boolean; +}; +export type WorkerRequest = { + id: number; + request: ExportRequest; + origin: string; +}; +export type WorkerResult = + | { id: number; bytes: ArrayBuffer; error?: never } + | { id: number; error: string; bytes?: never }; diff --git a/package.json b/package.json index 1b5612b..e31df37 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,14 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", - "build": "next build", + "dev": "bun scripts/prepare-image-export.mjs && next dev --turbopack", + "build": "bun scripts/prepare-image-export.mjs && next build", "start": "next start", "lint": "eslint .", "typecheck": "next typegen && tsc --noEmit", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", + "test": "bun scripts/prepare-image-export.mjs && vitest run", + "test:watch": "bun scripts/prepare-image-export.mjs && vitest", + "test:coverage": "bun scripts/prepare-image-export.mjs && vitest run --coverage", "logs": "kubectl logs --namespace buzz-sheet --selector 'app.kubernetes.io/name=buzz-sheet,app.kubernetes.io/component in (web,worker,discord-worker)' --all-containers=true --prefix=true --tail=100 --follow --max-log-requests=10", "db:generate": "drizzle-kit generate", "db:migrate": "bun scripts/migrate.ts", @@ -57,10 +57,12 @@ "sharp": "^0.35.4", "sonner": "^2.0.8", "tailwind-merge": "^3.6.0", + "takumi-js": "2.13.7", "tw-animate-css": "^1.4.0", "zod": "^4.4.3" }, "devDependencies": { + "@tailwindcss/node": "4.3.3", "@tailwindcss/postcss": "^4", "@types/bun": "^1.4.0", "@types/node": "^20", diff --git a/public/Anuphan-LICENSE.txt b/public/Anuphan-LICENSE.txt new file mode 100644 index 0000000..796d0d5 --- /dev/null +++ b/public/Anuphan-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Anuphan Project Authors (https://github.com/cadsondemak/Anuphan) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/Anuphan.ttf b/public/Anuphan.ttf new file mode 100644 index 0000000000000000000000000000000000000000..60faaba3f4dd50612cc11b5889eb71d44f20b40c GIT binary patch literal 231712 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 K=71@h`hNj2c+xEZ literal 0 HcmV?d00001 diff --git a/scripts/prepare-image-export.mjs b/scripts/prepare-image-export.mjs new file mode 100644 index 0000000..bc15b98 --- /dev/null +++ b/scripts/prepare-image-export.mjs @@ -0,0 +1,47 @@ +import { copyFile, readFile, writeFile, readdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { compile } from "@tailwindcss/node"; +import ts from "typescript"; + +await copyFile( + fileURLToPath(import.meta.resolve("@takumi-rs/wasm/takumi_wasm_bg.wasm")), + "public/takumi.wasm", +); +const globals = await readFile("app/globals.css", "utf8"); +const theme = globals.slice( + globals.indexOf("@theme inline {"), + globals.indexOf("\n}", globals.indexOf("@theme inline {")) + 2, +); +const colors = globals.slice( + globals.indexOf(":root,\n.dark"), + globals.indexOf("\n}", globals.indexOf(":root,\n.dark")) + 2, +); +const candidates = new Set(); +const files = (await readdir("lib/image-export/templates")) + .filter((name) => name.endsWith(".tsx")) + .map((name) => `lib/image-export/templates/${name}`); +files.push( + "lib/catalog/rarity.ts", + "lib/catalog/element.ts", + "lib/guides/stats.ts", +); +for (const path of files) { + const source = ts.createSourceFile( + path, + await readFile(path, "utf8"), + ts.ScriptTarget.Latest, + true, + path.endsWith("tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); + function visit(node) { + if (ts.isStringLiteralLike(node)) + node.text.split(/\s+/).forEach((token) => candidates.add(token)); + ts.forEachChild(node, visit); + } + visit(source); +} +const compiler = await compile( + `@import "tailwindcss";\n${theme}\n${colors}\n:root { --font-anuphan: Anuphan; --font-geist-mono: Anuphan; }`, + { base: process.cwd(), onDependency() {} }, +); +await writeFile("public/takumi.css", compiler.build([...candidates]));