/** * AccessoryRenderer - Renders unlockable accessories on avatars * Depends on: AvatarAccessories, AvatarRenderer */ (function() { 'use strict'; // Z-order constants for rendering layers var Z_ORDER = { BEHIND_HEAD: -10, BEHIND_HAIR: -5, ON_FACE: 10, ON_HEAD: 20, ABOVE_HEAD: 30, EFFECTS: 100 }; // Cached particle state per avatar var particleCache = new Map(); // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ function roundRect(ctx, x, y, w, h, r) { if (w < 2 * r) r = w / 2; if (h < 2 * r) r = h / 2; ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); ctx.fill(); } function darken(color, amount) { var hex = color.replace('#', ''); var r = parseInt(hex.substr(0, 2), 16); var g = parseInt(hex.substr(2, 2), 16); var b = parseInt(hex.substr(4, 2), 16); r = Math.max(0, Math.floor(r * (1 - amount))); g = Math.max(0, Math.floor(g * (1 - amount))); b = Math.max(0, Math.floor(b * (1 - amount))); return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0'); } function lighten(color, amount) { var hex = color.replace('#', ''); var r = parseInt(hex.substr(0, 2), 16); var g = parseInt(hex.substr(2, 2), 16); var b = parseInt(hex.substr(4, 2), 16); r = Math.min(255, Math.floor(r + (255 - r) * amount)); g = Math.min(255, Math.floor(g + (255 - g) * amount)); b = Math.min(255, Math.floor(b + (255 - b) * amount)); return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0'); } function drawStar(ctx, cx, cy, r, points) { ctx.beginPath(); for (var i = 0; i < points * 2; i++) { var angle = (i * Math.PI) / points - Math.PI / 2; var radius = i % 2 === 0 ? r : r * 0.5; var x = cx + Math.cos(angle) * radius; var y = cy + Math.sin(angle) * radius; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.closePath(); ctx.fill(); } function drawHeart(ctx, cx, cy, size) { ctx.beginPath(); ctx.moveTo(cx, cy + size * 0.3); ctx.bezierCurveTo(cx - size * 0.5, cy - size * 0.3, cx - size, cy + size * 0.3, cx, cy + size); ctx.bezierCurveTo(cx + size, cy + size * 0.3, cx + size * 0.5, cy - size * 0.3, cx, cy + size * 0.3); ctx.fill(); } function drawLightningBolt(ctx, x1, y1, x2, y2, scale) { var midX = (x1 + x2) / 2 + (Math.random() - 0.5) * 20 * scale; var midY = (y1 + y2) / 2; ctx.strokeStyle = '#fff'; ctx.lineWidth = 3 * scale; ctx.shadowColor = '#00bfff'; ctx.shadowBlur = 10; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(midX + 5 * scale, midY - 5 * scale); ctx.lineTo(midX - 5 * scale, midY + 5 * scale); ctx.lineTo(x2, y2); ctx.stroke(); ctx.shadowBlur = 0; } // ============================================================================ // CROWN HELPER // ============================================================================ function drawCrown(ctx, x, y, scale, color, points, hasGems) { var w = 30 * scale; var h = 15 * scale; var baseY = y - 5 * scale; // Crown base ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(x - w / 2, baseY); // Points for (var i = 0; i < points; i++) { var px = x - w / 2 + (i + 0.5) * (w / points); var tipY = baseY - h - (i === Math.floor(points / 2) ? 5 * scale : 0); ctx.lineTo(px - 3 * scale, baseY - 5 * scale); ctx.lineTo(px, tipY); ctx.lineTo(px + 3 * scale, baseY - 5 * scale); } ctx.lineTo(x + w / 2, baseY); ctx.closePath(); ctx.fill(); // Rim ctx.fillStyle = darken(color, 0.15); roundRect(ctx, x - w / 2, baseY, w, 4 * scale, 1); // Gems if (hasGems) { var gemColors = ['#e74c3c', '#3498db', '#2ecc71']; for (var g = 0; g < Math.min(points, 3); g++) { var gx = x - w / 3 + g * (w / 3); ctx.fillStyle = gemColors[g]; ctx.beginPath(); ctx.arc(gx, baseY - 3 * scale, 2.5 * scale, 0, Math.PI * 2); ctx.fill(); } } } function drawSparkles(ctx, x, y, scale, time) { var sparkleCount = 5; for (var i = 0; i < sparkleCount; i++) { var angle = (i / sparkleCount) * Math.PI * 2 + time; var dist = 8 * scale + Math.sin(time * 3 + i) * 3 * scale; var sx = x + Math.cos(angle) * dist; var sy = y + Math.sin(angle) * dist; var alpha = 0.5 + Math.sin(time * 4 + i * 2) * 0.5; ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')'; drawStar(ctx, sx, sy, 2 * scale, 4); } } function drawCosmicEffect(ctx, x, y, scale, time) { // Swirling stars for (var i = 0; i < 8; i++) { var angle = (i / 8) * Math.PI * 2 + time * 0.5; var dist = 12 * scale + Math.sin(time * 2 + i) * 4 * scale; var sx = x + Math.cos(angle) * dist; var sy = y + Math.sin(angle) * dist * 0.5; var hue = (time * 30 + i * 45) % 360; ctx.fillStyle = 'hsla(' + hue + ',100%,70%,0.8)'; drawStar(ctx, sx, sy, 2.5 * scale, 4); } // Central glow var gradient = ctx.createRadialGradient(x, y, 0, x, y, 15 * scale); gradient.addColorStop(0, 'rgba(155,89,182,0.3)'); gradient.addColorStop(1, 'rgba(155,89,182,0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(x, y, 15 * scale, 0, Math.PI * 2); ctx.fill(); } // ============================================================================ // PARTICLE SYSTEMS // ============================================================================ function getSparkleParticles(time, count) { var particles = []; for (var i = 0; i < count; i++) { var seed = i * 137.5; var lifetime = (time + seed) % 2; particles.push({ x: Math.sin(seed) * 30 + Math.sin(time + seed) * 10, y: -lifetime * 40 + 20, alpha: 1 - lifetime / 2, size: 0.5 + Math.sin(seed) * 0.5 }); } return particles; } function getFlameParticles(time, count) { var particles = []; for (var i = 0; i < count; i++) { var seed = i * 97.3; var lifetime = (time * 2 + seed) % 1; particles.push({ x: Math.sin(seed) * 15 + Math.sin(time * 3 + seed) * 5, y: -lifetime * 50, alpha: (1 - lifetime) * 0.6, size: (1 - lifetime) * 1.5, hue: 30 + Math.sin(seed) * 20 }); } return particles; } function getBubbleParticles(time, count) { var particles = []; for (var i = 0; i < count; i++) { var seed = i * 73.7; var lifetime = (time * 0.8 + seed) % 3; particles.push({ x: Math.sin(seed) * 25 + Math.sin(time * 0.5 + seed) * 8, y: -lifetime * 30, alpha: (1 - lifetime / 3) * 0.4, size: 3 + Math.sin(seed) * 2 }); } return particles; } function getSnowParticles(time, count) { var particles = []; for (var i = 0; i < count; i++) { var seed = i * 53.1; var lifetime = (time * 0.5 + seed) % 4; particles.push({ x: Math.sin(seed) * 35 + Math.sin(time + seed) * 10, y: lifetime * 25 - 30, alpha: 0.7, size: 2 + Math.sin(seed) * 1.5 }); } return particles; } function getMusicNotes(time, count) { var notes = []; for (var i = 0; i < count; i++) { var seed = i * 127.3; var lifetime = (time + seed) % 2.5; notes.push({ x: Math.sin(seed) * 30 + Math.sin(time * 2 + seed) * 15, y: -lifetime * 35 + 10, alpha: 1 - lifetime / 2.5, rotation: Math.sin(time * 3 + seed) * 0.3 }); } return notes; } // ============================================================================ // EYEWEAR RENDERERS (15 items) // ============================================================================ var EYEWEAR_RENDERERS = { 'sunglasses-classic': function(ctx, x, y, scale, color, time) { // Black rectangular frames with dark lenses ctx.fillStyle = '#222'; roundRect(ctx, x - 20 * scale, y, 15 * scale, 8 * scale, 2); roundRect(ctx, x + 5 * scale, y, 15 * scale, 8 * scale, 2); // Bridge ctx.fillRect(x - 5 * scale, y + 2 * scale, 10 * scale, 3 * scale); // Lens tint ctx.fillStyle = 'rgba(0,0,0,0.7)'; roundRect(ctx, x - 18 * scale, y + 2 * scale, 11 * scale, 4 * scale, 1); roundRect(ctx, x + 7 * scale, y + 2 * scale, 11 * scale, 4 * scale, 1); }, 'aviator': function(ctx, x, y, scale, color, time) { // Gold frames ctx.strokeStyle = '#d4af37'; ctx.lineWidth = 2 * scale; // Left lens (teardrop) ctx.beginPath(); ctx.moveTo(x - 18 * scale, y); ctx.quadraticCurveTo(x - 22 * scale, y + 5 * scale, x - 18 * scale, y + 10 * scale); ctx.quadraticCurveTo(x - 10 * scale, y + 12 * scale, x - 5 * scale, y + 8 * scale); ctx.quadraticCurveTo(x - 3 * scale, y + 2 * scale, x - 8 * scale, y); ctx.closePath(); ctx.stroke(); // Left lens fill (gradient) var gradL = ctx.createLinearGradient(x - 20 * scale, y, x - 5 * scale, y + 10 * scale); gradL.addColorStop(0, 'rgba(139,90,43,0.6)'); gradL.addColorStop(1, 'rgba(80,50,20,0.8)'); ctx.fillStyle = gradL; ctx.fill(); // Right lens (teardrop) ctx.beginPath(); ctx.moveTo(x + 18 * scale, y); ctx.quadraticCurveTo(x + 22 * scale, y + 5 * scale, x + 18 * scale, y + 10 * scale); ctx.quadraticCurveTo(x + 10 * scale, y + 12 * scale, x + 5 * scale, y + 8 * scale); ctx.quadraticCurveTo(x + 3 * scale, y + 2 * scale, x + 8 * scale, y); ctx.closePath(); ctx.stroke(); // Right lens fill var gradR = ctx.createLinearGradient(x + 5 * scale, y, x + 20 * scale, y + 10 * scale); gradR.addColorStop(0, 'rgba(139,90,43,0.6)'); gradR.addColorStop(1, 'rgba(80,50,20,0.8)'); ctx.fillStyle = gradR; ctx.fill(); // Bridge ctx.strokeStyle = '#d4af37'; ctx.beginPath(); ctx.moveTo(x - 5 * scale, y + 4 * scale); ctx.quadraticCurveTo(x, y + 2 * scale, x + 5 * scale, y + 4 * scale); ctx.stroke(); }, 'nerd-glasses': function(ctx, x, y, scale, color, time) { // Thick black circular frames ctx.strokeStyle = '#111'; ctx.lineWidth = 3 * scale; // Left lens ctx.beginPath(); ctx.arc(x - 12 * scale, y + 4 * scale, 10 * scale, 0, Math.PI * 2); ctx.stroke(); // Right lens ctx.beginPath(); ctx.arc(x + 12 * scale, y + 4 * scale, 10 * scale, 0, Math.PI * 2); ctx.stroke(); // Bridge ctx.beginPath(); ctx.moveTo(x - 2 * scale, y + 4 * scale); ctx.lineTo(x + 2 * scale, y + 4 * scale); ctx.stroke(); // Clear lenses with slight reflection ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.beginPath(); ctx.arc(x - 12 * scale, y + 4 * scale, 8 * scale, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(x + 12 * scale, y + 4 * scale, 8 * scale, 0, Math.PI * 2); ctx.fill(); // Reflection glint ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.beginPath(); ctx.arc(x - 15 * scale, y + 1 * scale, 2 * scale, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(x + 9 * scale, y + 1 * scale, 2 * scale, 0, Math.PI * 2); ctx.fill(); }, '3d-glasses': function(ctx, x, y, scale, color, time) { // White frame ctx.fillStyle = '#fff'; roundRect(ctx, x - 24 * scale, y - 1 * scale, 48 * scale, 12 * scale, 2); // Frame outline ctx.strokeStyle = '#ccc'; ctx.lineWidth = 1 * scale; ctx.stroke(); // Left lens (red) ctx.fillStyle = 'rgba(255,0,0,0.7)'; roundRect(ctx, x - 22 * scale, y + 1 * scale, 18 * scale, 8 * scale, 1); // Right lens (blue/cyan) ctx.fillStyle = 'rgba(0,200,255,0.7)'; roundRect(ctx, x + 4 * scale, y + 1 * scale, 18 * scale, 8 * scale, 1); }, 'goggles': function(ctx, x, y, scale, color, time) { // Strap ctx.fillStyle = '#444'; ctx.fillRect(x - 35 * scale, y + 2 * scale, 70 * scale, 4 * scale); // Goggle frames ctx.fillStyle = '#2c3e50'; ctx.beginPath(); ctx.ellipse(x - 12 * scale, y + 4 * scale, 14 * scale, 10 * scale, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(x + 12 * scale, y + 4 * scale, 14 * scale, 10 * scale, 0, 0, Math.PI * 2); ctx.fill(); // Lens ctx.fillStyle = 'rgba(100,200,255,0.6)'; ctx.beginPath(); ctx.ellipse(x - 12 * scale, y + 4 * scale, 11 * scale, 7 * scale, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(x + 12 * scale, y + 4 * scale, 11 * scale, 7 * scale, 0, 0, Math.PI * 2); ctx.fill(); // Reflection ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.beginPath(); ctx.ellipse(x - 15 * scale, y + 1 * scale, 4 * scale, 2 * scale, -0.3, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(x + 9 * scale, y + 1 * scale, 4 * scale, 2 * scale, -0.3, 0, Math.PI * 2); ctx.fill(); }, 'monocle': function(ctx, x, y, scale, color, time) { // Monocle on right eye ctx.strokeStyle = '#d4af37'; ctx.lineWidth = 2 * scale; ctx.beginPath(); ctx.arc(x + 12 * scale, y + 4 * scale, 10 * scale, 0, Math.PI * 2); ctx.stroke(); // Glass ctx.fillStyle = 'rgba(255,255,255,0.15)'; ctx.beginPath(); ctx.arc(x + 12 * scale, y + 4 * scale, 9 * scale, 0, Math.PI * 2); ctx.fill(); // Chain ctx.strokeStyle = '#d4af37'; ctx.lineWidth = 1 * scale; ctx.beginPath(); ctx.moveTo(x + 22 * scale, y + 4 * scale); ctx.quadraticCurveTo(x + 30 * scale, y + 15 * scale, x + 25 * scale, y + 30 * scale); ctx.stroke(); // Chain end detail ctx.fillStyle = '#d4af37'; ctx.beginPath(); ctx.arc(x + 25 * scale, y + 30 * scale, 2 * scale, 0, Math.PI * 2); ctx.fill(); }, 'star-glasses': function(ctx, x, y, scale, color, time) { var frameColor = color || '#e91e63'; // Left star frame ctx.fillStyle = frameColor; drawStar(ctx, x - 12 * scale, y + 4 * scale, 12 * scale, 5); // Right star frame drawStar(ctx, x + 12 * scale, y + 4 * scale, 12 * scale, 5); // Bridge ctx.fillRect(x - 3 * scale, y + 3 * scale, 6 * scale, 3 * scale); // Lens tint ctx.fillStyle = 'rgba(0,0,0,0.5)'; drawStar(ctx, x - 12 * scale, y + 4 * scale, 8 * scale, 5); drawStar(ctx, x + 12 * scale, y + 4 * scale, 8 * scale, 5); }, 'laser-eyes': function(ctx, x, y, scale, color, time) { var pulse = 0.7 + Math.sin(time * 8) * 0.3; // Glowing red eyes var eyeGlow = ctx.createRadialGradient(x - 10 * scale, y + 4 * scale, 0, x - 10 * scale, y + 4 * scale, 8 * scale); eyeGlow.addColorStop(0, 'rgba(255,0,0,' + pulse + ')'); eyeGlow.addColorStop(0.5, 'rgba(255,50,0,' + (pulse * 0.5) + ')'); eyeGlow.addColorStop(1, 'rgba(255,0,0,0)'); ctx.fillStyle = eyeGlow; ctx.beginPath(); ctx.arc(x - 10 * scale, y + 4 * scale, 8 * scale, 0, Math.PI * 2); ctx.fill(); var eyeGlow2 = ctx.createRadialGradient(x + 10 * scale, y + 4 * scale, 0, x + 10 * scale, y + 4 * scale, 8 * scale); eyeGlow2.addColorStop(0, 'rgba(255,0,0,' + pulse + ')'); eyeGlow2.addColorStop(0.5, 'rgba(255,50,0,' + (pulse * 0.5) + ')'); eyeGlow2.addColorStop(1, 'rgba(255,0,0,0)'); ctx.fillStyle = eyeGlow2; ctx.beginPath(); ctx.arc(x + 10 * scale, y + 4 * scale, 8 * scale, 0, Math.PI * 2); ctx.fill(); // Core bright spots ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(x - 10 * scale, y + 4 * scale, 2 * scale, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(x + 10 * scale, y + 4 * scale, 2 * scale, 0, Math.PI * 2); ctx.fill(); }, 'heart-glasses': function(ctx, x, y, scale, color, time) { var frameColor = color || '#e91e63'; // Left heart ctx.fillStyle = frameColor; ctx.save(); ctx.translate(x - 12 * scale, y + 6 * scale); ctx.scale(scale, scale); ctx.beginPath(); ctx.moveTo(0, -5); ctx.bezierCurveTo(-10, -15, -20, 0, 0, 12); ctx.bezierCurveTo(20, 0, 10, -15, 0, -5); ctx.fill(); ctx.restore(); // Right heart ctx.save(); ctx.translate(x + 12 * scale, y + 6 * scale); ctx.scale(scale, scale); ctx.beginPath(); ctx.moveTo(0, -5); ctx.bezierCurveTo(-10, -15, -20, 0, 0, 12); ctx.bezierCurveTo(20, 0, 10, -15, 0, -5); ctx.fill(); ctx.restore(); // Bridge ctx.fillRect(x - 3 * scale, y + 3 * scale, 6 * scale, 3 * scale); // Lens tint ctx.fillStyle = 'rgba(255,100,150,0.5)'; ctx.save(); ctx.translate(x - 12 * scale, y + 6 * scale); ctx.scale(scale * 0.7, scale * 0.7); ctx.beginPath(); ctx.moveTo(0, -5); ctx.bezierCurveTo(-10, -15, -20, 0, 0, 12); ctx.bezierCurveTo(20, 0, 10, -15, 0, -5); ctx.fill(); ctx.restore(); ctx.save(); ctx.translate(x + 12 * scale, y + 6 * scale); ctx.scale(scale * 0.7, scale * 0.7); ctx.beginPath(); ctx.moveTo(0, -5); ctx.bezierCurveTo(-10, -15, -20, 0, 0, 12); ctx.bezierCurveTo(20, 0, 10, -15, 0, -5); ctx.fill(); ctx.restore(); }, 'vr-headset': function(ctx, x, y, scale, color, time) { // Main visor ctx.fillStyle = '#1a1a2e'; roundRect(ctx, x - 28 * scale, y - 4 * scale, 56 * scale, 20 * scale, 4 * scale); // Visor screen var screenGlow = ctx.createLinearGradient(x - 24 * scale, y, x + 24 * scale, y); screenGlow.addColorStop(0, 'rgba(0,150,255,0.4)'); screenGlow.addColorStop(0.5, 'rgba(0,200,255,0.6)'); screenGlow.addColorStop(1, 'rgba(0,150,255,0.4)'); ctx.fillStyle = screenGlow; roundRect(ctx, x - 24 * scale, y - 1 * scale, 48 * scale, 14 * scale, 2 * scale); // Side lights var blink = Math.sin(time * 3) > 0 ? 1 : 0.3; ctx.fillStyle = 'rgba(0,255,100,' + blink + ')'; ctx.beginPath(); ctx.arc(x - 25 * scale, y + 6 * scale, 2 * scale, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(x + 25 * scale, y + 6 * scale, 2 * scale, 0, Math.PI * 2); ctx.fill(); // Strap hints ctx.fillStyle = '#333'; ctx.fillRect(x - 32 * scale, y + 2 * scale, 4 * scale, 8 * scale); ctx.fillRect(x + 28 * scale, y + 2 * scale, 4 * scale, 8 * scale); }, 'pixel-glasses': function(ctx, x, y, scale, color, time) { var pixelSize = 3 * scale; ctx.fillStyle = '#000'; // Left lens (pixelated rectangle) for (var py = 0; py < 3; py++) { for (var px = 0; px < 5; px++) { ctx.fillRect( x - 22 * scale + px * pixelSize, y + py * pixelSize, pixelSize - 0.5, pixelSize - 0.5 ); } } // Right lens for (var py2 = 0; py2 < 3; py2++) { for (var px2 = 0; px2 < 5; px2++) { ctx.fillRect( x + 4 * scale + px2 * pixelSize, y + py2 * pixelSize, pixelSize - 0.5, pixelSize - 0.5 ); } } // Bridge ctx.fillRect(x - 4 * scale, y + pixelSize, 8 * scale, pixelSize); }, 'round-glasses': function(ctx, x, y, scale, color, time) { var frameColor = color || '#8b4513'; // Left frame ctx.strokeStyle = frameColor; ctx.lineWidth = 2 * scale; ctx.beginPath(); ctx.arc(x - 12 * scale, y + 4 * scale, 9 * scale, 0, Math.PI * 2); ctx.stroke(); // Right frame ctx.beginPath(); ctx.arc(x + 12 * scale, y + 4 * scale, 9 * scale, 0, Math.PI * 2); ctx.stroke(); // Bridge ctx.beginPath(); ctx.moveTo(x - 3 * scale, y + 4 * scale); ctx.lineTo(x + 3 * scale, y + 4 * scale); ctx.stroke(); // Slight lens tint ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.beginPath(); ctx.arc(x - 12 * scale, y + 4 * scale, 7 * scale, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(x + 12 * scale, y + 4 * scale, 7 * scale, 0, Math.PI * 2); ctx.fill(); }, 'cat-eye-glasses': function(ctx, x, y, scale, color, time) { var frameColor = color || '#9b59b6'; ctx.fillStyle = frameColor; // Left cat-eye shape ctx.beginPath(); ctx.moveTo(x - 22 * scale, y + 2 * scale); ctx.lineTo(x - 18 * scale, y - 3 * scale); ctx.quadraticCurveTo(x - 10 * scale, y - 2 * scale, x - 5 * scale, y + 4 * scale); ctx.quadraticCurveTo(x - 10 * scale, y + 12 * scale, x - 18 * scale, y + 10 * scale); ctx.closePath(); ctx.fill(); // Right cat-eye shape ctx.beginPath(); ctx.moveTo(x + 22 * scale, y + 2 * scale); ctx.lineTo(x + 18 * scale, y - 3 * scale); ctx.quadraticCurveTo(x + 10 * scale, y - 2 * scale, x + 5 * scale, y + 4 * scale); ctx.quadraticCurveTo(x + 10 * scale, y + 12 * scale, x + 18 * scale, y + 10 * scale); ctx.closePath(); ctx.fill(); // Bridge ctx.fillRect(x - 5 * scale, y + 3 * scale, 10 * scale, 3 * scale); // Lens tint ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.beginPath(); ctx.moveTo(x - 20 * scale, y + 3 * scale); ctx.quadraticCurveTo(x - 12 * scale, y, x - 7 * scale, y + 4 * scale); ctx.quadraticCurveTo(x - 12 * scale, y + 9 * scale, x - 17 * scale, y + 8 * scale); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.moveTo(x + 20 * scale, y + 3 * scale); ctx.quadraticCurveTo(x + 12 * scale, y, x + 7 * scale, y + 4 * scale); ctx.quadraticCurveTo(x + 12 * scale, y + 9 * scale, x + 17 * scale, y + 8 * scale); ctx.closePath(); ctx.fill(); }, 'cyber-visor': function(ctx, x, y, scale, color, time) { // Sleek futuristic visor ctx.fillStyle = '#1a1a2e'; ctx.beginPath(); ctx.moveTo(x - 28 * scale, y + 6 * scale); ctx.quadraticCurveTo(x, y - 2 * scale, x + 28 * scale, y + 6 * scale); ctx.quadraticCurveTo(x, y + 14 * scale, x - 28 * scale, y + 6 * scale); ctx.fill(); // Visor glow var scanLine = (time * 50) % 50 - 25; var visorGlow = ctx.createLinearGradient(x - 25 * scale, y, x + 25 * scale, y); visorGlow.addColorStop(0, 'rgba(0,255,255,0.3)'); visorGlow.addColorStop(0.5, 'rgba(0,255,255,0.7)'); visorGlow.addColorStop(1, 'rgba(0,255,255,0.3)'); ctx.fillStyle = visorGlow; ctx.beginPath(); ctx.moveTo(x - 24 * scale, y + 6 * scale); ctx.quadraticCurveTo(x, y, x + 24 * scale, y + 6 * scale); ctx.quadraticCurveTo(x, y + 12 * scale, x - 24 * scale, y + 6 * scale); ctx.fill(); // Scan line effect ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.fillRect(x + scanLine * scale, y + 2 * scale, 2 * scale, 8 * scale); } }; // ============================================================================ // HEADWEAR RENDERERS (20 items) // ============================================================================ var HEADWEAR_RENDERERS = { 'baseball-cap': function(ctx, x, y, scale, color, time) { var capColor = color || '#c0392b'; // Cap dome ctx.fillStyle = capColor; ctx.beginPath(); ctx.arc(x, y - 5 * scale, 22 * scale, Math.PI, 0, false); ctx.fill(); // Bill ctx.fillStyle = darken(capColor, 0.2); ctx.beginPath(); ctx.ellipse(x, y + 2 * scale, 25 * scale, 6 * scale, 0, 0, Math.PI); ctx.fill(); // Button on top ctx.fillStyle = darken(capColor, 0.3); ctx.beginPath(); ctx.arc(x, y - 26 * scale, 3 * scale, 0, Math.PI * 2); ctx.fill(); }, 'backwards-cap': function(ctx, x, y, scale, color, time) { var capColor = color || '#3498db'; // Cap dome ctx.fillStyle = capColor; ctx.beginPath(); ctx.arc(x, y - 5 * scale, 22 * scale, Math.PI, 0, false); ctx.fill(); // Bill (in back) ctx.fillStyle = darken(capColor, 0.2); ctx.beginPath(); ctx.ellipse(x, y - 12 * scale, 20 * scale, 5 * scale, 0, Math.PI, 0); ctx.fill(); // Adjustment strap hint ctx.fillStyle = darken(capColor, 0.3); ctx.fillRect(x - 8 * scale, y - 2 * scale, 16 * scale, 3 * scale); // Button ctx.beginPath(); ctx.arc(x, y - 26 * scale, 3 * scale, 0, Math.PI * 2); ctx.fill(); }, 'beanie': function(ctx, x, y, scale, color, time) { var beanieColor = color || '#2c3e50'; // Main beanie ctx.fillStyle = beanieColor; ctx.beginPath(); ctx.moveTo(x - 24 * scale, y + 5 * scale); ctx.quadraticCurveTo(x - 26 * scale, y - 20 * scale, x, y - 28 * scale); ctx.quadraticCurveTo(x + 26 * scale, y - 20 * scale, x + 24 * scale, y + 5 * scale); ctx.closePath(); ctx.fill(); // Fold at bottom ctx.fillStyle = darken(beanieColor, 0.15); ctx.beginPath(); ctx.moveTo(x - 24 * scale, y + 5 * scale); ctx.quadraticCurveTo(x, y + 10 * scale, x + 24 * scale, y + 5 * scale); ctx.quadraticCurveTo(x, y, x - 24 * scale, y + 5 * scale); ctx.fill(); // Knit texture lines ctx.strokeStyle = lighten(beanieColor, 0.1); ctx.lineWidth = 1 * scale; for (var i = -2; i <= 2; i++) { ctx.beginPath(); ctx.moveTo(x + i * 8 * scale, y + 3 * scale); ctx.quadraticCurveTo(x + i * 6 * scale, y - 15 * scale, x + i * 4 * scale, y - 25 * scale); ctx.stroke(); } }, 'top-hat': function(ctx, x, y, scale, color, time) { // Tall black top hat ctx.fillStyle = '#111'; // Main cylinder ctx.beginPath(); ctx.rect(x - 15 * scale, y - 40 * scale, 30 * scale, 35 * scale); ctx.fill(); // Top ellipse ctx.beginPath(); ctx.ellipse(x, y - 40 * scale, 15 * scale, 5 * scale, 0, 0, Math.PI * 2); ctx.fill(); // Brim ctx.fillStyle = '#1a1a1a'; ctx.beginPath(); ctx.ellipse(x, y - 5 * scale, 25 * scale, 7 * scale, 0, 0, Math.PI * 2); ctx.fill(); // Band ctx.fillStyle = '#8b0000'; ctx.fillRect(x - 15 * scale, y - 12 * scale, 30 * scale, 5 * scale); }, 'crown-bronze': function(ctx, x, y, scale, color, time) { drawCrown(ctx, x, y, scale, '#cd7f32', 3, false); }, 'crown-silver': function(ctx, x, y, scale, color, time) { drawCrown(ctx, x, y, scale, '#c0c0c0', 4, false); }, 'crown-gold': function(ctx, x, y, scale, color, time) { drawCrown(ctx, x, y, scale, '#ffd700', 5, true); }, 'crown-diamond': function(ctx, x, y, scale, color, time) { drawCrown(ctx, x, y, scale, '#b9f2ff', 6, true); drawSparkles(ctx, x, y - 10 * scale, scale, time); }, 'crown-cosmic': function(ctx, x, y, scale, color, time) { drawCrown(ctx, x, y, scale, '#9b59b6', 7, true); drawCosmicEffect(ctx, x, y - 15 * scale, scale, time); }, 'wizard-hat': function(ctx, x, y, scale, color, time) { var hatColor = color || '#2c3e50'; // Tall pointed hat ctx.fillStyle = hatColor; ctx.beginPath(); ctx.moveTo(x - 25 * scale, y + 5 * scale); ctx.quadraticCurveTo(x - 10 * scale, y - 30 * scale, x + 5 * scale, y - 55 * scale); ctx.quadraticCurveTo(x + 15 * scale, y - 30 * scale, x + 25 * scale, y + 5 * scale); ctx.closePath(); ctx.fill(); // Brim ctx.fillStyle = darken(hatColor, 0.2); ctx.beginPath(); ctx.ellipse(x, y + 5 * scale, 28 * scale, 8 * scale, 0, 0, Math.PI * 2); ctx.fill(); // Stars decoration ctx.fillStyle = '#ffd700'; drawStar(ctx, x - 8 * scale, y - 20 * scale, 4 * scale, 5); drawStar(ctx, x + 5 * scale, y - 35 * scale, 3 * scale, 5); drawStar(ctx, x - 3 * scale, y - 10 * scale, 2.5 * scale, 5); // Moon ctx.fillStyle = '#f1c40f'; ctx.beginPath(); ctx.arc(x + 10 * scale, y - 25 * scale, 5 * scale, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = hatColor; ctx.beginPath(); ctx.arc(x + 12 * scale, y - 26 * scale, 4 * scale, 0, Math.PI * 2); ctx.fill(); }, 'pirate-hat': function(ctx, x, y, scale, color, time) { // Tricorn base ctx.fillStyle = '#1a1a1a'; ctx.beginPath(); ctx.moveTo(x - 30 * scale, y); ctx.quadraticCurveTo(x - 35 * scale, y - 15 * scale, x - 20 * scale, y - 25 * scale); ctx.lineTo(x, y - 15 * scale); ctx.lineTo(x + 20 * scale, y - 25 * scale); ctx.quadraticCurveTo(x + 35 * scale, y - 15 * scale, x + 30 * scale, y); ctx.quadraticCurveTo(x, y + 5 * scale, x - 30 * scale, y); ctx.fill(); // Gold trim ctx.strokeStyle = '#d4af37'; ctx.lineWidth = 2 * scale; ctx.stroke(); // Skull ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(x, y - 12 * scale, 6 * scale, 0, Math.PI * 2); ctx.fill(); // Skull eyes ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(x - 2 * scale, y - 13 * scale, 1.5 * scale, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(x + 2 * scale, y - 13 * scale, 1.5 * scale, 0, Math.PI * 2); ctx.fill(); // Crossbones hint ctx.strokeStyle = '#fff'; ctx.lineWidth = 2 * scale; ctx.beginPath(); ctx.moveTo(x - 8 * scale, y - 8 * scale); ctx.lineTo(x + 8 * scale, y - 16 * scale); ctx.moveTo(x + 8 * scale, y - 8 * scale); ctx.lineTo(x - 8 * scale, y - 16 * scale); ctx.stroke(); }, 'halo': function(ctx, x, y, scale, color, time) { var bob = Math.sin(time * 2) * 2; // Glow effect ctx.save(); ctx.shadowColor = '#ffd700'; ctx.shadowBlur = 15; // Main ring ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 4 * scale; ctx.beginPath(); ctx.ellipse(x, y - 25 * scale + bob, 18 * scale, 5 * scale, 0, 0, Math.PI * 2); ctx.stroke(); // Inner highlight ctx.strokeStyle = '#fff8dc'; ctx.lineWidth = 2 * scale; ctx.beginPath(); ctx.ellipse(x, y - 25 * scale + bob, 16 * scale, 4 * scale, 0, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); }, 'devil-horns': function(ctx, x, y, scale, color, time) { var hornColor = color || '#8b0000'; // Left horn ctx.fillStyle = hornColor; ctx.beginPath(); ctx.moveTo(x - 20 * scale, y + 5 * scale); ctx.quadraticCurveTo(x - 30 * scale, y - 15 * scale, x - 25 * scale, y - 30 * scale); ctx.quadraticCurveTo(x - 20 * scale, y - 20 * scale, x - 15 * scale, y + 5 * scale); ctx.closePath(); ctx.fill(); // Right horn ctx.beginPath(); ctx.moveTo(x + 20 * scale, y + 5 * scale); ctx.quadraticCurveTo(x + 30 * scale, y - 15 * scale, x + 25 * scale, y - 30 * scale); ctx.quadraticCurveTo(x + 20 * scale, y - 20 * scale, x + 15 * scale, y + 5 * scale); ctx.closePath(); ctx.fill(); // Horn ridges ctx.strokeStyle = darken(hornColor, 0.3); ctx.lineWidth = 1 * scale; for (var i = 0; i < 3; i++) { var hy = y - 5 * scale - i * 8 * scale; ctx.beginPath(); ctx.moveTo(x - 22 * scale + i * 2 * scale, hy); ctx.lineTo(x - 17 * scale + i * scale, hy); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x + 22 * scale - i * 2 * scale, hy); ctx.lineTo(x + 17 * scale - i * scale, hy); ctx.stroke(); } }, 'party-hat': function(ctx, x, y, scale, color, time) { var hatColor = color || '#e91e63'; // Cone ctx.fillStyle = hatColor; ctx.beginPath(); ctx.moveTo(x - 20 * scale, y + 5 * scale); ctx.lineTo(x, y - 40 * scale); ctx.lineTo(x + 20 * scale, y + 5 * scale); ctx.closePath(); ctx.fill(); // Stripes ctx.strokeStyle = '#fff'; ctx.lineWidth = 3 * scale; ctx.beginPath(); ctx.moveTo(x - 15 * scale, y); ctx.lineTo(x - 5 * scale, y - 25 * scale); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x + 5 * scale, y - 5 * scale); ctx.lineTo(x + 2 * scale, y - 30 * scale); ctx.stroke(); // Pom pom ctx.fillStyle = '#ffd700'; ctx.beginPath(); ctx.arc(x, y - 42 * scale, 5 * scale, 0, Math.PI * 2); ctx.fill(); // Elastic band ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 1 * scale; ctx.beginPath(); ctx.moveTo(x - 20 * scale, y + 5 * scale); ctx.quadraticCurveTo(x, y + 12 * scale, x + 20 * scale, y + 5 * scale); ctx.stroke(); }, 'cowboy-hat': function(ctx, x, y, scale, color, time) { var hatColor = color || '#8b4513'; // Brim ctx.fillStyle = hatColor; ctx.beginPath(); ctx.ellipse(x, y + 2 * scale, 32 * scale, 10 * scale, 0, 0, Math.PI * 2); ctx.fill(); // Crown ctx.beginPath(); ctx.moveTo(x - 18 * scale, y); ctx.quadraticCurveTo(x - 20 * scale, y - 25 * scale, x - 10 * scale, y - 28 * scale); ctx.quadraticCurveTo(x, y - 22 * scale, x + 10 * scale, y - 28 * scale); ctx.quadraticCurveTo(x + 20 * scale, y - 25 * scale, x + 18 * scale, y); ctx.closePath(); ctx.fill(); // Band ctx.fillStyle = darken(hatColor, 0.3); ctx.fillRect(x - 18 * scale, y - 5 * scale, 36 * scale, 4 * scale); // Band buckle ctx.fillStyle = '#d4af37'; ctx.fillRect(x - 4 * scale, y - 6 * scale, 8 * scale, 6 * scale); }, 'santa-hat': function(ctx, x, y, scale, color, time) { // Red hat ctx.fillStyle = '#c0392b'; ctx.beginPath(); ctx.moveTo(x - 22 * scale, y + 5 * scale); ctx.quadraticCurveTo(x - 5 * scale, y - 20 * scale, x + 25 * scale, y - 35 * scale); ctx.quadraticCurveTo(x + 15 * scale, y - 15 * scale, x + 22 * scale, y + 5 * scale); ctx.closePath(); ctx.fill(); // White trim ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.moveTo(x - 25 * scale, y + 5 * scale); ctx.quadraticCurveTo(x, y + 12 * scale, x + 25 * scale, y + 5 * scale); ctx.quadraticCurveTo(x, y, x - 25 * scale, y + 5 * scale); ctx.fill(); // Pom pom ctx.beginPath(); ctx.arc(x + 27 * scale, y - 35 * scale, 7 * scale, 0, Math.PI * 2); ctx.fill(); }, 'chef-hat': function(ctx, x, y, scale, color, time) { // Puffy top ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(x - 12 * scale, y - 30 * scale, 12 * scale, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(x + 12 * scale, y - 30 * scale, 12 * scale, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(x, y - 35 * scale, 14 * scale, 0, Math.PI * 2); ctx.fill(); // Base cylinder ctx.fillRect(x - 20 * scale, y - 20 * scale, 40 * scale, 25 * scale); // Band ctx.fillStyle = '#ecf0f1'; ctx.fillRect(x - 20 * scale, y, 40 * scale, 5 * scale); }, 'viking-helmet': function(ctx, x, y, scale, color, time) { // Helmet dome ctx.fillStyle = '#7f8c8d'; ctx.beginPath(); ctx.arc(x, y - 5 * scale, 24 * scale, Math.PI, 0, false); ctx.fill(); // Nose guard ctx.fillStyle = '#95a5a6'; ctx.beginPath(); ctx.moveTo(x - 3 * scale, y - 5 * scale); ctx.lineTo(x, y + 15 * scale); ctx.lineTo(x + 3 * scale, y - 5 * scale); ctx.fill(); // Horns ctx.fillStyle = '#f5f5dc'; // Left horn ctx.beginPath(); ctx.moveTo(x - 22 * scale, y - 5 * scale); ctx.quadraticCurveTo(x - 35 * scale, y - 20 * scale, x - 40 * scale, y - 35 * scale); ctx.quadraticCurveTo(x - 30 * scale, y - 25 * scale, x - 20 * scale, y - 10 * scale); ctx.closePath(); ctx.fill(); // Right horn ctx.beginPath(); ctx.moveTo(x + 22 * scale, y - 5 * scale); ctx.quadraticCurveTo(x + 35 * scale, y - 20 * scale, x + 40 * scale, y - 35 * scale); ctx.quadraticCurveTo(x + 30 * scale, y - 25 * scale, x + 20 * scale, y - 10 * scale); ctx.closePath(); ctx.fill(); // Horn stripes ctx.strokeStyle = '#d4c4a8'; ctx.lineWidth = 1 * scale; for (var i = 0; i < 3; i++) { ctx.beginPath(); ctx.moveTo(x - 25 * scale - i * 5 * scale, y - 12 * scale - i * 8 * scale); ctx.lineTo(x - 22 * scale - i * 5 * scale, y - 8 * scale - i * 8 * scale); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x + 25 * scale + i * 5 * scale, y - 12 * scale - i * 8 * scale); ctx.lineTo(x + 22 * scale + i * 5 * scale, y - 8 * scale - i * 8 * scale); ctx.stroke(); } }, 'headphones': function(ctx, x, y, scale, color, time) { var hpColor = color || '#2c3e50'; // Headband ctx.strokeStyle = hpColor; ctx.lineWidth = 4 * scale; ctx.beginPath(); ctx.arc(x, y - 5 * scale, 25 * scale, Math.PI, 0, false); ctx.stroke(); // Left ear cup ctx.fillStyle = hpColor; ctx.beginPath(); ctx.ellipse(x - 26 * scale, y + 5 * scale, 10 * scale, 14 * scale, 0, 0, Math.PI * 2); ctx.fill(); // Left cushion ctx.fillStyle = '#34495e'; ctx.beginPath(); ctx.ellipse(x - 26 * scale, y + 5 * scale, 7 * scale, 11 * scale, 0, 0, Math.PI * 2); ctx.fill(); // Right ear cup ctx.fillStyle = hpColor; ctx.beginPath(); ctx.ellipse(x + 26 * scale, y + 5 * scale, 10 * scale, 14 * scale, 0, 0, Math.PI * 2); ctx.fill(); // Right cushion ctx.fillStyle = '#34495e'; ctx.beginPath(); ctx.ellipse(x + 26 * scale, y + 5 * scale, 7 * scale, 11 * scale, 0, 0, Math.PI * 2); ctx.fill(); } }; // ============================================================================ // EFFECT RENDERERS (10 items) // ============================================================================ var EFFECT_RENDERERS = { 'sparkle-trail': function(ctx, avatar, x, y, scale, time) { var particles = getSparkleParticles(time, 20); particles.forEach(function(p) { var px = x + p.x * scale; var py = y + p.y * scale; ctx.fillStyle = 'rgba(255,215,0,' + p.alpha + ')'; drawStar(ctx, px, py, 3 * scale * p.size, 4); }); }, 'fire-aura': function(ctx, avatar, x, y, scale, time) { // Outer glow var gradient = ctx.createRadialGradient(x, y, 0, x, y, 45 * scale); gradient.addColorStop(0, 'rgba(255,69,0,0)'); gradient.addColorStop(0.5, 'rgba(255,69,0,0.15)'); gradient.addColorStop(1, 'rgba(255,69,0,0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(x, y, 45 * scale, 0, Math.PI * 2); ctx.fill(); // Fire particles var flames = getFlameParticles(time, 15); flames.forEach(function(f) { var fx = x + f.x * scale; var fy = y + f.y * scale; var flameGrad = ctx.createRadialGradient(fx, fy, 0, fx, fy, 6 * scale * f.size); flameGrad.addColorStop(0, 'hsla(' + f.hue + ',100%,60%,' + f.alpha + ')'); flameGrad.addColorStop(1, 'hsla(' + f.hue + ',100%,50%,0)'); ctx.fillStyle = flameGrad; ctx.beginPath(); ctx.arc(fx, fy, 6 * scale * f.size, 0, Math.PI * 2); ctx.fill(); }); }, 'lightning-crackle': function(ctx, avatar, x, y, scale, time) { // Background electric glow var pulse = 0.3 + Math.abs(Math.sin(time * 5)) * 0.2; var gradient = ctx.createRadialGradient(x, y, 0, x, y, 40 * scale); gradient.addColorStop(0, 'rgba(0,191,255,0)'); gradient.addColorStop(0.6, 'rgba(0,191,255,' + pulse + ')'); gradient.addColorStop(1, 'rgba(0,191,255,0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(x, y, 40 * scale, 0, Math.PI * 2); ctx.fill(); // Random lightning bolts if (Math.random() < 0.15) { var startX = x + (Math.random() - 0.5) * 30 * scale; var startY = y - 35 * scale; var endX = x + (Math.random() - 0.5) * 50 * scale; var endY = y + 35 * scale; drawLightningBolt(ctx, startX, startY, endX, endY, scale); } }, 'rainbow-glow': function(ctx, avatar, x, y, scale, time) { var hue = (time * 50) % 360; var gradient = ctx.createRadialGradient(x, y, 0, x, y, 50 * scale); gradient.addColorStop(0, 'hsla(' + hue + ',100%,50%,0)'); gradient.addColorStop(0.5, 'hsla(' + hue + ',100%,50%,0.2)'); gradient.addColorStop(0.7, 'hsla(' + ((hue + 60) % 360) + ',100%,50%,0.15)'); gradient.addColorStop(0.85, 'hsla(' + ((hue + 120) % 360) + ',100%,50%,0.1)'); gradient.addColorStop(1, 'hsla(' + ((hue + 180) % 360) + ',100%,50%,0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(x, y, 50 * scale, 0, Math.PI * 2); ctx.fill(); }, 'golden-glow': function(ctx, avatar, x, y, scale, time) { var pulse = 0.8 + Math.sin(time * 2) * 0.2; var gradient = ctx.createRadialGradient(x, y, 0, x, y, 50 * scale * pulse); gradient.addColorStop(0, 'rgba(255,215,0,0)'); gradient.addColorStop(0.4, 'rgba(255,215,0,0.2)'); gradient.addColorStop(0.7, 'rgba(255,215,0,0.15)'); gradient.addColorStop(1, 'rgba(255,215,0,0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(x, y, 50 * scale * pulse, 0, Math.PI * 2); ctx.fill(); // Subtle sparkles for (var i = 0; i < 5; i++) { var angle = (time + i * 1.2) * 0.5; var dist = 30 * scale + Math.sin(time * 2 + i) * 10 * scale; var sx = x + Math.cos(angle) * dist; var sy = y + Math.sin(angle) * dist; var alpha = 0.3 + Math.sin(time * 3 + i * 2) * 0.3; ctx.fillStyle = 'rgba(255,255,200,' + alpha + ')'; drawStar(ctx, sx, sy, 2 * scale, 4); } }, 'ice-aura': function(ctx, avatar, x, y, scale, time) { // Cold blue glow var gradient = ctx.createRadialGradient(x, y, 0, x, y, 45 * scale); gradient.addColorStop(0, 'rgba(173,216,230,0)'); gradient.addColorStop(0.5, 'rgba(173,216,230,0.2)'); gradient.addColorStop(1, 'rgba(173,216,230,0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(x, y, 45 * scale, 0, Math.PI * 2); ctx.fill(); // Snowflakes var snow = getSnowParticles(time, 12); snow.forEach(function(s) { var sx = x + s.x * scale; var sy = y + s.y * scale; ctx.fillStyle = 'rgba(255,255,255,' + s.alpha + ')'; drawStar(ctx, sx, sy, s.size * scale, 6); }); }, 'shadow-aura': function(ctx, avatar, x, y, scale, time) { // Dark pulsing aura var pulse = 0.9 + Math.sin(time * 1.5) * 0.1; var gradient = ctx.createRadialGradient(x, y, 0, x, y, 50 * scale * pulse); gradient.addColorStop(0, 'rgba(0,0,0,0)'); gradient.addColorStop(0.4, 'rgba(20,0,40,0.3)'); gradient.addColorStop(0.7, 'rgba(40,0,60,0.2)'); gradient.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(x, y, 50 * scale * pulse, 0, Math.PI * 2); ctx.fill(); // Wispy shadows for (var i = 0; i < 6; i++) { var angle = time * 0.3 + i * Math.PI / 3; var dist = 25 * scale + Math.sin(time + i) * 10 * scale; var wx = x + Math.cos(angle) * dist; var wy = y + Math.sin(angle) * dist; var wispGrad = ctx.createRadialGradient(wx, wy, 0, wx, wy, 8 * scale); wispGrad.addColorStop(0, 'rgba(30,0,50,0.4)'); wispGrad.addColorStop(1, 'rgba(30,0,50,0)'); ctx.fillStyle = wispGrad; ctx.beginPath(); ctx.arc(wx, wy, 8 * scale, 0, Math.PI * 2); ctx.fill(); } }, 'bubble-float': function(ctx, avatar, x, y, scale, time) { var bubbles = getBubbleParticles(time, 10); bubbles.forEach(function(b) { var bx = x + b.x * scale; var by = y + b.y * scale; // Bubble outline ctx.strokeStyle = 'rgba(100,200,255,' + b.alpha + ')'; ctx.lineWidth = 1 * scale; ctx.beginPath(); ctx.arc(bx, by, b.size * scale, 0, Math.PI * 2); ctx.stroke(); // Bubble highlight ctx.fillStyle = 'rgba(255,255,255,' + (b.alpha * 0.5) + ')'; ctx.beginPath(); ctx.arc(bx - b.size * 0.3 * scale, by - b.size * 0.3 * scale, b.size * 0.3 * scale, 0, Math.PI * 2); ctx.fill(); }); }, 'music-notes': function(ctx, avatar, x, y, scale, time) { var notes = getMusicNotes(time, 8); notes.forEach(function(n) { var nx = x + n.x * scale; var ny = y + n.y * scale; ctx.save(); ctx.translate(nx, ny); ctx.rotate(n.rotation); ctx.fillStyle = 'rgba(155,89,182,' + n.alpha + ')'; // Note head ctx.beginPath(); ctx.ellipse(0, 0, 4 * scale, 3 * scale, -0.3, 0, Math.PI * 2); ctx.fill(); // Note stem ctx.fillRect(3 * scale, -12 * scale, 1.5 * scale, 12 * scale); // Note flag ctx.beginPath(); ctx.moveTo(4.5 * scale, -12 * scale); ctx.quadraticCurveTo(8 * scale, -8 * scale, 4.5 * scale, -6 * scale); ctx.fill(); ctx.restore(); }); }, 'heart-float': function(ctx, avatar, x, y, scale, time) { for (var i = 0; i < 8; i++) { var seed = i * 83.7; var lifetime = (time * 0.8 + seed) % 2.5; var hx = x + Math.sin(seed) * 25 * scale + Math.sin(time + seed) * 8 * scale; var hy = y - lifetime * 30 * scale + 15 * scale; var alpha = (1 - lifetime / 2.5) * 0.7; var heartScale = (0.5 + Math.sin(seed) * 0.3) * scale; ctx.fillStyle = 'rgba(231,76,60,' + alpha + ')'; ctx.save(); ctx.translate(hx, hy); ctx.scale(heartScale, heartScale); ctx.beginPath(); ctx.moveTo(0, -3); ctx.bezierCurveTo(-5, -8, -10, -3, 0, 8); ctx.bezierCurveTo(10, -3, 5, -8, 0, -3); ctx.fill(); ctx.restore(); } } }; // ============================================================================ // MAIN RENDER FUNCTION // ============================================================================ function renderAccessories(ctx, avatar, mode, x, y, scale, time) { if (!avatar || !avatar.equipped) return; time = time || 0; // Calculate head position based on mode var headY; if (mode === 'FULL_BODY') { headY = y - 60 * scale; } else if (mode === 'UPPER_BODY') { headY = y - 32 * scale; } else if (mode === 'HEAD_AND_SHOULDERS') { headY = y - 16 * scale; } else { headY = y; } // 1. Render effects (they go behind/around everything) if (avatar.equipped.effect) { var effectId = avatar.equipped.effect; if (typeof window.AvatarAccessories !== 'undefined') { var effect = window.AvatarAccessories.getById(effectId); if (effect && EFFECT_RENDERERS[effect.id]) { EFFECT_RENDERERS[effect.id](ctx, avatar, x, headY, scale, time); } } else if (EFFECT_RENDERERS[effectId]) { EFFECT_RENDERERS[effectId](ctx, avatar, x, headY, scale, time); } } // 2. Render eyewear if (avatar.equipped.eyewear) { var eyewearId = avatar.equipped.eyewear; var eyeY = headY + 5 * scale; if (typeof window.AvatarAccessories !== 'undefined') { var eyewear = window.AvatarAccessories.getById(eyewearId); if (eyewear && EYEWEAR_RENDERERS[eyewear.id]) { EYEWEAR_RENDERERS[eyewear.id](ctx, x, eyeY, scale, null, time); } } else if (EYEWEAR_RENDERERS[eyewearId]) { EYEWEAR_RENDERERS[eyewearId](ctx, x, eyeY, scale, null, time); } } // 3. Render headwear if (avatar.equipped.headwear) { var headwearId = avatar.equipped.headwear; var hatY = headY - 20 * scale; if (typeof window.AvatarAccessories !== 'undefined') { var headwear = window.AvatarAccessories.getById(headwearId); if (headwear && HEADWEAR_RENDERERS[headwear.id]) { HEADWEAR_RENDERERS[headwear.id](ctx, x, hatY, scale, null, time); } } else if (HEADWEAR_RENDERERS[headwearId]) { HEADWEAR_RENDERERS[headwearId](ctx, x, hatY, scale, null, time); } } } // ============================================================================ // INDIVIDUAL RENDER FUNCTIONS // ============================================================================ function renderEyewear(ctx, itemId, x, y, scale, time) { time = time || 0; if (EYEWEAR_RENDERERS[itemId]) { EYEWEAR_RENDERERS[itemId](ctx, x, y, scale, null, time); } } function renderHeadwear(ctx, itemId, x, y, scale, time) { time = time || 0; if (HEADWEAR_RENDERERS[itemId]) { HEADWEAR_RENDERERS[itemId](ctx, x, y, scale, null, time); } } function renderEffect(ctx, itemId, avatar, x, y, scale, time) { time = time || 0; if (EFFECT_RENDERERS[itemId]) { EFFECT_RENDERERS[itemId](ctx, avatar, x, y, scale, time); } } // ============================================================================ // PREVIEW FUNCTION (for shop/inventory UI) // ============================================================================ function renderPreview(ctx, itemId, x, y, size) { var scale = size / 60; var time = Date.now() / 1000; // Try to find what type of item this is if (EYEWEAR_RENDERERS[itemId]) { renderEyewear(ctx, itemId, x, y, scale, time); } else if (HEADWEAR_RENDERERS[itemId]) { renderHeadwear(ctx, itemId, x, y - 10 * scale, scale, time); } else if (EFFECT_RENDERERS[itemId]) { renderEffect(ctx, itemId, null, x, y, scale, time); } else if (typeof window.AvatarAccessories !== 'undefined') { // Check via AvatarAccessories if available var item = window.AvatarAccessories.getById(itemId); if (item) { if (item.category === 'eyewear') { renderEyewear(ctx, itemId, x, y, scale, time); } else if (item.category === 'headwear') { renderHeadwear(ctx, itemId, x, y - 10 * scale, scale, time); } else if (item.category === 'effect') { renderEffect(ctx, itemId, null, x, y, scale, time); } } } } // ============================================================================ // BOUNDING BOX CALCULATION // ============================================================================ function getBounds(itemId, scale) { scale = scale || 1; // Default bounds var bounds = { x: -30 * scale, y: -30 * scale, width: 60 * scale, height: 60 * scale }; // Eyewear bounds (relatively small, centered on face) if (EYEWEAR_RENDERERS[itemId]) { bounds = { x: -25 * scale, y: -5 * scale, width: 50 * scale, height: 20 * scale }; } // Headwear bounds (above head, varies by type) if (HEADWEAR_RENDERERS[itemId]) { if (itemId === 'top-hat') { bounds = { x: -25 * scale, y: -45 * scale, width: 50 * scale, height: 50 * scale }; } else if (itemId === 'wizard-hat') { bounds = { x: -28 * scale, y: -60 * scale, width: 56 * scale, height: 65 * scale }; } else if (itemId === 'viking-helmet') { bounds = { x: -45 * scale, y: -40 * scale, width: 90 * scale, height: 55 * scale }; } else if (itemId.includes('crown')) { bounds = { x: -20 * scale, y: -25 * scale, width: 40 * scale, height: 30 * scale }; } else if (itemId === 'halo') { bounds = { x: -20 * scale, y: -35 * scale, width: 40 * scale, height: 20 * scale }; } else { bounds = { x: -30 * scale, y: -35 * scale, width: 60 * scale, height: 45 * scale }; } } // Effect bounds (larger, encompasses aura) if (EFFECT_RENDERERS[itemId]) { bounds = { x: -50 * scale, y: -50 * scale, width: 100 * scale, height: 100 * scale }; } return bounds; } // ============================================================================ // PUBLIC API // ============================================================================ window.AccessoryRenderer = { render: renderAccessories, renderEyewear: renderEyewear, renderHeadwear: renderHeadwear, renderEffect: renderEffect, renderPreview: renderPreview, getBounds: getBounds, Z_ORDER: Z_ORDER }; })();