feat: massive layout resemblence improvement
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@elysia/cors": "^1.4.2",
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
"fastest-levenshtein": "^1.0.16",
|
"fastest-levenshtein": "^1.0.16",
|
||||||
"oxfmt": "^0.47.0",
|
"oxfmt": "^0.47.0",
|
||||||
@@ -28,6 +29,8 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"@borewit/text-codec": ["@borewit/[email protected]", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
|
"@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=="],
|
"@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=="],
|
"@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"
|
"react-dom": "^19.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@elysia/cors": "^1.4.2",
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
"fastest-levenshtein": "^1.0.16",
|
"fastest-levenshtein": "^1.0.16",
|
||||||
"oxfmt": "^0.47.0",
|
"oxfmt": "^0.47.0",
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { version } from "../package.json";
|
|||||||
import { pm } from "./lib/auth";
|
import { pm } from "./lib/auth";
|
||||||
import { createLogger } from "./lib/logger";
|
import { createLogger } from "./lib/logger";
|
||||||
import { v1routes } from "./v1";
|
import { v1routes } from "./v1";
|
||||||
|
import { cors } from '@elysia/cors'
|
||||||
|
|
||||||
const log = createLogger("APP");
|
const log = createLogger("APP");
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
|
.use(cors())
|
||||||
.use(
|
.use(
|
||||||
openapi({
|
openapi({
|
||||||
exclude: { paths: ["/", "/health"] },
|
exclude: { paths: ["/", "/health"] },
|
||||||
|
|||||||
+45
-37
@@ -29,6 +29,7 @@ import { assets, batchStore, getAvatar } from "@/lib/assets";
|
|||||||
import { Deduplicator } from "@/lib/dedupe";
|
import { Deduplicator } from "@/lib/dedupe";
|
||||||
import { cardErrorLocs } from "@/lib/locs";
|
import { cardErrorLocs } from "@/lib/locs";
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
|
import { resolveShowcaseChar } from "@/lib/matcher";
|
||||||
import { mockData } from "@/lib/mock";
|
import { mockData } from "@/lib/mock";
|
||||||
import { buildDisplayRows } from "@/lib/rows";
|
import { buildDisplayRows } from "@/lib/rows";
|
||||||
import type { RollSummary } from "@/lib/substat";
|
import type { RollSummary } from "@/lib/substat";
|
||||||
@@ -36,9 +37,9 @@ import { buildRollSummary } from "@/lib/substat";
|
|||||||
import { Locale, SubstatOrder } from "@/type/enka";
|
import { Locale, SubstatOrder } from "@/type/enka";
|
||||||
import { FightPropMapEnum, PercentageFightProp } from "@/type/fightPropMap";
|
import { FightPropMapEnum, PercentageFightProp } from "@/type/fightPropMap";
|
||||||
|
|
||||||
|
import DiffPage from "../../hmr.html";
|
||||||
import { ErrorPage } from "./error";
|
import { ErrorPage } from "./error";
|
||||||
import { ListPage } from "./list";
|
import { ListPage } from "./list";
|
||||||
import { resolveShowcaseChar } from "@/lib/matcher";
|
|
||||||
|
|
||||||
const cache = new TTLCache();
|
const cache = new TTLCache();
|
||||||
const deduper = new Deduplicator();
|
const deduper = new Deduplicator();
|
||||||
@@ -51,6 +52,7 @@ queueMicrotask(() => {
|
|||||||
if (Date.now() - purging < 60000) return;
|
if (Date.now() - purging < 60000) return;
|
||||||
purging = Date.now();
|
purging = Date.now();
|
||||||
assets.getMissingAssets();
|
assets.getMissingAssets();
|
||||||
|
if (!feature("API")) batchStore("locs", "affixes");
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
if (Date.now() - purging < 60000) return;
|
if (Date.now() - purging < 60000) return;
|
||||||
@@ -70,6 +72,7 @@ export function card(app: App) {
|
|||||||
.derive(() => ({
|
.derive(() => ({
|
||||||
log: createLogger(),
|
log: createLogger(),
|
||||||
}))
|
}))
|
||||||
|
.get("/genshin/diff", DiffPage)
|
||||||
.head(
|
.head(
|
||||||
"/genshin/:uid/preload",
|
"/genshin/:uid/preload",
|
||||||
async ({ params: { uid }, log }) => {
|
async ({ params: { uid }, log }) => {
|
||||||
@@ -464,7 +467,7 @@ export function card(app: App) {
|
|||||||
|
|
||||||
// #region Rendering
|
// #region Rendering
|
||||||
const res = new ImageResponse(
|
const res = new ImageResponse(
|
||||||
<div tw={`text-[${32.25 * (height / 1024)}px]`}>
|
<div tw={`text-[${30.466 * (height / 1024)}px]`}>
|
||||||
<SubstatDisplay
|
<SubstatDisplay
|
||||||
substat={rollSummary}
|
substat={rollSummary}
|
||||||
element={avatar.Element}
|
element={avatar.Element}
|
||||||
@@ -479,34 +482,38 @@ export function card(app: App) {
|
|||||||
tw="w-full h-full object-cover"
|
tw="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 */}
|
{/* left section */}
|
||||||
<div
|
<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={{
|
style={{
|
||||||
textShadow:
|
textShadow:
|
||||||
"0 .08em .1em black,0 .1em .3em rgba(0,0,0,.4)",
|
"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 */}
|
{/* left side vignette */}
|
||||||
<div tw="absolute top-0 left-0 bottom-0 right-0">
|
<div tw="absolute top-0 left-0 bottom-0 right-0">
|
||||||
<img src="shade" alt="shade" tw="opacity-40" />
|
<img src="shade" alt="shade" tw="opacity-40" />
|
||||||
</div>
|
</div>
|
||||||
{/* char and username */}
|
{/* 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}
|
{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
|
<svg
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
tw="mt-[.2em] h-[.5em] w-[.5em]"
|
tw="mt-[.2em] h-[.7em] w-[.7em]"
|
||||||
>
|
>
|
||||||
<title>doritos</title>
|
<title>doritos</title>
|
||||||
<polygon
|
<polygon
|
||||||
@@ -518,12 +525,12 @@ export function card(app: App) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* level */}
|
{/* level */}
|
||||||
<div>
|
<div tw="text-[105%] pt-[.2em]">
|
||||||
Lv. <span tw="px-1">{charLvl}</span> /
|
Lv. <span tw="px-[.3em]">{charLvl}</span> /
|
||||||
<span tw="opacity-60 pl-1">{charLvlMax}</span>
|
<span tw="opacity-60 pl-[.3em]">{charLvlMax}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* fren */}
|
{/* fren */}
|
||||||
<div tw="flex gap-1 items-end">
|
<div tw="text-[110%] flex gap-[.3em] items-end pt-[.3em]">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
tw="h-[1.2em] mb-[.2em]"
|
tw="h-[1.2em] mb-[.2em]"
|
||||||
@@ -572,7 +579,7 @@ export function card(app: App) {
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
right: "5%",
|
right: "4.1%",
|
||||||
bottom: "6%",
|
bottom: "6%",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "flex-end",
|
alignItems: "flex-end",
|
||||||
@@ -595,7 +602,7 @@ export function card(app: App) {
|
|||||||
return (
|
return (
|
||||||
<div tw="mt-[.6em] relative">
|
<div tw="mt-[.6em] relative">
|
||||||
<TalentSvg
|
<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
|
<img
|
||||||
key={key}
|
key={key}
|
||||||
@@ -614,12 +621,12 @@ export function card(app: App) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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}
|
{data.uid}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* middle section */}
|
{/* 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 */}
|
{/* weapon */}
|
||||||
<div tw="flex flex-start" className="Weapon">
|
<div tw="flex flex-start" className="Weapon">
|
||||||
{/* display */}
|
{/* display */}
|
||||||
@@ -637,9 +644,9 @@ export function card(app: App) {
|
|||||||
}[weapon.store.Rarity],
|
}[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 }, () =>
|
{Array.from({ length: weapon.store.Rarity }, () =>
|
||||||
star(".7em"),
|
star(".8em"),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -657,7 +664,7 @@ export function card(app: App) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* stats */}
|
{/* stats */}
|
||||||
<div tw="pt-[3%] pb-[2%] flex gap-[.5em]">
|
<div tw="pt-[3%] pb-[2%] flex gap-[.7em]">
|
||||||
{weapon.flat.weaponStats?.map((s) => (
|
{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]">
|
<div tw="rounded-[.2em] leading-none bg-white/15 px-[.34em] py-[0.25em] flex items-center gap-[.7em] text-[1.03em]">
|
||||||
<EnkaIcon
|
<EnkaIcon
|
||||||
@@ -677,7 +684,7 @@ export function card(app: App) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</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="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]">
|
<div tw="pt-[.1em]">
|
||||||
R
|
R
|
||||||
@@ -688,7 +695,8 @@ export function card(app: App) {
|
|||||||
</div>
|
</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="rounded-[.2em] leading-none bg-black/50 px-[.34em] py-[0.175em] flex items-center gap-[.7em] text-[1.03em]">
|
||||||
<div tw="pt-[.1em]">
|
<div tw="pt-[.1em]">
|
||||||
Lv. {weapon.data.level} /
|
<span tw="pr-[.5em]">Lv.</span>{" "}
|
||||||
|
{weapon.data.level} /
|
||||||
<span tw="opacity-60 pl-1">
|
<span tw="opacity-60 pl-1">
|
||||||
{
|
{
|
||||||
promoteMaxLevelMap[
|
promoteMaxLevelMap[
|
||||||
@@ -702,7 +710,7 @@ export function card(app: App) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* stats table */}
|
{/* 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 */}
|
{/* <div /> justify spacer */}
|
||||||
{statRows.map((r, i) => (
|
{statRows.map((r, i) => (
|
||||||
<div tw="flex items-center whitespace-nowrap">
|
<div tw="flex items-center whitespace-nowrap">
|
||||||
@@ -721,7 +729,7 @@ export function card(app: App) {
|
|||||||
>
|
>
|
||||||
)[r.key] || (r.key as keyof typeof library)
|
)[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 tw="flex grow justify-between">
|
||||||
<div>{r.name}</div>
|
<div>{r.name}</div>
|
||||||
@@ -745,7 +753,7 @@ export function card(app: App) {
|
|||||||
))}
|
))}
|
||||||
{artifactSets.length && (
|
{artifactSets.length && (
|
||||||
<div
|
<div
|
||||||
tw="flex w-full pt-[.5em] gap-[.5em]"
|
tw="flex w-full gap-[.5em]"
|
||||||
style={{
|
style={{
|
||||||
textShadow:
|
textShadow:
|
||||||
"0 .08em .1em rgba(0,0,0,.4),0 .1em .3em rgba(0,0,0,.4)",
|
"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>
|
||||||
</div>
|
</div>
|
||||||
{/* right section */}
|
{/* 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) =>
|
{artifacts.map((a) =>
|
||||||
a ? (
|
a ? (
|
||||||
<div tw="h-[18%] bg-[#0000004f] rounded-[.3em] relative text-white overflow-hidden flex justify-between gap-[.5em]">
|
<div tw="h-[18%] bg-[#0000004f] rounded-[.3em] relative text-white overflow-hidden flex justify-between gap-[.5em]">
|
||||||
<img
|
<img
|
||||||
src={a.icon}
|
src={a.icon}
|
||||||
alt="artifact"
|
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 */}
|
{/* 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
|
<EnkaIcon
|
||||||
icon={a.main.mainPropId}
|
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
|
<div
|
||||||
tw="text-[1.1em] py-[2%] ml-auto"
|
tw="text-[1.1em] pt-[2%] ml-auto"
|
||||||
style={{ textShadow: "0 .07em .1em black" }}
|
style={{ textShadow: "0 .07em .1em black" }}
|
||||||
>
|
>
|
||||||
{PercentageFightProp.includes(
|
{PercentageFightProp.includes(
|
||||||
@@ -797,7 +805,7 @@ export function card(app: App) {
|
|||||||
? `${a.main.statValue.toFixed(1)}%`
|
? `${a.main.statValue.toFixed(1)}%`
|
||||||
: a.main.statValue.toLocaleString()}
|
: a.main.statValue.toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
<div tw="flex grow max-h-[20%] items-end justify-end">
|
<div tw="flex grow max-h-[20%] items-center justify-end">
|
||||||
<div>
|
<div>
|
||||||
{Array.from({ length: a.stars ?? 0 }, () =>
|
{Array.from({ length: a.stars ?? 0 }, () =>
|
||||||
star(".6em"),
|
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">
|
<div tw="flex flex-col flex-wrap mt-[.35em] mb-[.35em] mr-[.35em] w-full">
|
||||||
{a.sub.map((s, i) => (
|
{a.sub.map((s, i) => (
|
||||||
<div
|
<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 && (
|
{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">
|
<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
|
<EnkaIcon
|
||||||
icon={s.appendPropId}
|
icon={s.appendPropId}
|
||||||
tw="h-[1em] w-[1em]"
|
tw="h-[1.1em] w-[1.1em]"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div tw="text-[1.05em]">
|
||||||
+
|
+
|
||||||
{PercentageFightProp.includes(
|
{PercentageFightProp.includes(
|
||||||
s.appendPropId as (typeof PercentageFightProp)[number],
|
s.appendPropId as (typeof PercentageFightProp)[number],
|
||||||
|
|||||||
Reference in New Issue
Block a user