Files
gamercomp/frontend/public/assets/avatar/avatarRenderer.js
T
2026-04-30 02:14:25 +00:00

1799 lines
53 KiB
JavaScript

/**
* AvatarRenderer - Core rendering engine for Mii-style avatar system
* Draws avatars on canvas in different modes using Canvas 2D API
*
* Depends on: AvatarData, AvatarAnimations (must be loaded first)
*/
(function() {
'use strict';
// Rendering modes with their dimensions
var MODES = {
HEAD_ONLY: { width: 64, height: 64 },
HEAD_AND_SHOULDERS: { width: 64, height: 96 },
UPPER_BODY: { width: 64, height: 128 },
FULL_BODY: { width: 64, height: 160 }
};
// Gradient cache for performance
var gradientCache = {};
// ============================================
// DRAWING HELPERS
// ============================================
/**
* Draw a rounded rectangle
*/
function roundRect(ctx, x, y, w, h, r) {
if (typeof r === 'number') {
r = { tl: r, tr: r, br: r, bl: r };
} else {
r = Object.assign({ tl: 0, tr: 0, br: 0, bl: 0 }, r);
}
ctx.beginPath();
ctx.moveTo(x + r.tl, y);
ctx.lineTo(x + w - r.tr, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r.tr);
ctx.lineTo(x + w, y + h - r.br);
ctx.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h);
ctx.lineTo(x + r.bl, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r.bl);
ctx.lineTo(x, y + r.tl);
ctx.quadraticCurveTo(x, y, x + r.tl, y);
ctx.closePath();
}
/**
* Draw a bezier curve through points
*/
function drawCurve(ctx, points) {
if (points.length < 2) return;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
if (points.length === 2) {
ctx.lineTo(points[1].x, points[1].y);
} else if (points.length === 3) {
ctx.quadraticCurveTo(points[1].x, points[1].y, points[2].x, points[2].y);
} else {
for (var i = 1; i < points.length - 2; i++) {
var xc = (points[i].x + points[i + 1].x) / 2;
var yc = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
}
ctx.quadraticCurveTo(
points[points.length - 2].x,
points[points.length - 2].y,
points[points.length - 1].x,
points[points.length - 1].y
);
}
}
/**
* Create a gradient fill
*/
function gradientFill(ctx, x, y, w, h, color1, color2, vertical) {
var cacheKey = color1 + '-' + color2 + '-' + (vertical ? 'v' : 'h');
var gradient;
if (vertical) {
gradient = ctx.createLinearGradient(x, y, x, y + h);
} else {
gradient = ctx.createLinearGradient(x, y, x + w, y);
}
gradient.addColorStop(0, color1);
gradient.addColorStop(1, color2);
return gradient;
}
/**
* Adjust skin color for shading
*/
function skinShade(baseColor, amount) {
var hex = baseColor.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.min(255, r + amount));
g = Math.max(0, Math.min(255, g + amount));
b = Math.max(0, Math.min(255, b + amount));
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
/**
* Adjust any color brightness
*/
function adjustBrightness(color, amount) {
return skinShade(color, amount);
}
/**
* Add shine highlight to hair
*/
function addHairShine(ctx, x, y, w, h, color) {
var lighter = adjustBrightness(color, 40);
ctx.save();
ctx.globalAlpha = 0.3;
ctx.fillStyle = lighter;
ctx.beginPath();
ctx.ellipse(x + w / 2, y + h / 2, w / 2, h / 3, -0.3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
/**
* Draw an ellipse (polyfill for older browsers)
*/
function drawEllipse(ctx, x, y, radiusX, radiusY) {
ctx.beginPath();
if (ctx.ellipse) {
ctx.ellipse(x, y, radiusX, radiusY, 0, 0, Math.PI * 2);
} else {
ctx.save();
ctx.translate(x, y);
ctx.scale(radiusX, radiusY);
ctx.arc(0, 0, 1, 0, Math.PI * 2);
ctx.restore();
}
ctx.closePath();
}
// ============================================
// FACE RENDERING
// ============================================
/**
* Render the base face shape
*/
function renderFaceBase(ctx, avatar, x, y, scale) {
var faceShape = window.AvatarData ? window.AvatarData.getFaceShape(avatar.base.faceShape) : null;
var skinTone = avatar.base.skinTone || '#FFD5B8';
var shapeId = faceShape ? faceShape.id : avatar.base.faceShape || 'oval';
var faceWidth = 44 * scale;
var faceHeight = 48 * scale;
var cx = x + 32 * scale;
var cy = y + 32 * scale;
ctx.fillStyle = skinTone;
switch (shapeId) {
case 'round':
drawEllipse(ctx, cx, cy, faceWidth / 2, faceWidth / 2);
ctx.fill();
break;
case 'oval':
drawEllipse(ctx, cx, cy, faceWidth / 2, faceHeight / 2);
ctx.fill();
break;
case 'square':
roundRect(ctx, cx - faceWidth / 2, cy - faceHeight / 2, faceWidth, faceHeight, 6 * scale);
ctx.fill();
break;
case 'heart':
ctx.beginPath();
ctx.moveTo(cx, cy + faceHeight / 2);
ctx.quadraticCurveTo(cx - faceWidth / 2, cy + faceHeight / 4, cx - faceWidth / 2, cy - faceHeight / 6);
ctx.quadraticCurveTo(cx - faceWidth / 2, cy - faceHeight / 2, cx, cy - faceHeight / 3);
ctx.quadraticCurveTo(cx + faceWidth / 2, cy - faceHeight / 2, cx + faceWidth / 2, cy - faceHeight / 6);
ctx.quadraticCurveTo(cx + faceWidth / 2, cy + faceHeight / 4, cx, cy + faceHeight / 2);
ctx.closePath();
ctx.fill();
break;
case 'long':
drawEllipse(ctx, cx, cy, faceWidth / 2 - 2 * scale, faceHeight / 2 + 4 * scale);
ctx.fill();
break;
case 'diamond':
ctx.beginPath();
ctx.moveTo(cx, cy - faceHeight / 2);
ctx.lineTo(cx + faceWidth / 2, cy);
ctx.lineTo(cx, cy + faceHeight / 2);
ctx.lineTo(cx - faceWidth / 2, cy);
ctx.closePath();
ctx.fill();
break;
default:
drawEllipse(ctx, cx, cy, faceWidth / 2, faceHeight / 2);
ctx.fill();
}
// Add subtle shading
ctx.save();
ctx.globalAlpha = 0.1;
ctx.fillStyle = skinShade(skinTone, -30);
drawEllipse(ctx, cx, cy + 5 * scale, faceWidth / 2 - 5 * scale, faceHeight / 3);
ctx.fill();
ctx.restore();
}
/**
* Render ears
*/
function renderEars(ctx, avatar, x, y, scale, transforms) {
var skinTone = avatar.base.skinTone || '#FFD5B8';
var earSize = 8 * scale;
var cx = x + 32 * scale;
var cy = y + 32 * scale;
ctx.fillStyle = skinTone;
// Left ear
drawEllipse(ctx, cx - 22 * scale, cy, earSize / 2, earSize);
ctx.fill();
// Right ear
drawEllipse(ctx, cx + 22 * scale, cy, earSize / 2, earSize);
ctx.fill();
// Inner ear detail
ctx.fillStyle = skinShade(skinTone, -20);
drawEllipse(ctx, cx - 22 * scale, cy, earSize / 4, earSize / 2);
ctx.fill();
drawEllipse(ctx, cx + 22 * scale, cy, earSize / 4, earSize / 2);
ctx.fill();
}
/**
* Render eyes
*/
function renderEyes(ctx, avatar, x, y, scale, transforms) {
var eyes = avatar.base.eyes || {};
var eyeStyle = window.AvatarData ? window.AvatarData.getEyeStyle(eyes.style) : null;
var eyeColor = eyes.color || '#4A90D9';
var styleId = eyeStyle ? eyeStyle.id : eyes.style || 'normal';
var cx = x + 32 * scale;
var cy = y + 28 * scale;
var eyeSpacing = 12 * scale;
var eyeWidth = 6 * scale;
var eyeHeight = 8 * scale;
// Apply eye transforms if animating
ctx.save();
if (transforms && transforms.eyes) {
applyTransforms(ctx, transforms, 'eyes');
}
// Draw based on style
switch (styleId) {
case 'round':
eyeWidth = 7 * scale;
eyeHeight = 7 * scale;
break;
case 'narrow':
eyeWidth = 8 * scale;
eyeHeight = 5 * scale;
break;
case 'wide':
eyeWidth = 9 * scale;
eyeHeight = 7 * scale;
break;
case 'almond':
eyeWidth = 8 * scale;
eyeHeight = 6 * scale;
break;
}
// Left eye white
ctx.fillStyle = '#FFFFFF';
drawEllipse(ctx, cx - eyeSpacing, cy, eyeWidth, eyeHeight);
ctx.fill();
ctx.strokeStyle = '#333333';
ctx.lineWidth = 0.5 * scale;
ctx.stroke();
// Left iris
ctx.fillStyle = eyeColor;
drawEllipse(ctx, cx - eyeSpacing, cy + 1 * scale, eyeWidth * 0.6, eyeHeight * 0.7);
ctx.fill();
// Left pupil
ctx.fillStyle = '#000000';
drawEllipse(ctx, cx - eyeSpacing, cy + 1 * scale, eyeWidth * 0.3, eyeHeight * 0.4);
ctx.fill();
// Left highlight
ctx.fillStyle = '#FFFFFF';
drawEllipse(ctx, cx - eyeSpacing - 1 * scale, cy - 1 * scale, eyeWidth * 0.2, eyeHeight * 0.2);
ctx.fill();
// Right eye white
ctx.fillStyle = '#FFFFFF';
drawEllipse(ctx, cx + eyeSpacing, cy, eyeWidth, eyeHeight);
ctx.fill();
ctx.strokeStyle = '#333333';
ctx.lineWidth = 0.5 * scale;
ctx.stroke();
// Right iris
ctx.fillStyle = eyeColor;
drawEllipse(ctx, cx + eyeSpacing, cy + 1 * scale, eyeWidth * 0.6, eyeHeight * 0.7);
ctx.fill();
// Right pupil
ctx.fillStyle = '#000000';
drawEllipse(ctx, cx + eyeSpacing, cy + 1 * scale, eyeWidth * 0.3, eyeHeight * 0.4);
ctx.fill();
// Right highlight
ctx.fillStyle = '#FFFFFF';
drawEllipse(ctx, cx + eyeSpacing - 1 * scale, cy - 1 * scale, eyeWidth * 0.2, eyeHeight * 0.2);
ctx.fill();
ctx.restore();
}
/**
* Render eyebrows
*/
function renderEyebrows(ctx, avatar, x, y, scale) {
var eyebrows = avatar.base.eyebrows || {};
var browStyle = window.AvatarData ? window.AvatarData.getEyebrowStyle(eyebrows.style) : null;
var browColor = eyebrows.color || avatar.base.hair.color || '#3D2314';
var styleId = browStyle ? browStyle.id : eyebrows.style || 'normal';
var cx = x + 32 * scale;
var cy = y + 20 * scale;
var browSpacing = 12 * scale;
var browWidth = 10 * scale;
ctx.strokeStyle = browColor;
ctx.lineWidth = 2 * scale;
ctx.lineCap = 'round';
switch (styleId) {
case 'thick':
ctx.lineWidth = 3 * scale;
break;
case 'thin':
ctx.lineWidth = 1.5 * scale;
break;
case 'arched':
// Draw arched brows
ctx.beginPath();
ctx.moveTo(cx - browSpacing - browWidth / 2, cy + 2 * scale);
ctx.quadraticCurveTo(cx - browSpacing, cy - 3 * scale, cx - browSpacing + browWidth / 2, cy);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx + browSpacing - browWidth / 2, cy);
ctx.quadraticCurveTo(cx + browSpacing, cy - 3 * scale, cx + browSpacing + browWidth / 2, cy + 2 * scale);
ctx.stroke();
return;
case 'angry':
// Draw angled angry brows
ctx.beginPath();
ctx.moveTo(cx - browSpacing - browWidth / 2, cy - 2 * scale);
ctx.lineTo(cx - browSpacing + browWidth / 2, cy + 2 * scale);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx + browSpacing - browWidth / 2, cy + 2 * scale);
ctx.lineTo(cx + browSpacing + browWidth / 2, cy - 2 * scale);
ctx.stroke();
return;
}
// Default straight brows
ctx.beginPath();
ctx.moveTo(cx - browSpacing - browWidth / 2, cy);
ctx.lineTo(cx - browSpacing + browWidth / 2, cy);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx + browSpacing - browWidth / 2, cy);
ctx.lineTo(cx + browSpacing + browWidth / 2, cy);
ctx.stroke();
}
/**
* Render nose
*/
function renderNose(ctx, avatar, x, y, scale) {
var nose = avatar.base.nose || {};
var skinTone = avatar.base.skinTone || '#FFD5B8';
var noseStyle = window.AvatarData ? window.AvatarData.getNoseStyle(nose.style) : null;
var styleId = noseStyle ? noseStyle.id : nose.style || 'normal';
var cx = x + 32 * scale;
var cy = y + 36 * scale;
ctx.fillStyle = skinShade(skinTone, -15);
ctx.strokeStyle = skinShade(skinTone, -25);
ctx.lineWidth = 1 * scale;
switch (styleId) {
case 'small':
ctx.beginPath();
ctx.arc(cx, cy, 2 * scale, 0, Math.PI * 2);
ctx.fill();
break;
case 'large':
ctx.beginPath();
ctx.moveTo(cx, cy - 6 * scale);
ctx.lineTo(cx + 5 * scale, cy + 3 * scale);
ctx.lineTo(cx - 5 * scale, cy + 3 * scale);
ctx.closePath();
ctx.fill();
ctx.stroke();
break;
case 'pointed':
ctx.beginPath();
ctx.moveTo(cx, cy - 5 * scale);
ctx.lineTo(cx + 3 * scale, cy + 2 * scale);
ctx.lineTo(cx, cy + 4 * scale);
ctx.lineTo(cx - 3 * scale, cy + 2 * scale);
ctx.closePath();
ctx.fill();
break;
case 'button':
drawEllipse(ctx, cx, cy, 3 * scale, 2 * scale);
ctx.fill();
break;
default:
// Normal nose - simple curved line
ctx.beginPath();
ctx.moveTo(cx - 2 * scale, cy - 3 * scale);
ctx.quadraticCurveTo(cx, cy + 2 * scale, cx + 2 * scale, cy - 3 * scale);
ctx.stroke();
// Nostrils hint
ctx.beginPath();
ctx.arc(cx - 2 * scale, cy, 1 * scale, 0, Math.PI);
ctx.stroke();
ctx.beginPath();
ctx.arc(cx + 2 * scale, cy, 1 * scale, 0, Math.PI);
ctx.stroke();
}
}
/**
* Render mouth
*/
function renderMouth(ctx, avatar, x, y, scale) {
var mouth = avatar.base.mouth || {};
var mouthStyle = window.AvatarData ? window.AvatarData.getMouthStyle(mouth.style) : null;
var styleId = mouthStyle ? mouthStyle.id : mouth.style || 'smile';
var cx = x + 32 * scale;
var cy = y + 46 * scale;
ctx.strokeStyle = '#8B4513';
ctx.fillStyle = '#CC6666';
ctx.lineWidth = 1.5 * scale;
ctx.lineCap = 'round';
switch (styleId) {
case 'smile':
ctx.beginPath();
ctx.arc(cx, cy - 2 * scale, 6 * scale, 0.2 * Math.PI, 0.8 * Math.PI);
ctx.stroke();
break;
case 'grin':
ctx.beginPath();
ctx.arc(cx, cy - 4 * scale, 8 * scale, 0.15 * Math.PI, 0.85 * Math.PI);
ctx.stroke();
// Teeth
ctx.fillStyle = '#FFFFFF';
ctx.beginPath();
ctx.arc(cx, cy - 4 * scale, 7 * scale, 0.2 * Math.PI, 0.8 * Math.PI);
ctx.fill();
break;
case 'neutral':
ctx.beginPath();
ctx.moveTo(cx - 5 * scale, cy);
ctx.lineTo(cx + 5 * scale, cy);
ctx.stroke();
break;
case 'frown':
ctx.beginPath();
ctx.arc(cx, cy + 6 * scale, 6 * scale, 1.2 * Math.PI, 1.8 * Math.PI);
ctx.stroke();
break;
case 'open':
ctx.fillStyle = '#4A2020';
drawEllipse(ctx, cx, cy, 5 * scale, 4 * scale);
ctx.fill();
// Tongue hint
ctx.fillStyle = '#CC6666';
drawEllipse(ctx, cx, cy + 2 * scale, 3 * scale, 2 * scale);
ctx.fill();
break;
case 'smirk':
ctx.beginPath();
ctx.moveTo(cx - 5 * scale, cy);
ctx.quadraticCurveTo(cx, cy, cx + 5 * scale, cy - 3 * scale);
ctx.stroke();
break;
default:
ctx.beginPath();
ctx.arc(cx, cy - 2 * scale, 5 * scale, 0.2 * Math.PI, 0.8 * Math.PI);
ctx.stroke();
}
}
/**
* Render facial hair
*/
function renderFacialHair(ctx, avatar, x, y, scale) {
var facialHair = avatar.base.facialHair;
var hairColor = avatar.base.hair ? avatar.base.hair.color : '#3D2314';
if (!facialHair || facialHair === 'none') return;
var cx = x + 32 * scale;
var cy = y + 46 * scale;
ctx.fillStyle = hairColor;
ctx.strokeStyle = hairColor;
ctx.lineWidth = 1 * scale;
switch (facialHair) {
case 'stubble':
ctx.save();
ctx.globalAlpha = 0.4;
for (var i = 0; i < 30; i++) {
var sx = cx + (Math.random() - 0.5) * 24 * scale;
var sy = cy + Math.random() * 12 * scale;
ctx.beginPath();
ctx.arc(sx, sy, 0.5 * scale, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
break;
case 'mustache':
ctx.beginPath();
ctx.moveTo(cx - 10 * scale, cy - 2 * scale);
ctx.quadraticCurveTo(cx - 5 * scale, cy - 5 * scale, cx, cy - 3 * scale);
ctx.quadraticCurveTo(cx + 5 * scale, cy - 5 * scale, cx + 10 * scale, cy - 2 * scale);
ctx.quadraticCurveTo(cx + 5 * scale, cy, cx, cy - 1 * scale);
ctx.quadraticCurveTo(cx - 5 * scale, cy, cx - 10 * scale, cy - 2 * scale);
ctx.fill();
break;
case 'goatee':
ctx.beginPath();
ctx.moveTo(cx - 6 * scale, cy + 2 * scale);
ctx.quadraticCurveTo(cx, cy + 12 * scale, cx + 6 * scale, cy + 2 * scale);
ctx.quadraticCurveTo(cx, cy + 4 * scale, cx - 6 * scale, cy + 2 * scale);
ctx.fill();
break;
case 'full-beard':
ctx.beginPath();
ctx.moveTo(cx - 18 * scale, cy - 8 * scale);
ctx.quadraticCurveTo(cx - 20 * scale, cy + 5 * scale, cx - 10 * scale, cy + 15 * scale);
ctx.quadraticCurveTo(cx, cy + 20 * scale, cx + 10 * scale, cy + 15 * scale);
ctx.quadraticCurveTo(cx + 20 * scale, cy + 5 * scale, cx + 18 * scale, cy - 8 * scale);
ctx.quadraticCurveTo(cx, cy + 2 * scale, cx - 18 * scale, cy - 8 * scale);
ctx.fill();
break;
case 'sideburns':
// Left sideburn
ctx.beginPath();
ctx.moveTo(cx - 20 * scale, cy - 20 * scale);
ctx.lineTo(cx - 22 * scale, cy + 5 * scale);
ctx.lineTo(cx - 18 * scale, cy + 5 * scale);
ctx.lineTo(cx - 16 * scale, cy - 20 * scale);
ctx.fill();
// Right sideburn
ctx.beginPath();
ctx.moveTo(cx + 20 * scale, cy - 20 * scale);
ctx.lineTo(cx + 22 * scale, cy + 5 * scale);
ctx.lineTo(cx + 18 * scale, cy + 5 * scale);
ctx.lineTo(cx + 16 * scale, cy - 20 * scale);
ctx.fill();
break;
}
}
// ============================================
// HAIR RENDERING
// ============================================
/**
* Main hair rendering function
*/
function renderHair(ctx, avatar, x, y, scale, layer) {
var hair = avatar.base.hair || {};
var hairStyle = window.AvatarData ? window.AvatarData.getHairStyle(hair.style) : null;
var hairColor = hair.color || '#3D2314';
var styleId = hairStyle ? hairStyle.id : hair.style || 'short-messy';
if (!styleId || styleId === 'bald') return;
ctx.fillStyle = hairColor;
ctx.strokeStyle = adjustBrightness(hairColor, -20);
ctx.lineWidth = 1 * scale;
var cx = x + 32 * scale;
var cy = y + 32 * scale;
// Dispatch to specific hair drawing function
switch (styleId) {
case 'short-messy':
drawShortMessyHair(ctx, x, y, scale, hairColor, layer);
break;
case 'short-curly':
drawShortCurlyHair(ctx, x, y, scale, hairColor, layer);
break;
case 'short-neat':
drawShortNeatHair(ctx, x, y, scale, hairColor, layer);
break;
case 'ponytail':
drawPonytailHair(ctx, x, y, scale, hairColor, layer);
break;
case 'long-straight':
drawLongStraightHair(ctx, x, y, scale, hairColor, layer);
break;
case 'long-wavy':
drawLongWavyHair(ctx, x, y, scale, hairColor, layer);
break;
case 'mohawk':
drawMohawkHair(ctx, x, y, scale, hairColor, layer);
break;
case 'afro':
drawAfroHair(ctx, x, y, scale, hairColor, layer);
break;
case 'buzz':
drawBuzzHair(ctx, x, y, scale, hairColor, layer);
break;
case 'pigtails':
drawPigtailsHair(ctx, x, y, scale, hairColor, layer);
break;
case 'bob':
drawBobHair(ctx, x, y, scale, hairColor, layer);
break;
case 'spiky':
drawSpikyHair(ctx, x, y, scale, hairColor, layer);
break;
default:
// Default to short messy
drawShortMessyHair(ctx, x, y, scale, hairColor, layer);
}
}
/**
* Short messy hair style
*/
function drawShortMessyHair(ctx, x, y, scale, hairColor, layer) {
var cx = x + 32 * scale;
var cy = y + 32 * scale;
if (layer === 'back') return; // No back layer for short hair
ctx.fillStyle = hairColor;
// Base hair shape
ctx.beginPath();
ctx.moveTo(cx - 20 * scale, cy - 5 * scale);
ctx.quadraticCurveTo(cx - 22 * scale, cy - 20 * scale, cx - 10 * scale, cy - 22 * scale);
ctx.quadraticCurveTo(cx, cy - 28 * scale, cx + 10 * scale, cy - 22 * scale);
ctx.quadraticCurveTo(cx + 22 * scale, cy - 20 * scale, cx + 20 * scale, cy - 5 * scale);
ctx.quadraticCurveTo(cx + 15 * scale, cy - 10 * scale, cx, cy - 12 * scale);
ctx.quadraticCurveTo(cx - 15 * scale, cy - 10 * scale, cx - 20 * scale, cy - 5 * scale);
ctx.fill();
// Messy tufts
ctx.beginPath();
ctx.moveTo(cx - 8 * scale, cy - 22 * scale);
ctx.lineTo(cx - 12 * scale, cy - 30 * scale);
ctx.lineTo(cx - 5 * scale, cy - 24 * scale);
ctx.fill();
ctx.beginPath();
ctx.moveTo(cx + 3 * scale, cy - 24 * scale);
ctx.lineTo(cx + 8 * scale, cy - 32 * scale);
ctx.lineTo(cx + 12 * scale, cy - 25 * scale);
ctx.fill();
ctx.beginPath();
ctx.moveTo(cx + 15 * scale, cy - 20 * scale);
ctx.lineTo(cx + 22 * scale, cy - 26 * scale);
ctx.lineTo(cx + 18 * scale, cy - 18 * scale);
ctx.fill();
// Add shine
addHairShine(ctx, cx - 5 * scale, cy - 22 * scale, 15 * scale, 8 * scale, hairColor);
}
/**
* Short curly hair style
*/
function drawShortCurlyHair(ctx, x, y, scale, hairColor, layer) {
var cx = x + 32 * scale;
var cy = y + 32 * scale;
if (layer === 'back') return;
ctx.fillStyle = hairColor;
// Draw curly texture with multiple small circles
var curls = [
{ x: -15, y: -18 }, { x: -8, y: -22 }, { x: 0, y: -24 },
{ x: 8, y: -22 }, { x: 15, y: -18 }, { x: -18, y: -10 },
{ x: -12, y: -16 }, { x: -4, y: -20 }, { x: 4, y: -20 },
{ x: 12, y: -16 }, { x: 18, y: -10 }, { x: -10, y: -12 },
{ x: 0, y: -16 }, { x: 10, y: -12 }
];
curls.forEach(function(curl) {
ctx.beginPath();
ctx.arc(cx + curl.x * scale, cy + curl.y * scale, 6 * scale, 0, Math.PI * 2);
ctx.fill();
});
// Add highlights
ctx.save();
ctx.globalAlpha = 0.3;
ctx.fillStyle = adjustBrightness(hairColor, 40);
curls.slice(0, 5).forEach(function(curl) {
ctx.beginPath();
ctx.arc(cx + curl.x * scale - 1 * scale, cy + curl.y * scale - 1 * scale, 2 * scale, 0, Math.PI * 2);
ctx.fill();
});
ctx.restore();
}
/**
* Short neat hair style
*/
function drawShortNeatHair(ctx, x, y, scale, hairColor, layer) {
var cx = x + 32 * scale;
var cy = y + 32 * scale;
if (layer === 'back') return;
ctx.fillStyle = hairColor;
// Clean curved shape
ctx.beginPath();
ctx.moveTo(cx - 20 * scale, cy - 5 * scale);
ctx.quadraticCurveTo(cx - 22 * scale, cy - 18 * scale, cx - 12 * scale, cy - 22 * scale);
ctx.quadraticCurveTo(cx, cy - 26 * scale, cx + 12 * scale, cy - 22 * scale);
ctx.quadraticCurveTo(cx + 22 * scale, cy - 18 * scale, cx + 20 * scale, cy - 5 * scale);
ctx.lineTo(cx + 18 * scale, cy - 8 * scale);
ctx.quadraticCurveTo(cx, cy - 14 * scale, cx - 18 * scale, cy - 8 * scale);
ctx.closePath();
ctx.fill();
// Part line
ctx.strokeStyle = adjustBrightness(hairColor, -30);
ctx.lineWidth = 1 * scale;
ctx.beginPath();
ctx.moveTo(cx - 5 * scale, cy - 24 * scale);
ctx.quadraticCurveTo(cx - 8 * scale, cy - 18 * scale, cx - 15 * scale, cy - 10 * scale);
ctx.stroke();
addHairShine(ctx, cx, cy - 20 * scale, 12 * scale, 6 * scale, hairColor);
}
/**
* Ponytail hair style
*/
function drawPonytailHair(ctx, x, y, scale, hairColor, layer) {
var cx = x + 32 * scale;
var cy = y + 32 * scale;
ctx.fillStyle = hairColor;
if (layer === 'back') {
// Ponytail behind head
ctx.beginPath();
ctx.moveTo(cx, cy - 10 * scale);
ctx.quadraticCurveTo(cx + 25 * scale, cy - 5 * scale, cx + 20 * scale, cy + 30 * scale);
ctx.quadraticCurveTo(cx + 15 * scale, cy + 40 * scale, cx + 10 * scale, cy + 35 * scale);
ctx.quadraticCurveTo(cx + 5 * scale, cy + 25 * scale, cx + 8 * scale, cy);
ctx.quadraticCurveTo(cx + 5 * scale, cy - 8 * scale, cx, cy - 10 * scale);
ctx.fill();
// Hair tie
ctx.fillStyle = '#CC4444';
ctx.beginPath();
ctx.arc(cx + 12 * scale, cy - 2 * scale, 3 * scale, 0, Math.PI * 2);
ctx.fill();
return;
}
// Front hair
ctx.beginPath();
ctx.moveTo(cx - 20 * scale, cy - 5 * scale);
ctx.quadraticCurveTo(cx - 22 * scale, cy - 18 * scale, cx - 10 * scale, cy - 22 * scale);
ctx.quadraticCurveTo(cx, cy - 26 * scale, cx + 10 * scale, cy - 22 * scale);
ctx.quadraticCurveTo(cx + 22 * scale, cy - 18 * scale, cx + 20 * scale, cy - 5 * scale);
ctx.quadraticCurveTo(cx + 10 * scale, cy - 12 * scale, cx, cy - 14 * scale);
ctx.quadraticCurveTo(cx - 10 * scale, cy - 12 * scale, cx - 20 * scale, cy - 5 * scale);
ctx.fill();
// Side pieces
ctx.beginPath();
ctx.moveTo(cx - 20 * scale, cy - 5 * scale);
ctx.quadraticCurveTo(cx - 22 * scale, cy + 5 * scale, cx - 18 * scale, cy + 10 * scale);
ctx.lineTo(cx - 16 * scale, cy + 5 * scale);
ctx.quadraticCurveTo(cx - 18 * scale, cy, cx - 20 * scale, cy - 5 * scale);
ctx.fill();
addHairShine(ctx, cx - 5 * scale, cy - 20 * scale, 15 * scale, 8 * scale, hairColor);
}
/**
* Long straight hair style
*/
function drawLongStraightHair(ctx, x, y, scale, hairColor, layer) {
var cx = x + 32 * scale;
var cy = y + 32 * scale;
ctx.fillStyle = hairColor;
if (layer === 'back') {
// Long hair behind body
ctx.beginPath();
ctx.moveTo(cx - 22 * scale, cy - 10 * scale);
ctx.lineTo(cx - 25 * scale, cy + 60 * scale);
ctx.quadraticCurveTo(cx, cy + 70 * scale, cx + 25 * scale, cy + 60 * scale);
ctx.lineTo(cx + 22 * scale, cy - 10 * scale);
ctx.quadraticCurveTo(cx, cy - 5 * scale, cx - 22 * scale, cy - 10 * scale);
ctx.fill();
return;
}
// Front hair cap
ctx.beginPath();
ctx.moveTo(cx - 22 * scale, cy);
ctx.quadraticCurveTo(cx - 24 * scale, cy - 20 * scale, cx - 12 * scale, cy - 24 * scale);
ctx.quadraticCurveTo(cx, cy - 28 * scale, cx + 12 * scale, cy - 24 * scale);
ctx.quadraticCurveTo(cx + 24 * scale, cy - 20 * scale, cx + 22 * scale, cy);
ctx.lineTo(cx + 20 * scale, cy - 5 * scale);
ctx.quadraticCurveTo(cx, cy - 16 * scale, cx - 20 * scale, cy - 5 * scale);
ctx.closePath();
ctx.fill();
// Side strands
ctx.beginPath();
ctx.moveTo(cx - 22 * scale, cy);
ctx.lineTo(cx - 24 * scale, cy + 35 * scale);
ctx.lineTo(cx - 18 * scale, cy + 30 * scale);
ctx.lineTo(cx - 18 * scale, cy - 5 * scale);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(cx + 22 * scale, cy);
ctx.lineTo(cx + 24 * scale, cy + 35 * scale);
ctx.lineTo(cx + 18 * scale, cy + 30 * scale);
ctx.lineTo(cx + 18 * scale, cy - 5 * scale);
ctx.closePath();
ctx.fill();
addHairShine(ctx, cx - 8 * scale, cy - 18 * scale, 20 * scale, 10 * scale, hairColor);
}
/**
* Long wavy hair style
*/
function drawLongWavyHair(ctx, x, y, scale, hairColor, layer) {
var cx = x + 32 * scale;
var cy = y + 32 * scale;
ctx.fillStyle = hairColor;
if (layer === 'back') {
// Wavy back hair
ctx.beginPath();
ctx.moveTo(cx - 22 * scale, cy - 10 * scale);
ctx.bezierCurveTo(
cx - 28 * scale, cy + 20 * scale,
cx - 20 * scale, cy + 40 * scale,
cx - 22 * scale, cy + 60 * scale
);
ctx.quadraticCurveTo(cx, cy + 65 * scale, cx + 22 * scale, cy + 60 * scale);
ctx.bezierCurveTo(
cx + 20 * scale, cy + 40 * scale,
cx + 28 * scale, cy + 20 * scale,
cx + 22 * scale, cy - 10 * scale
);
ctx.quadraticCurveTo(cx, cy - 5 * scale, cx - 22 * scale, cy - 10 * scale);
ctx.fill();
return;
}
// Front wavy cap
ctx.beginPath();
ctx.moveTo(cx - 22 * scale, cy);
ctx.quadraticCurveTo(cx - 24 * scale, cy - 20 * scale, cx - 10 * scale, cy - 24 * scale);
ctx.quadraticCurveTo(cx, cy - 28 * scale, cx + 10 * scale, cy - 24 * scale);
ctx.quadraticCurveTo(cx + 24 * scale, cy - 20 * scale, cx + 22 * scale, cy);
ctx.lineTo(cx + 18 * scale, cy - 8 * scale);
ctx.quadraticCurveTo(cx, cy - 16 * scale, cx - 18 * scale, cy - 8 * scale);
ctx.closePath();
ctx.fill();
// Wavy side strands
ctx.beginPath();
ctx.moveTo(cx - 22 * scale, cy);
ctx.bezierCurveTo(
cx - 26 * scale, cy + 15 * scale,
cx - 20 * scale, cy + 25 * scale,
cx - 24 * scale, cy + 40 * scale
);
ctx.lineTo(cx - 18 * scale, cy + 35 * scale);
ctx.bezierCurveTo(
cx - 16 * scale, cy + 20 * scale,
cx - 20 * scale, cy + 10 * scale,
cx - 18 * scale, cy - 5 * scale
);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(cx + 22 * scale, cy);
ctx.bezierCurveTo(
cx + 26 * scale, cy + 15 * scale,
cx + 20 * scale, cy + 25 * scale,
cx + 24 * scale, cy + 40 * scale
);
ctx.lineTo(cx + 18 * scale, cy + 35 * scale);
ctx.bezierCurveTo(
cx + 16 * scale, cy + 20 * scale,
cx + 20 * scale, cy + 10 * scale,
cx + 18 * scale, cy - 5 * scale
);
ctx.closePath();
ctx.fill();
addHairShine(ctx, cx - 5 * scale, cy - 18 * scale, 15 * scale, 8 * scale, hairColor);
}
/**
* Mohawk hair style
*/
function drawMohawkHair(ctx, x, y, scale, hairColor, layer) {
var cx = x + 32 * scale;
var cy = y + 32 * scale;
if (layer === 'back') return;
ctx.fillStyle = hairColor;
// Shaved sides (subtle)
ctx.save();
ctx.globalAlpha = 0.3;
ctx.beginPath();
ctx.arc(cx, cy - 10 * scale, 22 * scale, Math.PI, 0);
ctx.fill();
ctx.restore();
// Mohawk strip
ctx.beginPath();
ctx.moveTo(cx - 6 * scale, cy - 5 * scale);
ctx.lineTo(cx - 8 * scale, cy - 35 * scale);
ctx.quadraticCurveTo(cx, cy - 42 * scale, cx + 8 * scale, cy - 35 * scale);
ctx.lineTo(cx + 6 * scale, cy - 5 * scale);
ctx.quadraticCurveTo(cx, cy - 10 * scale, cx - 6 * scale, cy - 5 * scale);
ctx.fill();
// Spiky details
var spikes = [
{ x: -4, y: -30, h: 8 },
{ x: 0, y: -32, h: 12 },
{ x: 4, y: -30, h: 8 }
];
spikes.forEach(function(spike) {
ctx.beginPath();
ctx.moveTo(cx + (spike.x - 2) * scale, cy + spike.y * scale);
ctx.lineTo(cx + spike.x * scale, cy + (spike.y - spike.h) * scale);
ctx.lineTo(cx + (spike.x + 2) * scale, cy + spike.y * scale);
ctx.fill();
});
addHairShine(ctx, cx - 3 * scale, cy - 28 * scale, 8 * scale, 6 * scale, hairColor);
}
/**
* Afro hair style
*/
function drawAfroHair(ctx, x, y, scale, hairColor, layer) {
var cx = x + 32 * scale;
var cy = y + 32 * scale;
if (layer === 'back') {
// Back of afro
ctx.fillStyle = hairColor;
ctx.beginPath();
ctx.arc(cx, cy - 5 * scale, 32 * scale, 0, Math.PI * 2);
ctx.fill();
return;
}
ctx.fillStyle = hairColor;
// Main afro shape
ctx.beginPath();
ctx.arc(cx, cy - 10 * scale, 30 * scale, 0, Math.PI * 2);
ctx.fill();
// Texture with small circles
ctx.save();
ctx.globalAlpha = 0.3;
ctx.fillStyle = adjustBrightness(hairColor, -20);
for (var i = 0; i < 20; i++) {
var angle = Math.random() * Math.PI * 2;
var dist = Math.random() * 20 * scale;
var tx = cx + Math.cos(angle) * dist;
var ty = cy - 10 * scale + Math.sin(angle) * dist;
ctx.beginPath();
ctx.arc(tx, ty, 4 * scale, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
// Cutout for face
ctx.save();
ctx.globalCompositeOperation = 'destination-out';
drawEllipse(ctx, cx, cy + 5 * scale, 18 * scale, 22 * scale);
ctx.fill();
ctx.restore();
addHairShine(ctx, cx - 10 * scale, cy - 25 * scale, 20 * scale, 12 * scale, hairColor);
}
/**
* Buzz cut hair style
*/
function drawBuzzHair(ctx, x, y, scale, hairColor, layer) {
var cx = x + 32 * scale;
var cy = y + 32 * scale;
if (layer === 'back') return;
ctx.save();
ctx.globalAlpha = 0.6;
ctx.fillStyle = hairColor;
// Very short hair - just a cap
ctx.beginPath();
ctx.arc(cx, cy - 8 * scale, 22 * scale, Math.PI, 0);
ctx.fill();
// Texture dots
ctx.globalAlpha = 0.4;
for (var i = 0; i < 40; i++) {
var angle = Math.PI + Math.random() * Math.PI;
var dist = 10 + Math.random() * 10;
var bx = cx + Math.cos(angle) * dist * scale;
var by = cy - 8 * scale + Math.sin(angle) * dist * scale;
if (by < cy - 2 * scale) {
ctx.beginPath();
ctx.arc(bx, by, 0.8 * scale, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
/**
* Pigtails hair style
*/
function drawPigtailsHair(ctx, x, y, scale, hairColor, layer) {
var cx = x + 32 * scale;
var cy = y + 32 * scale;
ctx.fillStyle = hairColor;
if (layer === 'back') {
// Back portions of pigtails
// Left pigtail
ctx.beginPath();
ctx.moveTo(cx - 22 * scale, cy - 5 * scale);
ctx.quadraticCurveTo(cx - 35 * scale, cy + 10 * scale, cx - 30 * scale, cy + 40 * scale);
ctx.quadraticCurveTo(cx - 25 * scale, cy + 50 * scale, cx - 20 * scale, cy + 40 * scale);
ctx.quadraticCurveTo(cx - 18 * scale, cy + 15 * scale, cx - 20 * scale, cy - 5 * scale);
ctx.fill();
// Right pigtail
ctx.beginPath();
ctx.moveTo(cx + 22 * scale, cy - 5 * scale);
ctx.quadraticCurveTo(cx + 35 * scale, cy + 10 * scale, cx + 30 * scale, cy + 40 * scale);
ctx.quadraticCurveTo(cx + 25 * scale, cy + 50 * scale, cx + 20 * scale, cy + 40 * scale);
ctx.quadraticCurveTo(cx + 18 * scale, cy + 15 * scale, cx + 20 * scale, cy - 5 * scale);
ctx.fill();
return;
}
// Front hair cap
ctx.beginPath();
ctx.moveTo(cx - 20 * scale, cy - 5 * scale);
ctx.quadraticCurveTo(cx - 22 * scale, cy - 18 * scale, cx - 10 * scale, cy - 22 * scale);
ctx.quadraticCurveTo(cx, cy - 26 * scale, cx + 10 * scale, cy - 22 * scale);
ctx.quadraticCurveTo(cx + 22 * scale, cy - 18 * scale, cx + 20 * scale, cy - 5 * scale);
ctx.quadraticCurveTo(cx + 10 * scale, cy - 12 * scale, cx, cy - 14 * scale);
ctx.quadraticCurveTo(cx - 10 * scale, cy - 12 * scale, cx - 20 * scale, cy - 5 * scale);
ctx.fill();
// Hair ties
ctx.fillStyle = '#FF69B4';
ctx.beginPath();
ctx.arc(cx - 24 * scale, cy, 3 * scale, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(cx + 24 * scale, cy, 3 * scale, 0, Math.PI * 2);
ctx.fill();
addHairShine(ctx, cx - 5 * scale, cy - 20 * scale, 12 * scale, 6 * scale, hairColor);
}
/**
* Bob hair style
*/
function drawBobHair(ctx, x, y, scale, hairColor, layer) {
var cx = x + 32 * scale;
var cy = y + 32 * scale;
if (layer === 'back') {
ctx.fillStyle = hairColor;
ctx.beginPath();
ctx.moveTo(cx - 24 * scale, cy - 15 * scale);
ctx.lineTo(cx - 26 * scale, cy + 15 * scale);
ctx.quadraticCurveTo(cx, cy + 20 * scale, cx + 26 * scale, cy + 15 * scale);
ctx.lineTo(cx + 24 * scale, cy - 15 * scale);
ctx.quadraticCurveTo(cx, cy - 10 * scale, cx - 24 * scale, cy - 15 * scale);
ctx.fill();
return;
}
ctx.fillStyle = hairColor;
// Front cap
ctx.beginPath();
ctx.moveTo(cx - 22 * scale, cy);
ctx.quadraticCurveTo(cx - 24 * scale, cy - 20 * scale, cx - 10 * scale, cy - 24 * scale);
ctx.quadraticCurveTo(cx, cy - 28 * scale, cx + 10 * scale, cy - 24 * scale);
ctx.quadraticCurveTo(cx + 24 * scale, cy - 20 * scale, cx + 22 * scale, cy);
ctx.lineTo(cx + 18 * scale, cy - 8 * scale);
ctx.quadraticCurveTo(cx, cy - 16 * scale, cx - 18 * scale, cy - 8 * scale);
ctx.closePath();
ctx.fill();
// Side flips
ctx.beginPath();
ctx.moveTo(cx - 22 * scale, cy);
ctx.quadraticCurveTo(cx - 26 * scale, cy + 10 * scale, cx - 20 * scale, cy + 18 * scale);
ctx.quadraticCurveTo(cx - 18 * scale, cy + 15 * scale, cx - 18 * scale, cy);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(cx + 22 * scale, cy);
ctx.quadraticCurveTo(cx + 26 * scale, cy + 10 * scale, cx + 20 * scale, cy + 18 * scale);
ctx.quadraticCurveTo(cx + 18 * scale, cy + 15 * scale, cx + 18 * scale, cy);
ctx.closePath();
ctx.fill();
addHairShine(ctx, cx - 8 * scale, cy - 20 * scale, 18 * scale, 8 * scale, hairColor);
}
/**
* Spiky hair style
*/
function drawSpikyHair(ctx, x, y, scale, hairColor, layer) {
var cx = x + 32 * scale;
var cy = y + 32 * scale;
if (layer === 'back') return;
ctx.fillStyle = hairColor;
// Base
ctx.beginPath();
ctx.arc(cx, cy - 12 * scale, 20 * scale, Math.PI, 0);
ctx.fill();
// Spikes
var spikes = [
{ x: -18, y: -12, dx: -8, dy: -20 },
{ x: -12, y: -18, dx: -4, dy: -28 },
{ x: -5, y: -20, dx: 0, dy: -32 },
{ x: 5, y: -20, dx: 5, dy: -30 },
{ x: 12, y: -18, dx: 12, dy: -26 },
{ x: 18, y: -12, dx: 20, dy: -18 },
{ x: -15, y: -8, dx: -22, dy: -12 },
{ x: 15, y: -8, dx: 24, dy: -10 }
];
spikes.forEach(function(spike) {
ctx.beginPath();
ctx.moveTo(cx + spike.x * scale, cy + spike.y * scale);
ctx.lineTo(cx + spike.dx * scale, cy + spike.dy * scale);
ctx.lineTo(cx + (spike.x + 4) * scale, cy + spike.y * scale);
ctx.fill();
});
addHairShine(ctx, cx - 3 * scale, cy - 20 * scale, 10 * scale, 6 * scale, hairColor);
}
/**
* Render back hair layer
*/
function renderHairBack(ctx, avatar, x, y, scale) {
renderHair(ctx, avatar, x, y, scale, 'back');
}
/**
* Render front hair layer
*/
function renderHairFront(ctx, avatar, x, y, scale) {
renderHair(ctx, avatar, x, y, scale, 'front');
}
// ============================================
// BODY RENDERING
// ============================================
/**
* Render neck
*/
function renderNeck(ctx, avatar, x, y, scale) {
var skinTone = avatar.base.skinTone || '#FFD5B8';
var cx = x + 32 * scale;
var neckTop = y + 56 * scale;
ctx.fillStyle = skinTone;
roundRect(ctx, cx - 8 * scale, neckTop, 16 * scale, 12 * scale, 2 * scale);
ctx.fill();
// Neck shadow
ctx.fillStyle = skinShade(skinTone, -15);
ctx.beginPath();
ctx.moveTo(cx - 6 * scale, neckTop);
ctx.lineTo(cx + 6 * scale, neckTop);
ctx.lineTo(cx + 4 * scale, neckTop + 4 * scale);
ctx.lineTo(cx - 4 * scale, neckTop + 4 * scale);
ctx.closePath();
ctx.fill();
}
/**
* Render shoulders
*/
function renderShoulders(ctx, avatar, x, y, scale, bodyType, clothing) {
var cx = x + 32 * scale;
var shoulderY = y + 64 * scale;
var clothingColor = clothing ? clothing.color : '#3366CC';
var clothingStyle = clothing ? clothing.style : 'casual';
var shoulderWidth = 48 * scale;
if (bodyType && bodyType.id === 'large') shoulderWidth = 56 * scale;
if (bodyType && bodyType.id === 'slim') shoulderWidth = 42 * scale;
ctx.fillStyle = clothingColor;
roundRect(ctx, cx - shoulderWidth / 2, shoulderY, shoulderWidth, 20 * scale, { tl: 8, tr: 8, bl: 2, br: 2 });
ctx.fill();
// Add style details
renderClothingDetails(ctx, clothingStyle, clothingColor, cx - shoulderWidth / 2, shoulderY, shoulderWidth, 20 * scale, scale);
}
/**
* Render torso
*/
function renderTorso(ctx, avatar, x, y, scale, bodyType, clothing, transforms) {
var cx = x + 32 * scale;
var torsoY = y + 84 * scale;
var clothingColor = clothing ? clothing.color : '#3366CC';
var clothingStyle = clothing ? clothing.style : 'casual';
var torsoWidth = 44 * scale;
var torsoHeight = 40 * scale;
if (bodyType && bodyType.id === 'large') {
torsoWidth = 52 * scale;
torsoHeight = 44 * scale;
}
if (bodyType && bodyType.id === 'slim') {
torsoWidth = 38 * scale;
torsoHeight = 38 * scale;
}
ctx.save();
if (transforms && transforms.torso) {
ctx.translate(cx, torsoY);
applyTransforms(ctx, transforms, 'torso');
ctx.translate(-cx, -torsoY);
}
ctx.fillStyle = clothingColor;
roundRect(ctx, cx - torsoWidth / 2, torsoY, torsoWidth, torsoHeight, 4 * scale);
ctx.fill();
// Clothing details
renderClothingDetails(ctx, clothingStyle, clothingColor, cx - torsoWidth / 2, torsoY, torsoWidth, torsoHeight, scale);
ctx.restore();
}
/**
* Render arms
*/
function renderArms(ctx, avatar, x, y, scale, bodyType, clothing, transforms) {
var skinTone = avatar.base.skinTone || '#FFD5B8';
var cx = x + 32 * scale;
var armY = y + 68 * scale;
var clothingColor = clothing ? clothing.color : '#3366CC';
var armWidth = 10 * scale;
var armLength = 45 * scale;
var armOffset = 26 * scale;
if (bodyType && bodyType.id === 'large') {
armWidth = 12 * scale;
armOffset = 30 * scale;
}
if (bodyType && bodyType.id === 'slim') {
armWidth = 8 * scale;
armOffset = 22 * scale;
}
// Left arm
ctx.save();
if (transforms && transforms.leftArm) {
ctx.translate(cx - armOffset, armY);
applyTransforms(ctx, transforms, 'leftArm');
ctx.translate(-(cx - armOffset), -armY);
}
// Sleeve
ctx.fillStyle = clothingColor;
roundRect(ctx, cx - armOffset - armWidth / 2, armY, armWidth, armLength * 0.5, 3 * scale);
ctx.fill();
// Skin part
ctx.fillStyle = skinTone;
roundRect(ctx, cx - armOffset - armWidth / 2 + 1 * scale, armY + armLength * 0.45, armWidth - 2 * scale, armLength * 0.55, 3 * scale);
ctx.fill();
// Hand
ctx.beginPath();
ctx.arc(cx - armOffset, armY + armLength, armWidth / 2 + 1 * scale, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Right arm
ctx.save();
if (transforms && transforms.rightArm) {
ctx.translate(cx + armOffset, armY);
applyTransforms(ctx, transforms, 'rightArm');
ctx.translate(-(cx + armOffset), -armY);
}
// Sleeve
ctx.fillStyle = clothingColor;
roundRect(ctx, cx + armOffset - armWidth / 2, armY, armWidth, armLength * 0.5, 3 * scale);
ctx.fill();
// Skin part
ctx.fillStyle = skinTone;
roundRect(ctx, cx + armOffset - armWidth / 2 + 1 * scale, armY + armLength * 0.45, armWidth - 2 * scale, armLength * 0.55, 3 * scale);
ctx.fill();
// Hand
ctx.beginPath();
ctx.arc(cx + armOffset, armY + armLength, armWidth / 2 + 1 * scale, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
/**
* Render legs
*/
function renderLegs(ctx, avatar, x, y, scale, bodyType, clothing, transforms) {
var cx = x + 32 * scale;
var legY = y + 124 * scale;
var pantsColor = clothing && clothing.pantsColor ? clothing.pantsColor : '#2D2D2D';
var shoeColor = clothing && clothing.shoeColor ? clothing.shoeColor : '#4A4A4A';
var legWidth = 12 * scale;
var legLength = 32 * scale;
var legSpacing = 6 * scale;
if (bodyType && bodyType.id === 'large') {
legWidth = 14 * scale;
}
if (bodyType && bodyType.id === 'slim') {
legWidth = 10 * scale;
}
// Left leg
ctx.save();
if (transforms && transforms.leftLeg) {
ctx.translate(cx - legSpacing - legWidth / 2, legY);
applyTransforms(ctx, transforms, 'leftLeg');
ctx.translate(-(cx - legSpacing - legWidth / 2), -legY);
}
ctx.fillStyle = pantsColor;
roundRect(ctx, cx - legSpacing - legWidth, legY, legWidth, legLength, 3 * scale);
ctx.fill();
// Left shoe
ctx.fillStyle = shoeColor;
roundRect(ctx, cx - legSpacing - legWidth - 2 * scale, legY + legLength - 4 * scale, legWidth + 4 * scale, 8 * scale, 2 * scale);
ctx.fill();
ctx.restore();
// Right leg
ctx.save();
if (transforms && transforms.rightLeg) {
ctx.translate(cx + legSpacing + legWidth / 2, legY);
applyTransforms(ctx, transforms, 'rightLeg');
ctx.translate(-(cx + legSpacing + legWidth / 2), -legY);
}
ctx.fillStyle = pantsColor;
roundRect(ctx, cx + legSpacing, legY, legWidth, legLength, 3 * scale);
ctx.fill();
// Right shoe
ctx.fillStyle = shoeColor;
roundRect(ctx, cx + legSpacing - 2 * scale, legY + legLength - 4 * scale, legWidth + 4 * scale, 8 * scale, 2 * scale);
ctx.fill();
ctx.restore();
}
/**
* Render clothing details based on style
*/
function renderClothingDetails(ctx, style, baseColor, x, y, w, h, scale) {
var styleData = window.AvatarData ? window.AvatarData.getClothingStyle(style) : null;
var styleId = styleData ? styleData.id : style || 'casual';
switch (styleId) {
case 'sporty':
// Sport stripes
ctx.fillStyle = '#FFFFFF';
ctx.globalAlpha = 0.7;
roundRect(ctx, x + 2 * scale, y + h * 0.3, 3 * scale, h * 0.5, 1 * scale);
ctx.fill();
roundRect(ctx, x + w - 5 * scale, y + h * 0.3, 3 * scale, h * 0.5, 1 * scale);
ctx.fill();
ctx.globalAlpha = 1;
break;
case 'formal':
// Collar
ctx.fillStyle = '#FFFFFF';
var collarY = y - 2 * scale;
ctx.beginPath();
ctx.moveTo(x + w / 2 - 8 * scale, collarY);
ctx.lineTo(x + w / 2 - 12 * scale, collarY + 10 * scale);
ctx.lineTo(x + w / 2 - 4 * scale, collarY + 8 * scale);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(x + w / 2 + 8 * scale, collarY);
ctx.lineTo(x + w / 2 + 12 * scale, collarY + 10 * scale);
ctx.lineTo(x + w / 2 + 4 * scale, collarY + 8 * scale);
ctx.closePath();
ctx.fill();
// Buttons
ctx.fillStyle = '#DDDDDD';
for (var i = 0; i < 3; i++) {
ctx.beginPath();
ctx.arc(x + w / 2, y + 12 * scale + i * 10 * scale, 2 * scale, 0, Math.PI * 2);
ctx.fill();
}
break;
case 'scifi':
// Glowing trim lines
ctx.save();
ctx.strokeStyle = '#00FFFF';
ctx.lineWidth = 1.5 * scale;
ctx.shadowColor = '#00FFFF';
ctx.shadowBlur = 5 * scale;
// Horizontal line
ctx.beginPath();
ctx.moveTo(x + 4 * scale, y + h * 0.4);
ctx.lineTo(x + w - 4 * scale, y + h * 0.4);
ctx.stroke();
// Diagonal accents
ctx.beginPath();
ctx.moveTo(x + 4 * scale, y + h * 0.6);
ctx.lineTo(x + w * 0.3, y + h * 0.8);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + w - 4 * scale, y + h * 0.6);
ctx.lineTo(x + w * 0.7, y + h * 0.8);
ctx.stroke();
ctx.restore();
break;
case 'casual':
default:
// Simple neckline
ctx.strokeStyle = adjustBrightness(baseColor, -30);
ctx.lineWidth = 1 * scale;
ctx.beginPath();
ctx.arc(x + w / 2, y - 2 * scale, 8 * scale, 0.1 * Math.PI, 0.9 * Math.PI);
ctx.stroke();
break;
}
}
// ============================================
// HEAD COMPOSITE RENDERING
// ============================================
/**
* Render complete head
*/
function renderHead(ctx, avatar, x, y, scale, transforms) {
// Ears (behind head)
renderEars(ctx, avatar, x, y, scale, transforms);
// Face base
renderFaceBase(ctx, avatar, x, y, scale);
// Face features
renderEyes(ctx, avatar, x, y, scale, transforms);
renderEyebrows(ctx, avatar, x, y, scale);
renderNose(ctx, avatar, x, y, scale);
renderMouth(ctx, avatar, x, y, scale);
// Facial hair
if (avatar.base && avatar.base.facialHair && avatar.base.facialHair !== 'none') {
renderFacialHair(ctx, avatar, x, y, scale);
}
}
// ============================================
// BODY COMPOSITE RENDERING
// ============================================
/**
* Render body based on mode
*/
function renderBody(ctx, avatar, x, y, scale, mode, transforms) {
var bodyType = window.AvatarData ? window.AvatarData.getBodyType(avatar.base.body ? avatar.base.body.type : 'average') : null;
var clothing = avatar.base.clothing || {};
if (mode === 'HEAD_AND_SHOULDERS' || mode === 'UPPER_BODY' || mode === 'FULL_BODY') {
renderNeck(ctx, avatar, x, y, scale);
renderShoulders(ctx, avatar, x, y, scale, bodyType, clothing);
}
if (mode === 'UPPER_BODY' || mode === 'FULL_BODY') {
renderTorso(ctx, avatar, x, y, scale, bodyType, clothing, transforms);
renderArms(ctx, avatar, x, y, scale, bodyType, clothing, transforms);
}
if (mode === 'FULL_BODY') {
renderLegs(ctx, avatar, x, y, scale, bodyType, clothing, transforms);
}
}
// ============================================
// TRANSFORM HELPERS
// ============================================
/**
* Apply animation transforms to context
*/
function applyTransforms(ctx, transforms, partName) {
if (!transforms || !transforms[partName]) return;
var t = transforms[partName];
if (t.x || t.y) {
ctx.translate(t.x || 0, t.y || 0);
}
if (t.rotation) {
ctx.rotate(t.rotation * Math.PI / 180);
}
if (t.scaleX !== undefined || t.scaleY !== undefined) {
ctx.scale(t.scaleX !== undefined ? t.scaleX : 1, t.scaleY !== undefined ? t.scaleY : 1);
}
}
// ============================================
// MAIN RENDER FUNCTION
// ============================================
/**
* Main render function
*/
function render(ctx, avatar, mode, x, y, options) {
options = options || {};
var scale = options.scale || 1;
var transforms = options.transforms || {};
var modeData = MODES[mode] || MODES.HEAD_ONLY;
// Validate avatar data
if (!avatar || !avatar.base) {
console.warn('AvatarRenderer: Invalid avatar data');
return;
}
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
// 1. Back hair layer (for long hair behind body)
if (mode !== 'HEAD_ONLY') {
renderHairBack(ctx, avatar, 0, 0, 1);
}
// 2. Body (if applicable)
if (mode !== 'HEAD_ONLY') {
renderBody(ctx, avatar, 0, 0, 1, mode, transforms);
}
// 3. Head
ctx.save();
applyTransforms(ctx, transforms, 'head');
renderHead(ctx, avatar, 0, 0, 1, transforms);
ctx.restore();
// 4. Front hair layer
renderHairFront(ctx, avatar, 0, 0, 1);
ctx.restore();
}
// ============================================
// PUBLIC API
// ============================================
window.AvatarRenderer = {
MODES: MODES,
/**
* Render an avatar to a canvas context
*/
render: render,
/**
* Render just the head
*/
renderHead: function(ctx, avatar, x, y, scale) {
ctx.save();
ctx.translate(x, y);
ctx.scale(scale || 1, scale || 1);
renderHead(ctx, avatar, 0, 0, 1, {});
renderHairFront(ctx, avatar, 0, 0, 1);
ctx.restore();
},
/**
* Render just the body
*/
renderBody: function(ctx, avatar, x, y, scale, mode) {
ctx.save();
ctx.translate(x, y);
ctx.scale(scale || 1, scale || 1);
renderBody(ctx, avatar, 0, 0, 1, mode || 'FULL_BODY', {});
ctx.restore();
},
/**
* Get dimensions for a rendering mode
*/
getModeSize: function(mode) {
return MODES[mode] || MODES.HEAD_ONLY;
},
/**
* Create an offscreen canvas with rendered avatar (for caching)
*/
createSprite: function(avatar, mode, scale) {
scale = scale || 1;
var size = MODES[mode] || MODES.HEAD_ONLY;
var canvas = document.createElement('canvas');
canvas.width = size.width * scale;
canvas.height = size.height * scale;
var ctx = canvas.getContext('2d');
this.render(ctx, avatar, mode, 0, 0, { scale: scale });
return canvas;
},
/**
* Batch render multiple avatars for performance
*/
renderBatch: function(ctx, avatars, mode, positions) {
if (!avatars || !positions || avatars.length !== positions.length) {
console.warn('AvatarRenderer.renderBatch: Invalid parameters');
return;
}
var canvasWidth = ctx.canvas.width;
var canvasHeight = ctx.canvas.height;
var modeSize = MODES[mode] || MODES.HEAD_ONLY;
for (var i = 0; i < avatars.length; i++) {
var pos = positions[i];
// Skip off-screen avatars
if (pos.x + modeSize.width < 0 || pos.x > canvasWidth ||
pos.y + modeSize.height < 0 || pos.y > canvasHeight) {
continue;
}
this.render(ctx, avatars[i], mode, pos.x, pos.y, pos.options || {});
}
},
/**
* Pre-render avatars to sprite cache for performance
*/
createSpriteCache: function(avatars, mode, scale) {
var cache = {};
scale = scale || 1;
for (var i = 0; i < avatars.length; i++) {
var avatar = avatars[i];
if (avatar && avatar.id) {
cache[avatar.id] = this.createSprite(avatar, mode, scale);
}
}
return cache;
},
/**
* Render from cached sprite
*/
renderFromCache: function(ctx, cache, avatarId, x, y) {
var sprite = cache[avatarId];
if (sprite) {
ctx.drawImage(sprite, x, y);
return true;
}
return false;
},
// Expose helper functions for custom rendering
helpers: {
roundRect: roundRect,
drawCurve: drawCurve,
drawEllipse: drawEllipse,
gradientFill: gradientFill,
skinShade: skinShade,
adjustBrightness: adjustBrightness,
addHairShine: addHairShine,
applyTransforms: applyTransforms
}
};
})();