aaa
This commit is contained in:
parent
f1c3af2b89
commit
2636d580a8
75 changed files with 3890 additions and 1084 deletions
101
js/assets.js
101
js/assets.js
|
|
@ -3,6 +3,7 @@ const images = new Map();
|
|||
const imageMetrics = new Map();
|
||||
const imagePromises = new Map();
|
||||
const imageAssetIndex = new Map();
|
||||
const imageLastUsedAt = new Map();
|
||||
|
||||
const DECOR_ASSETS = [
|
||||
{ id: "zunchi", path: "assets/objects/zunchi.webp" },
|
||||
|
|
@ -69,6 +70,7 @@ const scaledSpriteCache = new Map();
|
|||
const stainOverlayCache = new Map();
|
||||
const SPRITE_CACHE_LIMIT = 128;
|
||||
const STAIN_CACHE_LIMIT = 64;
|
||||
const IMAGE_KEEP_IDS = new Set(["smile"]);
|
||||
|
||||
const SUNBATH_IMAGE_IDS = new Set(SPRITES
|
||||
.map(asset => asset?.id || "")
|
||||
|
|
@ -182,6 +184,33 @@ function markImageLoaded(asset, img) {
|
|||
});
|
||||
}
|
||||
|
||||
function touchImage(id) {
|
||||
if (id) imageLastUsedAt.set(id, Date.now());
|
||||
}
|
||||
|
||||
function clearCanvasCacheForImage(id) {
|
||||
const prefix = `${id}:`;
|
||||
for (const key of [...scaledSpriteCache.keys()]) if (String(key).startsWith(prefix)) scaledSpriteCache.delete(key);
|
||||
for (const key of [...stainOverlayCache.keys()]) if (String(key).startsWith(prefix)) stainOverlayCache.delete(key);
|
||||
}
|
||||
|
||||
function unloadImageAsset(id) {
|
||||
if (!id || IMAGE_KEEP_IDS.has(id)) return false;
|
||||
const img = images.get(id);
|
||||
if (!img) return false;
|
||||
clearCanvasCacheForImage(id);
|
||||
images.delete(id);
|
||||
imagePromises.delete(id);
|
||||
imageLastUsedAt.delete(id);
|
||||
try {
|
||||
img.onload = null;
|
||||
img.onerror = null;
|
||||
img.removeAttribute?.("src");
|
||||
img.src = "";
|
||||
} catch (_) {}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isImageReady(id) {
|
||||
const img = images.get(id);
|
||||
return !!(img && img.complete && (img.naturalWidth || img.width));
|
||||
|
|
@ -191,16 +220,19 @@ function loadImageAsset(asset, priority = "auto") {
|
|||
if (!asset) return Promise.resolve(null);
|
||||
setupAssetIndex();
|
||||
primeImageMetrics(asset);
|
||||
if (isImageReady(asset.id)) return Promise.resolve(images.get(asset.id));
|
||||
if (isImageReady(asset.id)) { touchImage(asset.id); return Promise.resolve(images.get(asset.id)); }
|
||||
if (imagePromises.has(asset.id)) return imagePromises.get(asset.id);
|
||||
const img = images.get(asset.id) || new Image();
|
||||
img.decoding = "async";
|
||||
if ("fetchPriority" in img) img.fetchPriority = priority;
|
||||
img.alphaBounds = ALPHA_BOUNDS[asset.id] || null;
|
||||
images.set(asset.id, img);
|
||||
const promise = new Promise((resolve) => {
|
||||
img.onload = () => { markImageLoaded(asset, img); resolve(img); };
|
||||
let promise;
|
||||
promise = new Promise((resolve) => {
|
||||
img.onload = () => { markImageLoaded(asset, img); touchImage(asset.id); resolve(img); };
|
||||
img.onerror = () => { resolve(null); };
|
||||
}).finally(() => {
|
||||
if (imagePromises.get(asset.id) === promise) imagePromises.delete(asset.id);
|
||||
});
|
||||
imagePromises.set(asset.id, promise);
|
||||
if (!img.src) img.src = window.TARINAI_APP?.withVersion ? window.TARINAI_APP.withVersion(asset.path) : asset.path;
|
||||
|
|
@ -212,6 +244,7 @@ function ensureImage(id) {
|
|||
const asset = imageAssetIndex.get(id);
|
||||
if (!asset) return null;
|
||||
primeImageMetrics(asset);
|
||||
touchImage(id);
|
||||
const existing = images.get(id);
|
||||
if (!existing || !existing.src) loadImageAsset(asset, "low");
|
||||
return images.get(id) || null;
|
||||
|
|
@ -219,9 +252,9 @@ function ensureImage(id) {
|
|||
|
||||
function getRenderableImage(id, fallbackId = "smile") {
|
||||
if (id) ensureImage(id);
|
||||
if (isImageReady(id)) return images.get(id);
|
||||
if (isImageReady(id)) { touchImage(id); return images.get(id); }
|
||||
if (fallbackId && fallbackId !== id) ensureImage(fallbackId);
|
||||
if (isImageReady(fallbackId)) return images.get(fallbackId);
|
||||
if (isImageReady(fallbackId)) { touchImage(fallbackId); return images.get(fallbackId); }
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -239,19 +272,44 @@ function loadImages(onProgress = null) {
|
|||
return Promise.all(tasks).then(() => undefined);
|
||||
}
|
||||
|
||||
function trimImageMemory(aggressive = false) {
|
||||
trimCanvasCache(scaledSpriteCache, aggressive ? 48 : SPRITE_CACHE_LIMIT);
|
||||
trimCanvasCache(stainOverlayCache, aggressive ? 16 : STAIN_CACHE_LIMIT);
|
||||
function trimImageMemory(mode = "normal") {
|
||||
const aggressive = mode === true || mode === "aggressive" || mode === "hidden";
|
||||
const hidden = mode === "hidden";
|
||||
trimCanvasCache(scaledSpriteCache, aggressive ? 24 : SPRITE_CACHE_LIMIT);
|
||||
trimCanvasCache(stainOverlayCache, aggressive ? 8 : STAIN_CACHE_LIMIT);
|
||||
const now = Date.now();
|
||||
const maxLoaded = hidden ? 1 : (aggressive ? 20 : 48);
|
||||
const maxIdleMs = hidden ? 0 : (aggressive ? 20000 : 120000);
|
||||
const candidates = [];
|
||||
for (const [id, img] of images) {
|
||||
if (!img || IMAGE_KEEP_IDS.has(id)) continue;
|
||||
const lastUsed = imageLastUsedAt.get(id) || 0;
|
||||
candidates.push({ id, lastUsed });
|
||||
}
|
||||
candidates.sort((a, b) => a.lastUsed - b.lastUsed);
|
||||
let loadedCount = images.size;
|
||||
let released = 0;
|
||||
for (const entry of candidates) {
|
||||
if (!hidden && loadedCount <= maxLoaded && now - entry.lastUsed <= maxIdleMs) continue;
|
||||
if (unloadImageAsset(entry.id)) {
|
||||
loadedCount -= 1;
|
||||
released += 1;
|
||||
}
|
||||
}
|
||||
return { released, loaded: images.size, spriteCache: scaledSpriteCache.size, stainCache: stainOverlayCache.size };
|
||||
}
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) trimImageMemory(true);
|
||||
if (document.hidden) trimImageMemory("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.setInterval(() => trimImageMemory(false), 45000);
|
||||
window.setInterval(() => {
|
||||
const profile = window.TarinaiPerf?.simulationOptimizationTier?.();
|
||||
trimImageMemory(profile === "aggressive" ? "aggressive" : "normal");
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
function startBackgroundImageLoading() {
|
||||
|
|
@ -265,10 +323,17 @@ function startBackgroundImageLoading() {
|
|||
const schedule = window.requestIdleCallback
|
||||
? (fn, timeout = 1200) => window.requestIdleCallback(fn, { timeout })
|
||||
: (fn) => setTimeout(fn, 60);
|
||||
schedule(() => loadGroup(EARLY_IMAGE_IDS, "auto"), 450);
|
||||
schedule(() => {
|
||||
for (const asset of [...SPRITES, ...DECOR_ASSETS]) loadImageAsset(asset, "low");
|
||||
trimImageMemory(false);
|
||||
const profile = window.TarinaiPerf?.simulationOptimizationTier?.() || "balanced";
|
||||
const ids = profile === "aggressive" ? [...EARLY_IMAGE_IDS].slice(0, 18) : EARLY_IMAGE_IDS;
|
||||
loadGroup(ids, "auto");
|
||||
}, 450);
|
||||
schedule(() => {
|
||||
const profile = window.TarinaiPerf?.simulationOptimizationTier?.() || "balanced";
|
||||
if (profile === "full") {
|
||||
for (const asset of [...SPRITES, ...DECOR_ASSETS]) loadImageAsset(asset, "low");
|
||||
}
|
||||
trimImageMemory(profile === "aggressive" ? "aggressive" : "normal");
|
||||
}, 2200);
|
||||
}
|
||||
|
||||
|
|
@ -298,3 +363,13 @@ function getImageMetrics(idOrImg) {
|
|||
|
||||
setupAssetIndex();
|
||||
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.TarinaiAssetsMemory = Object.freeze({
|
||||
trim: trimImageMemory,
|
||||
unload: unloadImageAsset,
|
||||
stats() {
|
||||
return { loaded: images.size, pending: imagePromises.size, spriteCache: scaledSpriteCache.size, stainCache: stainOverlayCache.size };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue