/** * 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 } }; })();