feat: full visuals
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
"name": "sastify-api",
|
"name": "sastify-api",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/openapi": "^1.4.14",
|
"@elysiajs/openapi": "^1.4.14",
|
||||||
|
"@napi-rs/canvas": "^0.1.96",
|
||||||
"@takumi-rs/image-response": "^0.71.7",
|
"@takumi-rs/image-response": "^0.71.7",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"elysia": "latest",
|
"elysia": "latest",
|
||||||
@@ -56,6 +57,30 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas": ["@napi-rs/[email protected]", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.96", "@napi-rs/canvas-darwin-arm64": "0.1.96", "@napi-rs/canvas-darwin-x64": "0.1.96", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.96", "@napi-rs/canvas-linux-arm64-gnu": "0.1.96", "@napi-rs/canvas-linux-arm64-musl": "0.1.96", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.96", "@napi-rs/canvas-linux-x64-gnu": "0.1.96", "@napi-rs/canvas-linux-x64-musl": "0.1.96", "@napi-rs/canvas-win32-arm64-msvc": "0.1.96", "@napi-rs/canvas-win32-x64-msvc": "0.1.96" } }, "sha512-6NNmNxvoJKeucVjxaaRUt3La2i5jShgiAbaY3G/72s1Vp3U06XPrAIxkAjBxpDcamEn/t+WJ4OOlGmvILo4/Ew=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-android-arm64": ["@napi-rs/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-ew1sPrN3dGdZ3L4FoohPfnjq0f9/Jk7o+wP7HkQZokcXgIUD6FIyICEWGhMYzv53j63wUcPvZeAwgewX58/egg=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-darwin-arm64": ["@napi-rs/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Q/wOXZ5PzTqpdmA5eUOcegCf4Go/zz3aZ5DlzSeDpOjFmfwMKh8EzLAoweQ+mJVagcHQyzoJhaTEnrO68TNyNg=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-darwin-x64": ["@napi-rs/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-UrXiQz28tQEvGM1qvyptewOAfmUrrd5+wvi6Rzjj2VprZI8iZ2KIvBD2lTTG1bVF95AbeDeG7PJA0D9sLKaOFA=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-I90ODxweD8aEP6XKU/NU+biso95MwCtQ2F46dUvhec1HesFi0tq/tAJkYic/1aBSiO/1kGKmSeD1B0duOHhEHQ=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-Dx/0+RFV++w3PcRy+4xNXkghhXjA5d0Mw1bs95emn5Llinp1vihMaA6WJt3oYv2LAHc36+gnrhIBsPhUyI2SGw=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-UvOi7fii3IE2KDfEfhh8m+LpzSRvhGK7o1eho99M2M0HTik11k3GX+2qgVx9EtujN3/bhFFS1kSO3+vPMaJ0Mg=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-MBSukhGCQ5nRtf9NbFYWOU080yqkZU1PbuH4o1ROvB4CbPl12fchDR35tU83Wz8gWIM9JTn99lBn9DenPIv7Ig=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-I/ccu2SstyKiV3HIeVzyBIWfrJo8cN7+MSQZPnabewWV6hfJ2nY7Df2WqOHmobBRUw84uGR6zfQHsUEio/m5Vg=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-x64-musl": ["@napi-rs/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-H3uov7qnTl73GDT4h52lAqpJPsl1tIUyNPWJyhQ6gHakohNqqRq3uf80+NEpzcytKGEOENP1wX3yGwZxhjiWEQ=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-ATp6Y+djOjYtkfV/VRH7CZ8I1MEtkUQBmKUbuWw5zWEHHqfL0cEcInE4Cxgx7zkNAhEdBbnH8HMVrqNp+/gwxA=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-UYGdTltVd+Z8mcIuoqGmAXXUvwH5CLf2M6mIB5B0/JmX5J041jETjqtSYl7gN+aj3k1by/SG6sS0hAwCqyK7zw=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/[email protected]", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
"@nodelib/fs.scandir": ["@nodelib/[email protected]", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
"@nodelib/fs.stat": ["@nodelib/[email protected]", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
"@nodelib/fs.stat": ["@nodelib/[email protected]", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||||
|
|||||||
Vendored
+5
@@ -7,3 +7,8 @@ declare module "*.svg" {
|
|||||||
const file: string;
|
const file: string;
|
||||||
export default file;
|
export default file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "*.jpg" {
|
||||||
|
const file: string;
|
||||||
|
export default file;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/openapi": "^1.4.14",
|
"@elysiajs/openapi": "^1.4.14",
|
||||||
|
"@napi-rs/canvas": "^0.1.96",
|
||||||
"@takumi-rs/image-response": "^0.71.7",
|
"@takumi-rs/image-response": "^0.71.7",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"elysia": "latest",
|
"elysia": "latest",
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { createCanvas, loadImage } from "@napi-rs/canvas";
|
||||||
|
import { file } from "bun";
|
||||||
|
import overlayImage from "@/assets/overlay.jpg";
|
||||||
|
|
||||||
|
const elementColors: Record<string, string> = {
|
||||||
|
Rock: "#bb9f4b",
|
||||||
|
Wind: "#52B0B1",
|
||||||
|
Ice: "#46A8BA",
|
||||||
|
Water: "#84A1C6",
|
||||||
|
Electric: "#9876AD",
|
||||||
|
Fire: "#BA8C83",
|
||||||
|
Grass: "#2D8E34",
|
||||||
|
None: "#94a0a7",
|
||||||
|
};
|
||||||
|
|
||||||
|
function avgColor(data: Uint8ClampedArray) {
|
||||||
|
let r = 0,
|
||||||
|
g = 0,
|
||||||
|
b = 0,
|
||||||
|
n = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
const a = data[i + 3];
|
||||||
|
if (a < 10) continue;
|
||||||
|
|
||||||
|
r += data[i];
|
||||||
|
g += data[i + 1];
|
||||||
|
b += data[i + 2];
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!n) return [0, 0, 0];
|
||||||
|
|
||||||
|
return [Math.round(r / n), Math.round(g / n), Math.round(b / n)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgb(r: number, g: number, b: number) {
|
||||||
|
return `rgb(${r},${g},${b})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateBackground({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
element,
|
||||||
|
}: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
element: string;
|
||||||
|
}) {
|
||||||
|
const canvas = createCanvas(width * 1.2, height * 1.2);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
const elementColor = elementColors[element] ?? "#444444";
|
||||||
|
|
||||||
|
// base fill
|
||||||
|
ctx.fillStyle = elementColor;
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// overlay texture
|
||||||
|
const texture = await loadImage(await file(overlayImage).arrayBuffer());
|
||||||
|
|
||||||
|
ctx.globalCompositeOperation = "overlay";
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
texture,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
texture.width,
|
||||||
|
texture.height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
canvas.width,
|
||||||
|
canvas.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.globalCompositeOperation = "source-over";
|
||||||
|
|
||||||
|
// sample top middle
|
||||||
|
const sample = ctx.getImageData(Math.floor(canvas.width / 2), 0, 10, 1);
|
||||||
|
|
||||||
|
const [r, g, b] = avgColor(sample.data);
|
||||||
|
|
||||||
|
const accent = `linear-gradient(0deg,
|
||||||
|
${rgb(r, Math.max(0, g - 20), Math.max(0, b - 5))} 0%,
|
||||||
|
${rgb(r, Math.max(0, g - 10), Math.max(0, b - 15))} 100%)`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
canvas,
|
||||||
|
accent,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { createCanvas, loadImage } from "@napi-rs/canvas";
|
||||||
|
|
||||||
|
type Feather = [number, number, number, number];
|
||||||
|
|
||||||
|
export async function featherImage(
|
||||||
|
input: ArrayBuffer,
|
||||||
|
[top, right, bottom, left]: Feather = [0, 0.3, 0, 0],
|
||||||
|
) {
|
||||||
|
const img = await loadImage(Buffer.from(input));
|
||||||
|
|
||||||
|
const targetRatio = 1024 / 1124;
|
||||||
|
|
||||||
|
let cropW = img.width;
|
||||||
|
let cropH = img.height;
|
||||||
|
|
||||||
|
if (cropW / cropH > targetRatio) {
|
||||||
|
cropW = cropH * targetRatio;
|
||||||
|
} else {
|
||||||
|
cropH = cropW / targetRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sx = (img.width - cropW) / 2;
|
||||||
|
const sy = (img.height - cropH) / 2;
|
||||||
|
|
||||||
|
const canvas = createCanvas(cropW, cropH);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
ctx.drawImage(img, sx, sy, cropW, cropH, 0, 0, cropW, cropH);
|
||||||
|
|
||||||
|
// mask canvas
|
||||||
|
const mask = createCanvas(cropW, cropH);
|
||||||
|
const m = mask.getContext("2d");
|
||||||
|
|
||||||
|
const w = cropW;
|
||||||
|
const h = cropH;
|
||||||
|
|
||||||
|
// center
|
||||||
|
m.fillStyle = "white";
|
||||||
|
m.fillRect(w * left, h * top, w * (1 - left - right), h * (1 - top - bottom));
|
||||||
|
|
||||||
|
if (right > 0) {
|
||||||
|
const fw = w * right;
|
||||||
|
const g = m.createLinearGradient(w - fw, 0, w, 0);
|
||||||
|
g.addColorStop(0, "white");
|
||||||
|
g.addColorStop(1, "transparent");
|
||||||
|
m.fillStyle = g;
|
||||||
|
m.fillRect(w - fw, 0, fw, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left > 0) {
|
||||||
|
const fw = w * left;
|
||||||
|
const g = m.createLinearGradient(0, 0, fw, 0);
|
||||||
|
g.addColorStop(0, "transparent");
|
||||||
|
g.addColorStop(1, "white");
|
||||||
|
m.fillStyle = g;
|
||||||
|
m.fillRect(0, 0, fw, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top > 0) {
|
||||||
|
const fh = h * top;
|
||||||
|
const g = m.createLinearGradient(0, 0, 0, fh);
|
||||||
|
g.addColorStop(0, "transparent");
|
||||||
|
g.addColorStop(1, "white");
|
||||||
|
m.fillStyle = g;
|
||||||
|
m.fillRect(0, 0, w, fh);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bottom > 0) {
|
||||||
|
const fh = h * bottom;
|
||||||
|
const g = m.createLinearGradient(0, h - fh, 0, h);
|
||||||
|
g.addColorStop(0, "white");
|
||||||
|
g.addColorStop(1, "transparent");
|
||||||
|
m.fillStyle = g;
|
||||||
|
m.fillRect(0, h - fh, w, fh);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalCompositeOperation = "destination-in";
|
||||||
|
ctx.drawImage(mask, 0, 0);
|
||||||
|
|
||||||
|
return canvas.toBuffer("image/png");
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
+51
-32
@@ -11,10 +11,13 @@ import {
|
|||||||
} from "enka.ts";
|
} from "enka.ts";
|
||||||
import { twj } from "tw-to-css";
|
import { twj } from "tw-to-css";
|
||||||
import type { App } from "@";
|
import type { App } from "@";
|
||||||
|
import { generateBackground } from "@/assets/background";
|
||||||
import { ConstsSvg } from "@/assets/consts";
|
import { ConstsSvg } from "@/assets/consts";
|
||||||
|
import { featherImage } from "@/assets/feather";
|
||||||
import shadeSvg from "@/assets/shade.svg" with { type: "file" };
|
import shadeSvg from "@/assets/shade.svg" with { type: "file" };
|
||||||
import fontFile from "@/assets/shinshin.woff2" with { type: "file" };
|
import fontFile from "@/assets/shinshin.woff2" with { type: "file" };
|
||||||
import { TalentSvg } from "@/assets/talent";
|
import { TalentSvg } from "@/assets/talent";
|
||||||
|
import { mockData } from "@/lib/mock";
|
||||||
import { AvatarStore } from "@/type/enka";
|
import { AvatarStore } from "@/type/enka";
|
||||||
|
|
||||||
const cache = new TTLCache();
|
const cache = new TTLCache();
|
||||||
@@ -39,8 +42,8 @@ export function card(app: App) {
|
|||||||
try {
|
try {
|
||||||
const avatarStore = await getEnkaAvatarStore();
|
const avatarStore = await getEnkaAvatarStore();
|
||||||
console.log(`${Date.now() - start}ms - Avatar store loaded`);
|
console.log(`${Date.now() - start}ms - Avatar store loaded`);
|
||||||
// const data = mockData;
|
// const data = await fetchEnka(uid, false);
|
||||||
const data = await fetchEnka(uid, false);
|
const data = mockData;
|
||||||
console.log(`${Date.now() - start}ms - Enka data loaded`);
|
console.log(`${Date.now() - start}ms - Enka data loaded`);
|
||||||
if (typeof data.avatarInfoList === "undefined")
|
if (typeof data.avatarInfoList === "undefined")
|
||||||
return status(400, "The showcase for this UID is private");
|
return status(400, "The showcase for this UID is private");
|
||||||
@@ -55,38 +58,54 @@ export function card(app: App) {
|
|||||||
500,
|
500,
|
||||||
"Unsupported character, avatar store doesn't have this character",
|
"Unsupported character, avatar store doesn't have this character",
|
||||||
);
|
);
|
||||||
const resources = await Promise.all(
|
const resources = await Promise.all([
|
||||||
[
|
...[...avatar.Consts, ...Object.values(avatar.Skills)].map(
|
||||||
...avatar.Consts,
|
async (path) => ({
|
||||||
...Object.values(avatar.Skills),
|
src: path,
|
||||||
...Object.values(avatar.Costumes ?? {})
|
data: await fetch(
|
||||||
.map((c) => c.Art)
|
new URL(path, "https://enka.network"),
|
||||||
.slice(0, 1),
|
).then((res) => res.arrayBuffer()),
|
||||||
avatar.SideIconName.replace(
|
}),
|
||||||
"AvatarIcon_Side",
|
),
|
||||||
"Gacha_AvatarImg",
|
fetch(
|
||||||
),
|
new URL(
|
||||||
].map(async (path) => ({
|
avatar.SideIconName.replace(
|
||||||
src: path,
|
"AvatarIcon_Side",
|
||||||
data: await fetch(new URL(path, "https://enka.network")).then(
|
"Gacha_AvatarImg",
|
||||||
(res) => res.arrayBuffer(),
|
),
|
||||||
|
"https://enka.network",
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
.then((res) => res.arrayBuffer())
|
||||||
|
.then(async (buf) => ({
|
||||||
|
src: "/avatar",
|
||||||
|
data: await featherImage(buf, [0, 0.3, 0, 0]),
|
||||||
|
})),
|
||||||
|
generateBackground({
|
||||||
|
element: avatar.Element,
|
||||||
|
width: 1218,
|
||||||
|
height: 508,
|
||||||
|
}).then((r) => ({
|
||||||
|
src: "/bg",
|
||||||
|
data: r.canvas.toBuffer("image/webp"),
|
||||||
})),
|
})),
|
||||||
|
]);
|
||||||
|
console.log(
|
||||||
|
`${Date.now() - start}ms - Image resources loaded\n${resources.map((r) => `- ${r.src} ${(r.data.byteLength / 1000).toFixed(2)}Kb`).join("\n")}`,
|
||||||
);
|
);
|
||||||
console.log(`${Date.now() - start}ms - Image resources loaded`);
|
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<div tw="relative text-[14.8667px] text-white">
|
<div tw="relative text-[32.25px] text-white rounded-xl overflow-hidden">
|
||||||
<div tw="aspect-[1218/508] h-full"></div>
|
<div tw="aspect-[1218/508] h-full">
|
||||||
|
<img
|
||||||
|
src="/bg"
|
||||||
|
alt="background"
|
||||||
|
tw="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/* avatar */}
|
{/* avatar */}
|
||||||
<img
|
<img
|
||||||
src={
|
src="/avatar"
|
||||||
avatar.Costumes?.[0]?.Art ||
|
|
||||||
avatar.SideIconName.replace(
|
|
||||||
"AvatarIcon_Side",
|
|
||||||
"Gacha_AvatarImg",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
alt="char"
|
alt="char"
|
||||||
tw="object-cover h-full aspect-[1024/1124] absolute top-0 -left-[2.5em] scale-x-[1.1] scale-y-[1.15]"
|
tw="object-cover h-full aspect-[1024/1124] absolute top-0 -left-[2.5em] scale-x-[1.1] scale-y-[1.15]"
|
||||||
/>
|
/>
|
||||||
@@ -106,7 +125,7 @@ export function card(app: App) {
|
|||||||
{/* char and username */}
|
{/* char and username */}
|
||||||
<div tw="text-[130%] pb-[.5%] flex items-center gap-[.5em]">
|
<div tw="text-[130%] pb-[.5%] flex items-center gap-[.5em]">
|
||||||
{char.avatar.name}
|
{char.avatar.name}
|
||||||
<div tw="text-[11.596px] opacity-80">
|
<div tw="text-[0.75em] opacity-80">
|
||||||
▴ {data.playerInfo.nickname}
|
▴ {data.playerInfo.nickname}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,7 +194,7 @@ export function card(app: App) {
|
|||||||
{Object.entries(avatar.Skills).map(([key, src]) => {
|
{Object.entries(avatar.Skills).map(([key, src]) => {
|
||||||
const talentLevel = char.skillLevelMap?.[key] ?? 0;
|
const talentLevel = char.skillLevelMap?.[key] ?? 0;
|
||||||
return (
|
return (
|
||||||
<div tw="mt-[.3em] relative">
|
<div tw="mt-[.6em] relative">
|
||||||
<TalentSvg
|
<TalentSvg
|
||||||
tw={`Talents ${char.avatar.element} w-[3.6em] opacity-80 text-black`}
|
tw={`Talents ${char.avatar.element} w-[3.6em] opacity-80 text-black`}
|
||||||
/>
|
/>
|
||||||
@@ -199,13 +218,13 @@ export function card(app: App) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* middle section */}
|
{/* 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>
|
<div tw="absolute top-0 bottom-0 right-[23.1em] w-[21.5em] p-[1.4em_.7em_.7em]"></div>
|
||||||
{/* right section */}
|
{/* right section */}
|
||||||
<div tw="absolute top-0 bottom-0 right-0 w-[23.1em] p-[.7em] bg-blue-500/50"></div>
|
<div tw="absolute top-0 bottom-0 right-0 w-[23.1em] p-[.7em]"></div>
|
||||||
</div>,
|
</div>,
|
||||||
{
|
{
|
||||||
fonts: [await font.arrayBuffer()],
|
fonts: [await font.arrayBuffer()],
|
||||||
height: 508,
|
height: 1024,
|
||||||
persistentImages: [
|
persistentImages: [
|
||||||
{ data: await shade.arrayBuffer(), src: "shade" },
|
{ data: await shade.arrayBuffer(), src: "shade" },
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user