feat: massive layout resemblence improvement
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"react-dom": "^19.2.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elysia/cors": "^1.4.2",
|
||||
"bun-types": "latest",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"oxfmt": "^0.47.0",
|
||||
@@ -28,6 +29,8 @@
|
||||
"packages": {
|
||||
"@borewit/text-codec": ["@borewit/[email protected]", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
|
||||
|
||||
"@elysia/cors": ["@elysia/[email protected]", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-dXkfo1IxZWJkJReNYnPJorJqb+XpWno0D/YWlMEJSkiFIFd751zJ4R/YswAYdo88mLWo8cycEj673o7L+gbxsw=="],
|
||||
|
||||
"@elysiajs/openapi": ["@elysiajs/[email protected]", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-kWmJWdvP8/LwHwAJXSpz6xFfYUoyUyEPRimEYABuDU1rOnS27Da1u9T2jyU7frOopxKWV/wDfDxMP8z2xdCPJw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #000c;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button,
|
||||
select,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 1870px;
|
||||
height: 778px;
|
||||
margin: 12px auto;
|
||||
background: #000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="bar">
|
||||
<button id="refresh">refresh</button>
|
||||
<label>
|
||||
mode
|
||||
<select id="mode">
|
||||
<option value="edge">edge diff</option>
|
||||
<option value="diff">raw diff</option>
|
||||
<option value="overlay">red/green overlay</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
threshold
|
||||
<input id="threshold" type="range" min="0" max="120" value="85">
|
||||
<span id="thresholdText">85</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<canvas id="canvas" width="1870" height="778"></canvas>
|
||||
|
||||
<script>
|
||||
const MINE = "http://nyx:3000/v1/card/genshin/775956514/ororon";
|
||||
const ORIG = "https://media.discordapp.net/attachments/1234762106558218332/1500814127566684202/81a07207-4f8e-4adb-a3c3-dea113477246.png?ex=69f9cd93&is=69f87c13&hm=7708cde2a1161a7d0b26655902d29f93b297e54293725912e981e3e0701b37eb&=&format=webp&quality=lossless&width=1870&height=778";
|
||||
|
||||
const W = 1870, H = 778;
|
||||
/**
|
||||
* @type {HTMLCanvasElement}
|
||||
*/
|
||||
const canvas = document.getElementById("canvas");
|
||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||
|
||||
const mode = document.getElementById("mode");
|
||||
const threshold = document.getElementById("threshold");
|
||||
const thresholdText = document.getElementById("thresholdText");
|
||||
|
||||
let mineData, origData, refreshing = false;
|
||||
|
||||
function loadImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = src + (src.includes("?") ? "&" : "?") + "cacheBust=" + Date.now();
|
||||
});
|
||||
}
|
||||
|
||||
function drawToData(img) {
|
||||
const c = document.createElement("canvas");
|
||||
c.width = W;
|
||||
c.height = H;
|
||||
const x = c.getContext("2d", { willReadFrequently: true });
|
||||
x.drawImage(img, 0, 0, W, H);
|
||||
return x.getImageData(0, 0, W, H);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
if (refreshing) return;
|
||||
refreshing = true;
|
||||
ctx.fillStyle = "#00000088";
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillText("fetching...", 20, 30);
|
||||
|
||||
const [mineImg, origImg] = await Promise.all([
|
||||
loadImage(MINE),
|
||||
loadImage(ORIG),
|
||||
]);
|
||||
|
||||
ctx.fillText("loading...", 20, 46);
|
||||
mineData = drawToData(mineImg);
|
||||
if (!origData) origData = drawToData(origImg);
|
||||
|
||||
ctx.fillText("rendering...", 20, 62);
|
||||
render();
|
||||
refreshing = false;
|
||||
}
|
||||
|
||||
function lum(d, i) {
|
||||
return d[i] * 0.299 + d[i + 1] * 0.587 + d[i + 2] * 0.114;
|
||||
}
|
||||
|
||||
function edgeAt(data, x, y) {
|
||||
const d = data.data;
|
||||
const i = (y * W + x) * 4;
|
||||
|
||||
const l = lum(d, i);
|
||||
const lx = x > 0 ? lum(d, i - 4) : l;
|
||||
const rx = x < W - 1 ? lum(d, i + 4) : l;
|
||||
const uy = y > 0 ? lum(d, i - W * 4) : l;
|
||||
const dy = y < H - 1 ? lum(d, i + W * 4) : l;
|
||||
|
||||
return Math.abs(rx - lx) + Math.abs(dy - uy);
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!mineData || !origData) return;
|
||||
|
||||
const out = ctx.createImageData(W, H);
|
||||
const o = out.data;
|
||||
const m = mineData.data;
|
||||
const r = origData.data;
|
||||
const t = Number(threshold.value);
|
||||
|
||||
thresholdText.textContent = t;
|
||||
|
||||
for (let y = 0; y < H; y++) {
|
||||
for (let x = 0; x < W; x++) {
|
||||
const i = (y * W + x) * 4;
|
||||
|
||||
if (mode.value === "edge") {
|
||||
const me = edgeAt(mineData, x, y);
|
||||
const oe = edgeAt(origData, x, y);
|
||||
|
||||
const mr = me > t ? 255 : 0;
|
||||
const og = oe > t ? 255 : 0;
|
||||
|
||||
o[i] = mr; // mine = red
|
||||
o[i + 1] = og; // original = green
|
||||
o[i + 2] = 0;
|
||||
o[i + 3] = Math.max(mr, og);
|
||||
}
|
||||
|
||||
else if (mode.value === "diff") {
|
||||
const dr = Math.abs(m[i] - r[i]);
|
||||
const dg = Math.abs(m[i + 1] - r[i + 1]);
|
||||
const db = Math.abs(m[i + 2] - r[i + 2]);
|
||||
const diff = Math.max(dr, dg, db);
|
||||
|
||||
o[i] = diff;
|
||||
o[i + 1] = diff;
|
||||
o[i + 2] = diff;
|
||||
o[i + 3] = diff > t ? 255 : 0;
|
||||
}
|
||||
|
||||
else {
|
||||
o[i] = m[i]; // mine red-ish
|
||||
o[i + 1] = r[i + 1]; // original green-ish
|
||||
o[i + 2] = 0;
|
||||
o[i + 3] = 220;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(out, 0, 0);
|
||||
}
|
||||
|
||||
document.getElementById("refresh").onclick = refresh;
|
||||
mode.oninput = render;
|
||||
threshold.oninput = render;
|
||||
|
||||
refresh().catch(err => {
|
||||
console.error(err);
|
||||
ctx.fillStyle = "red";
|
||||
ctx.fillText(String(err), 20, 30);
|
||||
});
|
||||
|
||||
// ENTER = refresh (anywhere, unless typing in input)
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && !["INPUT", "SELECT"].includes(e.target.tagName)) {
|
||||
e.preventDefault();
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// WINDOW FOCUS = refresh
|
||||
window.addEventListener("focus", () => {
|
||||
refresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -18,6 +18,7 @@
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elysia/cors": "^1.4.2",
|
||||
"bun-types": "latest",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"oxfmt": "^0.47.0",
|
||||
|
||||
@@ -5,10 +5,12 @@ import { version } from "../package.json";
|
||||
import { pm } from "./lib/auth";
|
||||
import { createLogger } from "./lib/logger";
|
||||
import { v1routes } from "./v1";
|
||||
import { cors } from '@elysia/cors'
|
||||
|
||||
const log = createLogger("APP");
|
||||
|
||||
const app = new Elysia()
|
||||
.use(cors())
|
||||
.use(
|
||||
openapi({
|
||||
exclude: { paths: ["/", "/health"] },
|
||||
|
||||
+45
-37
@@ -29,6 +29,7 @@ import { assets, batchStore, getAvatar } from "@/lib/assets";
|
||||
import { Deduplicator } from "@/lib/dedupe";
|
||||
import { cardErrorLocs } from "@/lib/locs";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
import { resolveShowcaseChar } from "@/lib/matcher";
|
||||
import { mockData } from "@/lib/mock";
|
||||
import { buildDisplayRows } from "@/lib/rows";
|
||||
import type { RollSummary } from "@/lib/substat";
|
||||
@@ -36,9 +37,9 @@ import { buildRollSummary } from "@/lib/substat";
|
||||
import { Locale, SubstatOrder } from "@/type/enka";
|
||||
import { FightPropMapEnum, PercentageFightProp } from "@/type/fightPropMap";
|
||||
|
||||
import DiffPage from "../../hmr.html";
|
||||
import { ErrorPage } from "./error";
|
||||
import { ListPage } from "./list";
|
||||
import { resolveShowcaseChar } from "@/lib/matcher";
|
||||
|
||||
const cache = new TTLCache();
|
||||
const deduper = new Deduplicator();
|
||||
@@ -51,6 +52,7 @@ queueMicrotask(() => {
|
||||
if (Date.now() - purging < 60000) return;
|
||||
purging = Date.now();
|
||||
assets.getMissingAssets();
|
||||
if (!feature("API")) batchStore("locs", "affixes");
|
||||
|
||||
setInterval(async () => {
|
||||
if (Date.now() - purging < 60000) return;
|
||||
@@ -70,6 +72,7 @@ export function card(app: App) {
|
||||
.derive(() => ({
|
||||
log: createLogger(),
|
||||
}))
|
||||
.get("/genshin/diff", DiffPage)
|
||||
.head(
|
||||
"/genshin/:uid/preload",
|
||||
async ({ params: { uid }, log }) => {
|
||||
@@ -464,7 +467,7 @@ export function card(app: App) {
|
||||
|
||||
// #region Rendering
|
||||
const res = new ImageResponse(
|
||||
<div tw={`text-[${32.25 * (height / 1024)}px]`}>
|
||||
<div tw={`text-[${30.466 * (height / 1024)}px]`}>
|
||||
<SubstatDisplay
|
||||
substat={rollSummary}
|
||||
element={avatar.Element}
|
||||
@@ -479,34 +482,38 @@ export function card(app: App) {
|
||||
tw="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
{/* avatar */}
|
||||
<img
|
||||
src="/avatar"
|
||||
alt="char"
|
||||
tw="object-cover h-full aspect-[1024/1124] absolute top-0 -left-[2.0em] 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"
|
||||
tw="absolute top-0 bottom-0 left-0 w-[30.4em] p-[1.6em] flex flex-col"
|
||||
style={{
|
||||
textShadow:
|
||||
"0 .08em .1em black,0 .1em .3em rgba(0,0,0,.4)",
|
||||
}}
|
||||
>
|
||||
{/* avatar */}
|
||||
<img
|
||||
src="/avatar"
|
||||
alt="char"
|
||||
tw="absolute h-full aspect-[1024/1124] left-[50%] top-[55%] max-w-none"
|
||||
style={{
|
||||
transform: "translate(-50%, -50%) scale(1.3)",
|
||||
transformOrigin: "50% 50%",
|
||||
}}
|
||||
/>
|
||||
{/* left side vignette */}
|
||||
<div tw="absolute top-0 left-0 bottom-0 right-0">
|
||||
<img src="shade" alt="shade" tw="opacity-40" />
|
||||
</div>
|
||||
{/* char and username */}
|
||||
<div tw="text-[130%] pb-[.5%] flex items-center gap-[.5em]">
|
||||
<div tw="text-[120%] pb-[.5%] flex items-center gap-[.5em]">
|
||||
{char.avatar.name}
|
||||
<div tw="text-[0.75em] opacity-80 flex items-center gap-[.4em]">
|
||||
<div tw="text-[0.75em] opacity-80 flex items-center gap-[.7em] pl-[.4em] pt-[.5em]">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 100 100"
|
||||
tw="mt-[.2em] h-[.5em] w-[.5em]"
|
||||
tw="mt-[.2em] h-[.7em] w-[.7em]"
|
||||
>
|
||||
<title>doritos</title>
|
||||
<polygon
|
||||
@@ -518,12 +525,12 @@ export function card(app: App) {
|
||||
</div>
|
||||
</div>
|
||||
{/* level */}
|
||||
<div>
|
||||
Lv. <span tw="px-1">{charLvl}</span> /
|
||||
<span tw="opacity-60 pl-1">{charLvlMax}</span>
|
||||
<div tw="text-[105%] pt-[.2em]">
|
||||
Lv. <span tw="px-[.3em]">{charLvl}</span> /
|
||||
<span tw="opacity-60 pl-[.3em]">{charLvlMax}</span>
|
||||
</div>
|
||||
{/* fren */}
|
||||
<div tw="flex gap-1 items-end">
|
||||
<div tw="text-[110%] flex gap-[.3em] items-end pt-[.3em]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
tw="h-[1.2em] mb-[.2em]"
|
||||
@@ -572,7 +579,7 @@ export function card(app: App) {
|
||||
style={{
|
||||
display: "flex",
|
||||
position: "absolute",
|
||||
right: "5%",
|
||||
right: "4.1%",
|
||||
bottom: "6%",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-end",
|
||||
@@ -595,7 +602,7 @@ export function card(app: App) {
|
||||
return (
|
||||
<div tw="mt-[.6em] relative">
|
||||
<TalentSvg
|
||||
tw={`Talents ${avatar.Element} w-[3.6em] opacity-60 text-black`}
|
||||
tw={`Talents ${avatar.Element} w-[3.6em] opacity-60 text-black mb-[.5em] -mt-[.25em]`}
|
||||
/>
|
||||
<img
|
||||
key={key}
|
||||
@@ -614,12 +621,12 @@ export function card(app: App) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div tw="mb-[-1em] ml-[-.3em] mt-[.3em] text-[.9em] opacity-80">
|
||||
<div tw="mb-[-1em] ml-[-.3em] mt-[.3em] text-[.96em] opacity-80">
|
||||
{data.uid}
|
||||
</div>
|
||||
</div>
|
||||
{/* middle section */}
|
||||
<div tw="absolute top-0 bottom-0 right-[23.1em] w-[25.5em] p-[1.4em_.7em_.7em] flex flex-col">
|
||||
<div tw="absolute top-0 bottom-0 right-[24.2em] w-[25.5em] p-[1.5em_.7em_.7em] flex flex-col text-[1.01em]">
|
||||
{/* weapon */}
|
||||
<div tw="flex flex-start" className="Weapon">
|
||||
{/* display */}
|
||||
@@ -637,9 +644,9 @@ export function card(app: App) {
|
||||
}[weapon.store.Rarity],
|
||||
}}
|
||||
/>
|
||||
<div tw="absolute w-full top-[93%] flex justify-center">
|
||||
<div tw="absolute w-full top-[91%] flex justify-center">
|
||||
{Array.from({ length: weapon.store.Rarity }, () =>
|
||||
star(".7em"),
|
||||
star(".8em"),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -657,7 +664,7 @@ export function card(app: App) {
|
||||
</span>
|
||||
</div>
|
||||
{/* stats */}
|
||||
<div tw="pt-[3%] pb-[2%] flex gap-[.5em]">
|
||||
<div tw="pt-[3%] pb-[2%] flex gap-[.7em]">
|
||||
{weapon.flat.weaponStats?.map((s) => (
|
||||
<div tw="rounded-[.2em] leading-none bg-white/15 px-[.34em] py-[0.25em] flex items-center gap-[.7em] text-[1.03em]">
|
||||
<EnkaIcon
|
||||
@@ -677,7 +684,7 @@ export function card(app: App) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div tw="flex gap-[.5em]">
|
||||
<div tw="flex gap-[.85em]">
|
||||
<div tw="rounded-[.2em] leading-none bg-black/50 px-[.34em] py-[0.175em] flex items-center gap-[.7em] text-[1.03em] text-[wheat]">
|
||||
<div tw="pt-[.1em]">
|
||||
R
|
||||
@@ -688,7 +695,8 @@ export function card(app: App) {
|
||||
</div>
|
||||
<div tw="rounded-[.2em] leading-none bg-black/50 px-[.34em] py-[0.175em] flex items-center gap-[.7em] text-[1.03em]">
|
||||
<div tw="pt-[.1em]">
|
||||
Lv. {weapon.data.level} /
|
||||
<span tw="pr-[.5em]">Lv.</span>{" "}
|
||||
{weapon.data.level} /
|
||||
<span tw="opacity-60 pl-1">
|
||||
{
|
||||
promoteMaxLevelMap[
|
||||
@@ -702,7 +710,7 @@ export function card(app: App) {
|
||||
</div>
|
||||
</div>
|
||||
{/* stats table */}
|
||||
<div tw="text-white flex flex-col pt-[2em] pr-[3%] grow text-[.95em] justify-between">
|
||||
<div tw="text-white flex flex-col pt-[2.2em] pr-[4.7%] -mb-[.8em] grow text-[.96em] justify-between">
|
||||
{/* <div /> justify spacer */}
|
||||
{statRows.map((r, i) => (
|
||||
<div tw="flex items-center whitespace-nowrap">
|
||||
@@ -721,7 +729,7 @@ export function card(app: App) {
|
||||
>
|
||||
)[r.key] || (r.key as keyof typeof library)
|
||||
}
|
||||
tw="h-[1em] mr-[.8em] -mt-[.05em]"
|
||||
tw="h-[1em] mr-[1em] -mt-[.05em]"
|
||||
/>
|
||||
<div tw="flex grow justify-between">
|
||||
<div>{r.name}</div>
|
||||
@@ -745,7 +753,7 @@ export function card(app: App) {
|
||||
))}
|
||||
{artifactSets.length && (
|
||||
<div
|
||||
tw="flex w-full pt-[.5em] gap-[.5em]"
|
||||
tw="flex w-full gap-[.5em]"
|
||||
style={{
|
||||
textShadow:
|
||||
"0 .08em .1em rgba(0,0,0,.4),0 .1em .3em rgba(0,0,0,.4)",
|
||||
@@ -771,23 +779,23 @@ export function card(app: App) {
|
||||
</div>
|
||||
</div>
|
||||
{/* right section */}
|
||||
<div tw="absolute top-0 bottom-0 right-0 w-[23.1em] p-[.7em] flex flex-col justify-between">
|
||||
<div tw="absolute top-0 bottom-0 right-0 w-[25.7em] p-[.7em] flex flex-col justify-between">
|
||||
{artifacts.map((a) =>
|
||||
a ? (
|
||||
<div tw="h-[18%] bg-[#0000004f] rounded-[.3em] relative text-white overflow-hidden flex justify-between gap-[.5em]">
|
||||
<img
|
||||
src={a.icon}
|
||||
alt="artifact"
|
||||
tw="absolute top-1/2 -left-[8%] w-[45%] -translate-y-1/2 -translate-x-[10%] object-contain aspect-square scale-90"
|
||||
tw="absolute top-[48%] -left-[8%] w-[45%] -translate-y-1/2 -translate-x-[10%] object-contain aspect-square scale-90"
|
||||
/>
|
||||
{/* main */}
|
||||
<div tw="h-full w-[30%] text-right shrink-0 flex flex-col justify-center mr-[.25em]">
|
||||
<div tw="h-full w-[30%] text-[105%] text-right shrink-0 flex flex-col justify-center mr-[.25em]">
|
||||
<EnkaIcon
|
||||
icon={a.main.mainPropId}
|
||||
tw="h-[1em] w-[1em] ml-auto max-h-[22%]"
|
||||
tw="h-[1em] w-[1em] ml-auto max-h-[22%] mt-[.5em]"
|
||||
/>
|
||||
<div
|
||||
tw="text-[1.1em] py-[2%] ml-auto"
|
||||
tw="text-[1.1em] pt-[2%] ml-auto"
|
||||
style={{ textShadow: "0 .07em .1em black" }}
|
||||
>
|
||||
{PercentageFightProp.includes(
|
||||
@@ -797,7 +805,7 @@ export function card(app: App) {
|
||||
? `${a.main.statValue.toFixed(1)}%`
|
||||
: a.main.statValue.toLocaleString()}
|
||||
</div>
|
||||
<div tw="flex grow max-h-[20%] items-end justify-end">
|
||||
<div tw="flex grow max-h-[20%] items-center justify-end">
|
||||
<div>
|
||||
{Array.from({ length: a.stars ?? 0 }, () =>
|
||||
star(".6em"),
|
||||
@@ -816,7 +824,7 @@ export function card(app: App) {
|
||||
<div tw="flex flex-col flex-wrap mt-[.35em] mb-[.35em] mr-[.35em] w-full">
|
||||
{a.sub.map((s, i) => (
|
||||
<div
|
||||
tw={`flex items-center pl-[.25em] relative gap-[.25em] w-1/2 h-1/2 ${i === 3 ? "opacity-80" : ""}`}
|
||||
tw={`flex items-center pl-[.7em] relative gap-[.5em] w-1/2 h-1/2 ${i === 3 ? "opacity-80" : ""} ${i > 1 ? "pl-[.3em]" : ""}`}
|
||||
>
|
||||
{enableRollQuality && (
|
||||
<div tw="absolute top-0 m-[.1em] left-[-.25em] bottom-0 right-[.25em] bg-gradient-to-t from-transparent to-black/25 rounded-[.2em] flex gap-0.5">
|
||||
@@ -840,9 +848,9 @@ export function card(app: App) {
|
||||
)}
|
||||
<EnkaIcon
|
||||
icon={s.appendPropId}
|
||||
tw="h-[1em] w-[1em]"
|
||||
tw="h-[1.1em] w-[1.1em]"
|
||||
/>
|
||||
<div>
|
||||
<div tw="text-[1.05em]">
|
||||
+
|
||||
{PercentageFightProp.includes(
|
||||
s.appendPropId as (typeof PercentageFightProp)[number],
|
||||
|
||||
Reference in New Issue
Block a user