update stock props
BIN
stock/.playwright-mcp/page-2026-01-22T07-30-13-408Z.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
stock/.playwright-mcp/page-2026-01-22T07-34-16-652Z.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
stock/.playwright-mcp/page-2026-01-22T07-45-58-532Z.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
26
stock/avatar/body-arm-left.svg
Normal 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 |
26
stock/avatar/body-arm-right.svg
Normal 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 |
32
stock/avatar/body-hand-left.svg
Normal 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 |
32
stock/avatar/body-hand-right.svg
Normal 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 |
17
stock/avatar/body-neck.svg
Normal 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 |
24
stock/avatar/body-sparkle.svg
Normal 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 |
34
stock/avatar/body-torso.svg
Normal 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 |
238
stock/avatar/create-stock-avatar.sh
Executable 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 "=========================================="
|
||||
45
stock/avatar/face-backup.svg
Normal 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
|
|
@ -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>
|
||||
|
|
@ -429,6 +429,16 @@
|
|||
<h3>Good Pol</h3>
|
||||
<div class="prop-items" id="goodpol-props" role="group" aria-label="Good Pol props"></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>
|
||||
</section>
|
||||
|
||||
|
|
@ -548,7 +558,9 @@
|
|||
soda: ['cola', 'lemonlime', 'orange', 'grape', 'rootbeer'],
|
||||
tea: ['iced', 'pot', 'cup', 'cup-empty', 'bag'],
|
||||
misc: ['iou', 'signed-dollar', 'thankyou', 'yousuck'],
|
||||
goodpol: ['cccp', 'china', 'palestine']
|
||||
goodpol: ['cccp', 'china', 'palestine'],
|
||||
screen: ['projector', 'projector-with-stand'],
|
||||
keyboard: ['media']
|
||||
};
|
||||
|
||||
// Flags
|
||||
|
|
@ -638,6 +650,8 @@
|
|||
const teaContainer = document.getElementById('tea-props');
|
||||
const miscContainer = document.getElementById('misc-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) {
|
||||
await loadPropPreview('hookah', name, hookahContainer);
|
||||
|
|
@ -657,6 +671,12 @@
|
|||
for (const name of props.goodpol) {
|
||||
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
|
||||
const firstCard = document.querySelector('#props-tab .prop-card');
|
||||
|
|
|
|||
47
stock/props/keyboard-media.svg
Normal 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 |
44
stock/props/screen-projector-with-stand.svg
Normal 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 |
24
stock/props/screen-projector.svg
Normal 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 |
|
|
@ -88,6 +88,12 @@ get_tags() {
|
|||
misc)
|
||||
echo '["misc", "droppable"]'
|
||||
;;
|
||||
screen)
|
||||
echo '["screen", "projector", "droppable"]'
|
||||
;;
|
||||
keyboard)
|
||||
echo '["keyboard", "media", "droppable"]'
|
||||
;;
|
||||
*)
|
||||
echo '["prop", "droppable"]'
|
||||
;;
|
||||
|
|
|
|||