/** * ContextRenderer.js - Main renderer for applying contexts to avatars * * Handles the complete rendering pipeline for avatars with contexts, * including vehicles, costumes, and pure animation contexts. * * Dependencies (must be loaded before this file): * - window.AvatarSystem * - window.AvatarRenderer * - window.AvatarAnimations * - window.AccessoryRenderer * - window.VehicleContexts * - window.CostumeContexts * - window.PureContexts */ (function() { 'use strict'; // ============================================================ // CONTEXT APPLICATION // ============================================================ /** * Apply a context to an avatar, creating a renderable object * @param {Object} avatar - The avatar object * @param {string} contextId - The context ID to apply * @param {Object} options - Optional configuration * @returns {Object} Avatar with context object */ function apply(avatar, contextId, options) { var context = getContextById(contextId); if (!context) { console.error('ContextRenderer: Unknown context ID:', contextId); return null; } // Validate avatar has required mode or can be rendered in it var mode = context.avatarMode; if (!mode) { console.warn('ContextRenderer: Context missing avatarMode:', contextId); mode = 'profile'; // Default fallback } return { avatar: avatar, context: context, mode: mode, state: { animation: context.defaultAnimation || 'idle', frame: 0, time: 0, animationStartTime: 0, animationOptions: {}, effects: [], props: {}, custom: {} }, options: options || {} }; } // ============================================================ // MAIN RENDER FUNCTION // ============================================================ /** * Render complete scene with avatar and context * @param {Object} avatarWithContext - The avatar+context object from apply() * @param {CanvasRenderingContext2D} ctx - Canvas context * @param {number} x - Center X position * @param {number} y - Center Y position * @param {Object} options - Render options (scale, time, etc) */ function render(avatarWithContext, ctx, x, y, options) { if (!avatarWithContext || !avatarWithContext.context) { console.error('ContextRenderer.render: Invalid avatarWithContext'); return; } options = options || {}; var scale = options.scale || 1; var time = options.time || 0; var awc = avatarWithContext; var context = awc.context; var avatar = awc.avatar; var mode = awc.mode; // Get context dimensions var contextWidth = context.width || 64; var contextHeight = context.height || 64; var scaledWidth = contextWidth * scale; var scaledHeight = contextHeight * scale; // Get animation transforms var transforms = {}; if (awc.state.animation && window.AvatarAnimations) { var anim = window.AvatarAnimations.getAnimation(mode, awc.state.animation); if (anim) { var progress = anim.frameCount > 0 ? awc.state.frame / anim.frameCount : 0; transforms = window.AvatarAnimations.interpolateFrame(anim, progress); } } // Calculate avatar position within context var avatarPosX = context.avatarPosition ? context.avatarPosition.x : 0.5; var avatarPosY = context.avatarPosition ? context.avatarPosition.y : 0.5; var avatarX = x + (avatarPosX - 0.5) * scaledWidth; var avatarY = y + (avatarPosY - 0.5) * scaledHeight; var avatarScale = scale * (context.avatarScale || 1); // Route to appropriate render pipeline based on category var category = context.category || 'pure'; if (category === 'vehicle') { renderVehicleContext(ctx, awc, avatar, context, x, y, avatarX, avatarY, scale, avatarScale, scaledWidth, scaledHeight, transforms, time); } else if (category === 'costume') { renderCostumeContext(ctx, awc, avatar, context, x, y, avatarX, avatarY, scale, avatarScale, scaledWidth, scaledHeight, transforms, time); } else { // Pure context (default) renderPureContext(ctx, awc, avatar, context, x, y, avatarX, avatarY, scale, avatarScale, scaledWidth, scaledHeight, transforms, time); } } // ============================================================ // VEHICLE CONTEXT RENDERING // ============================================================ function renderVehicleContext(ctx, awc, avatar, context, x, y, avatarX, avatarY, scale, avatarScale, width, height, transforms, time) { // 1. Context background (vehicle body behind avatar) if (context.renderBackground && typeof context.renderBackground === 'function') { ctx.save(); context.renderBackground(ctx, width, height, time, awc.state); ctx.restore(); } // 2. Render avatar if (window.AvatarRenderer && window.AvatarRenderer.render) { window.AvatarRenderer.render(ctx, avatar, awc.mode, avatarX, avatarY, { scale: avatarScale, transforms: transforms }); } // 3. Render accessories respecting visibility rules renderAccessoriesForContext(ctx, avatar, context, avatarX, avatarY, avatarScale, time); // 4. Context foreground (steering wheel, windshield, etc) if (context.renderForeground && typeof context.renderForeground === 'function') { ctx.save(); context.renderForeground(ctx, width, height, time, awc.state); ctx.restore(); } // 5. Context effects (exhaust, speed lines, etc) renderContextEffects(ctx, awc, x, y, scale, time); } // ============================================================ // COSTUME CONTEXT RENDERING // ============================================================ function renderCostumeContext(ctx, awc, avatar, context, x, y, avatarX, avatarY, scale, avatarScale, width, height, transforms, time) { // 1. Costume under-layer (body armor, suit body, etc) if (context.renderCostumeUnder && typeof context.renderCostumeUnder === 'function') { ctx.save(); context.renderCostumeUnder(ctx, avatar, avatarX, avatarY, avatarScale, transforms, time); ctx.restore(); } // 2. Avatar HEAD ONLY (body is covered by costume) renderAvatarHeadOnly(ctx, avatar, avatarX, avatarY, avatarScale, transforms); // 3. Costume over-layer (helmet, held items, cape) if (context.renderCostumeOver && typeof context.renderCostumeOver === 'function') { ctx.save(); context.renderCostumeOver(ctx, avatar, avatarX, avatarY, avatarScale, transforms, time); ctx.restore(); } // 4. EARNED ACCESSORIES ON TOP (crown shows above helmet, etc) renderEarnedAccessoriesOnTop(ctx, avatar, context, avatarX, avatarY, avatarScale, time); // 5. Effects (auras are always visible) renderEffectsForContext(ctx, avatar, context, avatarX, avatarY, avatarScale, time); // 6. Context-specific effects renderContextEffects(ctx, awc, x, y, scale, time); } // ============================================================ // PURE CONTEXT RENDERING // ============================================================ function renderPureContext(ctx, awc, avatar, context, x, y, avatarX, avatarY, scale, avatarScale, width, height, transforms, time) { // 1. Behind effects (shadows, dust clouds, etc) if (context.renderEffectsBehind && typeof context.renderEffectsBehind === 'function') { ctx.save(); context.renderEffectsBehind(ctx, avatar, avatarX, avatarY, avatarScale, transforms, time, awc.state); ctx.restore(); } // 2. Full avatar with all accessories if (window.AvatarRenderer && window.AvatarRenderer.render) { window.AvatarRenderer.render(ctx, avatar, awc.mode, avatarX, avatarY, { scale: avatarScale, transforms: transforms }); } if (window.AccessoryRenderer && window.AccessoryRenderer.render) { window.AccessoryRenderer.render(ctx, avatar, awc.mode, avatarX, avatarY, avatarScale, time); } // 3. Over effects (speed lines, stars, sparkles) if (context.renderEffectsOver && typeof context.renderEffectsOver === 'function') { ctx.save(); context.renderEffectsOver(ctx, avatar, avatarX, avatarY, avatarScale, transforms, time, awc.state); ctx.restore(); } // 4. Active state effects renderContextEffects(ctx, awc, x, y, scale, time); } // ============================================================ // ANIMATION CONTROL // ============================================================ /** * Start an animation on an avatar with context * @param {Object} avatarWithContext - The avatar+context object * @param {string} animationId - Animation ID to play * @param {Object} options - Animation options * @returns {boolean} True if animation started successfully */ function animate(avatarWithContext, animationId, options) { if (!avatarWithContext || !avatarWithContext.context) { console.error('ContextRenderer.animate: Invalid avatarWithContext'); return false; } var awc = avatarWithContext; var context = awc.context; // Validate animation is available for this context if (context.animations && Array.isArray(context.animations)) { if (context.animations.indexOf(animationId) === -1) { console.warn('ContextRenderer: Animation not available for context:', animationId, 'in', context.id); return false; } } // Check if animation exists in the animation system if (window.AvatarAnimations) { var anim = window.AvatarAnimations.getAnimation(awc.mode, animationId); if (!anim) { console.warn('ContextRenderer: Animation not found:', animationId, 'for mode', awc.mode); return false; } } // Set animation state awc.state.animation = animationId; awc.state.frame = 0; awc.state.time = 0; awc.state.animationStartTime = performance.now(); awc.state.animationOptions = options || {}; // Trigger context-specific effects for this animation if (context.contextEffects && context.contextEffects[animationId]) { triggerEffect(awc, context.contextEffects[animationId]); } return true; } /** * Update animation state based on elapsed time * @param {Object} avatarWithContext - The avatar+context object * @param {number} deltaTime - Time elapsed in seconds */ function updateAnimation(avatarWithContext, deltaTime) { if (!avatarWithContext) return; var awc = avatarWithContext; if (!awc.state.animation) return; // Get animation definition var anim = null; if (window.AvatarAnimations) { anim = window.AvatarAnimations.getAnimation(awc.mode, awc.state.animation); } if (!anim) return; // Update time awc.state.time += deltaTime; // Calculate frame based on time var frameDuration = (anim.frameDuration || 100) / 1000; // Convert ms to seconds var totalFrames = anim.frameCount || 1; if (anim.loop) { // Looping animation awc.state.frame = (awc.state.time / frameDuration) % totalFrames; } else { // One-shot animation awc.state.frame = Math.min(awc.state.time / frameDuration, totalFrames - 1); // Check if animation completed if (awc.state.frame >= totalFrames - 1) { // Trigger completion callback if set if (awc.state.animationOptions && awc.state.animationOptions.onComplete) { awc.state.animationOptions.onComplete(); } } } // Update active effects updateEffects(awc, deltaTime); } // ============================================================ // EFFECT SYSTEM // ============================================================ /** * Trigger a visual effect * @param {Object} avatarWithContext - The avatar+context object * @param {Object} effectDef - Effect definition */ function triggerEffect(avatarWithContext, effectDef) { if (!avatarWithContext || !effectDef) return; var awc = avatarWithContext; // Handle effect definition as string (lookup) or object var effect; if (typeof effectDef === 'string') { effect = { type: effectDef, color: '#ffffff', startTime: performance.now(), duration: 0.5, intensity: 1, data: {} }; } else { effect = { type: effectDef.type || 'flash', color: effectDef.color || '#ffffff', startTime: performance.now(), duration: effectDef.duration || 0.5, intensity: effectDef.intensity || 1, data: effectDef.data || {} }; } awc.state.effects.push(effect); } /** * Update all active effects * @param {Object} avatarWithContext - The avatar+context object * @param {number} deltaTime - Time elapsed in seconds */ function updateEffects(avatarWithContext, deltaTime) { if (!avatarWithContext) return; var awc = avatarWithContext; var now = performance.now(); // Filter out expired effects and update active ones awc.state.effects = awc.state.effects.filter(function(effect) { var elapsed = (now - effect.startTime) / 1000; if (elapsed >= effect.duration) { return false; // Remove expired effect } // Update effect progress effect.progress = elapsed / effect.duration; return true; }); } /** * Clear all active effects * @param {Object} avatarWithContext - The avatar+context object */ function clearEffects(avatarWithContext) { if (!avatarWithContext) return; avatarWithContext.state.effects = []; } /** * Render context effects */ function renderContextEffects(ctx, awc, x, y, scale, time) { if (!awc.state.effects || awc.state.effects.length === 0) return; var now = performance.now(); awc.state.effects.forEach(function(effect) { var elapsed = (now - effect.startTime) / 1000; var progress = Math.min(elapsed / effect.duration, 1); ctx.save(); switch (effect.type) { case 'flash': renderFlashEffect(ctx, x, y, scale, effect, progress); break; case 'burst': renderBurstEffect(ctx, x, y, scale, effect, progress); break; case 'trail': renderTrailEffect(ctx, x, y, scale, effect, progress); break; case 'glow': renderGlowEffect(ctx, x, y, scale, effect, progress); break; case 'particles': renderParticleEffect(ctx, x, y, scale, effect, progress, time); break; case 'shake': // Shake is handled via transforms, not direct rendering break; case 'speedLines': renderSpeedLinesEffect(ctx, x, y, scale, effect, progress); break; default: // Unknown effect type - custom effects can be rendered by context break; } ctx.restore(); }); } // Effect renderers function renderFlashEffect(ctx, x, y, scale, effect, progress) { var alpha = (1 - progress) * effect.intensity; var radius = 30 * scale * (1 + progress); ctx.globalAlpha = alpha; ctx.fillStyle = effect.color; ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill(); } function renderBurstEffect(ctx, x, y, scale, effect, progress) { var alpha = (1 - progress) * effect.intensity; var rayCount = effect.data.rayCount || 8; var maxLength = (effect.data.maxLength || 40) * scale; var length = maxLength * progress; ctx.globalAlpha = alpha; ctx.strokeStyle = effect.color; ctx.lineWidth = 2 * scale; for (var i = 0; i < rayCount; i++) { var angle = (i / rayCount) * Math.PI * 2; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo( x + Math.cos(angle) * length, y + Math.sin(angle) * length ); ctx.stroke(); } } function renderTrailEffect(ctx, x, y, scale, effect, progress) { var alpha = (1 - progress) * effect.intensity * 0.5; var trailLength = (effect.data.length || 20) * scale; var direction = effect.data.direction || -1; // -1 = trailing left var gradient = ctx.createLinearGradient( x, y, x + trailLength * direction, y ); gradient.addColorStop(0, effect.color); gradient.addColorStop(1, 'transparent'); ctx.globalAlpha = alpha; ctx.fillStyle = gradient; ctx.fillRect(x, y - 10 * scale, trailLength * direction, 20 * scale); } function renderGlowEffect(ctx, x, y, scale, effect, progress) { var alpha = effect.intensity * (0.5 + 0.5 * Math.sin(progress * Math.PI * 2)); var radius = (effect.data.radius || 25) * scale; var gradient = ctx.createRadialGradient(x, y, 0, x, y, radius); gradient.addColorStop(0, effect.color); gradient.addColorStop(1, 'transparent'); ctx.globalAlpha = alpha; ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill(); } function renderParticleEffect(ctx, x, y, scale, effect, progress, time) { var particleCount = effect.data.count || 10; var alpha = (1 - progress) * effect.intensity; var spread = (effect.data.spread || 30) * scale; ctx.globalAlpha = alpha; ctx.fillStyle = effect.color; // Use seeded random based on effect start time for consistency var seed = effect.startTime; for (var i = 0; i < particleCount; i++) { var pseudoRandom = Math.sin(seed + i * 12.9898) * 43758.5453; pseudoRandom = pseudoRandom - Math.floor(pseudoRandom); var angle = pseudoRandom * Math.PI * 2; var distance = spread * progress * (0.5 + pseudoRandom * 0.5); var size = 2 * scale * (1 - progress); ctx.beginPath(); ctx.arc( x + Math.cos(angle) * distance, y + Math.sin(angle) * distance - progress * 20 * scale, // Rise up size, 0, Math.PI * 2 ); ctx.fill(); } } function renderSpeedLinesEffect(ctx, x, y, scale, effect, progress) { var alpha = effect.intensity * (1 - progress * 0.5); var lineCount = effect.data.lineCount || 5; var lineLength = (effect.data.lineLength || 30) * scale; ctx.globalAlpha = alpha; ctx.strokeStyle = effect.color; ctx.lineWidth = 1 * scale; for (var i = 0; i < lineCount; i++) { var yOffset = (i - lineCount / 2) * 8 * scale; var xStart = x - 20 * scale - lineLength * progress; ctx.beginPath(); ctx.moveTo(xStart, y + yOffset); ctx.lineTo(xStart + lineLength, y + yOffset); ctx.stroke(); } } // ============================================================ // HELPER RENDERING FUNCTIONS // ============================================================ /** * Render only the avatar's head (for costumes that cover body) */ function renderAvatarHeadOnly(ctx, avatar, x, y, scale, transforms) { if (window.AvatarRenderer && window.AvatarRenderer.renderHead) { window.AvatarRenderer.renderHead(ctx, avatar, x, y, { scale: scale, transforms: transforms }); } else if (window.AvatarRenderer && window.AvatarRenderer.render) { // Fallback: render full avatar if renderHead not available // This is a compromise - costume should hide body anyway ctx.save(); // Clip to head region (approximate) var headY = y - 12 * scale; // Head is above center var headRadius = 10 * scale; ctx.beginPath(); ctx.arc(x, headY, headRadius * 1.5, 0, Math.PI * 2); ctx.clip(); window.AvatarRenderer.render(ctx, avatar, 'profile', x, y, { scale: scale, transforms: transforms }); ctx.restore(); } } /** * Render accessories respecting context visibility rules */ function renderAccessoriesForContext(ctx, avatar, context, x, y, scale, time) { if (!window.AccessoryRenderer) return; var vis = context.accessoryVisibility || {}; // Default visibility if not specified var showHeadwear = vis.headwear !== false; var showHandheld = vis.handheld !== false; var showEffects = vis.effects !== false; var showFace = vis.face !== false; // Use AccessoryRenderer with visibility filter if (window.AccessoryRenderer.renderFiltered) { window.AccessoryRenderer.renderFiltered(ctx, avatar, context.avatarMode || 'profile', x, y, scale, time, { headwear: showHeadwear, handheld: showHandheld, effects: showEffects, face: showFace }); } else if (window.AccessoryRenderer.render) { // Fallback to standard render if filtered not available // Context-specific visibility would need to be handled differently window.AccessoryRenderer.render(ctx, avatar, context.avatarMode || 'profile', x, y, scale, time); } } /** * Render EARNED accessories on top of everything (crowns above helmets, etc) */ function renderEarnedAccessoriesOnTop(ctx, avatar, context, x, y, scale, time) { if (!window.AccessoryRenderer) return; if (!avatar.accessories) return; var vis = context.accessoryVisibility || {}; var costumeOffset = context.costumeHeadOffset || { x: 0, y: -5 }; // Crown sits higher on helmet // Render headwear if visibility allows and avatar has it if (vis.headwear === true && avatar.accessories.headwear) { var headwearY = y + costumeOffset.y * scale; var headwearX = x + costumeOffset.x * scale; if (window.AccessoryRenderer.renderSingle) { window.AccessoryRenderer.renderSingle(ctx, avatar, 'headwear', headwearX, headwearY, scale, time); } } // Render effect accessories (auras, trails) if allowed if (vis.effects === true && avatar.accessories.effect) { if (window.AccessoryRenderer.renderSingle) { window.AccessoryRenderer.renderSingle(ctx, avatar, 'effect', x, y, scale, time); } } } /** * Render effect accessories (auras, etc) for costume context */ function renderEffectsForContext(ctx, avatar, context, x, y, scale, time) { if (!window.AccessoryRenderer) return; if (!avatar.accessories || !avatar.accessories.effect) return; var vis = context.accessoryVisibility || {}; // Effects (auras) are almost always visible if (vis.effects !== false) { if (window.AccessoryRenderer.renderSingle) { window.AccessoryRenderer.renderSingle(ctx, avatar, 'effect', x, y, scale, time); } } } // ============================================================ // CONTEXT LOOKUP // ============================================================ /** * Get context definition by ID */ function getContextById(contextId) { // Check ContextCatalog first if (window.ContextCatalog && window.ContextCatalog.getById) { var context = window.ContextCatalog.getById(contextId); if (context) return context; } // Fallback to individual context modules if (window.VehicleContexts && window.VehicleContexts[contextId]) { return window.VehicleContexts[contextId]; } if (window.CostumeContexts && window.CostumeContexts[contextId]) { return window.CostumeContexts[contextId]; } if (window.PureContexts && window.PureContexts[contextId]) { return window.PureContexts[contextId]; } return null; } /** * Get required avatar mode for a context */ function getRequiredMode(contextId) { var context = getContextById(contextId); return context ? context.avatarMode : null; } /** * Get available animations for a context */ function getAvailableAnimations(contextId) { var context = getContextById(contextId); return context && context.animations ? context.animations.slice() : []; } // ============================================================ // STATE MANAGEMENT // ============================================================ /** * Set custom state value */ function setState(avatarWithContext, key, value) { if (!avatarWithContext || !avatarWithContext.state) return; avatarWithContext.state.custom[key] = value; } /** * Get custom state value */ function getState(avatarWithContext, key) { if (!avatarWithContext || !avatarWithContext.state) return undefined; return avatarWithContext.state.custom[key]; } /** * Set prop state (for interactive elements) */ function setProp(avatarWithContext, propId, state) { if (!avatarWithContext || !avatarWithContext.state) return; avatarWithContext.state.props[propId] = state; } /** * Get prop state */ function getProp(avatarWithContext, propId) { if (!avatarWithContext || !avatarWithContext.state) return undefined; return avatarWithContext.state.props[propId]; } // ============================================================ // EXPORT UTILITIES // ============================================================ /** * Create a sprite sheet from avatar with context * @param {Object} avatarWithContext - The avatar+context object * @param {Array} animations - Array of animation IDs to include * @param {Object} options - Sprite sheet options * @returns {Object} Sprite sheet data { canvas, frames, meta } */ function createSpriteSheet(avatarWithContext, animations, options) { if (!avatarWithContext) return null; options = options || {}; var frameWidth = options.frameWidth || 64; var frameHeight = options.frameHeight || 64; var scale = options.scale || 1; var awc = avatarWithContext; var allFrames = []; // Collect all frames for each animation animations.forEach(function(animId) { var anim = null; if (window.AvatarAnimations) { anim = window.AvatarAnimations.getAnimation(awc.mode, animId); } if (!anim) return; for (var i = 0; i < anim.frameCount; i++) { allFrames.push({ animation: animId, frame: i, frameCount: anim.frameCount }); } }); if (allFrames.length === 0) return null; // Calculate sprite sheet dimensions var cols = Math.ceil(Math.sqrt(allFrames.length)); var rows = Math.ceil(allFrames.length / cols); var sheetWidth = cols * frameWidth; var sheetHeight = rows * frameHeight; // Create canvas var canvas = document.createElement('canvas'); canvas.width = sheetWidth; canvas.height = sheetHeight; var ctx = canvas.getContext('2d'); // Render each frame var frames = []; allFrames.forEach(function(frameData, index) { var col = index % cols; var row = Math.floor(index / cols); var x = col * frameWidth + frameWidth / 2; var y = row * frameHeight + frameHeight / 2; // Temporarily set animation state awc.state.animation = frameData.animation; awc.state.frame = frameData.frame; // Render render(awc, ctx, x, y, { scale: scale, time: 0 }); frames.push({ animation: frameData.animation, frame: frameData.frame, x: col * frameWidth, y: row * frameHeight, width: frameWidth, height: frameHeight }); }); return { canvas: canvas, frames: frames, meta: { width: sheetWidth, height: sheetHeight, frameWidth: frameWidth, frameHeight: frameHeight, animations: animations } }; } /** * Export avatar with context for game use * @param {Object} avatarWithContext - The avatar+context object * @returns {Object} Exportable data */ function exportForGame(avatarWithContext) { if (!avatarWithContext) return null; var awc = avatarWithContext; var context = awc.context; return { avatarId: awc.avatar.id || null, contextId: context.id, mode: awc.mode, category: context.category, animations: context.animations || [], defaultAnimation: context.defaultAnimation || 'idle', dimensions: { width: context.width || 64, height: context.height || 64 }, avatarPosition: context.avatarPosition || { x: 0.5, y: 0.5 }, avatarScale: context.avatarScale || 1, accessoryVisibility: context.accessoryVisibility || {}, customState: Object.assign({}, awc.state.custom) }; } // ============================================================ // CATALOG ACCESS (delegates to ContextCatalog) // ============================================================ function getCatalog() { if (window.ContextCatalog && window.ContextCatalog.getAll) { return window.ContextCatalog.getAll(); } // Fallback: aggregate from individual modules var catalog = []; if (window.VehicleContexts) { Object.keys(window.VehicleContexts).forEach(function(id) { if (typeof window.VehicleContexts[id] === 'object') { catalog.push(window.VehicleContexts[id]); } }); } if (window.CostumeContexts) { Object.keys(window.CostumeContexts).forEach(function(id) { if (typeof window.CostumeContexts[id] === 'object') { catalog.push(window.CostumeContexts[id]); } }); } if (window.PureContexts) { Object.keys(window.PureContexts).forEach(function(id) { if (typeof window.PureContexts[id] === 'object') { catalog.push(window.PureContexts[id]); } }); } return catalog; } function getContext(id) { return getContextById(id); } // ============================================================ // PUBLIC API // ============================================================ window.ContextRenderer = { // Core methods apply: apply, render: render, animate: animate, updateAnimation: updateAnimation, // Effect control triggerEffect: triggerEffect, updateEffects: updateEffects, clearEffects: clearEffects, // State management setState: setState, getState: getState, setProp: setProp, getProp: getProp, // Context queries getRequiredMode: getRequiredMode, getAvailableAnimations: getAvailableAnimations, // Catalog access (delegates to ContextCatalog) getCatalog: getCatalog, getContext: getContext, // Utility createSpriteSheet: createSpriteSheet, exportForGame: exportForGame, // Version version: '1.0.0' }; })();