feat: new character list card
@@ -22,3 +22,8 @@ declare module "*.png" {
|
|||||||
const file: string;
|
const file: string;
|
||||||
export default file;
|
export default file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "*.webp" {
|
||||||
|
const file: string;
|
||||||
|
export default file;
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,8 +31,6 @@
|
|||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
display: block;
|
display: block;
|
||||||
width: 1870px;
|
|
||||||
height: 778px;
|
|
||||||
margin: 12px auto;
|
margin: 12px auto;
|
||||||
background: #000;
|
background: #000;
|
||||||
}
|
}
|
||||||
@@ -64,14 +62,28 @@
|
|||||||
<canvas id="canvas" width="1870" height="778"></canvas>
|
<canvas id="canvas" width="1870" height="778"></canvas>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const MINE = "/v1/card/genshin/775956514/ororon";
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const ORIG = "/v1/card/genshin/diff/ororo.png";
|
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}
|
* @type {HTMLCanvasElement}
|
||||||
*/
|
*/
|
||||||
const canvas = document.getElementById("canvas");
|
const canvas = document.getElementById("canvas");
|
||||||
|
canvas.width = W;
|
||||||
|
canvas.height = H;
|
||||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||||
|
|
||||||
const mode = document.getElementById("mode");
|
const mode = document.getElementById("mode");
|
||||||
@@ -231,9 +243,9 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// window.addEventListener("focus", () => {
|
// window.addEventListener("focus", () => {
|
||||||
// refresh();
|
// refresh();
|
||||||
// });
|
// });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</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 { font, iconFont, sansFont } from "@/assets/fonts";
|
||||||
import { EnkaIcon } from "@/assets/icons";
|
import { EnkaIcon } from "@/assets/icons";
|
||||||
import type { library } from "@/assets/icons-library";
|
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 DiffPageExample from "@/assets/ororo.png" with { type: "file" };
|
||||||
import shadeSvg from "@/assets/shade.svg" with { type: "file" };
|
import shadeSvg from "@/assets/shade.svg" with { type: "file" };
|
||||||
import { star } from "@/assets/stars";
|
import { star } from "@/assets/stars";
|
||||||
@@ -75,6 +76,7 @@ export function card(app: App) {
|
|||||||
}))
|
}))
|
||||||
.get("/genshin/diff", DiffPage)
|
.get("/genshin/diff", DiffPage)
|
||||||
.get("/genshin/diff/ororo.png", () => file(DiffPageExample))
|
.get("/genshin/diff/ororo.png", () => file(DiffPageExample))
|
||||||
|
.get("/genshin/diff/listoriginal.webp", () => file(ListDiffExample))
|
||||||
.head(
|
.head(
|
||||||
"/genshin/:uid/preload",
|
"/genshin/:uid/preload",
|
||||||
async ({ params: { uid }, log }) => {
|
async ({ params: { uid }, log }) => {
|
||||||
@@ -135,7 +137,7 @@ export function card(app: App) {
|
|||||||
),
|
),
|
||||||
feature("API") || uid !== "775956514"
|
feature("API") || uid !== "775956514"
|
||||||
? deduper.acquire(`enka-${uid}`, () =>
|
? deduper.acquire(`enka-${uid}`, () =>
|
||||||
fetchEnka(uid, true).finally(
|
fetchEnka(uid, false).finally(
|
||||||
() => log`Enka data loaded`,
|
() => log`Enka data loaded`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -149,9 +151,7 @@ export function card(app: App) {
|
|||||||
|
|
||||||
return ListPage(
|
return ListPage(
|
||||||
data,
|
data,
|
||||||
pfps[
|
pfps[`${data.playerInfo.profilePicture.id ?? 1}`].IconPath,
|
||||||
`${data.playerInfo.profilePicture.id ?? 1}`
|
|
||||||
].IconPath.replace("_Circle", ""),
|
|
||||||
namecards[`${data.playerInfo.nameCardId ?? 210001}`].Icon,
|
namecards[`${data.playerInfo.nameCardId ?? 210001}`].Icon,
|
||||||
namecards,
|
namecards,
|
||||||
avatarStore,
|
avatarStore,
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
import { EmbedTemplate } from "@/lib/comp/embed";
|
||||||
|
|
||||||
import { CardResponse } from "./response";
|
import { CardResponse } from "./response";
|
||||||
|
|
||||||
export async function ErrorPage(message: string) {
|
export async function ErrorPage(message: string) {
|
||||||
return CardResponse(
|
return CardResponse(
|
||||||
<div tw="flex p-[1em] gap-[.5em]">
|
<EmbedTemplate>
|
||||||
<div></div>
|
<div tw="flex p-[1em] gap-[.5em]">
|
||||||
<pre tw="m-0 text-[1.1em] leading-none whitespace-pre-wrap">
|
<div></div>
|
||||||
{message}
|
<pre tw="m-0 text-[1.1em] leading-none whitespace-pre-wrap">
|
||||||
</pre>
|
{message}
|
||||||
</div>,
|
</pre>
|
||||||
|
</div>
|
||||||
|
</EmbedTemplate>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
import type { ResolvedRoot } from "enka.ts";
|
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 { assets, getAvatar } from "@/lib/assets";
|
||||||
import type { Logger } from "@/lib/logger";
|
import type { Logger } from "@/lib/logger";
|
||||||
import { CharacterNamecardMap } from "@/lib/namecards";
|
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";
|
import { CardResponse } from "./response";
|
||||||
|
|
||||||
export async function ListPage(
|
export async function ListPage(
|
||||||
{ playerInfo: p, uid }: ResolvedRoot,
|
{ playerInfo: p, uid, avatarInfoList }: ResolvedRoot,
|
||||||
pfp: string,
|
pfp: string,
|
||||||
namecard: string,
|
namecard: string,
|
||||||
namecards: EnkaNamecards,
|
namecards: EnkaNamecards,
|
||||||
@@ -16,7 +31,7 @@ export async function ListPage(
|
|||||||
locs: EnkaLocsLanguage,
|
locs: EnkaLocsLanguage,
|
||||||
log: Logger,
|
log: Logger,
|
||||||
) {
|
) {
|
||||||
return CardResponse(
|
const _s = (
|
||||||
<div tw="flex font-[Open_Sans]">
|
<div tw="flex font-[Open_Sans]">
|
||||||
{/* left bar */}
|
{/* left bar */}
|
||||||
<div
|
<div
|
||||||
@@ -162,19 +177,281 @@ export async function ListPage(
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</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>,
|
</div>,
|
||||||
[
|
[
|
||||||
|
{ src: "/bg", data: listBg.arrayBuffer(), persistent: true },
|
||||||
{ src: "/pfp", data: assets.load(pfp) },
|
{ src: "/pfp", data: assets.load(pfp) },
|
||||||
{ src: "/namecard", data: assets.load(namecard) },
|
{ src: "/namecard", data: assets.load(namecard) },
|
||||||
{ src: "/i/achi", data: assets.load("/img/achievements.png") },
|
{ src: "/frame", data: listNcFrame.arrayBuffer(), persistent: true },
|
||||||
{ src: "/i/abyss", data: assets.load("/img/abyss.png") },
|
{ src: "/rtop", data: listRtop.arrayBuffer(), persistent: true },
|
||||||
{ src: "/i/theater", data: assets.load("/ui/UI_Icon_Tower_Star1.png") },
|
{ 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",
|
src: "/i/stygian",
|
||||||
data: assets.load(
|
data: assets.load(
|
||||||
`/ui/UI_LeyLineChallenge_Medal_${p.stygianIndex ?? 1}.png`,
|
`/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
|
...(p.showAvatarInfoList
|
||||||
.flatMap((a) => {
|
.flatMap((a) => {
|
||||||
const avatar = getAvatar(avatars, p, a.avatar.avatarId);
|
const avatar = getAvatar(avatars, p, a.avatar.avatarId);
|
||||||
@@ -189,12 +466,16 @@ export async function ListPage(
|
|||||||
: null,
|
: null,
|
||||||
{
|
{
|
||||||
src: `/pfp/${a.avatar.avatarId}`,
|
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> }[]),
|
.filter(Boolean) as { src: string; data: Promise<ArrayBuffer> }[]),
|
||||||
],
|
],
|
||||||
|
{ width: 1200, height: 732 },
|
||||||
log,
|
log,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,59 +9,58 @@ import { createLogger } from "@/lib/logger";
|
|||||||
|
|
||||||
export async function CardResponse(
|
export async function CardResponse(
|
||||||
children: ReactNode,
|
children: ReactNode,
|
||||||
assets: MaybePromise<{
|
assets: (
|
||||||
src: string;
|
| MaybePromise<{
|
||||||
data: MaybePromise<ArrayBuffer | Buffer>;
|
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"),
|
log = createLogger("RES"),
|
||||||
) {
|
) {
|
||||||
return new ImageResponse(
|
const resources = await Promise.all(assets)
|
||||||
<div
|
.then((list) =>
|
||||||
tw="text-white bg-[#131416aa] w-full h-full flex flex-col rounded-[.5em] border-solid"
|
Promise.all(
|
||||||
style={{
|
list.map(async (a) => ({
|
||||||
borderColor: "#2D2E32",
|
src: a.src,
|
||||||
borderWidth: "1px",
|
data: await a.data,
|
||||||
borderLeftColor: "#323337",
|
persistent: a.persistent,
|
||||||
borderLeftWidth: "4px",
|
})),
|
||||||
}}
|
),
|
||||||
>
|
)
|
||||||
{children}
|
.then((l) => {
|
||||||
<div tw="absolute bottom-0 left-0 ml-[.5em] flex items-center">
|
const res = l.filter((r) => {
|
||||||
<div tw="text-[2em] font-[shicon] pb-[.1em] -mr-[.3em] text-white/25">
|
if (typeof r.data === "function") return true;
|
||||||
|
if (r.data.byteLength) return true;
|
||||||
</div>
|
if (!feature("API")) console.warn(`[DEV] ${r.src} is blank`);
|
||||||
<div tw="flex flex-col">
|
return false;
|
||||||
<div tw="leading-none text-white/75">Powered By</div>
|
});
|
||||||
<div tw="text-[1.3em] leading-[1] font-[ShinShin]">Enka.Network</div>
|
log`Image resources loaded\n${res.map((r) => `- ${r.src} ${typeof r.data === "function" ? "[FN]" : `${(r.data.byteLength / 1000).toFixed(2)}Kb`}`).join("\n")}`;
|
||||||
</div>
|
return res;
|
||||||
</div>
|
});
|
||||||
</div>,
|
|
||||||
{
|
return new ImageResponse(children, {
|
||||||
fonts: await Promise.all([
|
fonts: await Promise.all([
|
||||||
font.arrayBuffer(),
|
font.arrayBuffer(),
|
||||||
sansFont.arrayBuffer(),
|
sansFont.arrayBuffer(),
|
||||||
iconFont.arrayBuffer(),
|
iconFont.arrayBuffer(),
|
||||||
]),
|
]),
|
||||||
format: "webp",
|
format: "webp",
|
||||||
// same ratio as card
|
fetchedResources: resources.filter((a) => !a.persistent) as {
|
||||||
width: 1228,
|
src: string;
|
||||||
fetchedResources: await Promise.all(assets)
|
data: ArrayBuffer | Buffer;
|
||||||
.then((list) =>
|
}[],
|
||||||
Promise.all(
|
persistentImages: resources.filter((a) => a.persistent),
|
||||||
list.map(async (a) => ({ src: a.src, data: await a.data })),
|
...size,
|
||||||
),
|
// drawDebugBorder: true,
|
||||||
)
|
});
|
||||||
.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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||