New avatar.
This commit is contained in:
parent
59a815f85a
commit
87b6b3362d
497
gitlog.jsonl
497
gitlog.jsonl
File diff suppressed because one or more lines are too long
@ -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 {
|
||||
|
@ -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())
|
||||
|
871
src/snek/view/avatar_animal.py
Normal file
871
src/snek/view/avatar_animal.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user