update stock props

This commit is contained in:
Evan Carroll 2026-01-23 20:36:03 -06:00
parent fe40fd32ab
commit 98590f63e7
18 changed files with 898 additions and 1 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View file

@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<radialGradient id="armGradient" cx="30%" cy="30%" r="70%">
<stop offset="0%" stop-color="#FFB347"/>
<stop offset="100%" stop-color="#FF8C00"/>
</radialGradient>
<filter id="armShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.2"/>
</filter>
</defs>
<!-- Left arm - stubby sausage shape reaching up and out -->
<path d="M 120 90
Q 100 85 80 60
Q 65 40 50 25
Q 40 15 30 20
Q 20 25 25 40
Q 30 55 50 70
Q 70 85 95 95
Q 110 100 120 95
Z"
fill="url(#armGradient)" filter="url(#armShadow)"/>
<!-- Arm highlight -->
<path d="M 90 70 Q 70 50 55 35" stroke="#FFCC80" stroke-width="8" fill="none" stroke-linecap="round" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 960 B

View file

@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<radialGradient id="armGradient" cx="70%" cy="30%" r="70%">
<stop offset="0%" stop-color="#FFB347"/>
<stop offset="100%" stop-color="#FF8C00"/>
</radialGradient>
<filter id="armShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.2"/>
</filter>
</defs>
<!-- Right arm - stubby sausage shape reaching up and out (mirrored) -->
<path d="M 0 90
Q 20 85 40 60
Q 55 40 70 25
Q 80 15 90 20
Q 100 25 95 40
Q 90 55 70 70
Q 50 85 25 95
Q 10 100 0 95
Z"
fill="url(#armGradient)" filter="url(#armShadow)"/>
<!-- Arm highlight -->
<path d="M 30 70 Q 50 50 65 35" stroke="#FFCC80" stroke-width="8" fill="none" stroke-linecap="round" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 967 B

View file

@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<radialGradient id="handGradient" cx="40%" cy="40%" r="60%">
<stop offset="0%" stop-color="#FFB347"/>
<stop offset="100%" stop-color="#FF8C00"/>
</radialGradient>
<filter id="handShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.2"/>
</filter>
</defs>
<!-- Waving hand - palm open, fingers spread -->
<!-- Palm -->
<ellipse cx="70" cy="75" rx="28" ry="25" fill="url(#handGradient)" filter="url(#handShadow)"/>
<!-- Thumb -->
<ellipse cx="95" cy="90" rx="12" ry="10" fill="url(#handGradient)" filter="url(#handShadow)"/>
<!-- Fingers - spread out in wave -->
<ellipse cx="45" cy="50" rx="10" ry="20" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(-30 45 50)"/>
<ellipse cx="62" cy="42" rx="9" ry="22" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(-10 62 42)"/>
<ellipse cx="80" cy="40" rx="9" ry="22" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(10 80 40)"/>
<ellipse cx="96" cy="48" rx="8" ry="18" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(25 96 48)"/>
<!-- Palm highlight -->
<ellipse cx="65" cy="70" rx="12" ry="10" fill="#FFCC80" opacity="0.5"/>
<!-- Motion lines for waving -->
<path d="M 20 30 Q 15 35 20 40" stroke="#4cc9f0" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.6"/>
<path d="M 12 45 Q 7 50 12 55" stroke="#4cc9f0" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M 25 55 Q 20 60 25 65" stroke="#4cc9f0" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<radialGradient id="handGradient" cx="60%" cy="40%" r="60%">
<stop offset="0%" stop-color="#FFB347"/>
<stop offset="100%" stop-color="#FF8C00"/>
</radialGradient>
<filter id="handShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.2"/>
</filter>
</defs>
<!-- Waving hand - palm open, fingers spread (mirrored) -->
<!-- Palm -->
<ellipse cx="50" cy="75" rx="28" ry="25" fill="url(#handGradient)" filter="url(#handShadow)"/>
<!-- Thumb -->
<ellipse cx="25" cy="90" rx="12" ry="10" fill="url(#handGradient)" filter="url(#handShadow)"/>
<!-- Fingers - spread out in wave -->
<ellipse cx="75" cy="50" rx="10" ry="20" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(30 75 50)"/>
<ellipse cx="58" cy="42" rx="9" ry="22" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(10 58 42)"/>
<ellipse cx="40" cy="40" rx="9" ry="22" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(-10 40 40)"/>
<ellipse cx="24" cy="48" rx="8" ry="18" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(-25 24 48)"/>
<!-- Palm highlight -->
<ellipse cx="55" cy="70" rx="12" ry="10" fill="#FFCC80" opacity="0.5"/>
<!-- Motion lines for waving -->
<path d="M 100 30 Q 105 35 100 40" stroke="#4cc9f0" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.6"/>
<path d="M 108 45 Q 113 50 108 55" stroke="#4cc9f0" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M 95 55 Q 100 60 95 65" stroke="#4cc9f0" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<!-- Matching the face colors -->
<linearGradient id="neckGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#CC9900"/>
<stop offset="30%" stop-color="#FFCC00"/>
<stop offset="70%" stop-color="#FFCC00"/>
<stop offset="100%" stop-color="#CC9900"/>
</linearGradient>
</defs>
<!-- Neck - cylindrical shape connecting to torso below -->
<rect x="42" y="0" width="36" height="120" fill="url(#neckGradient)"/>
<!-- Subtle center highlight -->
<rect x="52" y="0" width="16" height="120" fill="#FFE566" opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 687 B

