diff --git a/app/[character]/opengraph-image.tsx b/app/[character]/opengraph-image.tsx index c52b2b6..f3a6e45 100644 --- a/app/[character]/opengraph-image.tsx +++ b/app/[character]/opengraph-image.tsx @@ -18,11 +18,42 @@ export default async function Image({ await connection(); const guide = await getCachedPublicGuide((await params).character); if (!guide) return new Response("Not found", { status: 404 }); + const recommendedWeapon = guide.weapons[0]; + const recommendedArtifact = guide.artifactGroups.find( + (group) => group.rows.length > 0, + )?.rows[0]; return generateOgImage({ title: guide.name, sub: guide.overview, characterCard: true, backgroundUrl: guide.character.nameCardImageUrl, characterUrl: guide.character.splashImageUrl ?? guide.character.imageUrl, + recommendedWeapon: recommendedWeapon + ? { + name: recommendedWeapon.weapon.name, + refinement: recommendedWeapon.refinement, + rarity: recommendedWeapon.weapon.rarity, + imageUrl: recommendedWeapon.weapon.imageUrl, + overallPercent: recommendedWeapon.overallPercent, + personalPercent: recommendedWeapon.personalPercent, + } + : null, + recommendedArtifact: recommendedArtifact + ? { + first: { + name: recommendedArtifact.first.name, + rarity: recommendedArtifact.first.rarity, + imageUrl: recommendedArtifact.first.imageUrl, + }, + second: recommendedArtifact.second + ? { + name: recommendedArtifact.second.name, + rarity: recommendedArtifact.second.rarity, + imageUrl: recommendedArtifact.second.imageUrl, + } + : null, + percent: recommendedArtifact.percent, + } + : null, }); } diff --git a/lib/og-image.tsx b/lib/og-image.tsx index a406615..7424a90 100644 --- a/lib/og-image.tsx +++ b/lib/og-image.tsx @@ -6,11 +6,15 @@ import { join } from "node:path"; import { ImageResponse } from "next/og"; import sharp from "sharp"; +import { rarityGradient } from "@/lib/catalog/rarity"; +import { artifactTierCount } from "@/lib/guides/artifacts"; + export const ogImageSize = { width: 1222, height: 560 } as const; export const ogImageContentType = "image/png"; let bannerDataPromise: Promise | undefined; let fontDataPromise: Promise | undefined; +let characterFontDataPromise: Promise | undefined; const imageDataPromises = new Map>(); function shorten(value: string, length: number): string { @@ -20,6 +24,12 @@ function shorten(value: string, length: number): string { : normalized; } +function metric(value: string): string { + return Number(value) === 0 + ? "-%" + : `${Number(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}%`; +} + function getBannerData(): Promise { bannerDataPromise ??= readFile( join(process.cwd(), "public/web_banner.png"), @@ -33,6 +43,11 @@ function getFontData(): Promise { return fontDataPromise; } +function getCharacterFontData(): Promise { + characterFontDataPromise ??= readFile(join(process.cwd(), "public/zh-cn.ttf")); + return characterFontDataPromise; +} + function getImageData(url: string): Promise { const cached = imageDataPromises.get(url); if (cached) return cached; @@ -56,19 +71,57 @@ export async function generateOgImage({ characterCard = false, backgroundUrl, characterUrl, + recommendedWeapon, + recommendedArtifact, }: { title: string; sub: string; characterCard?: boolean; backgroundUrl?: string | null; characterUrl?: string | null; + recommendedWeapon?: { + name: string; + refinement: number; + rarity: number | null; + imageUrl: string; + overallPercent: string | null; + personalPercent: string | null; + } | null; + recommendedArtifact?: { + first: { + name: string; + rarity: number | null; + imageUrl: string; + }; + second: { + name: string; + rarity: number | null; + imageUrl: string; + } | null; + percent: string; + } | null; }) { - const [bannerSrc, kanit, characterBackgroundSrc, characterArtSrc] = + const [ + bannerSrc, + kanit, + characterFont, + characterBackgroundSrc, + characterArtSrc, + weaponArtSrc, + firstArtifactArtSrc, + secondArtifactArtSrc, + ] = await Promise.all([ characterCard ? null : getBannerData(), getFontData(), + characterCard ? getCharacterFontData() : null, characterCard && backgroundUrl ? getImageData(backgroundUrl) : null, characterCard && characterUrl ? getImageData(characterUrl) : null, + recommendedWeapon ? getImageData(recommendedWeapon.imageUrl) : null, + recommendedArtifact ? getImageData(recommendedArtifact.first.imageUrl) : null, + recommendedArtifact?.second + ? getImageData(recommendedArtifact.second.imageUrl) + : null, ]); const backgroundSrc = characterCard ? characterBackgroundSrc : bannerSrc; @@ -80,6 +133,7 @@ export async function generateOgImage({ height: "560px", fontFamily: "Kanit", color: "#fff", + textShadow: "0 2px 8px rgba(0, 0, 0, 0.8)", position: "relative", overflow: "hidden", }} @@ -123,7 +177,7 @@ export async function generateOgImage({ display: "flex", flexDirection: "column", right: characterCard ? "560px" : "450px", - bottom: "100px", + bottom: recommendedWeapon || recommendedArtifact ? "28px" : "100px", }} > {characterCard ? ( @@ -131,7 +185,7 @@ export async function generateOgImage({ เกนชินไม่ใช่เกมมือถือไกด์ ) : null} -
+
{shorten(title, 24)}
{shorten(sub, 92)}
+ {recommendedWeapon ? ( +
+
+ {weaponArtSrc ? ( + + ) : null} +
+ {shorten(recommendedWeapon.name, 19)} R{recommendedWeapon.refinement} +
+
+ {recommendedWeapon.overallPercent || recommendedWeapon.personalPercent ? ( +
+ {recommendedWeapon.overallPercent ? ( +
+
{metric(recommendedWeapon.overallPercent)}
+
ทั้งทีม
+
+ ) : null} + {recommendedWeapon.personalPercent ? ( +
+
{metric(recommendedWeapon.personalPercent)}
+
ส่วนตัว
+
+ ) : null} +
+ ) : null} +
+ ) : null} + {recommendedArtifact ? ( +
+
+ {firstArtifactArtSrc ? ( +
+ +
+ ) : null} + {recommendedArtifact.second && secondArtifactArtSrc ? ( +
+ +
+ ) : null} +
+
+ {artifactTierCount(recommendedArtifact.second?.name ?? null)} {shorten(recommendedArtifact.first.name, recommendedArtifact.second ? 12 : 21)} + {recommendedArtifact.second ? ` + ${shorten(recommendedArtifact.second.name, 12)}` : ""} +
+
+ {metric(recommendedArtifact.percent)} +
+
+ ) : null}
, { ...ogImageSize, - fonts: [{ name: "Kanit", data: kanit, style: "normal", weight: 600 }], + fonts: [ + { name: "Kanit", data: kanit, style: "normal", weight: 600 }, + ...(characterFont + ? [{ name: "Genshin", data: characterFont, style: "normal" as const, weight: 400 as const }] + : []), + ], }, ); } diff --git a/public/profile.png b/public/profile.png new file mode 100644 index 0000000..cb3dce2 Binary files /dev/null and b/public/profile.png differ diff --git a/public/zh-cn.ttf b/public/zh-cn.ttf new file mode 100644 index 0000000..1ea5700 Binary files /dev/null and b/public/zh-cn.ttf differ