New avatar.

This commit is contained in:
retoor 2025-05-20 04:20:10 +02:00
parent 59a815f85a
commit 87b6b3362d
4 changed files with 1780 additions and 0 deletions

File diff suppressed because one or more lines are too long

View File

@ -5,11 +5,86 @@ import { app } from "/app.js";
const STAR_COUNT = 200;
const body = document.body;
function getStarPosition(star) {
const leftPercent = parseFloat(star.style.left);
const topPercent = parseFloat(star.style.top);
let position;
if (topPercent < 40 && leftPercent >= 40 && leftPercent <= 60) {
position = 'North';
} else if (topPercent > 60 && leftPercent >= 40 && leftPercent <= 60) {
position = 'South';
} else if (leftPercent < 40 && topPercent >= 40 && topPercent <= 60) {
position = 'West';
} else if (leftPercent > 60 && topPercent >= 40 && topPercent <= 60) {
position = 'East';
} else if (topPercent >= 40 && topPercent <= 60 && leftPercent >= 40 && leftPercent <= 60) {
position = 'Center';
} else {
position = 'Corner or Edge';
}
return position
}
let stars = {}
window.stars = stars
function createStar() {
const star = document.createElement('div');
star.classList.add('star');
star.style.left = `${Math.random() * 100}%`;
star.style.top = `${Math.random() * 100}%`;
star.shuffle = () => {
star.style.left = `${Math.random() * 100}%`;
star.style.top = `${Math.random() * 100}%`;
star.position = getStarPosition(star)
}
star.position = getStarPosition(star)
function moveStarToPosition(star, position) {
let top, left;
switch (position) {
case 'North':
top = `${Math.random() * 20}%`;
left = `${40 + Math.random() * 20}%`;
break;
case 'South':
top = `${80 + Math.random() * 10}%`;
left = `${40 + Math.random() * 20}%`;
break;
case 'West':
top = `${40 + Math.random() * 20}%`;
left = `${Math.random() * 20}%`;
break;
case 'East':
top = `${40 + Math.random() * 20}%`;
left = `${80 + Math.random() * 10}%`;
break;
case 'Center':
top = `${45 + Math.random() * 10}%`;
left = `${45 + Math.random() * 10}%`;
break;
default: // 'Corner or Edge' fallback
top = `${Math.random() * 100}%`;
left = `${Math.random() * 100}%`;
break;
}
star.style.top = top;
star.style.left = left;
star.position = getStarPosition(star)
}
if(!stars[star.position])
stars[star.position] = []
stars[star.position].push(star)
const size = Math.random() * 2 + 1;
star.style.width = `${size}px`;
star.style.height = `${size}px`;
@ -53,6 +128,320 @@ app.updateStarColor = updateStarColorDelayed;
app.ws.addEventListener("set_typing", (data) => {
updateStarColorDelayed(data.data.color);
});
window.createAvatar = () => {
let avatar = document.createElement("avatar-face")
document.querySelector("main").appendChild(avatar)
return avatar
}
class AvatarFace extends HTMLElement {
static get observedAttributes(){
return ['emotion','face-color','eye-color','text','balloon-color','text-color'];
}
constructor(){
super();
this._shadow = this.attachShadow({mode:'open'});
this._shadow.innerHTML = `
<style>
:host { display:block; position:relative; }
canvas { width:100%; height:100%; display:block; }
</style>
<canvas></canvas>
`;
this._c = this._shadow.querySelector('canvas');
this._ctx = this._c.getContext('2d');
// state
this._mouse = {x:0,y:0};
this._blinkTimer = 0;
this._blinking = false;
this._lastTime = 0;
// defaults
this._emotion = 'neutral';
this._faceColor = '#ffdfba';
this._eyeColor = '#000';
this._text = '';
this._balloonColor = '#fff';
this._textColor = '#000';
}
attributeChangedCallback(name,_old,newV){
if (name==='emotion') this._emotion = newV||'neutral';
else if (name==='face-color') this._faceColor = newV||'#ffdfba';
else if (name==='eye-color') this._eyeColor = newV||'#000';
else if (name==='text') this._text = newV||'';
else if (name==='balloon-color')this._balloonColor = newV||'#fff';
else if (name==='text-color') this._textColor = newV||'#000';
}
connectedCallback(){
// watch size so canvas buffer matches display
this._ro = new ResizeObserver(entries=>{
for(const ent of entries){
const w = ent.contentRect.width;
const h = ent.contentRect.height;
const dpr = devicePixelRatio||1;
this._c.width = w*dpr;
this._c.height = h*dpr;
this._ctx.scale(dpr,dpr);
}
});
this._ro.observe(this);
// track mouse so eyes follow
this._shadow.addEventListener('mousemove', e=>{
const r = this._c.getBoundingClientRect();
this._mouse.x = e.clientX - r.left;
this._mouse.y = e.clientY - r.top;
});
this._lastTime = performance.now();
this._raf = requestAnimationFrame(t=>this._loop(t));
}
disconnectedCallback(){
cancelAnimationFrame(this._raf);
this._ro.disconnect();
}
_updateBlink(dt){
this._blinkTimer -= dt;
if (this._blinkTimer<=0){
this._blinking = !this._blinking;
this._blinkTimer = this._blinking
? 0.1
: 2 + Math.random()*3;
}
}
_roundRect(x,y,w,h,r){
const ctx = this._ctx;
ctx.beginPath();
ctx.moveTo(x+r,y);
ctx.lineTo(x+w-r,y);
ctx.quadraticCurveTo(x+w,y, x+w,y+r);
ctx.lineTo(x+w,y+h-r);
ctx.quadraticCurveTo(x+w,y+h, x+w-r,y+h);
ctx.lineTo(x+r,y+h);
ctx.quadraticCurveTo(x,y+h, x,y+h-r);
ctx.lineTo(x,y+r);
ctx.quadraticCurveTo(x,y, x+r,y);
ctx.closePath();
}
_draw(ts){
const ctx = this._ctx;
const W = this._c.clientWidth;
const H = this._c.clientHeight;
ctx.clearRect(0,0,W,H);
// HEAD + BOB
const cx = W/2;
const cy = H/2 + Math.sin(ts*0.002)*8;
const R = Math.min(W,H)*0.25;
// SPEECH BALLOON
if (this._text){
const pad = 6;
ctx.font = `${R*0.15}px sans-serif`;
const m = ctx.measureText(this._text);
const tw = m.width, th = R*0.18;
const bw = tw + pad*2, bh = th + pad*2;
const bx = cx - bw/2, by = cy - R - bh - 10;
// bubble
ctx.fillStyle = this._balloonColor;
this._roundRect(bx,by,bw,bh,6);
ctx.fill();
ctx.strokeStyle = '#888';
ctx.lineWidth = 1.2;
ctx.stroke();
// tail
ctx.beginPath();
ctx.moveTo(cx-6, by+bh);
ctx.lineTo(cx+6, by+bh);
ctx.lineTo(cx, cy-R+4);
ctx.closePath();
ctx.fill();
ctx.stroke();
// text
ctx.fillStyle = this._textColor;
ctx.textBaseline = 'top';
ctx.fillText(this._text, bx+pad, by+pad);
}
// FACE
ctx.fillStyle = this._faceColor;
ctx.beginPath();
ctx.arc(cx,cy,R,0,2*Math.PI);
ctx.fill();
// EYES
const eyeY = cy - R*0.2;
const eyeX = R*0.4;
const eyeR= R*0.12;
const pupR= eyeR*0.5;
for(let i=0;i<2;i++){
const ex = cx + (i? eyeX:-eyeX);
const ey = eyeY;
// eyeball
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(ex,ey,eyeR,0,2*Math.PI);
ctx.fill();
// pupil follows
let dx = this._mouse.x - ex;
let dy = this._mouse.y - ey;
const d = Math.hypot(dx,dy);
const max = eyeR - pupR - 2;
if (d>max){ dx=dx/d*max; dy=dy/d*max; }
if (this._blinking){
ctx.strokeStyle='#000';
ctx.lineWidth=3;
ctx.beginPath();
ctx.moveTo(ex-eyeR,ey);
ctx.lineTo(ex+eyeR,ey);
ctx.stroke();
} else {
ctx.fillStyle = this._eyeColor;
ctx.beginPath();
ctx.arc(ex+dx,ey+dy,pupR,0,2*Math.PI);
ctx.fill();
}
}
// ANGRY BROWS
if (this._emotion==='angry'){
ctx.strokeStyle='#000';
ctx.lineWidth=4;
[[-eyeX,1],[ eyeX,-1]].forEach(([off,dir])=>{
const sx = cx+off - eyeR;
const sy = eyeY - eyeR*1.3;
const ex = cx+off + eyeR;
const ey2= sy + dir*6;
ctx.beginPath();
ctx.moveTo(sx,sy);
ctx.lineTo(ex,ey2);
ctx.stroke();
});
}
// MOUTH by emotion
const mw = R*0.6;
const my = cy + R*0.25;
ctx.strokeStyle='#a33';
ctx.lineWidth=4;
if (this._emotion==='surprised'){
ctx.fillStyle='#a33';
ctx.beginPath();
ctx.arc(cx,my,mw*0.3,0,2*Math.PI);
ctx.fill();
}
else if (this._emotion==='sad'){
ctx.beginPath();
ctx.arc(cx,my,mw/2,1.15*Math.PI,1.85*Math.PI,true);
ctx.stroke();
}
else if (this._emotion==='angry'){
ctx.beginPath();
ctx.moveTo(cx-mw/2,my+2);
ctx.lineTo(cx+mw/2,my-2);
ctx.stroke();
}
else {
const s = this._emotion==='happy'? 0.15*Math.PI:0.2*Math.PI;
const e = this._emotion==='happy'? 0.85*Math.PI:0.8*Math.PI;
ctx.beginPath();
ctx.arc(cx,my,mw/2,s,e);
ctx.stroke();
}
}
_loop(ts){
const dt = (ts - this._lastTime)/1000;
this._lastTime = ts;
this._updateBlink(dt);
this._draw(ts);
this._raf = requestAnimationFrame(t=>this._loop(t));
}
}
customElements.define('avatar-face', AvatarFace);
class AvatarReplacer {
constructor(target, opts={}){
this.target = target;
// record original inline styles so we can restore
this._oldVis = target.style.visibility || '';
this._oldPos = target.style.position || '';
// hide the target
target.style.visibility = 'hidden';
// measure
const rect = target.getBoundingClientRect();
// create avatar
this.avatar = document.createElement('avatar-face');
// copy all supported opts into attributes
['emotion','faceColor','eyeColor','text','balloonColor','textColor']
.forEach(k => {
const attr = k.replace(/[A-Z]/g,m=>'-'+m.toLowerCase());
if (opts[k] != null) this.avatar.setAttribute(attr, opts[k]);
});
// position absolutely
const scrollX = window.pageXOffset;
const scrollY = window.pageYOffset;
Object.assign(this.avatar.style, {
position: 'absolute',
left: (rect.left + scrollX) + 'px',
top: (rect.top + scrollY) + 'px',
width: rect.width + 'px',
height: rect.height + 'px',
zIndex: 9999
});
document.body.appendChild(this.avatar);
}
detach(){
// remove avatar and restore target
if (this.avatar && this.avatar.parentNode) {
this.avatar.parentNode.removeChild(this.avatar);
this.avatar = null;
}
this.target.style.visibility = this._oldVis;
this.target.style.position = this._oldPos;
}
// static convenience method
static attach(target, opts){
return new AvatarReplacer(target, opts);
}
}
/*
// DEMO wiring
const btnGo = document.getElementById('go');
const btnReset = document.getElementById('reset');
let repl1, repl2;
btnGo.addEventListener('click', ()=>{
// replace #one with a happy avatar saying "Hi!"
repl1 = AvatarReplacer.attach(
document.getElementById('one'),
{emotion:'happy', text:'Hi!', balloonColor:'#fffbdd', textColor:'#333'}
);
// replace #two with a surprised avatar
repl2 = AvatarReplacer.attach(
document.getElementById('two'),
{emotion:'surprised', faceColor:'#eeffcc', text:'Wow!', balloonColor:'#ddffdd'}
);
});
btnReset.addEventListener('click', ()=>{
if (repl1) repl1.detach();
if (repl2) repl2.detach();
});
*/
/*
class StarField {

View File

@ -29,12 +29,35 @@ from aiohttp import web
from multiavatar import multiavatar
from snek.system.view import BaseView
from snek.view.avatar_animal import generate_avatar_with_options
import functools
class AvatarView(BaseView):
login_required = False
def __init__(self, *args,**kwargs):
super().__init__(*args,**kwargs)
self.avatars = {}
async def get(self):
uid = self.request.match_info.get("uid")
while True:
try:
return web.Response(text=self._get(uid), content_type="image/svg+xml")
except Exception as e:
pass
def _get(self, uid):
if uid in self.avatars:
return self.avatars[uid]
avatar = generate_avatar_with_options(self.request.query)
self.avatars[uid] = avatar
return avatar
async def get2(self):
uid = self.request.match_info.get("uid")
if uid == "unique":
uid = str(uuid.uuid4())

View File

@ -0,0 +1,871 @@
import random
import math
import argparse
import json
from typing import Dict, List, Tuple, Optional, Union
class AnimalAvatarGenerator:
"""A generator for animal-themed avatar SVGs."""
# Constants
ANIMALS = [
"cat", "dog", "fox", "wolf", "bear", "panda", "koala", "tiger", "lion",
"rabbit", "monkey", "elephant", "giraffe", "zebra", "penguin", "owl",
"deer", "raccoon", "squirrel", "hedgehog", "otter", "frog"
]
COLOR_PALETTES = {
"natural": {
"cat": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000", "#808080", "#FFFFFF", "#FFA500"],
"dog": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000", "#808080", "#FFFFFF", "#FFA500"],
"fox": ["#FF6600", "#FF7F00", "#FF8C00", "#FFA500", "#FFFFFF", "#000000"],
"wolf": ["#808080", "#A9A9A9", "#D3D3D3", "#FFFFFF", "#000000", "#696969"],
"bear": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000", "#FFFFFF"],
"panda": ["#000000", "#FFFFFF"],
"koala": ["#808080", "#A9A9A9", "#D3D3D3", "#FFFFFF", "#000000"],
"tiger": ["#FF8C00", "#FF7F00", "#FFFFFF", "#000000"],
"lion": ["#DAA520", "#B8860B", "#CD853F", "#D2B48C", "#FFFFFF", "#000000"],
"rabbit": ["#FFFFFF", "#F5F5F5", "#D3D3D3", "#A9A9A9", "#FFC0CB", "#000000"],
"monkey": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000"],
"elephant": ["#808080", "#A9A9A9", "#D3D3D3", "#FFFFFF", "#000000"],
"giraffe": ["#DAA520", "#B8860B", "#F5DEB3", "#FFFFFF", "#000000"],
"zebra": ["#000000", "#FFFFFF"],
"penguin": ["#000000", "#FFFFFF", "#FFA500"],
"owl": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000", "#FFFFFF", "#FFC0CB"],
"deer": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#FFFFFF", "#000000"],
"raccoon": ["#808080", "#A9A9A9", "#D3D3D3", "#FFFFFF", "#000000"],
"squirrel": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000"],
"hedgehog": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000"],
"otter": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000"],
"frog": ["#008000", "#00FF00", "#ADFF2F", "#7FFF00", "#000000", "#FFFFFF"]
},
"pastel": {
"all": ["#FFB6C1", "#FFD700", "#FFDAB9", "#98FB98", "#ADD8E6", "#DDA0DD", "#F0E68C", "#FFFFE0"]
},
"vibrant": {
"all": ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#FFA500", "#FF1493"]
},
"mono": {
"all": ["#000000", "#333333", "#666666", "#999999", "#CCCCCC", "#FFFFFF"]
}
}
EYE_STYLES = ["round", "oval", "almond", "wide", "narrow", "cute"]
FACE_SHAPES = ["round", "oval", "square", "heart", "triangular", "diamond"]
EAR_STYLES = {
"cat": ["pointed", "folded", "round", "large", "small"],
"dog": ["floppy", "pointed", "round", "large", "small"],
"fox": ["pointed", "large", "small"],
"wolf": ["pointed", "large", "small"],
"bear": ["round", "small"],
"panda": ["round", "small"],
"koala": ["round", "large"],
"tiger": ["round", "small"],
"lion": ["round", "small"],
"rabbit": ["long", "floppy", "standing"],
"monkey": ["round", "small"],
"elephant": ["large", "wide"],
"giraffe": ["small", "pointed"],
"zebra": ["pointed", "small"],
"penguin": ["none"],
"owl": ["none", "tufted"],
"deer": ["small", "pointed"],
"raccoon": ["round", "small"],
"squirrel": ["pointed", "small"],
"hedgehog": ["round", "small"],
"otter": ["round", "small"],
"frog": ["none"]
}
NOSE_STYLES = ["round", "triangular", "small", "large", "heart", "button"]
SPECIAL_FEATURES = {
"cat": ["whiskers", "stripes", "spots"],
"dog": ["spots", "patch", "whiskers"],
"fox": ["mask", "whiskers", "brush_tail"],
"wolf": ["mask", "whiskers", "brush_tail"],
"bear": ["none", "patch"],
"panda": ["eye_patches", "none"],
"koala": ["none", "nose_patch"],
"tiger": ["stripes", "none"],
"lion": ["mane", "none"],
"rabbit": ["whiskers", "nose_patch"],
"monkey": ["none", "cheek_patches"],
"elephant": ["tusks", "none"],
"giraffe": ["spots", "none"],
"zebra": ["stripes", "none"],
"penguin": ["bib", "none"],
"owl": ["feather_tufts", "none"],
"deer": ["antlers", "spots", "none"],
"raccoon": ["mask", "whiskers", "none"],
"squirrel": ["bushy_tail", "none"],
"hedgehog": ["spikes", "none"],
"otter": ["whiskers", "none"],
"frog": ["spots", "none"]
}
EXPRESSIONS = ["happy", "serious", "surprised", "sleepy", "wink"]
def __init__(self, seed: Optional[int] = None):
"""Initialize the avatar generator with an optional seed for reproducibility."""
if seed is not None:
random.seed(seed)
def _get_colors(self, animal: str, color_palette: str) -> List[str]:
"""Get colors for the given animal and palette."""
if color_palette in self.COLOR_PALETTES:
if animal in self.COLOR_PALETTES[color_palette]:
return self.COLOR_PALETTES[color_palette][animal]
elif "all" in self.COLOR_PALETTES[color_palette]:
return self.COLOR_PALETTES[color_palette]["all"]
# Default to natural palette for the animal or general natural colors
if animal in self.COLOR_PALETTES["natural"]:
return self.COLOR_PALETTES["natural"][animal]
# If no specific colors found, use a mix of browns and grays
return ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#808080", "#A9A9A9", "#FFFFFF"]
def _get_ear_style(self, animal: str) -> str:
"""Get a random ear style appropriate for the animal."""
if animal in self.EAR_STYLES:
return random.choice(self.EAR_STYLES[animal])
return "none" # Default for animals not in the list
def _get_special_feature(self, animal: str) -> str:
"""Get a random special feature appropriate for the animal."""
if animal in self.SPECIAL_FEATURES:
return random.choice(self.SPECIAL_FEATURES[animal])
return "none" # Default for animals not in the list
def _draw_circle(self, cx: float, cy: float, r: float, fill: str,
stroke: str = "none", stroke_width: float = 1.0) -> str:
"""Generate SVG for a circle."""
return f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}" />'
def _draw_ellipse(self, cx: float, cy: float, rx: float, ry: float,
fill: str, stroke: str = "none", stroke_width: float = 1.0) -> str:
"""Generate SVG for an ellipse."""
return f'<ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}" />'
def _draw_path(self, d: str, fill: str, stroke: str = "none",
stroke_width: float = 1.0, stroke_linecap: str = "round") -> str:
"""Generate SVG for a path."""
return f'<path d="{d}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}" stroke-linecap="{stroke_linecap}" />'
def _draw_polygon(self, points: str, fill: str, stroke: str = "none",
stroke_width: float = 1.0) -> str:
"""Generate SVG for a polygon."""
return f'<polygon points="{points}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}" />'
def _draw_face(self, animal: str, face_shape: str, face_color: str,
x: float, y: float, size: float) -> str:
"""Draw the animal's face based on face shape."""
elements = []
if face_shape == "round":
elements.append(self._draw_circle(x, y, size * 0.4, face_color))
elif face_shape == "oval":
elements.append(self._draw_ellipse(x, y, size * 0.35, size * 0.45, face_color))
elif face_shape == "square":
points = (f"{x-size*0.35},{y-size*0.35} {x+size*0.35},{y-size*0.35} "
f"{x+size*0.35},{y+size*0.35} {x-size*0.35},{y+size*0.35}")
elements.append(self._draw_polygon(points, face_color))
elif face_shape == "heart":
# Create a heart shape using paths
cx, cy = x, y + size * 0.05
r = size * 0.2
path = (f"M {cx} {cy-r*0.4} "
f"C {cx-r*1.5} {cy-r*1.5}, {cx-r*2} {cy+r*0.5}, {cx} {cy+r} "
f"C {cx+r*2} {cy+r*0.5}, {cx+r*1.5} {cy-r*1.5}, {cx} {cy-r*0.4} Z")
elements.append(self._draw_path(path, face_color))
elif face_shape == "triangular":
points = f"{x},{y-size*0.4} {x+size*0.4},{y+size*0.3} {x-size*0.4},{y+size*0.3}"
elements.append(self._draw_polygon(points, face_color))
elif face_shape == "diamond":
points = f"{x},{y-size*0.4} {x+size*0.35},{y} {x},{y+size*0.4} {x-size*0.35},{y}"
elements.append(self._draw_polygon(points, face_color))
return "\n".join(elements)
def _draw_ears(self, animal: str, ear_style: str, face_color: str,
inner_color: str, x: float, y: float, size: float) -> str:
"""Draw the animal's ears based on ear style."""
elements = []
if ear_style == "none":
return ""
if ear_style == "pointed":
# Left ear
points_left = f"{x-size*0.2},{y-size*0.1} {x-size*0.35},{y-size*0.45} {x-size*0.05},{y-size*0.15}"
elements.append(self._draw_polygon(points_left, face_color))
# Right ear
points_right = f"{x+size*0.2},{y-size*0.1} {x+size*0.35},{y-size*0.45} {x+size*0.05},{y-size*0.15}"
elements.append(self._draw_polygon(points_right, face_color))
# Inner ears
points_inner_left = f"{x-size*0.2},{y-size*0.13} {x-size*0.3},{y-size*0.38} {x-size*0.1},{y-size*0.17}"
elements.append(self._draw_polygon(points_inner_left, inner_color))
points_inner_right = f"{x+size*0.2},{y-size*0.13} {x+size*0.3},{y-size*0.38} {x+size*0.1},{y-size*0.17}"
elements.append(self._draw_polygon(points_inner_right, inner_color))
elif ear_style == "round":
# Left ear
elements.append(self._draw_circle(x-size*0.25, y-size*0.25, size*0.15, face_color))
# Right ear
elements.append(self._draw_circle(x+size*0.25, y-size*0.25, size*0.15, face_color))
# Inner ears
elements.append(self._draw_circle(x-size*0.25, y-size*0.25, size*0.08, inner_color))
elements.append(self._draw_circle(x+size*0.25, y-size*0.25, size*0.08, inner_color))
elif ear_style == "folded" or ear_style == "floppy":
# Left ear
path_left = (f"M {x-size*0.15} {y-size*0.15} "
f"C {x-size*0.3} {y-size*0.4}, {x-size*0.4} {y-size*0.2}, {x-size*0.35} {y}")
elements.append(self._draw_path(path_left, face_color, stroke="none", stroke_width=size*0.08))
# Right ear
path_right = (f"M {x+size*0.15} {y-size*0.15} "
f"C {x+size*0.3} {y-size*0.4}, {x+size*0.4} {y-size*0.2}, {x+size*0.35} {y}")
elements.append(self._draw_path(path_right, face_color, stroke="none", stroke_width=size*0.08))
elif ear_style == "long" or ear_style == "standing":
# Left ear
points_left = f"{x-size*0.2},{y-size*0.15} {x-size*0.3},{y-size*0.6} {x-size*0.05},{y-size*0.15}"
elements.append(self._draw_polygon(points_left, face_color))
# Right ear
points_right = f"{x+size*0.2},{y-size*0.15} {x+size*0.3},{y-size*0.6} {x+size*0.05},{y-size*0.15}"
elements.append(self._draw_polygon(points_right, face_color))
# Inner ears
points_inner_left = f"{x-size*0.18},{y-size*0.18} {x-size*0.25},{y-size*0.5} {x-size*0.1},{y-size*0.18}"
elements.append(self._draw_polygon(points_inner_left, inner_color))
points_inner_right = f"{x+size*0.18},{y-size*0.18} {x+size*0.25},{y-size*0.5} {x+size*0.1},{y-size*0.18}"
elements.append(self._draw_polygon(points_inner_right, inner_color))
elif ear_style == "large":
# Left ear
elements.append(self._draw_ellipse(x-size*0.25, y-size*0.3, size*0.2, size*0.25, face_color))
# Right ear
elements.append(self._draw_ellipse(x+size*0.25, y-size*0.3, size*0.2, size*0.25, face_color))
# Inner ears
elements.append(self._draw_ellipse(x-size*0.25, y-size*0.3, size*0.1, size*0.15, inner_color))
elements.append(self._draw_ellipse(x+size*0.25, y-size*0.3, size*0.1, size*0.15, inner_color))
elif ear_style == "small":
# Left ear
elements.append(self._draw_ellipse(x-size*0.22, y-size*0.22, size*0.1, size*0.12, face_color))
# Right ear
elements.append(self._draw_ellipse(x+size*0.22, y-size*0.22, size*0.1, size*0.12, face_color))
# Inner ears
elements.append(self._draw_ellipse(x-size*0.22, y-size*0.22, size*0.05, size*0.06, inner_color))
elements.append(self._draw_ellipse(x+size*0.22, y-size*0.22, size*0.05, size*0.06, inner_color))
elif ear_style == "tufted" and animal == "owl":
# Left ear tuft
points_left = f"{x-size*0.2},{y-size*0.15} {x-size*0.35},{y-size*0.45} {x-size*0.05},{y-size*0.15}"
elements.append(self._draw_polygon(points_left, face_color))
# Right ear tuft
points_right = f"{x+size*0.2},{y-size*0.15} {x+size*0.35},{y-size*0.45} {x+size*0.05},{y-size*0.15}"
elements.append(self._draw_polygon(points_right, face_color))
elif ear_style == "wide" and animal == "elephant":
# Left ear
path_left = (f"M {x-size*0.15} {y-size*0.1} "
f"C {x-size*0.5} {y-size*0.2}, {x-size*0.6} {y+size*0.2}, {x-size*0.15} {y+size*0.2}")
elements.append(self._draw_path(path_left, face_color, stroke=face_color, stroke_width=size*0.04))
# Right ear
path_right = (f"M {x+size*0.15} {y-size*0.1} "
f"C {x+size*0.5} {y-size*0.2}, {x+size*0.6} {y+size*0.2}, {x+size*0.15} {y+size*0.2}")
elements.append(self._draw_path(path_right, face_color, stroke=face_color, stroke_width=size*0.04))
return "\n".join(elements)
def _draw_eyes(self, eye_style: str, expression: str, eye_color: str,
x: float, y: float, size: float) -> str:
"""Draw the animal's eyes based on eye style and expression."""
elements = []
eye_spacing = size * 0.2
if eye_style == "round":
eye_size = size * 0.08
# Left eye
elements.append(self._draw_circle(x - eye_spacing, y - size * 0.05, eye_size, "#FFFFFF"))
elements.append(self._draw_circle(x - eye_spacing, y - size * 0.05, eye_size * 0.6, eye_color))
# Right eye
elements.append(self._draw_circle(x + eye_spacing, y - size * 0.05, eye_size, "#FFFFFF"))
elements.append(self._draw_circle(x + eye_spacing, y - size * 0.05, eye_size * 0.6, eye_color))
elif eye_style == "oval":
eye_width = size * 0.1
eye_height = size * 0.07
# Left eye
elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width * 0.6, eye_height * 0.6, eye_color))
# Right eye
elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width * 0.6, eye_height * 0.6, eye_color))
elif eye_style == "almond":
# Left eye - almond shape
path_left = (f"M {x-eye_spacing-size*0.1} {y-size*0.05} "
f"C {x-eye_spacing} {y-size*0.12}, {x-eye_spacing} {y+size*0.02}, {x-eye_spacing+size*0.1} {y-size*0.05} "
f"C {x-eye_spacing} {y+size*0.02}, {x-eye_spacing} {y-size*0.12}, {x-eye_spacing-size*0.1} {y-size*0.05} Z")
elements.append(self._draw_path(path_left, "#FFFFFF"))
elements.append(self._draw_circle(x - eye_spacing, y - size * 0.05, size * 0.05, eye_color))
# Right eye - almond shape
path_right = (f"M {x+eye_spacing-size*0.1} {y-size*0.05} "
f"C {x+eye_spacing} {y-size*0.12}, {x+eye_spacing} {y+size*0.02}, {x+eye_spacing+size*0.1} {y-size*0.05} "
f"C {x+eye_spacing} {y+size*0.02}, {x+eye_spacing} {y-size*0.12}, {x+eye_spacing-size*0.1} {y-size*0.05} Z")
elements.append(self._draw_path(path_right, "#FFFFFF"))
elements.append(self._draw_circle(x + eye_spacing, y - size * 0.05, size * 0.05, eye_color))
elif eye_style == "wide":
eye_width = size * 0.12
eye_height = size * 0.08
# Left eye
elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width * 0.5, eye_height * 0.6, eye_color))
# Right eye
elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width * 0.5, eye_height * 0.6, eye_color))
elif eye_style == "narrow":
eye_width = size * 0.12
eye_height = size * 0.04
# Left eye
elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width * 0.4, eye_height * 0.7, eye_color))
# Right eye
elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width * 0.4, eye_height * 0.7, eye_color))
elif eye_style == "cute":
eye_size = size * 0.1
# Left eye
elements.append(self._draw_circle(x - eye_spacing, y - size * 0.05, eye_size, "#FFFFFF"))
elements.append(self._draw_circle(x - eye_spacing - size * 0.02, y - size * 0.07, eye_size * 0.5, eye_color))
elements.append(self._draw_circle(x - eye_spacing - size * 0.03, y - size * 0.08, eye_size * 0.15, "#FFFFFF"))
# Right eye
elements.append(self._draw_circle(x + eye_spacing, y - size * 0.05, eye_size, "#FFFFFF"))
elements.append(self._draw_circle(x + eye_spacing - size * 0.02, y - size * 0.07, eye_size * 0.5, eye_color))
elements.append(self._draw_circle(x + eye_spacing - size * 0.03, y - size * 0.08, eye_size * 0.15, "#FFFFFF"))
# Apply expression
if expression == "happy":
# Close bottom half of eyes slightly
path_left = (f"M {x-eye_spacing-size*0.1} {y-size*0.03} "
f"Q {x-eye_spacing} {y+size*0.02}, {x-eye_spacing+size*0.1} {y-size*0.03}")
elements.append(self._draw_path(path_left, face_color, stroke="#000000", stroke_width=size*0.01))
path_right = (f"M {x+eye_spacing-size*0.1} {y-size*0.03} "
f"Q {x+eye_spacing} {y+size*0.02}, {x+eye_spacing+size*0.1} {y-size*0.03}")
elements.append(self._draw_path(path_right, face_color, stroke="#000000", stroke_width=size*0.01))
elif expression == "serious":
# Serious eyebrows
path_left = (f"M {x-eye_spacing-size*0.08} {y-size*0.12} "
f"L {x-eye_spacing+size*0.08} {y-size*0.15}")
elements.append(self._draw_path(path_left, "none", stroke="#000000", stroke_width=size*0.02))
path_right = (f"M {x+eye_spacing-size*0.08} {y-size*0.15} "
f"L {x+eye_spacing+size*0.08} {y-size*0.12}")
elements.append(self._draw_path(path_right, "none", stroke="#000000", stroke_width=size*0.02))
elif expression == "surprised":
# Raise eyebrows
path_left = (f"M {x-eye_spacing-size*0.08} {y-size*0.15} "
f"Q {x-eye_spacing} {y-size*0.18}, {x-eye_spacing+size*0.08} {y-size*0.15}")
elements.append(self._draw_path(path_left, "none", stroke="#000000", stroke_width=size*0.015))
path_right = (f"M {x+eye_spacing-size*0.08} {y-size*0.15} "
f"Q {x+eye_spacing} {y-size*0.18}, {x+eye_spacing+size*0.08} {y-size*0.15}")
elements.append(self._draw_path(path_right, "none", stroke="#000000", stroke_width=size*0.015))
elif expression == "sleepy":
# Half-closed eyes
path_left = (f"M {x-eye_spacing-size*0.1} {y-size*0.08} "
f"Q {x-eye_spacing} {y-size*0.01}, {x-eye_spacing+size*0.1} {y-size*0.08}")
elements.append(self._draw_path(path_left, "none", stroke="#000000", stroke_width=size*0.015))
path_right = (f"M {x+eye_spacing-size*0.1} {y-size*0.08} "
f"Q {x+eye_spacing} {y-size*0.01}, {x+eye_spacing+size*0.1} {y-size*0.08}")
elements.append(self._draw_path(path_right, "none", stroke="#000000", stroke_width=size*0.015))
elif expression == "wink":
# Right eye normal
# Left eye winking
path_left = (f"M {x-eye_spacing-size*0.1} {y-size*0.05} "
f"Q {x-eye_spacing} {y-size*0.1}, {x-eye_spacing+size*0.1} {y-size*0.05}")
elements.append(self._draw_path(path_left, "none", stroke="#000000", stroke_width=size*0.02))
return "\n".join(elements)
def _draw_nose(self, animal: str, nose_style: str, nose_color: str,
x: float, y: float, size: float) -> str:
"""Draw the animal's nose based on nose style."""
elements = []
if nose_style == "round":
elements.append(self._draw_circle(x, y + size * 0.05, size * 0.08, nose_color))
elif nose_style == "triangular":
points = f"{x},{y+size*0.02} {x-size*0.08},{y+size*0.12} {x+size*0.08},{y+size*0.12}"
elements.append(self._draw_polygon(points, nose_color))
elif nose_style == "small":
elements.append(self._draw_circle(x, y + size * 0.05, size * 0.05, nose_color))
elif nose_style == "large":
if animal in ["dog", "bear", "panda", "koala"]:
elements.append(self._draw_ellipse(x, y + size * 0.05, size * 0.1, size * 0.08, nose_color))
else:
elements.append(self._draw_circle(x, y + size * 0.05, size * 0.1, nose_color))
elif nose_style == "heart":
# Heart-shaped nose
cx, cy = x, y + size * 0.05
r = size * 0.06
path = (f"M {cx} {cy-r*0.2} "
f"C {cx-r*1.5} {cy-r*1.2}, {cx-r*1.8} {cy+r*0.6}, {cx} {cy+r*0.8} "
f"C {cx+r*1.8} {cy+r*0.6}, {cx+r*1.5} {cy-r*1.2}, {cx} {cy-r*0.2} Z")
elements.append(self._draw_path(path, nose_color))
elif nose_style == "button":
elements.append(self._draw_circle(x, y + size * 0.05, size * 0.06, nose_color))
elements.append(self._draw_circle(x, y + size * 0.05, size * 0.04, nose_color, stroke="#000000", stroke_width=size*0.01))
return "\n".join(elements)
def _draw_mouth(self, expression: str, x: float, y: float, size: float) -> str:
"""Draw the animal's mouth based on expression."""
elements = []
if expression == "happy":
path = (f"M {x-size*0.15} {y+size*0.12} "
f"Q {x} {y+size*0.25}, {x+size*0.15} {y+size*0.12}")
elements.append(self._draw_path(path, "none", stroke="#000000", stroke_width=size*0.02))
elif expression == "serious":
path = (f"M {x-size*0.12} {y+size*0.15} "
f"L {x+size*0.12} {y+size*0.15}")
elements.append(self._draw_path(path, "none", stroke="#000000", stroke_width=size*0.02))
elif expression == "surprised":
elements.append(self._draw_ellipse(x, y + size * 0.15, size * 0.06, size * 0.08, "#FFFFFF",
stroke="#000000", stroke_width=size*0.01))
elif expression == "sleepy":
path = (f"M {x-size*0.08} {y+size*0.15} "
f"Q {x} {y+size*0.12}, {x+size*0.08} {y+size*0.15}")
elements.append(self._draw_path(path, "none", stroke="#000000", stroke_width=size*0.015))
elif expression == "wink":
path = (f"M {x-size*0.15} {y+size*0.12} "
f"Q {x} {y+size*0.22}, {x+size*0.15} {y+size*0.12}")
elements.append(self._draw_path(path, "none", stroke="#000000", stroke_width=size*0.02))
return "\n".join(elements)
def _draw_special_features(self, animal: str, special_feature: str, face_color: str,
accent_color: str, x: float, y: float, size: float) -> str:
"""Draw special features based on animal and feature type."""
elements = []
if special_feature == "none":
return ""
elif special_feature == "whiskers":
# Left whiskers
for i in range(3):
angle = -30 + i * 30
length = size * 0.25
end_x = x - size * 0.15 + length * math.cos(math.radians(angle))
end_y = y + size * 0.05 + length * math.sin(math.radians(angle))
elements.append(self._draw_path(
f"M {x-size*0.15} {y+size*0.05} L {end_x} {end_y}",
"none", stroke="#000000", stroke_width=size*0.01))
# Right whiskers
for i in range(3):
angle = -150 + i * 30
length = size * 0.25
end_x = x + size * 0.15 + length * math.cos(math.radians(angle))
end_y = y + size * 0.05 + length * math.sin(math.radians(angle))
elements.append(self._draw_path(
f"M {x+size*0.15} {y+size*0.05} L {end_x} {end_y}",
"none", stroke="#000000", stroke_width=size*0.01))
elif special_feature == "stripes":
# Vertical stripes
if animal == "tiger":
for i in range(3):
offset = -size * 0.2 + i * size * 0.2
path = (f"M {x+offset} {y-size*0.3} "
f"Q {x+offset+size*0.1} {y}, {x+offset} {y+size*0.3}")
elements.append(self._draw_path(path, "none", stroke=accent_color, stroke_width=size*0.04))
# Horizontal stripes for zebra
elif animal == "zebra":
for i in range(3):
offset = -size * 0.2 + i * size * 0.2
path = (f"M {x-size*0.3} {y+offset} "
f"Q {x} {y+offset+size*0.1}, {x+size*0.3} {y+offset}")
elements.append(self._draw_path(path, "none", stroke=accent_color, stroke_width=size*0.04))
elif special_feature == "spots":
# Random spots
num_spots = random.randint(3, 6)
for _ in range(num_spots):
spot_x = x + random.uniform(-size * 0.3, size * 0.3)
spot_y = y + random.uniform(-size * 0.3, size * 0.3)
spot_size = random.uniform(size * 0.03, size * 0.08)
elements.append(self._draw_circle(spot_x, spot_y, spot_size, accent_color))
elif special_feature == "patch":
# Eye patch or face patch
if animal == "dog":
elements.append(self._draw_ellipse(x - size * 0.2, y, size * 0.2, size * 0.25, accent_color))
else:
# Generic face patch
elements.append(self._draw_ellipse(x, y + size * 0.2, size * 0.2, size * 0.15, accent_color))
elif special_feature == "mask":
if animal == "raccoon":
# Raccoon mask
path = (f"M {x-size*0.3} {y-size*0.1} "
f"Q {x} {y-size*0.3}, {x+size*0.3} {y-size*0.1} "
f"Q {x+size*0.2} {y+size*0.1}, {x} {y+size*0.15} "
f"Q {x-size*0.2} {y+size*0.1}, {x-size*0.3} {y-size*0.1} Z")
elements.append(self._draw_path(path, accent_color))
elif animal in ["fox", "wolf"]:
# Fox/wolf mask
path = (f"M {x-size*0.3} {y-size*0.1} "
f"L {x} {y+size*0.1} "
f"L {x+size*0.3} {y-size*0.1} "
f"Q {x+size*0.15} {y-size*0.05}, {x} {y-size*0.1} "
f"Q {x-size*0.15} {y-size*0.05}, {x-size*0.3} {y-size*0.1} Z")
elements.append(self._draw_path(path, accent_color))
elif special_feature == "eye_patches" and animal == "panda":
# Panda eye patches
elements.append(self._draw_ellipse(x - size * 0.2, y - size * 0.05, size * 0.15, size * 0.2, accent_color))
elements.append(self._draw_ellipse(x + size * 0.2, y - size * 0.05, size * 0.15, size * 0.2, accent_color))
elif special_feature == "nose_patch":
elements.append(self._draw_ellipse(x, y + size * 0.05, size * 0.12, size * 0.1, accent_color))
elif special_feature == "mane" and animal == "lion":
# Lion mane
for i in range(12):
angle = i * 30
outer_x = x + size * 0.5 * math.cos(math.radians(angle))
outer_y = y + size * 0.5 * math.sin(math.radians(angle))
inner_x = x + size * 0.3 * math.cos(math.radians(angle))
inner_y = y + size * 0.3 * math.sin(math.radians(angle))
# Draw mane sections
path = (f"M {inner_x} {inner_y} "
f"L {outer_x} {outer_y} "
f"A {size*0.5} {size*0.5} 0 0 1 "
f"{x + size * 0.5 * math.cos(math.radians(angle + 30))} "
f"{y + size * 0.5 * math.sin(math.radians(angle + 30))} "
f"L {x + size * 0.3 * math.cos(math.radians(angle + 30))} "
f"{y + size * 0.3 * math.sin(math.radians(angle + 30))} Z")
elements.append(self._draw_path(path, accent_color))
elif special_feature == "tusks" and animal == "elephant":
# Elephant tusks
path_left = (f"M {x-size*0.15} {y+size*0.1} "
f"Q {x-size*0.3} {y+size*0.3}, {x-size*0.35} {y+size*0.5}")
elements.append(self._draw_path(path_left, "#FFFFF0", stroke="#F5F5DC", stroke_width=size*0.04))
path_right = (f"M {x+size*0.15} {y+size*0.1} "
f"Q {x+size*0.3} {y+size*0.3}, {x+size*0.35} {y+size*0.5}")
elements.append(self._draw_path(path_right, "#FFFFF0", stroke="#F5F5DC", stroke_width=size*0.04))
elif special_feature == "antlers" and animal == "deer":
# Deer antlers
# Left antler
path_left = (f"M {x-size*0.15} {y-size*0.2} "
f"L {x-size*0.3} {y-size*0.45} "
f"L {x-size*0.4} {y-size*0.4} "
f"M {x-size*0.3} {y-size*0.45} "
f"L {x-size*0.2} {y-size*0.5}")
elements.append(self._draw_path(path_left, "none", stroke=accent_color, stroke_width=size*0.03))
# Right antler
path_right = (f"M {x+size*0.15} {y-size*0.2} "
f"L {x+size*0.3} {y-size*0.45} "
f"L {x+size*0.4} {y-size*0.4} "
f"M {x+size*0.3} {y-size*0.45} "
f"L {x+size*0.2} {y-size*0.5}")
elements.append(self._draw_path(path_right, "none", stroke=accent_color, stroke_width=size*0.03))
elif special_feature == "bushy_tail" and animal == "squirrel":
# Squirrel bushy tail
path = (f"M {x+size*0.1} {y+size*0.2} "
f"Q {x+size*0.5} {y}, {x+size*0.3} {y-size*0.3} "
f"Q {x+size*0.4} {y-size*0.4}, {x+size*0.5} {y-size*0.35} "
f"Q {x+size*0.45} {y-size*0.25}, {x+size*0.6} {y-size*0.3} "
f"Q {x+size*0.55} {y-size*0.1}, {x+size*0.4} {y+size*0.1} "
f"Q {x+size*0.3} {y+size*0.3}, {x+size*0.1} {y+size*0.2} Z")
elements.append(self._draw_path(path, accent_color))
elif special_feature == "brush_tail" and animal in ["fox", "wolf"]:
# Fox/wolf brush tail
path = (f"M {x+size*0.1} {y+size*0.2} "
f"Q {x+size*0.4} {y+size*0.1}, {x+size*0.5} {y-size*0.1} "
f"Q {x+size*0.6} {y-size*0.2}, {x+size*0.7} {y-size*0.1} "
f"Q {x+size*0.65} {y}, {x+size*0.6} {y+size*0.1} "
f"Q {x+size*0.5} {y+size*0.2}, {x+size*0.3} {y+size*0.3} Z")
elements.append(self._draw_path(path, face_color))
# Tail tip
elements.append(self._draw_ellipse(x + size * 0.6, y - size * 0.05, size * 0.12, size * 0.08, accent_color))
elif special_feature == "bib" and animal == "penguin":
# Penguin bib/chest
path = (f"M {x-size*0.2} {y} "
f"Q {x} {y+size*0.4}, {x+size*0.2} {y} "
f"Q {x} {y+size*0.1}, {x-size*0.2} {y} Z")
elements.append(self._draw_path(path, "#FFFFFF"))
elif special_feature == "feather_tufts" and animal == "owl":
# Owl feather tufts
path_left = (f"M {x-size*0.1} {y-size*0.3} "
f"Q {x-size*0.15} {y-size*0.45}, {x-size*0.05} {y-size*0.5}")
elements.append(self._draw_path(path_left, face_color, stroke="#000000", stroke_width=size*0.01))
path_right = (f"M {x+size*0.1} {y-size*0.3} "
f"Q {x+size*0.15} {y-size*0.45}, {x+size*0.05} {y-size*0.5}")
elements.append(self._draw_path(path_right, face_color, stroke="#000000", stroke_width=size*0.01))
elif special_feature == "spikes" and animal == "hedgehog":
# Hedgehog spikes
for i in range(12):
angle = i * 30
inner_x = x + size * 0.3 * math.cos(math.radians(angle))
inner_y = y + size * 0.3 * math.sin(math.radians(angle))
outer_x = x + size * 0.5 * math.cos(math.radians(angle))
outer_y = y + size * 0.5 * math.sin(math.radians(angle))
path = f"M {inner_x} {inner_y} L {outer_x} {outer_y}"
elements.append(self._draw_path(path, "none", stroke=accent_color, stroke_width=size*0.03))
elif special_feature == "cheek_patches" and animal == "monkey":
# Monkey cheek patches
elements.append(self._draw_circle(x - size * 0.25, y + size * 0.1, size * 0.12, accent_color))
elements.append(self._draw_circle(x + size * 0.25, y + size * 0.1, size * 0.12, accent_color))
return "\n".join(elements)
def generate_avatar(self, animal: Optional[str] = None, color_palette: str = "natural",
face_shape: Optional[str] = None, eye_style: Optional[str] = None,
ear_style: Optional[str] = None, nose_style: Optional[str] = None,
expression: Optional[str] = None, special_feature: Optional[str] = None,
size: int = 500) -> str:
"""
Generate an animal avatar with the specified parameters.
Args:
animal: Animal type (e.g., "cat", "dog"). If None, a random animal is selected.
color_palette: Color palette to use (e.g., "natural", "pastel", "vibrant", "mono").
face_shape: Shape of the face. If None, a random shape is selected.
eye_style: Style of the eyes. If None, a random style is selected.
ear_style: Style of the ears. If None, a random style is selected for the animal.
nose_style: Style of the nose. If None, a random style is selected.
expression: Facial expression. If None, a random expression is selected.
special_feature: Special feature to add. If None, a random feature is selected for the animal.
size: Size of the avatar in pixels.
Returns:
SVG string representation of the generated avatar.
"""
# Select random animal if not specified
if animal is None or animal not in self.ANIMALS:
animal = random.choice(self.ANIMALS)
# Select random options if not specified
if face_shape is None or face_shape not in self.FACE_SHAPES:
face_shape = random.choice(self.FACE_SHAPES)
if eye_style is None or eye_style not in self.EYE_STYLES:
eye_style = random.choice(self.EYE_STYLES)
if ear_style is None:
ear_style = self._get_ear_style(animal)
if nose_style is None or nose_style not in self.NOSE_STYLES:
nose_style = random.choice(self.NOSE_STYLES)
if expression is None or expression not in self.EXPRESSIONS:
expression = random.choice(self.EXPRESSIONS)
if special_feature is None:
special_feature = self._get_special_feature(animal)
# Get colors
colors = self._get_colors(animal, color_palette)
face_color = random.choice(colors)
# Make sure accent color is different from face color
remaining_colors = [c for c in colors if c != face_color]
if not remaining_colors:
remaining_colors = ["#000000", "#FFFFFF"]
accent_color = random.choice(remaining_colors)
# Ensure inner ear color is different from face color
inner_ear_color = random.choice(remaining_colors)
# Eye color options
eye_colors = ["#000000", "#331800", "#0000FF", "#008000", "#FFA500", "#800080"]
eye_color = random.choice(eye_colors)
# Nose color options based on animal
if animal in ["dog", "cat", "fox", "wolf", "bear", "panda", "koala", "tiger", "lion"]:
nose_color = "#000000"
else:
nose_color = accent_color
# Center coordinates
x, y = size / 2, size / 2
# Generate SVG elements
elements = []
# Draw the face first
elements.append(self._draw_face(animal, face_shape, face_color, x, y, size))
# Draw special features behind the face if needed
if special_feature in ["mane", "spikes"]:
elements.append(self._draw_special_features(animal, special_feature, face_color,
accent_color, x, y, size))
# Draw ears
elements.append(self._draw_ears(animal, ear_style, face_color, inner_ear_color, x, y, size))
# Draw eyes
elements.append(self._draw_eyes(eye_style, expression, eye_color, x, y, size))
# Draw nose
elements.append(self._draw_nose(animal, nose_style, nose_color, x, y, size))
# Draw mouth
elements.append(self._draw_mouth(expression, x, y, size))
# Draw special features that should be in front
if special_feature not in ["mane", "spikes"]:
elements.append(self._draw_special_features(animal, special_feature, face_color,
accent_color, x, y, size))
# Assemble SVG
svg_content = '\n'.join(elements)
svg = (f'<svg viewBox="0 0 {size} {size}" xmlns="http://www.w3.org/2000/svg">\n'
f'{svg_content}\n'
f'</svg>')
return svg
def get_avatar_options(self) -> Dict[str, List[str]]:
"""Return all available avatar options."""
return {
"animals": self.ANIMALS,
"color_palettes": list(self.COLOR_PALETTES.keys()),
"face_shapes": self.FACE_SHAPES,
"eye_styles": self.EYE_STYLES,
"ear_styles": {animal: styles for animal, styles in self.EAR_STYLES.items()},
"nose_styles": self.NOSE_STYLES,
"expressions": self.EXPRESSIONS,
"special_features": {animal: features for animal, features in self.SPECIAL_FEATURES.items()}
}
def generate_random_avatar(self, size: int = 500) -> str:
"""Generate a completely random avatar."""
return self.generate_avatar(size=size)
def generate_avatar_with_options(options: Dict) -> str:
"""Generate an avatar with the given options."""
generator = AnimalAvatarGenerator(seed=options.get("seed"))
return generator.generate_avatar(
animal=options.get("animal"),
color_palette=options.get("color_palette", "natural"),
face_shape=options.get("face_shape"),
eye_style=options.get("eye_style"),
ear_style=options.get("ear_style"),
nose_style=options.get("nose_style"),
expression=options.get("expression"),
special_feature=options.get("special_feature"),
size=options.get("size", 500)
)
def list_avatar_options() -> Dict[str, List[str]]:
"""Return all available avatar options."""
generator = AnimalAvatarGenerator()
return generator.get_avatar_options()
def create_avatar_app():
"""Command-line interface for the avatar generator."""
parser = argparse.ArgumentParser(description="Generate animal avatars")
parser.add_argument("--animal", help="Animal type", choices=AnimalAvatarGenerator.ANIMALS)
parser.add_argument("--color-palette", help="Color palette", default="natural",
choices=["natural", "pastel", "vibrant", "mono"])
parser.add_argument("--face-shape", help="Face shape", choices=AnimalAvatarGenerator.FACE_SHAPES)
parser.add_argument("--eye-style", help="Eye style", choices=AnimalAvatarGenerator.EYE_STYLES)
parser.add_argument("--ear-style", help="Ear style")
parser.add_argument("--nose-style", help="Nose style", choices=AnimalAvatarGenerator.NOSE_STYLES)
parser.add_argument("--expression", help="Expression", choices=AnimalAvatarGenerator.EXPRESSIONS)
parser.add_argument("--special-feature", help="Special feature")
parser.add_argument("--size", help="Size in pixels", type=int, default=500)
parser.add_argument("--seed", help="Random seed for reproducibility", type=int)
parser.add_argument("--output", help="Output file path", default="avatar.svg")
parser.add_argument("--list-options", help="List all available options", action="store_true")
args = parser.parse_args()
if args.list_options:
options = list_avatar_options()
print(json.dumps(options, indent=2))
return
generator = AnimalAvatarGenerator(seed=args.seed)
svg = generator.generate_avatar(
animal=args.animal,
color_palette=args.color_palette,
face_shape=args.face_shape,
eye_style=args.eye_style,
ear_style=args.ear_style,
nose_style=args.nose_style,
expression=args.expression,
special_feature=args.special_feature,
size=args.size
)
with open(args.output, "w") as f:
f.write(svg)
print(f"Avatar saved to {args.output}")
if __name__ == "__main__":
create_avatar_app()