View file

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<!-- Decorative sparkles/stars above the head -->
<!-- Main star -->
<polygon points="60,10 63,25 78,28 65,35 68,50 60,40 52,50 55,35 42,28 57,25"
fill="#FFD700" opacity="0.9"/>
<!-- Smaller stars -->
<polygon points="25,45 27,52 34,53 28,57 30,64 25,59 20,64 22,57 16,53 23,52"
fill="#4cc9f0" opacity="0.7"/>
<polygon points="95,40 97,47 104,48 98,52 100,59 95,54 90,59 92,52 86,48 93,47"
fill="#4cc9f0" opacity="0.7"/>
<!-- Tiny sparkle dots -->
<circle cx="40" cy="25" r="3" fill="#FFFFFF" opacity="0.8"/>
<circle cx="80" cy="20" r="2.5" fill="#FFFFFF" opacity="0.7"/>
<circle cx="15" cy="70" r="2" fill="#FFD700" opacity="0.6"/>
<circle cx="105" cy="75" r="2" fill="#FFD700" opacity="0.6"/>
<!-- Swirl decorations -->
<path d="M 30 85 Q 35 80 40 85 Q 45 90 50 85" stroke="#FF69B4" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M 70 90 Q 75 85 80 90 Q 85 95 90 90" stroke="#FF69B4" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<!-- Matching face yellow tones -->
<radialGradient id="torsoGradient" cx="35%" cy="25%" r="70%">
<stop offset="0%" stop-color="#FFE566"/>
<stop offset="50%" stop-color="#FFCC00"/>
<stop offset="100%" stop-color="#CC9900"/>
</radialGradient>
<linearGradient id="neckGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#CC9900"/>
<stop offset="30%" stop-color="#FFCC00"/>
<stop offset="70%" stop-color="#FFCC00"/>
<stop offset="100%" stop-color="#CC9900"/>
</linearGradient>
<filter id="bodyShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.25"/>
</filter>
</defs>
<!-- Neck stub connecting from above -->
<rect x="42" y="0" width="36" height="20" fill="url(#neckGradient)"/>
<rect x="52" y="0" width="16" height="20" fill="#FFE566" opacity="0.3"/>
<!-- Main torso - round friendly blob -->
<ellipse cx="60" cy="65" rx="55" ry="50" fill="url(#torsoGradient)" filter="url(#bodyShadow)" stroke="#CC9900" stroke-width="1.5"/>
<!-- Belly highlight -->
<ellipse cx="45" cy="50" rx="22" ry="18" fill="#FFFFFF" opacity="0.25"/>
<!-- Belly button -->
<ellipse cx="60" cy="70" rx="5" ry="6" fill="#B8860B" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,238 @@
#!/bin/bash
# Create a stock avatar from uploaded props and set it as server default.
#
# Usage: ./stock/avatar/create-stock-avatar.sh [--force|-f] [HOST]
#
# Prerequisites:
# 1. Props must be uploaded first: ./stock/avatar/upload-stockavatars.sh
# 2. Dev server must be running: ./run-dev.sh -f
#
# This script:
# 1. Queries existing props by slug to get UUIDs
# 2. Creates a server avatar with all emotion slots populated
# 3. Sets the avatar as the server default for all gender/age combinations
set -e
# Parse arguments
FORCE=""
HOST="http://localhost:3001"
DB="chattyness"
for arg in "$@"; do
case "$arg" in
--force|-f)
FORCE="?force=true"
;;
http://*)
HOST="$arg"
;;
esac
done
echo "=========================================="
echo "Creating Stock Avatar"
echo "=========================================="
echo "Host: $HOST"
echo "Database: $DB"
echo ""
# Check if server is running
echo "Checking server health..."
health_response=$(curl -s -o /dev/null -w "%{http_code}" "$HOST/api/admin/health" 2>/dev/null || echo "000")
if [ "$health_response" != "200" ]; then
echo "ERROR: Server is not responding at $HOST (HTTP $health_response)"
echo "Make sure the server is running: ./run-dev.sh -f"
exit 1
fi
echo "Server is healthy!"
echo ""
# Query prop UUIDs by slug
echo "Querying prop UUIDs..."
get_prop_id() {
local slug="$1"
psql -d "$DB" -t -A -c "SELECT id FROM server.props WHERE slug = '$slug'" 2>/dev/null | tr -d '[:space:]'
}
# Get face prop (skin layer)
FACE_ID=$(get_prop_id "face")
if [ -z "$FACE_ID" ]; then
echo "ERROR: Face prop not found. Run upload-stockavatars.sh first."
exit 1
fi
echo " face: $FACE_ID"
# Get emotion props
NEUTRAL_ID=$(get_prop_id "neutral")
SMILE_ID=$(get_prop_id "smile") # This is "happy" emotion
SAD_ID=$(get_prop_id "sad")
ANGRY_ID=$(get_prop_id "angry")
SURPRISED_ID=$(get_prop_id "surprised")
THINKING_ID=$(get_prop_id "thinking")
LAUGHING_ID=$(get_prop_id "laughing")
CRYING_ID=$(get_prop_id "crying")
LOVE_ID=$(get_prop_id "love")
CONFUSED_ID=$(get_prop_id "confused")
SLEEPING_ID=$(get_prop_id "sleeping")
WINK_ID=$(get_prop_id "wink")
# Validate all emotion props exist
missing=""
[ -z "$NEUTRAL_ID" ] && missing="$missing neutral"
[ -z "$SMILE_ID" ] && missing="$missing smile"
[ -z "$SAD_ID" ] && missing="$missing sad"
[ -z "$ANGRY_ID" ] && missing="$missing angry"
[ -z "$SURPRISED_ID" ] && missing="$missing surprised"
[ -z "$THINKING_ID" ] && missing="$missing thinking"
[ -z "$LAUGHING_ID" ] && missing="$missing laughing"
[ -z "$CRYING_ID" ] && missing="$missing crying"
[ -z "$LOVE_ID" ] && missing="$missing love"
[ -z "$CONFUSED_ID" ] && missing="$missing confused"
[ -z "$SLEEPING_ID" ] && missing="$missing sleeping"
[ -z "$WINK_ID" ] && missing="$missing wink"
if [ -n "$missing" ]; then
echo "ERROR: Missing emotion props:$missing"
echo "Run upload-stockavatars.sh first."
exit 1
fi
echo " neutral: $NEUTRAL_ID"
echo " smile (happy): $SMILE_ID"
echo " sad: $SAD_ID"
echo " angry: $ANGRY_ID"
echo " surprised: $SURPRISED_ID"
echo " thinking: $THINKING_ID"
echo " laughing: $LAUGHING_ID"
echo " crying: $CRYING_ID"
echo " love: $LOVE_ID"
echo " confused: $CONFUSED_ID"
echo " sleeping: $SLEEPING_ID"
echo " wink: $WINK_ID"
echo ""
# Check if avatar already exists
existing_avatar=$(psql -d "$DB" -t -A -c "SELECT id FROM server.avatars WHERE slug = 'stock-avatar'" 2>/dev/null | tr -d '[:space:]')
if [ -n "$existing_avatar" ] && [ -z "$FORCE" ]; then
echo "Stock avatar already exists with ID: $existing_avatar"
echo "Use --force to recreate it."
AVATAR_ID="$existing_avatar"
else
# Create the avatar via API
echo "Creating stock avatar via API..."
# Build the JSON payload
avatar_json=$(cat <<EOF
{
"name": "Stock Avatar",
"slug": "stock-avatar",
"description": "Default stock avatar with all emotion faces",
"is_public": true,
"l_skin_4": "$FACE_ID",
"e_neutral_4": "$NEUTRAL_ID",
"e_happy_4": "$SMILE_ID",
"e_sad_4": "$SAD_ID",
"e_angry_4": "$ANGRY_ID",
"e_surprised_4": "$SURPRISED_ID",
"e_thinking_4": "$THINKING_ID",
"e_laughing_4": "$LAUGHING_ID",
"e_crying_4": "$CRYING_ID",
"e_love_4": "$LOVE_ID",
"e_confused_4": "$CONFUSED_ID",
"e_sleeping_4": "$SLEEPING_ID",
"e_wink_4": "$WINK_ID"
}
EOF
)
# Delete existing if force mode
if [ -n "$existing_avatar" ]; then
echo " Deleting existing avatar..."
curl -s -X DELETE "$HOST/api/admin/avatars/$existing_avatar" > /dev/null
fi
# Create the avatar
response=$(curl -s -w "\n%{http_code}" -X POST "$HOST/api/admin/avatars" \
-H "Content-Type: application/json" \
-d "$avatar_json")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" = "200" ] || [ "$http_code" = "201" ]; then
AVATAR_ID=$(echo "$body" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
echo " ✓ Created avatar: $AVATAR_ID"
else
echo " ✗ Failed to create avatar (HTTP $http_code): $body"
exit 1
fi
fi
echo ""
# Set as server default for all gender/age combinations
echo "Setting stock avatar as server defaults..."
psql -d "$DB" -c "
UPDATE server.config SET
default_avatar_neutral_child = '$AVATAR_ID',
default_avatar_neutral_adult = '$AVATAR_ID',
default_avatar_male_child = '$AVATAR_ID',
default_avatar_male_adult = '$AVATAR_ID',
default_avatar_female_child = '$AVATAR_ID',
default_avatar_female_adult = '$AVATAR_ID',
updated_at = now()
WHERE id = '00000000-0000-0000-0000-000000000001'
" > /dev/null
echo " ✓ Set all 6 default avatar columns"
echo ""
# Verify
echo "=========================================="
echo "Verification"
echo "=========================================="
# Check avatar slots
echo "Avatar emotion slots populated:"
psql -d "$DB" -t -c "
SELECT
CASE WHEN l_skin_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' body (l_skin_4)',
CASE WHEN e_neutral_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' neutral',
CASE WHEN e_happy_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' happy',
CASE WHEN e_sad_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' sad',
CASE WHEN e_angry_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' angry',
CASE WHEN e_surprised_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' surprised',
CASE WHEN e_thinking_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' thinking',
CASE WHEN e_laughing_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' laughing',
CASE WHEN e_crying_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' crying',
CASE WHEN e_love_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' love',
CASE WHEN e_confused_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' confused',
CASE WHEN e_sleeping_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' sleeping',
CASE WHEN e_wink_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' wink'
FROM server.avatars WHERE slug = 'stock-avatar'
" | tr '|' '\n' | grep -v '^$' | sed 's/^ */ /'
echo ""
# Check server defaults
echo "Server config defaults:"
psql -d "$DB" -t -c "
SELECT
CASE WHEN default_avatar_neutral_adult IS NOT NULL THEN '✓' ELSE '✗' END || ' default_avatar_neutral_adult',
CASE WHEN default_avatar_neutral_child IS NOT NULL THEN '✓' ELSE '✗' END || ' default_avatar_neutral_child',
CASE WHEN default_avatar_male_adult IS NOT NULL THEN '✓' ELSE '✗' END || ' default_avatar_male_adult',
CASE WHEN default_avatar_male_child IS NOT NULL THEN '✓' ELSE '✗' END || ' default_avatar_male_child',
CASE WHEN default_avatar_female_adult IS NOT NULL THEN '✓' ELSE '✗' END || ' default_avatar_female_adult',
CASE WHEN default_avatar_female_child IS NOT NULL THEN '✓' ELSE '✗' END || ' default_avatar_female_child'
FROM server.config WHERE id = '00000000-0000-0000-0000-000000000001'
" | tr '|' '\n' | grep -v '^$' | sed 's/^ */ /'
echo ""
echo "=========================================="
echo "Stock avatar setup complete!"
echo "Avatar ID: $AVATAR_ID"
echo "=========================================="

View file

@ -0,0 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<g transform="scale(2.5)">
<style>
:root {
--face-primary: #FFCC00;
--face-highlight: #FFE566;
--face-shadow: #CC9900;
}
.face-primary { stop-color: var(--face-primary); }
.face-highlight { stop-color: var(--face-highlight); }
.face-shadow { stop-color: var(--face-shadow); }
.face-stroke { stroke: var(--face-shadow); }
.bevel-fill { fill: var(--face-primary); }
</style>
<defs>
<!-- Radial gradient for 3D sphere effect -->
<radialGradient id="faceGradient" cx="35%" cy="35%" r="65%">
<stop offset="0%" stop-color="#FFFFFF"/>
<stop offset="50%" class="face-highlight"/>
<stop offset="100%" class="face-primary"/>
</radialGradient>
<!-- Darker gradient for bottom edge (bevel effect) -->
<radialGradient id="shadowGradient" cx="50%" cy="0%" r="100%">
<stop offset="60%" class="face-primary"/>
<stop offset="100%" class="face-shadow"/>
</radialGradient>
<!-- Drop shadow filter -->
<filter id="dropShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.3"/>
</filter>
</defs>
<!-- Main face with gradient and shadow -->
<circle cx="24" cy="24" r="20" fill="url(#faceGradient)" class="face-stroke" stroke-width="1.5" filter="url(#dropShadow)"/>
<!-- Subtle bottom bevel overlay -->
<ellipse cx="24" cy="32" rx="18" ry="12" fill="url(#shadowGradient)" opacity="0.3"/>
<!-- Specular highlight (top-left light reflection) -->
<ellipse cx="16" cy="14" rx="6" ry="4" fill="#FFFFFF" opacity="0.6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

282
stock/avatar/index.html Normal file
View file

@ -0,0 +1,282 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Avatar Renderer</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #1a1a2e;
color: #eee;
margin: 0;
padding: 2rem;
min-height: 100vh;
}
h1 {
text-align: center;
margin-bottom: 2rem;
}
.controls {
display: flex;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.control-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.control-group label {
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
input[type="color"] {
width: 60px;
height: 40px;
border: none;
border-radius: 8px;
cursor: pointer;
}
.avatar-preview {
display: flex;
justify-content: center;
margin-bottom: 3rem;
}
.avatar-grid-3x3 {
display: grid;
grid-template-columns: repeat(3, 120px);
grid-template-rows: repeat(3, 120px);
gap: 0;
}
.avatar-grid-3x3 .cell {
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-grid-3x3 .cell > img {
width: 100%;
height: 100%;
object-fit: contain;
}
.avatar-container {
position: relative;
width: 100%;
height: 100%;
}
.avatar-container img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.avatar-grid {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
justify-content: center;
}
.avatar-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1rem;
background: #16213e;
border-radius: 12px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.avatar-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.avatar-card.selected {
outline: 3px solid #4cc9f0;
}
.avatar-card .avatar-small {
position: relative;
width: 120px;
height: 120px;
}
.avatar-card .avatar-small img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.avatar-card span {
font-size: 0.875rem;
text-transform: capitalize;
}
</style>
</head>
<body>
<h1>Avatar Renderer</h1>
<div class="controls">
<div class="control-group">
<label for="primaryColor">Face Color</label>
<input type="color" id="primaryColor" value="#FFCC00">
</div>
<div class="control-group">
<label for="highlightColor">Highlight</label>
<input type="color" id="highlightColor" value="#FFE566">
</div>
<div class="control-group">
<label for="shadowColor">Shadow</label>
<input type="color" id="shadowColor" value="#CC9900">
</div>
</div>
<div class="avatar-preview">
<div class="avatar-grid-3x3">
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell center">
<div class="avatar-container" id="mainAvatar">
<img src="face.svg" alt="Face base" class="face-layer">
<img src="smile.svg" alt="Expression" class="expression-layer">
</div>
</div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell">
<img src="body-torso.svg" alt="Torso">
</div>
<div class="cell"></div>
</div>
</div>
<h2 style="text-align: center; margin-bottom: 1rem;">Expressions</h2>
<div class="avatar-grid" id="avatarGrid"></div>
<script>
const expressions = [
'smile',
'neutral',
'angry',
'sad',
'laughing',
'surprised',
'confused',
'love',
'wink',
'thinking',
'sleeping',
'crying'
];
const avatarGrid = document.getElementById('avatarGrid');
const mainAvatar = document.getElementById('mainAvatar');
const primaryColorInput = document.getElementById('primaryColor');
const highlightColorInput = document.getElementById('highlightColor');
const shadowColorInput = document.getElementById('shadowColor');
let currentExpression = 'smile';
// Create avatar cards
expressions.forEach(expression => {
const card = document.createElement('div');
card.className = 'avatar-card' + (expression === 'smile' ? ' selected' : '');
card.dataset.expression = expression;
card.innerHTML = `
<div class="avatar-small">
<img src="face.svg" alt="Face base" class="face-layer">
<img src="${expression}.svg" alt="${expression}" class="expression-layer">
</div>
<span>${expression}</span>
`;
card.addEventListener('click', () => selectExpression(expression));
avatarGrid.appendChild(card);
});
function selectExpression(expression) {
currentExpression = expression;
// Update main preview
mainAvatar.querySelector('.expression-layer').src = `${expression}.svg`;
// Update selected state
document.querySelectorAll('.avatar-card').forEach(card => {
card.classList.toggle('selected', card.dataset.expression === expression);
});
}
// Color manipulation
async function updateColors() {
const primary = primaryColorInput.value;
const highlight = highlightColorInput.value;
const shadow = shadowColorInput.value;
// Fetch and modify the face SVG
const response = await fetch('face.svg');
const svgText = await response.text();
// Replace the CSS variable defaults with our colors
const modifiedSvg = svgText
.replace(/--face-primary:\s*#[A-Fa-f0-9]+/g, `--face-primary: ${primary}`)
.replace(/--face-highlight:\s*#[A-Fa-f0-9]+/g, `--face-highlight: ${highlight}`)
.replace(/--face-shadow:\s*#[A-Fa-f0-9]+/g, `--face-shadow: ${shadow}`)
// Also update the hardcoded gradient colors in defs
.replace(/stop-color="#FFFFFF"/g, 'stop-color="#FFFFFF"') // Keep white
.replace(/<stop offset="50%" class="face-highlight"\/>/g, `<stop offset="50%" stop-color="${highlight}"/>`)
.replace(/<stop offset="100%" class="face-primary"\/>/g, `<stop offset="100%" stop-color="${primary}"/>`)
.replace(/<stop offset="60%" class="face-primary"\/>/g, `<stop offset="60%" stop-color="${primary}"/>`)
.replace(/<stop offset="100%" class="face-shadow"\/>/g, `<stop offset="100%" stop-color="${shadow}"/>`);
// Create blob URL for modified SVG
const blob = new Blob([modifiedSvg], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
// Update all face layers
document.querySelectorAll('.face-layer').forEach(img => {
// Revoke old blob URL if exists
if (img.dataset.blobUrl) {
URL.revokeObjectURL(img.dataset.blobUrl);
}
img.src = url;
img.dataset.blobUrl = url;
});
}
primaryColorInput.addEventListener('input', updateColors);
highlightColorInput.addEventListener('input', updateColors);
shadowColorInput.addEventListener('input', updateColors);
</script>
</body>
</html>

View file

@ -429,6 +429,16 @@
<h3>Good Pol</h3> <h3>Good Pol</h3>
<div class="prop-items" id="goodpol-props" role="group" aria-label="Good Pol props"></div> <div class="prop-items" id="goodpol-props" role="group" aria-label="Good Pol props"></div>
</div> </div>
<div class="prop-category">
<h3>Screens</h3>
<div class="prop-items" id="screen-props" role="group" aria-label="Screen props"></div>
</div>
<div class="prop-category">
<h3>Keyboards</h3>
<div class="prop-items" id="keyboard-props" role="group" aria-label="Keyboard props"></div>
</div>
</div> </div>
</section> </section>
@ -548,7 +558,9 @@
soda: ['cola', 'lemonlime', 'orange', 'grape', 'rootbeer'], soda: ['cola', 'lemonlime', 'orange', 'grape', 'rootbeer'],
tea: ['iced', 'pot', 'cup', 'cup-empty', 'bag'], tea: ['iced', 'pot', 'cup', 'cup-empty', 'bag'],
misc: ['iou', 'signed-dollar', 'thankyou', 'yousuck'], misc: ['iou', 'signed-dollar', 'thankyou', 'yousuck'],
goodpol: ['cccp', 'china', 'palestine'] goodpol: ['cccp', 'china', 'palestine'],
screen: ['projector', 'projector-with-stand'],
keyboard: ['media']
}; };
// Flags // Flags
@ -638,6 +650,8 @@
const teaContainer = document.getElementById('tea-props'); const teaContainer = document.getElementById('tea-props');
const miscContainer = document.getElementById('misc-props'); const miscContainer = document.getElementById('misc-props');
const goodpolContainer = document.getElementById('goodpol-props'); const goodpolContainer = document.getElementById('goodpol-props');
const screenContainer = document.getElementById('screen-props');
const keyboardContainer = document.getElementById('keyboard-props');
for (const name of props.hookah) { for (const name of props.hookah) {
await loadPropPreview('hookah', name, hookahContainer); await loadPropPreview('hookah', name, hookahContainer);
@ -657,6 +671,12 @@
for (const name of props.goodpol) { for (const name of props.goodpol) {
await loadPropPreview('goodpol', name, goodpolContainer); await loadPropPreview('goodpol', name, goodpolContainer);
} }
for (const name of props.screen) {
await loadPropPreview('screen', name, screenContainer);
}
for (const name of props.keyboard) {
await loadPropPreview('keyboard', name, keyboardContainer);
}
// Select first prop by default // Select first prop by default
const firstCard = document.querySelector('#props-tab .prop-card'); const firstCard = document.querySelector('#props-tab .prop-card');

View file

@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<linearGradient id="keyboardBody" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#3a3a3a"/>
<stop offset="100%" stop-color="#2a2a2a"/>
</linearGradient>
<linearGradient id="keyTop" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#4a4a4a"/>
<stop offset="100%" stop-color="#3a3a3a"/>
</linearGradient>
<filter id="keyShadow" x="-10%" y="-10%" width="120%" height="130%">
<feDropShadow dx="0" dy="1" stdDeviation="0.5" flood-color="#000000" flood-opacity="0.4"/>
</filter>
</defs>
<!-- Keyboard body -->
<rect x="8" y="40" width="104" height="50" rx="4" fill="url(#keyboardBody)"/>
<rect x="8" y="40" width="104" height="50" rx="4" fill="none" stroke="#222" stroke-width="1"/>
<!-- Top row: Play, Pause, Stop -->
<!-- Play key -->
<rect x="14" y="46" width="22" height="16" rx="2" fill="url(#keyTop)" filter="url(#keyShadow)"/>
<polygon points="22,50 22,58 29,54" fill="#4CAF50"/>
<!-- Pause key -->
<rect x="40" y="46" width="22" height="16" rx="2" fill="url(#keyTop)" filter="url(#keyShadow)"/>
<rect x="47" y="50" width="3" height="8" fill="#FFC107"/>
<rect x="52" y="50" width="3" height="8" fill="#FFC107"/>
<!-- Stop key -->
<rect x="66" y="46" width="22" height="16" rx="2" fill="url(#keyTop)" filter="url(#keyShadow)"/>
<rect x="72" y="50" width="10" height="8" fill="#F44336"/>
<!-- Mute key -->
<rect x="92" y="46" width="14" height="16" rx="2" fill="url(#keyTop)" filter="url(#keyShadow)"/>
<!-- Speaker icon -->
<polygon points="95,52 97,52 100,49 100,59 97,56 95,56" fill="#fff"/>
<!-- X for mute -->
<line x1="101" y1="51" x2="104" y2="57" stroke="#F44336" stroke-width="1.5" stroke-linecap="round"/>
<line x1="104" y1="51" x2="101" y2="57" stroke="#F44336" stroke-width="1.5" stroke-linecap="round"/>
<!-- URL bar -->
<rect x="14" y="66" width="92" height="18" rx="2" fill="#fff" filter="url(#keyShadow)"/>
<rect x="14" y="66" width="92" height="18" rx="2" fill="none" stroke="#888" stroke-width="0.5"/>
<text x="18" y="78" font-family="monospace" font-size="7" fill="#4CAF50">https://</text>
<line x1="52" y1="69" x2="52" y2="81" stroke="#333" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<linearGradient id="screenSurface" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#FFFFFF"/>
<stop offset="100%" stop-color="#F0F0F0"/>
</linearGradient>
<linearGradient id="caseGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#4A4A4A"/>
<stop offset="100%" stop-color="#2A2A2A"/>
</linearGradient>
<filter id="screenShadow" x="-5%" y="-5%" width="110%" height="110%">
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.3"/>
</filter>
</defs>
<!-- Mounting bracket / case at top -->
<rect x="10" y="8" width="100" height="8" rx="2" fill="url(#caseGrad)"/>
<!-- Screen surface - 16:9 aspect ratio (96x54) -->
<rect x="12" y="18" width="96" height="54" fill="url(#screenSurface)" filter="url(#screenShadow)"/>
<!-- Screen border/frame -->
<rect x="12" y="18" width="96" height="54" fill="none" stroke="#333" stroke-width="1.5"/>
<!-- Bottom weight bar -->
<rect x="12" y="70" width="96" height="4" rx="1" fill="#3A3A3A"/>
<!-- Pull tab -->
<rect x="54" y="74" width="12" height="6" rx="1" fill="#555"/>
<circle cx="60" cy="80" r="3" fill="#666"/>
<circle cx="60" cy="80" r="1.5" fill="#444"/>
<!-- Tripod stand -->
<rect x="58" y="84" width="4" height="20" fill="#333"/>
<!-- Tripod legs -->
<line x1="60" y1="104" x2="30" y2="115" stroke="#333" stroke-width="3" stroke-linecap="round"/>
<line x1="60" y1="104" x2="90" y2="115" stroke="#333" stroke-width="3" stroke-linecap="round"/>
<line x1="60" y1="104" x2="60" y2="116" stroke="#333" stroke-width="3" stroke-linecap="round"/>
<!-- Rubber feet -->
<circle cx="30" cy="115" r="2" fill="#222"/>
<circle cx="90" cy="115" r="2" fill="#222"/>
<circle cx="60" cy="116" r="2" fill="#222"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<linearGradient id="ustScreen" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#FAFAFA"/>
<stop offset="50%" stop-color="#FFFFFF"/>
<stop offset="100%" stop-color="#F5F5F5"/>
</linearGradient>
<filter id="ustShadow" x="-5%" y="-5%" width="110%" height="120%">
<feDropShadow dx="0" dy="3" stdDeviation="3" flood-color="#000000" flood-opacity="0.25"/>
</filter>
</defs>
<!-- Thin black frame - 16:9 ratio (106x60) centered -->
<rect x="7" y="30" width="106" height="60" rx="1" fill="#1a1a1a" filter="url(#ustShadow)"/>
<!-- Screen surface - 16:9 with thin bezel -->
<rect x="9" y="32" width="102" height="56" fill="url(#ustScreen)"/>
<!-- Subtle inner shadow on screen edges -->
<rect x="9" y="32" width="102" height="56" fill="none" stroke="#E0E0E0" stroke-width="0.5"/>
<!-- Frame edge highlight (top) -->
<line x1="8" y1="30" x2="112" y2="30" stroke="#333" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -88,6 +88,12 @@ get_tags() {
misc) misc)
echo '["misc", "droppable"]' echo '["misc", "droppable"]'
;; ;;
screen)
echo '["screen", "projector", "droppable"]'
;;
keyboard)
echo '["keyboard", "media", "droppable"]'
;;
*) *)
echo '["prop", "droppable"]' echo '["prop", "droppable"]'
;; ;;