feat: left section complete
This commit is contained in:
+157
-48
@@ -7,12 +7,18 @@ import {
|
||||
InvalidFormatError,
|
||||
MaintenanceError,
|
||||
RatelimitError,
|
||||
TTLCache,
|
||||
} from "enka.ts";
|
||||
import { twj } from "tw-to-css";
|
||||
import type { App } from "@";
|
||||
import { ConstsSvg } from "@/assets/consts";
|
||||
import shadeSvg from "@/assets/shade.svg" with { type: "file" };
|
||||
import fontFile from "@/assets/shinshin.woff2" with { type: "file" };
|
||||
import { TalentSvg } from "@/assets/talent";
|
||||
import { mockData } from "@/lib/mock";
|
||||
import { AvatarStore } from "@/type/enka";
|
||||
|
||||
const cache = new TTLCache();
|
||||
|
||||
const font = file(fontFile);
|
||||
const shade = file(shadeSvg);
|
||||
@@ -21,16 +27,22 @@ export function card(app: App) {
|
||||
return app
|
||||
.group("/card", (app) =>
|
||||
app
|
||||
// .onBeforeHandle(({ key }) => {
|
||||
// if (!key.valid) throw status(401);
|
||||
// if (!key.has("card")) throw status(403);
|
||||
// })
|
||||
.state("start", Date.now())
|
||||
.derive(({ store }) => {
|
||||
store.start = Date.now();
|
||||
})
|
||||
.onAfterResponse(({ store: { start } }) =>
|
||||
console.log(`${Date.now() - start}ms - Rendering completed`),
|
||||
)
|
||||
.get(
|
||||
"/genshin/:uid/:char",
|
||||
async ({ params: { uid, char: charId } }) => {
|
||||
async ({ params: { uid, char: charId }, store: { start } }) => {
|
||||
try {
|
||||
// const data = mockData;
|
||||
const data = await fetchEnka(uid, false);
|
||||
const avatarStore = await getEnkaAvatarStore();
|
||||
console.log(`${Date.now() - start}ms - Avatar store loaded`);
|
||||
// const data = await fetchEnka(uid, false);
|
||||
const data = mockData;
|
||||
console.log(`${Date.now() - start}ms - Enka data loaded`);
|
||||
if (typeof data.avatarInfoList === "undefined")
|
||||
return status(400, "The showcase for this UID is private");
|
||||
const char = data.avatarInfoList.find(
|
||||
@@ -38,39 +50,76 @@ export function card(app: App) {
|
||||
);
|
||||
if (!char) return status(400, "Character not found in showcase");
|
||||
|
||||
const avatar = avatarStore[char.avatar.avatarId];
|
||||
if (!avatar)
|
||||
return status(
|
||||
500,
|
||||
"Unsupported character, avatar store doesn't have this character",
|
||||
);
|
||||
const resources = await Promise.all(
|
||||
[
|
||||
...avatar.Consts,
|
||||
...Object.values(avatar.Skills),
|
||||
...Object.values(avatar.Costumes ?? {})
|
||||
.map((c) => c.Art)
|
||||
.slice(0, 1),
|
||||
avatar.SideIconName.replace(
|
||||
"AvatarIcon_Side",
|
||||
"Gacha_AvatarImg",
|
||||
),
|
||||
].map(async (path) => ({
|
||||
src: path,
|
||||
data: await fetch(new URL(path, "https://enka.network")).then(
|
||||
(res) => res.arrayBuffer(),
|
||||
),
|
||||
})),
|
||||
);
|
||||
console.log(`${Date.now() - start}ms - Image resources loaded`);
|
||||
|
||||
return new ImageResponse(
|
||||
<div style={twj("relative text-[14.8667px] text-white")}>
|
||||
<div style={twj("aspect-[1218/508] h-full")}></div>
|
||||
<div tw="relative text-[14.8667px] text-white">
|
||||
<div tw="aspect-[1218/508] h-full"></div>
|
||||
{/* avatar */}
|
||||
<img
|
||||
src={
|
||||
avatar.Costumes?.[0]?.Art ||
|
||||
avatar.SideIconName.replace(
|
||||
"AvatarIcon_Side",
|
||||
"Gacha_AvatarImg",
|
||||
)
|
||||
}
|
||||
alt="char"
|
||||
tw="object-cover h-full aspect-[1024/1124] absolute top-0 -left-[2.5em] scale-x-[1.1] scale-y-[1.15]"
|
||||
/>
|
||||
|
||||
{/* left section */}
|
||||
<div
|
||||
tw="absolute top-0 bottom-0 left-0 w-[27.4em] p-[1.4em] flex flex-col"
|
||||
style={{
|
||||
...twj(
|
||||
"absolute top-0 bottom-0 left-0 w-[27.4em] p-[1.4em] bg-red-500/50",
|
||||
"flex flex-col",
|
||||
),
|
||||
textShadow:
|
||||
"0 .08em .1em black,0 .1em .3em rgba(0,0,0,.4)",
|
||||
}}
|
||||
>
|
||||
<div style={twj("absolute top-0 left-0 bottom-0 right-0")}>
|
||||
<img src="shade" alt="shade" style={twj("opacity-40")} />
|
||||
{/* left side vignette */}
|
||||
<div tw="absolute top-0 left-0 bottom-0 right-0">
|
||||
<img src="shade" alt="shade" tw="opacity-40" />
|
||||
</div>
|
||||
<div
|
||||
style={twj(
|
||||
"text-[130%] pb-[.5%] flex items-center gap-[.5em]",
|
||||
)}
|
||||
>
|
||||
{/* char and username */}
|
||||
<div tw="text-[130%] pb-[.5%] flex items-center gap-[.5em]">
|
||||
{char.avatar.name}
|
||||
<div style={twj("text-[11.596px] opacity-80")}>
|
||||
<div tw="text-[11.596px] opacity-80">
|
||||
▴ {data.playerInfo.nickname}
|
||||
</div>
|
||||
</div>
|
||||
{/* level */}
|
||||
<div>
|
||||
Lv. 80 /<span style={twj("opacity-60 pl-1")}>80</span>
|
||||
Lv. 80 /<span tw="opacity-60 pl-1">80</span>
|
||||
</div>
|
||||
<div style={twj("flex gap-1 items-end")}>
|
||||
{/* fren */}
|
||||
<div tw="flex gap-1 items-end">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={twj("h-[1em] mb-1")}
|
||||
tw="h-[1em] mb-1"
|
||||
viewBox="0 0 14 14"
|
||||
>
|
||||
<title>fren</title>
|
||||
@@ -82,39 +131,78 @@ export function card(app: App) {
|
||||
8
|
||||
</div>
|
||||
|
||||
<div style={twj("grow")} />
|
||||
{/* sep */}
|
||||
<div tw="grow" />
|
||||
{/* constellation */}
|
||||
<div
|
||||
style={twj("flex align-start flex-col text-[1.12em]")}
|
||||
tw="flex align-start flex-col text-[1.12em]"
|
||||
className={`${char.avatar.element} locked`}
|
||||
>
|
||||
{Array(6)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<div>
|
||||
<ConstsSvg
|
||||
locked={i >= (char.talentIdList?.length || 0)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{avatar.Consts.map((c, i) => (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
margin: "-.5em 0 0em -.6em",
|
||||
}}
|
||||
>
|
||||
<ConstsSvg
|
||||
locked={i >= (char.talentIdList?.length || 0)}
|
||||
/>
|
||||
<img
|
||||
src={c}
|
||||
alt={`Constellation ${i + 1}`}
|
||||
style={twj(
|
||||
"absolute top-0 left-0 w-[1.6em] mt-[.72em] ml-[.7em] pointer-events-auto",
|
||||
i >= (char.talentIdList?.length || 0)
|
||||
? "opacity-40"
|
||||
: "",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* talent-wrap */}
|
||||
<div
|
||||
style={twj(
|
||||
"mb-[-1em] ml-[-.3em] mt-[.3em] text-[.9em] opacity-80",
|
||||
)}
|
||||
style={{
|
||||
display: "flex",
|
||||
position: "absolute",
|
||||
right: "5%",
|
||||
bottom: "6%",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-end",
|
||||
width: "60%",
|
||||
}}
|
||||
>
|
||||
{Object.entries(avatar.Skills).map(([key, src]) => {
|
||||
const talentLevel = char.skillLevelMap?.[key] ?? 0;
|
||||
return (
|
||||
<div tw="mt-[.3em] relative">
|
||||
<TalentSvg
|
||||
tw={`Talents ${char.avatar.element} w-[3.6em] opacity-80 text-black`}
|
||||
/>
|
||||
<img
|
||||
key={key}
|
||||
src={src}
|
||||
alt={`Talent ${key}`}
|
||||
tw="w-[2.5em] absolute left-[.15em] top-[.15em] m-[.4em_0_0_.4em]"
|
||||
/>
|
||||
<div tw="absolute -bottom-[.1em] w-full flex justify-center">
|
||||
<div tw="bg-[#323232b3] py-[5%] pl-[7%] pr-[15%] text-[.8em] rounded-[1.4em] flex items-center justify-center">
|
||||
{talentLevel}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div tw="mb-[-1em] ml-[-.3em] mt-[.3em] text-[.9em] opacity-80">
|
||||
{data.uid}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={twj(
|
||||
"absolute top-0 bottom-0 right-[23.1em] w-[21.5em] p-[1.4em_.7em_.7em] bg-green-500/50",
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
style={twj(
|
||||
"absolute top-0 bottom-0 right-0 w-[23.1em] p-[.7em] bg-blue-500/50",
|
||||
)}
|
||||
></div>
|
||||
{/* middle section */}
|
||||
<div tw="absolute top-0 bottom-0 right-[23.1em] w-[21.5em] p-[1.4em_.7em_.7em] bg-green-500/50"></div>
|
||||
{/* right section */}
|
||||
<div tw="absolute top-0 bottom-0 right-0 w-[23.1em] p-[.7em] bg-blue-500/50"></div>
|
||||
</div>,
|
||||
{
|
||||
fonts: [await font.arrayBuffer()],
|
||||
@@ -122,16 +210,25 @@ export function card(app: App) {
|
||||
persistentImages: [
|
||||
{ data: await shade.arrayBuffer(), src: "shade" },
|
||||
],
|
||||
fetchedResources: resources,
|
||||
format: "webp",
|
||||
// drawDebugBorder: true,
|
||||
stylesheets: [
|
||||
`
|
||||
.Rock { color: #ffc64a; }
|
||||
.Talents.Rock { color: #4a3a18!important }
|
||||
.Wind { color: #71fdec; }
|
||||
.Talents.Wind { color: #1d4d47!important }
|
||||
.Ice { color: #98efff; }
|
||||
.Talents.Ice { color: #193d43!important }
|
||||
.Water { color: #56a2ff; }
|
||||
.Talents.Water { color: #18293e!important }
|
||||
.Electric { color: #ffa1ff; }
|
||||
.Talents.Electric { color: #34123b!important }
|
||||
.Fire { color: #f58453; }
|
||||
.Talents.Fire { color: #541900!important }
|
||||
.Grass { color: #a5ffab; }
|
||||
.Talents.Grass { color: #134517!important }
|
||||
`,
|
||||
],
|
||||
},
|
||||
@@ -196,3 +293,15 @@ export function card(app: App) {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function getEnkaAvatarStore(): Promise<AvatarStore> {
|
||||
const cached = cache.get("enkaAvatarStore");
|
||||
if (cached) return cached as AvatarStore;
|
||||
|
||||
const response = await fetch(
|
||||
"https://github.com/EnkaNetwork/API-docs/raw/refs/heads/master/store/gi/avatars.json",
|
||||
);
|
||||
const data = await response.json();
|
||||
cache.set("enkaAvatarStore", data, 86400); // 24 hours
|
||||
return data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user