feat: new character list card
@@ -22,3 +22,8 @@ declare module "*.png" {
|
||||
const file: string;
|
||||
export default file;
|
||||
}
|
||||
|
||||
declare module "*.webp" {
|
||||
const file: string;
|
||||
export default file;
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 1870px;
|
||||
height: 778px;
|
||||
margin: 12px auto;
|
||||
background: #000;
|
||||
}
|
||||
@@ -64,14 +62,28 @@
|
||||
<canvas id="canvas" width="1870" height="778"></canvas>
|
||||
|
||||
<script>
|
||||
const MINE = "/v1/card/genshin/775956514/ororon";
|
||||
const ORIG = "/v1/card/genshin/diff/ororo.png";
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const type = urlParams.get('type');
|
||||
|
||||
let MINE, ORIG, W, H;
|
||||
if (type === 'list') {
|
||||
MINE = "/v1/card/genshin/775956514";
|
||||
ORIG = "/v1/card/genshin/diff/listoriginal.webp";
|
||||
W = 1200;
|
||||
H = 732;
|
||||
} else {
|
||||
MINE = "/v1/card/genshin/775956514/ororon";
|
||||
ORIG = "/v1/card/genshin/diff/ororo.png";
|
||||
W = 1870;
|
||||
H = 778;
|
||||
}
|
||||
|
||||
const W = 1870, H = 778;
|
||||
/**
|
||||
* @type {HTMLCanvasElement}
|
||||
*/
|
||||
const canvas = document.getElementById("canvas");
|
||||
canvas.width = W;
|
||||
canvas.height = H;
|
||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||
|
||||
const mode = document.getElementById("mode");
|
||||
@@ -231,9 +243,9 @@
|
||||
}
|
||||
});
|
||||
|
||||
// window.addEventListener("focus", () => {
|
||||
// refresh();
|
||||
// });
|
||||
// window.addEventListener("focus", () => {
|
||||
// refresh();
|
||||
// });
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,45 @@
|
||||
import { file } from "bun";
|
||||
|
||||
import ElementElectric from "./elements/Electric.webp";
|
||||
import ElementFire from "./elements/Fire.webp";
|
||||
import ElementGrass from "./elements/Grass.webp";
|
||||
import ElementIce from "./elements/Ice.webp";
|
||||
import ElementRock from "./elements/Rock.webp";
|
||||
import ElementWater from "./elements/Water.webp";
|
||||
import ElementWind from "./elements/Wind.webp";
|
||||
import BadgeAbyssStar from "./listbadge/abyss-star.png";
|
||||
import BadgeAbyss from "./listbadge/abyss.png";
|
||||
import BadgeAchievements from "./listbadge/achievements.png";
|
||||
import BadgeFren from "./listbadge/fren.png";
|
||||
import BadgeStygian from "./listbadge/stygian.png";
|
||||
import BadgeTheaterStar from "./listbadge/theater-star.png";
|
||||
import BadgeTheater from "./listbadge/theater.png";
|
||||
import ListBg from "./listbg.webp";
|
||||
import ListNcFrame from "./listframe.png";
|
||||
import ListRtop from "./listrtop.png";
|
||||
import PaimonSmug from "./listsmug.webp";
|
||||
|
||||
export const listBg = file(ListBg);
|
||||
export const listNcFrame = file(ListNcFrame);
|
||||
export const listRtop = file(ListRtop);
|
||||
export const smug = file(PaimonSmug);
|
||||
|
||||
export const listBadges = {
|
||||
abyss: file(BadgeAbyss),
|
||||
achievements: file(BadgeAchievements),
|
||||
fren: file(BadgeFren),
|
||||
stygian: file(BadgeStygian),
|
||||
theater: file(BadgeTheater),
|
||||
abyssStar: file(BadgeAbyssStar),
|
||||
theaterStar: file(BadgeTheaterStar),
|
||||
};
|
||||
|
||||
export const elements = {
|
||||
electric: file(ElementElectric),
|
||||
fire: file(ElementFire),
|
||||
grass: file(ElementGrass),
|
||||
ice: file(ElementIce),
|
||||
rock: file(ElementRock),
|
||||
water: file(ElementWater),
|
||||
wind: file(ElementWind),
|
||||
};
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 577 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,26 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function EmbedTemplate({ children }: { children?: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
tw="text-white bg-[#131416aa] w-full h-full flex flex-col rounded-[.5em] border-solid"
|
||||
style={{
|
||||
borderColor: "#2D2E32",
|
||||
borderWidth: "1px",
|
||||
borderLeftColor: "#323337",
|
||||
borderLeftWidth: "4px",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<div tw="absolute bottom-0 left-0 ml-[.5em] flex items-center">
|
||||
<div tw="text-[2em] font-[shicon] pb-[.1em] -mr-[.3em] text-white/25">
|
||||
|
||||
</div>
|
||||
<div tw="flex flex-col">
|
||||
<div tw="leading-none text-white/75">Powered By</div>
|
||||
<div tw="text-[1.3em] leading-[1] font-[ShinShin]">Enka.Network</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function arrRandom<T>(arr: readonly T[]): T | undefined {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { featherImage } from "@/assets/feather";
|
||||
import { font, iconFont, sansFont } from "@/assets/fonts";
|
||||
import { EnkaIcon } from "@/assets/icons";
|
||||
import type { library } from "@/assets/icons-library";
|
||||
import ListDiffExample from "@/assets/listoriginal.webp" with { type: "file" };
|
||||
import DiffPageExample from "@/assets/ororo.png" with { type: "file" };
|
||||
import shadeSvg from "@/assets/shade.svg" with { type: "file" };
|
||||
import { star } from "@/assets/stars";
|
||||
@@ -75,6 +76,7 @@ export function card(app: App) {
|
||||
}))
|
||||
.get("/genshin/diff", DiffPage)
|
||||
.get("/genshin/diff/ororo.png", () => file(DiffPageExample))
|
||||
.get("/genshin/diff/listoriginal.webp", () => file(ListDiffExample))
|
||||
.head(
|
||||
"/genshin/:uid/preload",
|
||||
async ({ params: { uid }, log }) => {
|
||||
@@ -135,7 +137,7 @@ export function card(app: App) {
|
||||
),
|
||||
feature("API") || uid !== "775956514"
|
||||
? deduper.acquire(`enka-${uid}`, () =>
|
||||
fetchEnka(uid, true).finally(
|
||||
fetchEnka(uid, false).finally(
|
||||
() => log`Enka data loaded`,
|
||||
),
|
||||
)
|
||||
@@ -149,9 +151,7 @@ export function card(app: App) {
|
||||
|
||||
return ListPage(
|
||||
data,
|
||||
pfps[
|
||||
`${data.playerInfo.profilePicture.id ?? 1}`
|
||||
].IconPath.replace("_Circle", ""),
|
||||
pfps[`${data.playerInfo.profilePicture.id ?? 1}`].IconPath,
|
||||
namecards[`${data.playerInfo.nameCardId ?? 210001}`].Icon,
|
||||
namecards,
|
||||
avatarStore,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { EmbedTemplate } from "@/lib/comp/embed";
|
||||
|
||||
import { CardResponse } from "./response";
|
||||
|
||||
export async function ErrorPage(message: string) {
|
||||
return CardResponse(
|
||||
<div tw="flex p-[1em] gap-[.5em]">
|
||||
<div></div>
|
||||
<pre tw="m-0 text-[1.1em] leading-none whitespace-pre-wrap">
|
||||
{message}
|
||||
</pre>
|
||||
</div>,
|
||||
<EmbedTemplate>
|
||||
<div tw="flex p-[1em] gap-[.5em]">
|
||||
<div></div>
|
||||
<pre tw="m-0 text-[1.1em] leading-none whitespace-pre-wrap">
|
||||
{message}
|
||||
</pre>
|
||||
</div>
|
||||
</EmbedTemplate>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import type { ResolvedRoot } from "enka.ts";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import {
|
||||
elements,
|
||||
listBadges,
|
||||
listBg,
|
||||
listNcFrame,
|
||||
listRtop,
|
||||
smug,
|
||||
} from "@/assets";
|
||||
import { assets, getAvatar } from "@/lib/assets";
|
||||
import type { Logger } from "@/lib/logger";
|
||||
import { CharacterNamecardMap } from "@/lib/namecards";
|
||||
import type { AvatarStore, EnkaLocsLanguage, EnkaNamecards } from "@/type/enka";
|
||||
import { arrRandom } from "@/lib/util";
|
||||
import {
|
||||
ElementList,
|
||||
type AvatarStore,
|
||||
type EnkaLocsLanguage,
|
||||
type EnkaNamecards,
|
||||
} from "@/type/enka";
|
||||
|
||||
import { CardResponse } from "./response";
|
||||
|
||||
export async function ListPage(
|
||||
{ playerInfo: p, uid }: ResolvedRoot,
|
||||
{ playerInfo: p, uid, avatarInfoList }: ResolvedRoot,
|
||||
pfp: string,
|
||||
namecard: string,
|
||||
namecards: EnkaNamecards,
|
||||
@@ -16,7 +31,7 @@ export async function ListPage(
|
||||
locs: EnkaLocsLanguage,
|
||||
log: Logger,
|
||||
) {
|
||||
return CardResponse(
|
||||
const _s = (
|
||||
<div tw="flex font-[Open_Sans]">
|
||||
{/* left bar */}
|
||||
<div
|
||||
@@ -162,19 +177,281 @@ export async function ListPage(
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return CardResponse(
|
||||
<div tw="flex bg-black relative h-full w-full font-[ShinShin] text-[#7E6E5C]">
|
||||
<img src="/bg" tw="absolute inset-0" />
|
||||
|
||||
{/* left section */}
|
||||
<div tw="absolute left-[2.2em] top-[.7em] h-[44.4em] w-[33em] flex flex-col">
|
||||
{/* top */}
|
||||
<div tw="relative flex min-w-0 w-full min-h-0 aspect-[840/400] overflow-hidden shrink-0">
|
||||
<img
|
||||
src="/namecard"
|
||||
tw="absolute top-0 left-0 right-0 min-h-full object-cover scale-101"
|
||||
/>
|
||||
<img
|
||||
src="/frame"
|
||||
tw="absolute top-0 left-0 right-0 min-h-full object-cover opacity-20"
|
||||
/>
|
||||
|
||||
<div tw="absolute inset-0 z-10 flex flex-col items-center">
|
||||
<div
|
||||
tw="text-white text-[1.13em] mt-[1.05em] tracking-tight"
|
||||
style={{
|
||||
textShadow:
|
||||
"0 1px 1px rgb(0 0 0 / 0.55), 0 0 2px rgb(0 0 0 / 0.35)",
|
||||
}}
|
||||
>
|
||||
UID {uid}
|
||||
</div>
|
||||
|
||||
<div
|
||||
tw="relative rounded-full overflow-hidden mt-[.6em]"
|
||||
style={{
|
||||
width: "92px",
|
||||
height: "92px",
|
||||
background: "#DCDAB9",
|
||||
padding: "1px",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
tw="rounded-full overflow-hidden w-full h-full"
|
||||
style={{
|
||||
border: "4px solid #F3EBD9",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<img src="/pfp" tw="w-full h-full object-cover bg-[#DAAB86]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
tw="text-white text-[1.53em] mt-[.74em] tracking-tight"
|
||||
style={{
|
||||
textShadow:
|
||||
"0 1px 2px rgb(0 0 0 / 0.65), 0 0 4px rgb(0 0 0 / 0.4)",
|
||||
}}
|
||||
>
|
||||
{p.nickname}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div tw="p-[1.3em] px-[1.8em] h-full flex flex-col gap-[1.5em]">
|
||||
{/* middle */}
|
||||
<div tw="flex flex-col gap-[.4em]">
|
||||
<div tw="flex items-center justify-between bg-[#A7B982] text-white tracking-tight px-[.5em] py-[.08em] text-[1.2em]">
|
||||
<span tw="text-[.97em]">Adventure Rank</span>
|
||||
<span tw="text-[1.04em]">{p.level}</span>
|
||||
</div>
|
||||
<div tw="flex items-center justify-between bg-[#CDBBA5] text-white tracking-tight px-[.5em] py-[.08em] text-[1.2em]">
|
||||
<span tw="text-[.97em]">World Level</span>
|
||||
<span tw="text-[1.04em]">{p.worldLevel}</span>
|
||||
</div>
|
||||
<div
|
||||
tw="tracking-tight px-[.4em] py-[.15em] text-[1.165em] h-[4.25em]"
|
||||
style={{ border: "2px #E4DCD0 solid" }}
|
||||
>
|
||||
{p.signature}
|
||||
</div>
|
||||
</div>
|
||||
{/* bottom */}
|
||||
<div tw="flex flex-col gap-[.25em]">
|
||||
{(
|
||||
[
|
||||
[
|
||||
"/b/achievements",
|
||||
"Total Achievements",
|
||||
p.finishAchievementNum,
|
||||
"",
|
||||
],
|
||||
[
|
||||
"/b/fren",
|
||||
"Characters at maximum Friendship Level",
|
||||
p.fetterCount ?? 0,
|
||||
"text-[.89em] leading-none max-w-2/3",
|
||||
],
|
||||
[
|
||||
"/b/abyss",
|
||||
"Spiral Abyss",
|
||||
p.towerFloorIndex ? (
|
||||
<div tw="flex items-center leading-none">
|
||||
{p.towerFloorIndex}-{p.towerLevelIndex}
|
||||
{p.towerStarIndex && (
|
||||
<div tw="flex items-center">
|
||||
<div tw="mx-[.3em] h-2/3 mb-[.2em] w-0.5 bg-[#E3DED6] shrink-0" />
|
||||
<span>
|
||||
{p.towerStarIndex.toString().padStart(2, " ")}
|
||||
</span>
|
||||
<img src="/b/abyssStar" tw="-mt-1.5 mx-[.1em]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div tw="text-[#C7B7A5]">Not yet attempted</div>
|
||||
),
|
||||
"flex items-center",
|
||||
],
|
||||
[
|
||||
"/b/theater",
|
||||
"Imaginarium Theater",
|
||||
p.theaterActIndex ? (
|
||||
<div tw="flex items-center">
|
||||
Act {p.theaterActIndex}
|
||||
<div tw="mx-[.3em] h-2/3 mb-[.2em] w-0.5 bg-[#E3DED6] shrink-0" />
|
||||
<span>
|
||||
{p.theaterStarIndex!.toString().padStart(2, " ")}
|
||||
</span>
|
||||
<img src="/b/theaterStar" tw="-mt-1.5" />
|
||||
</div>
|
||||
) : (
|
||||
<div tw="text-[#C7B7A5]">Not yet attempted</div>
|
||||
),
|
||||
"flex items-center",
|
||||
],
|
||||
[
|
||||
"/b/stygian",
|
||||
"Stygian Onslaught",
|
||||
p.stygianSeconds ? (
|
||||
<div tw="flex items-center gap-[.2em]">
|
||||
<img src="/i/stygian" tw="w-8 h-8" />
|
||||
<div tw="mt-[.2em]">{p.stygianSeconds}s</div>
|
||||
</div>
|
||||
) : (
|
||||
<div tw="text-[#C7B7A5]">Not yet attempted</div>
|
||||
),
|
||||
"flex items-center",
|
||||
],
|
||||
] satisfies [string, string, ReactNode, string][]
|
||||
).map(([icon, label, val, tw], i) => (
|
||||
<>
|
||||
{i > 0 && <div tw="w-full bg-[#E9E2D9] h-0.5" />}
|
||||
<div tw="flex text-[1.12em] tracking-tight gap-[.45em]">
|
||||
<img src={icon} />
|
||||
<div tw={tw}>{label}</div>
|
||||
<div tw="grow" />
|
||||
<span tw="shrink-0 flex items-center">{val}</span>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* right section */}
|
||||
<div tw="absolute right-[1.8em] top-[2.3em] h-[41.5em] w-[34em] flex flex-col">
|
||||
{/* top */}
|
||||
<div tw="relative flex min-w-0 w-full min-h-0 aspect-[546/48] shrink-0">
|
||||
<img
|
||||
src="/rtop"
|
||||
tw="absolute top-0 left-0 right-0 min-h-full object-cover"
|
||||
/>
|
||||
<div tw="absolute inset-0 z-10 flex items-center gap-1">
|
||||
<img src="/smug" tw="h-12 w-12 ml-2" />
|
||||
<div tw="flex flex-col">
|
||||
<div tw="-mb-1">See each character:</div>
|
||||
<div tw="flex text-[1.1em] leading-none">
|
||||
https://card.astrxl.dev/u/{uid}/
|
||||
<div tw="border-b-2 border-b-current border-dashed bg-black/10 -mt-0.5 rounded-t-sm">
|
||||
{(arrRandom(
|
||||
p.showAvatarInfoList
|
||||
.map((e) => e.avatar.name)
|
||||
.filter((e) => e.length <= 7),
|
||||
) ??
|
||||
arrRandom(
|
||||
Object.values(avatars)
|
||||
.map((e) => locs[e.NameTextMapHash])
|
||||
.filter((e) => e.length <= 7),
|
||||
))!.toLowerCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* grid */}
|
||||
<div tw="mt-[2em] mx-[1.35em] h-[28.5em] grid grid-cols-4 gap-x-[1.2em] gap-y-[1.2em]">
|
||||
{p.showAvatarInfoList
|
||||
.map((a) => ({
|
||||
a,
|
||||
c: avatars[a.avatar.avatarId],
|
||||
cons: avatarInfoList!.find(
|
||||
(e) => e.avatar.avatarId === a.avatar.avatarId,
|
||||
)!.talentIdList?.length,
|
||||
}))
|
||||
.map(({ a, c, cons }) => (
|
||||
<div tw="relative flex flex-col rounded-[.3em] overflow-hidden shadow-[0_.08em_.35em_rgba(0,0,0,.18)]">
|
||||
<div tw="bg-[#E9E6DB] absolute top-0 left-0 w-full aspect-square" />
|
||||
<img
|
||||
src={`/pfp/${a.avatar.avatarId}`}
|
||||
tw="aspect-square rounded-br-[1.5em]"
|
||||
style={{
|
||||
background: `rgba(${
|
||||
c?.QualityType === "QUALITY_ORANGE"
|
||||
? "200,124,36"
|
||||
: "148,112,187"
|
||||
}) linear-gradient(136deg,rgba(92,82,120,.42),rgba(255,255,255,.04),transparent)`,
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
tw="absolute top-[.2em] left-[.2em] w-[1.7em] h-[1.7em]"
|
||||
src={`/e/${ElementList[(a as unknown as { energyType: number }).energyType]?.toLowerCase()}`}
|
||||
/>
|
||||
<div tw="flex text-[#404B57] bg-[#E9E6DB] items-center justify-center grow text-[1.1em]">
|
||||
Lv. <div>{a.level}</div>
|
||||
</div>
|
||||
{cons && (
|
||||
<div
|
||||
tw={`absolute bottom-[2em] left-[.2em] flex items-center justify-center pt-[.2em] w-[1.2em] rounded-[.2em] h-[1.2em] ${cons >= 6 ? "bg-[#EAC847] text-[#8C5E10]" : "text-white bg-black/50"}`}
|
||||
>
|
||||
{cons}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* powered by */}
|
||||
<div tw="absolute bottom-0 left-0 ml-[.5em] flex items-center">
|
||||
<div tw="text-[2em] font-[shicon] pb-[.1em] -mr-[.3em] text-black/25">
|
||||
|
||||
</div>
|
||||
<div tw="flex flex-col">
|
||||
<div tw="leading-none text-black/75">Powered By</div>
|
||||
<div tw="text-[1.3em] leading-[1] font-[ShinShin] text-black">
|
||||
Enka.Network
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
[
|
||||
{ src: "/bg", data: listBg.arrayBuffer(), persistent: true },
|
||||
{ src: "/pfp", data: assets.load(pfp) },
|
||||
{ src: "/namecard", data: assets.load(namecard) },
|
||||
{ src: "/i/achi", data: assets.load("/img/achievements.png") },
|
||||
{ src: "/i/abyss", data: assets.load("/img/abyss.png") },
|
||||
{ src: "/i/theater", data: assets.load("/ui/UI_Icon_Tower_Star1.png") },
|
||||
{ src: "/frame", data: listNcFrame.arrayBuffer(), persistent: true },
|
||||
{ src: "/rtop", data: listRtop.arrayBuffer(), persistent: true },
|
||||
{ src: "/smug", data: smug.arrayBuffer(), persistent: true },
|
||||
// { src: "/i/achi", data: assets.load("/img/achievements.png") },
|
||||
// { src: "/i/abyss", data: assets.load("/img/abyss.png") },
|
||||
// { src: "/i/theater", data: assets.load("/ui/UI_Icon_Tower_Star1.png") },
|
||||
{
|
||||
src: "/i/stygian",
|
||||
data: assets.load(
|
||||
`/ui/UI_LeyLineChallenge_Medal_${p.stygianIndex ?? 1}.png`,
|
||||
),
|
||||
},
|
||||
...Object.entries(listBadges).map(([k, v]) => ({
|
||||
src: `/b/${k}`,
|
||||
data: () => v.arrayBuffer(),
|
||||
persistent: true as const,
|
||||
})),
|
||||
...Object.entries(elements).map(([k, v]) => ({
|
||||
src: `/e/${k}`,
|
||||
data: () => v.arrayBuffer(),
|
||||
persistent: true as const,
|
||||
})),
|
||||
...(p.showAvatarInfoList
|
||||
.flatMap((a) => {
|
||||
const avatar = getAvatar(avatars, p, a.avatar.avatarId);
|
||||
@@ -189,12 +466,16 @@ export async function ListPage(
|
||||
: null,
|
||||
{
|
||||
src: `/pfp/${a.avatar.avatarId}`,
|
||||
data: assets.load(avatar.SideIconName.replace("_Side", "")),
|
||||
data: assets.load(
|
||||
(a.costumeId && avatar.Costumes?.[a.costumeId].Icon) ||
|
||||
avatar.SideIconName.replace("_Side", ""),
|
||||
),
|
||||
},
|
||||
];
|
||||
})
|
||||
.filter(Boolean) as { src: string; data: Promise<ArrayBuffer> }[]),
|
||||
],
|
||||
{ width: 1200, height: 732 },
|
||||
log,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,59 +9,58 @@ import { createLogger } from "@/lib/logger";
|
||||
|
||||
export async function CardResponse(
|
||||
children: ReactNode,
|
||||
assets: MaybePromise<{
|
||||
src: string;
|
||||
data: MaybePromise<ArrayBuffer | Buffer>;
|
||||
}>[] = [],
|
||||
assets: (
|
||||
| MaybePromise<{
|
||||
src: string;
|
||||
data: MaybePromise<ArrayBuffer | Buffer>;
|
||||
persistent?: false;
|
||||
}>
|
||||
| MaybePromise<{
|
||||
src: string;
|
||||
data:
|
||||
| MaybePromise<ArrayBuffer | Buffer>
|
||||
| (() => MaybePromise<ArrayBuffer | Buffer>);
|
||||
persistent: true;
|
||||
}>
|
||||
)[] = [],
|
||||
// same ratio as card
|
||||
size = { width: 1228, height: 512 },
|
||||
log = createLogger("RES"),
|
||||
) {
|
||||
return new ImageResponse(
|
||||
<div
|
||||
tw="text-white bg-[#131416aa] w-full h-full flex flex-col rounded-[.5em] border-solid"
|
||||
style={{
|
||||
borderColor: "#2D2E32",
|
||||
borderWidth: "1px",
|
||||
borderLeftColor: "#323337",
|
||||
borderLeftWidth: "4px",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<div tw="absolute bottom-0 left-0 ml-[.5em] flex items-center">
|
||||
<div tw="text-[2em] font-[shicon] pb-[.1em] -mr-[.3em] text-white/25">
|
||||
|
||||
</div>
|
||||
<div tw="flex flex-col">
|
||||
<div tw="leading-none text-white/75">Powered By</div>
|
||||
<div tw="text-[1.3em] leading-[1] font-[ShinShin]">Enka.Network</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
fonts: await Promise.all([
|
||||
font.arrayBuffer(),
|
||||
sansFont.arrayBuffer(),
|
||||
iconFont.arrayBuffer(),
|
||||
]),
|
||||
format: "webp",
|
||||
// same ratio as card
|
||||
width: 1228,
|
||||
fetchedResources: await Promise.all(assets)
|
||||
.then((list) =>
|
||||
Promise.all(
|
||||
list.map(async (a) => ({ src: a.src, data: await a.data })),
|
||||
),
|
||||
)
|
||||
.then((l) => {
|
||||
const res = l.filter((r) => {
|
||||
if (r.data.byteLength) return true;
|
||||
if (!feature("API")) console.warn(`[DEV] ${r.src} is blank`);
|
||||
return false;
|
||||
});
|
||||
log`Image resources loaded\n${res.map((r) => `- ${r.src} ${(r.data.byteLength / 1000).toFixed(2)}Kb`).join("\n")}`;
|
||||
return res;
|
||||
}),
|
||||
height: 512,
|
||||
// drawDebugBorder: true,
|
||||
},
|
||||
);
|
||||
const resources = await Promise.all(assets)
|
||||
.then((list) =>
|
||||
Promise.all(
|
||||
list.map(async (a) => ({
|
||||
src: a.src,
|
||||
data: await a.data,
|
||||
persistent: a.persistent,
|
||||
})),
|
||||
),
|
||||
)
|
||||
.then((l) => {
|
||||
const res = l.filter((r) => {
|
||||
if (typeof r.data === "function") return true;
|
||||
if (r.data.byteLength) return true;
|
||||
if (!feature("API")) console.warn(`[DEV] ${r.src} is blank`);
|
||||
return false;
|
||||
});
|
||||
log`Image resources loaded\n${res.map((r) => `- ${r.src} ${typeof r.data === "function" ? "[FN]" : `${(r.data.byteLength / 1000).toFixed(2)}Kb`}`).join("\n")}`;
|
||||
return res;
|
||||
});
|
||||
|
||||
return new ImageResponse(children, {
|
||||
fonts: await Promise.all([
|
||||
font.arrayBuffer(),
|
||||
sansFont.arrayBuffer(),
|
||||
iconFont.arrayBuffer(),
|
||||
]),
|
||||
format: "webp",
|
||||
fetchedResources: resources.filter((a) => !a.persistent) as {
|
||||
src: string;
|
||||
data: ArrayBuffer | Buffer;
|
||||
}[],
|
||||
persistentImages: resources.filter((a) => a.persistent),
|
||||
...size,
|
||||
// drawDebugBorder: true,
|
||||
});
|
||||
}
|
||||
|
||||