added thumbs generation for performation and also added a new deck format for registration

This commit is contained in:
2026-03-08 05:40:53 -07:00
parent 78abb582dd
commit 4713bbd54b
11 changed files with 1255 additions and 44 deletions

View File

@@ -98,6 +98,14 @@
swords: ["swords"],
disks: ["disks", "pentacles", "coins"]
};
const defaultPipRankOrder = ["Ace", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"];
const defaultThumbnailConfig = {
root: "thumbs",
width: 240,
height: 360,
fit: "inside",
quality: 82
};
const DECK_REGISTRY_PATH = "asset/tarot deck/decks.json";
@@ -105,6 +113,7 @@
const manifestCache = new Map();
const cardBackCache = new Map();
const cardBackThumbnailCache = new Map();
let activeDeckId = DEFAULT_DECK_ID;
function canonicalMajorName(cardName) {
@@ -278,6 +287,56 @@
return null;
}
function normalizeThumbnailConfig(rawConfig, fallbackRoot = "") {
if (rawConfig === false) {
return false;
}
if (!rawConfig || typeof rawConfig !== "object") {
const root = String(fallbackRoot || "").trim();
if (!root) {
return null;
}
return {
...defaultThumbnailConfig,
root
};
}
const root = String(rawConfig.root || fallbackRoot || defaultThumbnailConfig.root).trim();
if (!root) {
return null;
}
return {
root,
width: Number.isInteger(Number(rawConfig.width)) && Number(rawConfig.width) > 0
? Number(rawConfig.width)
: defaultThumbnailConfig.width,
height: Number.isInteger(Number(rawConfig.height)) && Number(rawConfig.height) > 0
? Number(rawConfig.height)
: defaultThumbnailConfig.height,
fit: String(rawConfig.fit || defaultThumbnailConfig.fit).trim() || defaultThumbnailConfig.fit,
quality: Number.isInteger(Number(rawConfig.quality)) && Number(rawConfig.quality) >= 1 && Number(rawConfig.quality) <= 100
? Number(rawConfig.quality)
: defaultThumbnailConfig.quality
};
}
function resolveDeckThumbnailPath(manifest, relativePath) {
if (!manifest || !manifest.thumbnails || manifest.thumbnails === false) {
return null;
}
const normalizedPath = String(relativePath || "").trim().replace(/^\.\//, "");
if (!normalizedPath || isRemoteAssetPath(normalizedPath) || normalizedPath.startsWith("/")) {
return null;
}
return toDeckAssetPath(manifest, `${manifest.thumbnails.root}/${normalizedPath}`) || null;
}
function readManifestJsonSync(path) {
try {
const request = new XMLHttpRequest();
@@ -314,7 +373,8 @@
label: String(entry?.label || id),
basePath,
manifestPath,
cardBackPath: String(entry?.cardBackPath || "").trim()
cardBackPath: String(entry?.cardBackPath || "").trim(),
thumbnailRoot: String(entry?.thumbnailRoot || "").trim()
};
});
@@ -385,6 +445,7 @@
basePath: String(source.basePath || "").replace(/\/$/, ""),
cardBack: String(rawManifest.cardBack || "").trim(),
cardBackPath: String(source.cardBackPath || "").trim(),
thumbnails: normalizeThumbnailConfig(rawManifest.thumbnails, source.thumbnailRoot),
majors: rawManifest.majors || {},
minors: rawManifest.minors || {},
nameOverrides,
@@ -418,7 +479,13 @@
return normalizedManifest;
}
function getRankIndex(minorRule, parsedMinor) {
function getRankOrder(minorRule, fallbackRankOrder = []) {
const explicitRankOrder = Array.isArray(minorRule?.rankOrder) ? minorRule.rankOrder : [];
const rankOrderSource = explicitRankOrder.length ? explicitRankOrder : fallbackRankOrder;
return rankOrderSource.map((entry) => String(entry || "").trim()).filter(Boolean);
}
function getRankIndex(minorRule, parsedMinor, fallbackRankOrder = []) {
if (!minorRule || !parsedMinor) {
return null;
}
@@ -434,7 +501,7 @@
}
}
const rankOrder = Array.isArray(minorRule.rankOrder) ? minorRule.rankOrder : [];
const rankOrder = getRankOrder(minorRule, fallbackRankOrder);
for (let i = 0; i < rankOrder.length; i += 1) {
const candidate = String(rankOrder[i] || "").toLowerCase();
if (candidate && (candidate === lowerRankWord || candidate === lowerRankKey)) {
@@ -445,6 +512,34 @@
return null;
}
function resolveMinorNumberTemplateGroup(groupRule, parsedMinor, fallbackRankOrder = []) {
if (!groupRule || typeof groupRule !== "object") {
return null;
}
const rankIndex = getRankIndex(groupRule, parsedMinor, fallbackRankOrder);
if (!Number.isInteger(rankIndex) || rankIndex < 0) {
return null;
}
const suitBaseRaw = Number(groupRule?.suitBase?.[parsedMinor.suitId]);
if (!Number.isFinite(suitBaseRaw)) {
return null;
}
const numberPad = Number.isInteger(groupRule.numberPad) ? groupRule.numberPad : 2;
const cardNumber = String(suitBaseRaw + rankIndex).padStart(numberPad, "0");
const template = String(groupRule.template || "{number}.png");
return applyTemplate(template, {
number: cardNumber,
suitId: parsedMinor.suitId,
rank: parsedMinor.rankWord,
rankKey: parsedMinor.rankKey,
index: rankIndex
});
}
function resolveMajorFile(manifest, canonicalName) {
const majorRule = manifest?.majors;
if (!majorRule || typeof majorRule !== "object") {
@@ -487,6 +582,14 @@
return null;
}
if (minorRule.mode === "split-number-template") {
if (Number.isFinite(parsedMinor.pipValue)) {
return resolveMinorNumberTemplateGroup(minorRule.smalls, parsedMinor, defaultPipRankOrder);
}
return resolveMinorNumberTemplateGroup(minorRule.courts, parsedMinor);
}
const rankIndex = getRankIndex(minorRule, parsedMinor);
if (!Number.isInteger(rankIndex) || rankIndex < 0) {
return null;
@@ -555,8 +658,7 @@
return null;
}
function resolveWithDeck(deckId, cardName) {
const manifest = getDeckManifest(deckId);
function resolveCardRelativePath(manifest, cardName) {
if (!manifest) {
return null;
}
@@ -564,7 +666,7 @@
const canonical = canonicalMajorName(cardName);
const majorFile = resolveMajorFile(manifest, canonical);
if (majorFile) {
return `${manifest.basePath}/${majorFile}`;
return majorFile;
}
const parsedMinor = parseMinorCard(cardName);
@@ -572,12 +674,25 @@
return null;
}
const minorFile = resolveMinorFile(manifest, parsedMinor);
if (!minorFile) {
return resolveMinorFile(manifest, parsedMinor);
}
function resolveWithDeck(deckId, cardName, variant = "full") {
const manifest = getDeckManifest(deckId);
if (!manifest) {
return null;
}
return `${manifest.basePath}/${minorFile}`;
const relativePath = resolveCardRelativePath(manifest, cardName);
if (!relativePath) {
return null;
}
if (variant === "thumbnail") {
return resolveDeckThumbnailPath(manifest, relativePath) || `${manifest.basePath}/${relativePath}`;
}
return `${manifest.basePath}/${relativePath}`;
}
function resolveTarotCardImage(cardName) {
@@ -589,6 +704,16 @@
return null;
}
function resolveTarotCardThumbnail(cardName, optionsOrDeckId) {
const { resolvedDeckId } = resolveDeckOptions(optionsOrDeckId);
const thumbnailPath = resolveWithDeck(resolvedDeckId, cardName, "thumbnail");
if (thumbnailPath) {
return encodeURI(thumbnailPath);
}
return null;
}
function resolveTarotCardBackImage(optionsOrDeckId) {
const { resolvedDeckId } = resolveDeckOptions(optionsOrDeckId);
@@ -608,6 +733,22 @@
return null;
}
function resolveTarotCardBackThumbnail(optionsOrDeckId) {
const { resolvedDeckId } = resolveDeckOptions(optionsOrDeckId);
if (cardBackThumbnailCache.has(resolvedDeckId)) {
const cachedPath = cardBackThumbnailCache.get(resolvedDeckId);
return cachedPath ? encodeURI(cachedPath) : null;
}
const manifest = getDeckManifest(resolvedDeckId);
const relativeBackPath = String(manifest?.cardBack || manifest?.cardBackPath || "").trim();
const thumbnailPath = resolveDeckThumbnailPath(manifest, relativeBackPath) || resolveDeckCardBackPath(manifest);
cardBackThumbnailCache.set(resolvedDeckId, thumbnailPath || null);
return thumbnailPath ? encodeURI(thumbnailPath) : null;
}
function resolveDisplayNameWithDeck(deckId, cardName, trumpNumber) {
const manifest = getDeckManifest(deckId);
const fallbackName = String(cardName || "").trim();
@@ -712,7 +853,9 @@
window.TarotCardImages = {
resolveTarotCardImage,
resolveTarotCardThumbnail,
resolveTarotCardBackImage,
resolveTarotCardBackThumbnail,
getTarotCardDisplayName,
getTarotCardSearchAliases,
setActiveDeck,