kinetic-slider
Version:
A WebGL-powered kinetic slider component using PIXI.js
560 lines (557 loc) • 22.7 kB
JavaScript
import { Assets, Rectangle, Texture } from 'pixi.js';
// Development environment check
const isDevelopment = "production" === 'development';
/**
* Status of an atlas or image loading operation
*/
var LoadStatus;
(function (LoadStatus) {
LoadStatus["NotLoaded"] = "not_loaded";
LoadStatus["Loading"] = "loading";
LoadStatus["Loaded"] = "loaded";
LoadStatus["Failed"] = "failed";
})(LoadStatus || (LoadStatus = {}));
/**
* Manager for handling texture atlases in the KineticSlider
*
* This class provides methods for:
* - Loading texture atlases from JSON and image files
* - Retrieving textures for individual frames
* - Managing the lifecycle of atlas resources
* - Providing proper cleanup when an atlas is no longer needed
*/
class AtlasManager {
// Loaded atlases
atlases = new Map();
// Atlas textures (the full atlas texture)
atlasTextures = new Map();
// Cached frame textures (individual sprites cut from the atlas)
frameTextures = new Map();
// Image textures (for individual images, used as fallback)
imageTextures = new Map();
// Loading status for atlases
atlasStatus = new Map();
// Loading status for individual images
imageStatus = new Map();
// Reference to ResourceManager for tracking resources
resourceManager;
// Options for the atlas manager
options;
/**
* Create a new atlas manager
*
* @param options - Options for the atlas manager
* @param resourceManager - Optional ResourceManager for tracking resources
*/
constructor(options = {}, resourceManager) {
this.options = {
debug: isDevelopment,
preferAtlas: true,
cacheFrameTextures: true,
basePath: '',
...options
};
this.resourceManager = resourceManager;
if (this.options.debug) {
console.log('AtlasManager initialized with options:', this.options);
}
}
/**
* Extract the filename from a path for atlas frame lookup
*
* @param imagePath - The full path to the image
* @returns The filename part of the path
*/
getFilenameFromPath(imagePath) {
// Check if the path is empty or null
if (!imagePath)
return '';
// Extract filename from path (everything after the last /)
const filename = imagePath.split('/').pop() || imagePath;
if (this.options.debug) {
console.log(`Extracted filename "${filename}" from path "${imagePath}"`);
}
return filename;
}
/**
* Load a texture atlas from a JSON file
*
* @param atlasId - Identifier for the atlas
* @param jsonUrl - URL to the atlas JSON file
* @param imageUrl - Optional URL to the atlas image file (if not specified in JSON)
* @returns Promise resolving when the atlas is loaded
*/
async loadAtlas(atlasId, jsonUrl, imageUrl) {
// Check if atlas is already loaded or loading
if (this.atlasStatus.get(atlasId) === LoadStatus.Loaded) {
this.log(`Atlas '${atlasId}' already loaded, skipping`);
return true;
}
if (this.atlasStatus.get(atlasId) === LoadStatus.Loading) {
this.log(`Atlas '${atlasId}' is already loading, waiting...`);
// Wait for atlas to finish loading
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
const status = this.atlasStatus.get(atlasId);
if (status === LoadStatus.Loaded) {
clearInterval(checkInterval);
resolve(true);
}
else if (status === LoadStatus.Failed) {
clearInterval(checkInterval);
resolve(false);
}
}, 100);
});
}
try {
// Mark atlas as loading
this.atlasStatus.set(atlasId, LoadStatus.Loading);
this.log(`Loading atlas '${atlasId}' from ${jsonUrl}`);
// Load the atlas JSON
let atlasData;
try {
const response = await fetch(jsonUrl);
if (!response.ok) {
throw new Error(`Failed to load atlas JSON: ${response.statusText}`);
}
atlasData = await response.json();
}
catch (error) {
this.log(`Error loading atlas JSON: ${error}`, 'error');
this.atlasStatus.set(atlasId, LoadStatus.Failed);
return false;
}
// Validate atlas data
if (!atlasData.frames || !atlasData.meta) {
this.log(`Invalid atlas data: missing frames or meta`, 'error');
this.atlasStatus.set(atlasId, LoadStatus.Failed);
return false;
}
// Determine the image URL
const atlasImageUrl = imageUrl ||
(atlasData.meta.image ?
(this.options.basePath ? `${this.options.basePath}/${atlasData.meta.image}` : atlasData.meta.image) :
null);
if (!atlasImageUrl) {
this.log(`Invalid atlas data: no image URL specified`, 'error');
this.atlasStatus.set(atlasId, LoadStatus.Failed);
return false;
}
// Load the atlas texture
let atlasTexture;
try {
// Check if the texture is already in the Assets cache
if (Assets.cache.has(atlasImageUrl)) {
atlasTexture = Assets.cache.get(atlasImageUrl);
}
else {
// Load the texture
this.log(`Loading atlas texture from ${atlasImageUrl}`);
atlasTexture = await Assets.load(atlasImageUrl);
}
// Track the texture with ResourceManager
if (this.resourceManager) {
this.resourceManager.trackTexture(atlasImageUrl, atlasTexture);
}
}
catch (error) {
this.log(`Error loading atlas texture: ${error}`, 'error');
this.atlasStatus.set(atlasId, LoadStatus.Failed);
return false;
}
// Store the atlas data and texture
this.atlases.set(atlasId, atlasData);
this.atlasTextures.set(atlasId, atlasTexture);
// Mark atlas as loaded
this.atlasStatus.set(atlasId, LoadStatus.Loaded);
this.log(`Atlas '${atlasId}' loaded successfully with ${Object.keys(atlasData.frames).length} frames`);
return true;
}
catch (error) {
this.log(`Unexpected error loading atlas: ${error}`, 'error');
this.atlasStatus.set(atlasId, LoadStatus.Failed);
return false;
}
}
/**
* Check if a frame exists in any loaded atlas
*
* @param frameName - Name of the frame to check
* @returns The ID of the atlas containing the frame, or null if not found
*/
hasFrame(frameName) {
// First check with the exact name
for (const [atlasId, atlas] of this.atlases.entries()) {
if (atlas.frames[frameName]) {
return atlasId;
}
}
// If not found and it looks like a path, extract the filename and try again
if (frameName.includes('/')) {
const filename = this.getFilenameFromPath(frameName);
for (const [atlasId, atlas] of this.atlases.entries()) {
if (atlas.frames[filename]) {
if (this.options.debug) {
console.log(`Found frame "${filename}" in atlas "${atlasId}" by extracting from path "${frameName}"`);
}
return atlasId;
}
}
}
return null;
}
/**
* Get a texture for a frame from an atlas
*
* @param frameName - Name of the frame
* @param atlasId - Optional ID of the atlas to use (if not specified, all atlases will be searched)
* @returns The texture for the frame, or null if not found
*/
getFrameTexture(frameName, atlasId) {
// Use full path for cache key to avoid collisions
const cacheKey = atlasId ? `${atlasId}:${frameName}` : frameName;
// Check if we already have a cached texture for this frame
if (this.options.cacheFrameTextures && this.frameTextures.has(cacheKey)) {
return this.frameTextures.get(cacheKey);
}
// Find the atlas containing the frame
let targetAtlasId = atlasId;
let frameData = null;
let lookupName = frameName;
// If frameName looks like a path, try to extract just the filename
if (frameName.includes('/')) {
lookupName = this.getFilenameFromPath(frameName);
}
if (targetAtlasId) {
// Use the specified atlas
const atlas = this.atlases.get(targetAtlasId);
if (!atlas) {
this.log(`Atlas '${targetAtlasId}' not found`, 'warn');
return null;
}
// Try first with original name
frameData = atlas.frames[frameName] || null;
// If not found, try with extracted filename
if (!frameData && frameName !== lookupName) {
frameData = atlas.frames[lookupName] || null;
if (frameData && this.options.debug) {
console.log(`Found frame "${lookupName}" in atlas "${targetAtlasId}" by extracting from path "${frameName}"`);
}
}
if (!frameData) {
this.log(`Frame '${frameName}' not found in atlas '${targetAtlasId}'`, 'warn');
return null;
}
}
else {
// Search all atlases
for (const [id, atlas] of this.atlases.entries()) {
// Try first with original name
if (atlas.frames[frameName]) {
targetAtlasId = id;
frameData = atlas.frames[frameName];
break;
}
// If not found, try with extracted filename
if (frameName !== lookupName && atlas.frames[lookupName]) {
targetAtlasId = id;
frameData = atlas.frames[lookupName];
if (this.options.debug) {
console.log(`Found frame "${lookupName}" in atlas "${id}" by extracting from path "${frameName}"`);
}
break;
}
}
if (!targetAtlasId || !frameData) {
this.log(`Frame '${frameName}' not found in any atlas`, 'warn');
return null;
}
}
// Get the atlas texture
const atlasTexture = this.atlasTextures.get(targetAtlasId);
if (!atlasTexture) {
this.log(`Atlas texture for '${targetAtlasId}' not found`, 'warn');
return null;
}
// Create a new texture from the atlas using the frame data
try {
// Create the frame rectangle
const rect = new Rectangle(frameData.frame.x, frameData.frame.y, frameData.frame.w, frameData.frame.h);
// Create the texture using the PixiJS v8 approach
// Get the source from the atlas texture
const source = atlasTexture.source;
// Create the frame texture with the new approach
const frameTexture = new Texture({
source, // Use the same source as the atlas texture
frame: rect, // The frame rectangle within the atlas
orig: frameData.trimmed && frameData.spriteSourceSize ?
new Rectangle(frameData.spriteSourceSize.x, frameData.spriteSourceSize.y, frameData.spriteSourceSize.w, frameData.spriteSourceSize.h) : undefined,
trim: frameData.trimmed ? rect : undefined,
rotate: frameData.rotated ? 2 : 0 // 2 = DEGREES_90, 0 = DEGREES_0
});
// Cache the frame texture if enabled - use the original frameName as the key for consistency
if (this.options.cacheFrameTextures) {
this.frameTextures.set(cacheKey, frameTexture);
// Track with ResourceManager if available
if (this.resourceManager) {
// We don't directly track frame textures as they share the base texture
// But we could track them if needed for special cases
}
}
return frameTexture;
}
catch (error) {
this.log(`Error creating frame texture: ${error}`, 'error');
return null;
}
}
/**
* Get a list of all frame names in an atlas
*
* @param atlasId - ID of the atlas
* @returns Array of frame names, or empty array if atlas not found
*/
getFrameNames(atlasId) {
const atlas = this.atlases.get(atlasId);
if (!atlas) {
return [];
}
return Object.keys(atlas.frames);
}
/**
* Get a texture for an image, either from an atlas or as an individual texture
*
* @param imagePath - Path to the image
* @param atlasId - Optional ID of a specific atlas to check first
* @returns Promise resolving to the texture, or null if not found
*/
async getTexture(imagePath, atlasId) {
// First check if we have the texture as a frame in the specified atlas or any atlas
if (this.options.preferAtlas) {
// Try to find the frame using the full imagePath first
let frameTexture = this.getFrameTexture(imagePath, atlasId);
// If not found and it looks like a path, extract the filename and try again
if (!frameTexture && imagePath.includes('/')) {
const filename = this.getFilenameFromPath(imagePath);
frameTexture = this.getFrameTexture(filename, atlasId);
if (frameTexture && this.options.debug) {
console.log(`Using atlas frame for ${imagePath} (found using filename ${filename}) ${atlasId ? `from atlas '${atlasId}'` : ''}`);
}
return frameTexture;
}
if (frameTexture) {
this.log(`Using atlas frame for ${imagePath} ${atlasId ? `from atlas '${atlasId}'` : ''}`);
return frameTexture;
}
}
// If not found in atlas or atlases are not preferred, try to load the individual image
// Check if we already have the texture loaded
if (this.imageTextures.has(imagePath)) {
return this.imageTextures.get(imagePath);
}
// Check the loading status
const status = this.imageStatus.get(imagePath);
if (status === LoadStatus.Loading) {
this.log(`Image ${imagePath} is already loading, waiting...`);
// Wait for image to finish loading
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
const currentStatus = this.imageStatus.get(imagePath);
if (currentStatus === LoadStatus.Loaded) {
clearInterval(checkInterval);
resolve(this.imageTextures.get(imagePath) || null);
}
else if (currentStatus === LoadStatus.Failed) {
clearInterval(checkInterval);
resolve(null);
}
}, 100);
});
}
// Load the image
try {
this.imageStatus.set(imagePath, LoadStatus.Loading);
// Check if the texture is already in the Assets cache
let texture;
if (Assets.cache.has(imagePath)) {
texture = Assets.cache.get(imagePath);
}
else {
// Load the texture
this.log(`Loading individual texture from ${imagePath}`);
texture = await Assets.load(imagePath);
}
// Store and track the texture
this.imageTextures.set(imagePath, texture);
this.imageStatus.set(imagePath, LoadStatus.Loaded);
// Track with ResourceManager
if (this.resourceManager) {
this.resourceManager.trackTexture(imagePath, texture);
}
return texture;
}
catch (error) {
this.log(`Error loading individual texture ${imagePath}: ${error}`, 'error');
this.imageStatus.set(imagePath, LoadStatus.Failed);
return null;
}
}
/**
* Preload a set of images, preferably from atlas(es)
*
* @param imagePaths - Array of image paths to preload
* @param atlasIds - Optional array of atlas IDs to search for frames
* @param progressCallback - Optional callback for loading progress
* @returns Promise resolving when all images are loaded
*/
async preloadImages(imagePaths, atlasIds, progressCallback) {
if (!imagePaths.length) {
return;
}
let loadedCount = 0;
const totalCount = imagePaths.length;
// Function to update progress
const updateProgress = () => {
loadedCount++;
if (progressCallback) {
progressCallback(loadedCount / totalCount);
}
};
// Process each image
const loadPromises = imagePaths.map(async (imagePath) => {
try {
// Try to get the texture, optionally limiting to specified atlas IDs
if (atlasIds && atlasIds.length > 0) {
// Try each specified atlas in order
for (const atlasId of atlasIds) {
const texture = await this.getTexture(imagePath, atlasId);
if (texture) {
updateProgress();
return; // Found in one of the specified atlases
}
}
// If not found in specified atlases, try individual texture
await this.getTexture(imagePath);
}
else {
// No atlas IDs specified, try any atlas or individual texture
await this.getTexture(imagePath);
}
updateProgress();
}
catch (error) {
this.log(`Error preloading image ${imagePath}: ${error}`, 'warn');
updateProgress();
}
});
// Wait for all images to load
await Promise.all(loadPromises);
}
/**
* Unload an atlas and its resources
*
* @param atlasId - ID of the atlas to unload
*/
unloadAtlas(atlasId) {
// Check if the atlas exists
if (!this.atlases.has(atlasId)) {
this.log(`Atlas '${atlasId}' not found, cannot unload`, 'warn');
return;
}
try {
// Get the atlas texture
const atlasTexture = this.atlasTextures.get(atlasId);
// Remove all frame textures for this atlas
if (this.options.cacheFrameTextures) {
const prefix = `${atlasId}:`;
for (const [key] of this.frameTextures.entries()) {
if (key.startsWith(prefix)) {
// Do not destroy the texture as it shares the base texture with the atlas
this.frameTextures.delete(key);
}
}
}
// Remove the atlas data
this.atlases.delete(atlasId);
// Remove the atlas texture
if (atlasTexture) {
this.atlasTextures.delete(atlasId);
// Let ResourceManager handle the actual destruction
// We don't need to call destroy here as that's handled by ResourceManager
}
// Clear the loading status
this.atlasStatus.delete(atlasId);
this.log(`Atlas '${atlasId}' unloaded`);
}
catch (error) {
this.log(`Error unloading atlas '${atlasId}': ${error}`, 'error');
}
}
/**
* Unload an individual image texture
*
* @param imagePath - Path to the image
*/
unloadTexture(imagePath) {
// Check if the texture exists
if (!this.imageTextures.has(imagePath)) {
return;
}
try {
// Remove the texture
this.imageTextures.delete(imagePath);
// Clear the loading status
this.imageStatus.delete(imagePath);
// ResourceManager handles the actual texture destruction
this.log(`Texture ${imagePath} unloaded`);
}
catch (error) {
this.log(`Error unloading texture ${imagePath}: ${error}`, 'error');
}
}
/**
* Clean up all resources
*/
dispose() {
try {
// Clear all cached data
this.atlases.clear();
this.atlasTextures.clear();
this.frameTextures.clear();
this.imageTextures.clear();
this.atlasStatus.clear();
this.imageStatus.clear();
// ResourceManager handles actual resource destruction
this.log('AtlasManager disposed');
}
catch (error) {
this.log(`Error disposing AtlasManager: ${error}`, 'error');
}
}
/**
* Log a message with the appropriate level
*
* @param message - Message to log
* @param level - Log level
*/
log(message, level = 'log') {
if (!this.options.debug && level === 'log') {
return;
}
const prefix = '[AtlasManager]';
switch (level) {
case 'warn':
console.warn(`${prefix} ${message}`);
break;
case 'error':
console.error(`${prefix} ${message}`);
break;
default:
console.log(`${prefix} ${message}`);
break;
}
}
}
export { AtlasManager, LoadStatus };
//# sourceMappingURL=AtlasManager.js.map