1572 lines
52 KiB
JavaScript
1572 lines
52 KiB
JavaScript
/**
|
|
* 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
|
|
};
|
|
|
|
})();
|