Initial commit
This commit is contained in:
@@ -0,0 +1,959 @@
|
||||
/**
|
||||
* 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'
|
||||
};
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user