This commit is contained in:
33333-33333 2026-07-10 16:40:06 +09:00
commit 2636d580a8
75 changed files with 3890 additions and 1084 deletions

View file

@ -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 };
},
});
}