Files
gamercomp/frontend/public/asset-browser.html
T
2026-04-30 02:14:25 +00:00

1843 lines
65 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Asset Browser - GamerComp Asset Library</title>
<style>
:root {
--bg-dark: #0f0f1a;
--bg-card: #1a1a2e;
--bg-hover: #252542;
--accent: #6366f1;
--accent-hover: #818cf8;
--text: #e2e8f0;
--text-muted: #94a3b8;
--border: #334155;
--success: #22c55e;
--warning: #f59e0b;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-dark);
color: var(--text);
min-height: 100vh;
}
/* Header */
.header {
background: var(--bg-card);
border-bottom: 1px solid var(--border);
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
}
.header h1 {
font-size: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.header h1 span {
color: var(--accent);
}
.header-stats {
display: flex;
gap: 2rem;
font-size: 0.875rem;
color: var(--text-muted);
}
.header-stats strong {
color: var(--accent);
}
/* Tabs */
.tabs {
display: flex;
background: var(--bg-card);
border-bottom: 1px solid var(--border);
padding: 0 2rem;
}
.tab {
padding: 1rem 1.5rem;
cursor: pointer;
border-bottom: 2px solid transparent;
color: var(--text-muted);
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.tab:hover {
color: var(--text);
background: var(--bg-hover);
}
.tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
.tab-icon {
font-size: 1.25rem;
}
/* Main Layout */
.main {
display: flex;
min-height: calc(100vh - 120px);
}
/* Sidebar Filters */
.sidebar {
width: 280px;
background: var(--bg-card);
border-right: 1px solid var(--border);
padding: 1.5rem;
overflow-y: auto;
flex-shrink: 0;
}
.filter-section {
margin-bottom: 1.5rem;
}
.filter-section h3 {
font-size: 0.75rem;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0.75rem;
letter-spacing: 0.05em;
}
.filter-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.filter-chip {
padding: 0.375rem 0.75rem;
background: var(--bg-hover);
border: 1px solid var(--border);
border-radius: 9999px;
font-size: 0.8125rem;
cursor: pointer;
transition: all 0.15s;
}
.filter-chip:hover {
border-color: var(--accent);
color: var(--accent);
}
.filter-chip.active {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.search-input {
width: 100%;
padding: 0.625rem 1rem;
background: var(--bg-dark);
border: 1px solid var(--border);
border-radius: 0.5rem;
color: var(--text);
font-size: 0.875rem;
}
.search-input:focus {
outline: none;
border-color: var(--accent);
}
/* Content Grid */
.content {
flex: 1;
padding: 1.5rem 2rem;
overflow-y: auto;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.content-header h2 {
font-size: 1.25rem;
}
.result-count {
color: var(--text-muted);
font-size: 0.875rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.25rem;
}
/* Asset Cards */
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 0.75rem;
overflow: hidden;
transition: all 0.2s;
cursor: pointer;
}
.card:hover {
border-color: var(--accent);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(99, 102, 241, 0.15);
}
.card-preview {
height: 160px;
background: var(--bg-dark);
position: relative;
overflow: hidden;
}
.card-preview canvas {
width: 100%;
height: 100%;
}
.card-body {
padding: 1rem;
}
.card-title {
font-weight: 600;
margin-bottom: 0.25rem;
}
.card-subtitle {
font-size: 0.8125rem;
color: var(--text-muted);
margin-bottom: 0.75rem;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.tag {
padding: 0.25rem 0.5rem;
background: var(--bg-hover);
border-radius: 0.25rem;
font-size: 0.6875rem;
color: var(--text-muted);
}
.tag.mood {
background: rgba(99, 102, 241, 0.2);
color: var(--accent);
}
/* Preview Panel */
.preview-panel {
position: fixed;
top: 0;
right: 0;
width: 450px;
height: 100vh;
background: var(--bg-card);
border-left: 1px solid var(--border);
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 200;
display: flex;
flex-direction: column;
}
.preview-panel.open {
transform: translateX(0);
}
.preview-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-header h3 {
font-size: 1.125rem;
}
.close-btn {
background: none;
border: none;
color: var(--text-muted);
font-size: 1.5rem;
cursor: pointer;
padding: 0.25rem;
}
.close-btn:hover {
color: var(--text);
}
.preview-content {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
}
.preview-canvas-container {
background: var(--bg-dark);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
aspect-ratio: 16/9;
display: flex;
align-items: center;
justify-content: center;
}
.preview-canvas-container canvas {
max-width: 100%;
max-height: 100%;
}
.preview-meta {
margin-bottom: 1.5rem;
}
.preview-meta dt {
font-size: 0.75rem;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0.25rem;
}
.preview-meta dd {
margin-bottom: 1rem;
}
.code-block {
background: var(--bg-dark);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1rem;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.75rem;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.copy-btn {
background: var(--accent);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.75rem;
}
.copy-btn:hover {
background: var(--accent-hover);
}
.copy-btn.copied {
background: var(--success);
}
/* Music Controls */
.music-controls {
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
}
.play-btn {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--accent);
border: none;
color: white;
font-size: 1.25rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.play-btn:hover {
background: var(--accent-hover);
}
.play-btn.playing {
background: var(--warning);
}
.volume-slider {
flex: 1;
display: flex;
align-items: center;
gap: 0.75rem;
}
.volume-slider input {
flex: 1;
height: 4px;
-webkit-appearance: none;
background: var(--border);
border-radius: 2px;
}
.volume-slider input::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: var(--accent);
border-radius: 50%;
cursor: pointer;
}
/* Avatar Builder */
.avatar-builder {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1.5rem;
}
.avatar-option {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.avatar-option label {
font-size: 0.75rem;
text-transform: uppercase;
color: var(--text-muted);
}
.avatar-option select {
padding: 0.5rem;
background: var(--bg-dark);
border: 1px solid var(--border);
border-radius: 0.375rem;
color: var(--text);
font-size: 0.875rem;
}
/* Preset Explorer */
.preset-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 0.75rem;
overflow: hidden;
}
.preset-header {
padding: 1rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.preset-previews {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: var(--border);
}
.preset-preview-item {
background: var(--bg-dark);
aspect-ratio: 4/3;
position: relative;
}
.preset-preview-item canvas {
width: 100%;
height: 100%;
}
.preset-preview-label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.7);
padding: 0.25rem 0.5rem;
font-size: 0.625rem;
text-align: center;
}
/* Random Generator */
.random-generator {
background: linear-gradient(135deg, var(--accent), #8b5cf6);
border-radius: 0.75rem;
padding: 1.5rem;
text-align: center;
margin-bottom: 2rem;
}
.random-generator h3 {
margin-bottom: 0.5rem;
}
.random-generator p {
font-size: 0.875rem;
opacity: 0.9;
margin-bottom: 1rem;
}
.random-btn {
background: white;
color: var(--accent);
border: none;
padding: 0.75rem 2rem;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.random-btn:hover {
transform: scale(1.05);
}
/* Loading Spinner */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 4rem;
color: var(--text-muted);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 1rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Overlay */
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 150;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.overlay.visible {
opacity: 1;
pointer-events: auto;
}
/* Animation Control */
.animation-select {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.anim-btn {
padding: 0.375rem 0.75rem;
background: var(--bg-hover);
border: 1px solid var(--border);
border-radius: 0.375rem;
color: var(--text);
cursor: pointer;
font-size: 0.75rem;
}
.anim-btn:hover, .anim-btn.active {
background: var(--accent);
border-color: var(--accent);
}
/* Responsive */
@media (max-width: 1024px) {
.sidebar {
width: 240px;
}
.preview-panel {
width: 380px;
}
}
@media (max-width: 768px) {
.header {
flex-direction: column;
gap: 0.75rem;
}
.header-stats {
font-size: 0.75rem;
gap: 1rem;
}
.main {
flex-direction: column;
}
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--border);
}
.preview-panel {
width: 100%;
}
.tabs {
overflow-x: auto;
padding: 0 1rem;
}
.tab {
padding: 0.75rem 1rem;
font-size: 0.875rem;
}
}
</style>
</head>
<body>
<header class="header">
<h1><span>GamerComp</span> Asset Browser</h1>
<div class="header-stats">
<span><strong>160</strong> Music Tracks</span>
<span><strong>120</strong> Backgrounds</span>
<span><strong>26</strong> Contexts</span>
<span><strong>220</strong> Presets</span>
</div>
</header>
<nav class="tabs">
<div class="tab active" data-tab="music">
<span class="tab-icon">🎵</span>
<span>Music</span>
</div>
<div class="tab" data-tab="backgrounds">
<span class="tab-icon">🏞️</span>
<span>Backgrounds</span>
</div>
<div class="tab" data-tab="avatars">
<span class="tab-icon">👤</span>
<span>Avatars</span>
</div>
<div class="tab" data-tab="contexts">
<span class="tab-icon">🎮</span>
<span>Contexts</span>
</div>
<div class="tab" data-tab="presets">
<span class="tab-icon">📦</span>
<span>Presets</span>
</div>
</nav>
<main class="main">
<!-- Sidebar Filters -->
<aside class="sidebar" id="sidebar">
<!-- Filters populated by JS per tab -->
</aside>
<!-- Content Grid -->
<section class="content" id="content">
<div class="loading">
<div class="spinner"></div>
Loading assets...
</div>
</section>
</main>
<!-- Preview Panel -->
<div class="overlay" id="overlay"></div>
<aside class="preview-panel" id="previewPanel">
<div class="preview-header">
<h3 id="previewTitle">Asset Details</h3>
<button class="close-btn" id="closePreview">&times;</button>
</div>
<div class="preview-content" id="previewContent">
<!-- Dynamic content -->
</div>
</aside>
<!-- Load Asset Libraries -->
<script src="/assets/backgrounds/backgroundEngine.js"></script>
<script src="/assets/backgrounds/effects.js"></script>
<script src="/assets/backgrounds/themes/space.js"></script>
<script src="/assets/backgrounds/themes/nature.js"></script>
<script src="/assets/backgrounds/themes/urban.js"></script>
<script src="/assets/backgrounds/themes/fantasy.js"></script>
<script src="/assets/backgrounds/themes/abstract.js"></script>
<script src="/assets/backgrounds/themes/retro.js"></script>
<script src="/assets/backgrounds/themes/indoor.js"></script>
<script src="/assets/backgrounds/themes/weather.js"></script>
<script src="/assets/backgrounds/themes/sports.js"></script>
<script src="/assets/backgrounds/themes/seasonal.js"></script>
<script src="/assets/backgrounds/themes/underwater.js"></script>
<script src="/assets/backgrounds/themes/sky.js"></script>
<script src="/assets/backgrounds/themes/mechanical.js"></script>
<script src="/assets/backgrounds/themes/spooky.js"></script>
<script src="/assets/backgrounds/themes/colorful.js"></script>
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
<script src="/assets/music/moodCategories.js"></script>
<script src="/assets/music/musicEngine.js"></script>
<script src="/assets/music/library/songs1-80.js"></script>
<script src="/assets/music/library/songs81-160.js"></script>
<script src="/assets/avatar/avatarData.js"></script>
<script src="/assets/avatar/accessories.js"></script>
<script src="/assets/avatar/animations.js"></script>
<script src="/assets/avatar/avatarRenderer.js"></script>
<script src="/assets/avatar/accessoryRenderer.js"></script>
<script src="/assets/avatar/avatarSystem.js"></script>
<script src="/assets/avatar/contexts/vehicles.js"></script>
<script src="/assets/avatar/contexts/costumes.js"></script>
<script src="/assets/avatar/contexts/pure.js"></script>
<script src="/assets/avatar/contextRenderer.js"></script>
<script src="/assets/avatar/contextCatalog.js"></script>
<script src="/assets/assetManager.js"></script>
<script src="/assets/assetCatalog.js"></script>
<script src="/assets/presets.js"></script>
<script src="/assets/compatibility.js"></script>
<script>
// ═══════════════════════════════════════════════════════════════════════
// ASSET BROWSER APPLICATION
// ═══════════════════════════════════════════════════════════════════════
const AssetBrowser = (function() {
// State
let currentTab = 'music';
let activeFilters = {};
let selectedAsset = null;
let audioContext = null;
let currentMusic = null;
let animationFrame = null;
// DOM Elements
const sidebar = document.getElementById('sidebar');
const content = document.getElementById('content');
const previewPanel = document.getElementById('previewPanel');
const previewTitle = document.getElementById('previewTitle');
const previewContent = document.getElementById('previewContent');
const overlay = document.getElementById('overlay');
// ─── Tab Definitions ─────────────────────────────────────────
const tabConfigs = {
music: {
title: 'Music Library',
filters: [
{ key: 'mood', label: 'Mood', options: ['Epic', 'Chill', 'Intense', 'Playful', 'Mysterious', 'Heroic', 'Quirky', 'Ambient'] },
{ key: 'energy', label: 'Energy', options: ['Low (1-3)', 'Medium (4-6)', 'High (7-10)'] },
{ key: 'tempo', label: 'Tempo', options: ['Slow', 'Medium', 'Fast'] }
],
getData: getMusicData,
renderCard: renderMusicCard,
renderPreview: renderMusicPreview
},
backgrounds: {
title: 'Background Library',
filters: [
{ key: 'theme', label: 'Theme', options: ['space', 'nature', 'urban', 'fantasy', 'abstract', 'retro', 'indoor', 'weather', 'sports', 'seasonal', 'underwater', 'sky', 'mechanical', 'spooky', 'colorful'] },
{ key: 'mood', label: 'Mood', options: ['wonder', 'tense', 'serene', 'energetic', 'focused', 'playful', 'mysterious', 'nostalgic', 'cozy', 'spooky', 'epic', 'dreamy', 'vibrant', 'industrial', 'tropical', 'festive'] }
],
getData: getBackgroundData,
renderCard: renderBackgroundCard,
renderPreview: renderBackgroundPreview
},
avatars: {
title: 'Avatar System',
filters: [
{ key: 'mode', label: 'Render Mode', options: ['HEAD_ONLY', 'HEAD_AND_SHOULDERS', 'UPPER_BODY', 'FULL_BODY'] }
],
getData: getAvatarData,
renderCard: renderAvatarCard,
renderPreview: renderAvatarPreview
},
contexts: {
title: 'Game Contexts',
filters: [
{ key: 'category', label: 'Category', options: ['vehicles', 'costumes', 'pure'] },
{ key: 'mode', label: 'Avatar Mode', options: ['HEAD_ONLY', 'HEAD_AND_SHOULDERS', 'UPPER_BODY', 'FULL_BODY'] }
],
getData: getContextData,
renderCard: renderContextCard,
renderPreview: renderContextPreview
},
presets: {
title: 'Asset Presets',
filters: [
{ key: 'style', label: 'Game Style', options: ['action', 'puzzle', 'story', 'racing', 'pet', 'trivia', 'music', 'creative', 'rpg', 'sports', 'physics'] }
],
getData: getPresetData,
renderCard: renderPresetCard,
renderPreview: renderPresetPreview
}
};
// ─── Data Getters ────────────────────────────────────────────
function getMusicData() {
if (!window.MusicEngine) return [];
const catalog = window.MusicEngine.getCatalog ? window.MusicEngine.getCatalog() : [];
return catalog.filter(song => {
if (activeFilters.mood && song.mood !== activeFilters.mood) return false;
if (activeFilters.energy) {
const e = song.energy || 5;
if (activeFilters.energy === 'Low (1-3)' && e > 3) return false;
if (activeFilters.energy === 'Medium (4-6)' && (e < 4 || e > 6)) return false;
if (activeFilters.energy === 'High (7-10)' && e < 7) return false;
}
if (activeFilters.tempo) {
const bpm = song.bpm || 120;
if (activeFilters.tempo === 'Slow' && bpm > 90) return false;
if (activeFilters.tempo === 'Medium' && (bpm < 90 || bpm > 140)) return false;
if (activeFilters.tempo === 'Fast' && bpm < 140) return false;
}
if (activeFilters.search) {
const s = activeFilters.search.toLowerCase();
return song.title.toLowerCase().includes(s) ||
song.mood.toLowerCase().includes(s) ||
(song.tags && song.tags.some(t => t.toLowerCase().includes(s)));
}
return true;
});
}
function getBackgroundData() {
if (!window.BackgroundEngine) return [];
const catalog = window.BackgroundEngine.getCatalog ? window.BackgroundEngine.getCatalog() : [];
return catalog.filter(bg => {
if (activeFilters.theme && bg.theme !== activeFilters.theme) return false;
if (activeFilters.mood && bg.mood !== activeFilters.mood) return false;
if (activeFilters.search) {
const s = activeFilters.search.toLowerCase();
return bg.theme.toLowerCase().includes(s) ||
bg.variant.toLowerCase().includes(s) ||
(bg.tags && bg.tags.some(t => t.toLowerCase().includes(s)));
}
return true;
});
}
function getAvatarData() {
// Avatar tab shows animation/accessory demos, not individual data items
const modes = ['HEAD_ONLY', 'HEAD_AND_SHOULDERS', 'UPPER_BODY', 'FULL_BODY'];
if (activeFilters.mode) {
return modes.filter(m => m === activeFilters.mode).map(m => ({ mode: m }));
}
return modes.map(m => ({ mode: m }));
}
function getContextData() {
if (!window.ContextCatalog) return [];
const catalog = window.ContextCatalog.getAll ? window.ContextCatalog.getAll() : [];
return catalog.filter(ctx => {
if (activeFilters.category && ctx.category !== activeFilters.category) return false;
if (activeFilters.mode && ctx.avatarMode !== activeFilters.mode) return false;
if (activeFilters.search) {
const s = activeFilters.search.toLowerCase();
return ctx.id.toLowerCase().includes(s) ||
ctx.name.toLowerCase().includes(s) ||
(ctx.tags && ctx.tags.some(t => t.toLowerCase().includes(s)));
}
return true;
});
}
function getPresetData() {
if (!window.AssetPresets) return [];
const presets = window.AssetPresets.getAll ? window.AssetPresets.getAll() : [];
return presets.filter(p => {
if (activeFilters.style && p.style !== activeFilters.style) return false;
if (activeFilters.search) {
const s = activeFilters.search.toLowerCase();
return p.name.toLowerCase().includes(s) ||
p.style.toLowerCase().includes(s);
}
return true;
});
}
// ─── Card Renderers ──────────────────────────────────────────
function renderMusicCard(song) {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<div class="card-preview" style="background: linear-gradient(135deg, ${getMoodColor(song.mood)}, ${getMoodColorDark(song.mood)}); display: flex; align-items: center; justify-content: center;">
<span style="font-size: 4rem; opacity: 0.8;">🎵</span>
</div>
<div class="card-body">
<div class="card-title">${song.title || song.id}</div>
<div class="card-subtitle">${song.mood}${song.bpm || 120} BPM • Energy ${song.energy || 5}</div>
<div class="card-tags">
<span class="tag mood">${song.mood}</span>
${(song.tags || []).slice(0, 3).map(t => `<span class="tag">${t}</span>`).join('')}
</div>
</div>
`;
card.onclick = () => openPreview('music', song);
return card;
}
function renderBackgroundCard(bg) {
const card = document.createElement('div');
card.className = 'card';
const preview = document.createElement('div');
preview.className = 'card-preview';
const canvas = document.createElement('canvas');
canvas.width = 320;
canvas.height = 180;
preview.appendChild(canvas);
card.appendChild(preview);
card.innerHTML += `
<div class="card-body">
<div class="card-title">${bg.theme} / ${bg.variant}</div>
<div class="card-subtitle">${bg.description ? bg.description.slice(0, 60) + '...' : ''}</div>
<div class="card-tags">
<span class="tag mood">${bg.mood || 'none'}</span>
${(bg.tags || []).slice(0, 3).map(t => `<span class="tag">${t}</span>`).join('')}
</div>
</div>
`;
// Render background preview
requestAnimationFrame(() => {
if (window.BackgroundEngine && window.BackgroundEngine.render) {
const ctx = canvas.getContext('2d');
window.BackgroundEngine.render(ctx, bg.theme, bg.variant, canvas.width, canvas.height, 0);
}
});
card.onclick = () => openPreview('backgrounds', bg);
return card;
}
function renderAvatarCard(item) {
const card = document.createElement('div');
card.className = 'card';
const dims = {
'HEAD_ONLY': { w: 64, h: 64 },
'HEAD_AND_SHOULDERS': { w: 64, h: 96 },
'UPPER_BODY': { w: 64, h: 128 },
'FULL_BODY': { w: 64, h: 160 }
};
const d = dims[item.mode];
card.innerHTML = `
<div class="card-preview" style="display: flex; align-items: center; justify-content: center;">
<canvas id="avatar-${item.mode}" width="${d.w * 2}" height="${d.h * 2}" style="image-rendering: pixelated;"></canvas>
</div>
<div class="card-body">
<div class="card-title">${item.mode.replace(/_/g, ' ')}</div>
<div class="card-subtitle">${d.w}×${d.h} pixels</div>
<div class="card-tags">
<span class="tag">Canvas 2D</span>
<span class="tag">Procedural</span>
</div>
</div>
`;
// Render avatar preview
requestAnimationFrame(() => {
const canvas = document.getElementById(`avatar-${item.mode}`);
if (canvas && window.AvatarSystem) {
const ctx = canvas.getContext('2d');
ctx.scale(2, 2);
const testAvatar = {
faceShape: 'round',
skinTone: 'medium',
hairStyle: 'short',
hairColor: '#3d2314',
eyeShape: 'round',
eyeColor: '#4a7c59',
noseShape: 'small',
mouthShape: 'smile'
};
if (window.AvatarSystem.render) {
window.AvatarSystem.render(ctx, testAvatar, item.mode, 'idle', 0);
}
}
});
card.onclick = () => openPreview('avatars', item);
return card;
}
function renderContextCard(ctx) {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<div class="card-preview" style="display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #1e3a5f, #0f1d30);">
<canvas id="ctx-${ctx.id}" width="200" height="150"></canvas>
</div>
<div class="card-body">
<div class="card-title">${ctx.name || ctx.id}</div>
<div class="card-subtitle">${ctx.category}${ctx.avatarMode}</div>
<div class="card-tags">
<span class="tag mood">${ctx.category}</span>
${(ctx.tags || []).slice(0, 2).map(t => `<span class="tag">${t}</span>`).join('')}
</div>
</div>
`;
// Render context preview
requestAnimationFrame(() => {
const canvas = document.getElementById(`ctx-${ctx.id}`);
if (canvas && window.ContextRenderer) {
const c = canvas.getContext('2d');
const testAvatar = {
faceShape: 'round',
skinTone: 'medium',
hairStyle: 'short',
hairColor: '#3d2314',
eyeShape: 'round',
eyeColor: '#4a7c59'
};
if (window.ContextRenderer.render) {
window.ContextRenderer.render(c, ctx.id, testAvatar, 100, 75, 'idle', 0);
}
}
});
card.onclick = () => openPreview('contexts', ctx);
return card;
}
function renderPresetCard(preset) {
const card = document.createElement('div');
card.className = 'card preset-card';
card.innerHTML = `
<div class="preset-header">
<div>
<div class="card-title">${preset.name}</div>
<div class="card-subtitle">${preset.style}</div>
</div>
<span class="tag mood">${preset.style}</span>
</div>
<div class="card-body">
<div class="card-tags">
<span class="tag">🎵 ${preset.music?.mood || 'N/A'}</span>
<span class="tag">🏞️ ${preset.background?.theme || 'N/A'}</span>
<span class="tag">🎮 ${preset.context?.id || 'N/A'}</span>
</div>
</div>
`;
card.onclick = () => openPreview('presets', preset);
return card;
}
// ─── Preview Renderers ───────────────────────────────────────
function renderMusicPreview(song) {
return `
<div class="music-controls">
<button class="play-btn" id="playBtn" onclick="AssetBrowser.toggleMusic('${song.id}')">▶</button>
<div class="volume-slider">
<span>🔊</span>
<input type="range" min="0" max="100" value="70" id="volumeSlider" onchange="AssetBrowser.setVolume(this.value)">
</div>
</div>
<dl class="preview-meta">
<dt>Title</dt>
<dd>${song.title || song.id}</dd>
<dt>Mood</dt>
<dd>${song.mood}</dd>
<dt>BPM</dt>
<dd>${song.bpm || 120}</dd>
<dt>Energy Level</dt>
<dd>${song.energy || 5} / 10</dd>
<dt>Tags</dt>
<dd>${(song.tags || []).join(', ') || 'None'}</dd>
<dt>Description</dt>
<dd>${song.description || 'No description available'}</dd>
</dl>
<h4 style="margin-bottom: 0.5rem;">Usage Code</h4>
<div class="code-block">GAME_CONFIG = {
assets: {
music: {
id: "${song.id}",
mood: "${song.mood}",
energy: ${song.energy || 5},
enabled: true
}
}
};</div>
<button class="copy-btn" onclick="AssetBrowser.copyCode(this, 'music', '${song.id}')">📋 Copy Code</button>
`;
}
function renderBackgroundPreview(bg) {
return `
<div class="preview-canvas-container">
<canvas id="bgPreviewCanvas" width="400" height="225"></canvas>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="checkbox" id="animateToggle" checked onchange="AssetBrowser.toggleBgAnimation(this.checked)">
Animate Background
</label>
</div>
<dl class="preview-meta">
<dt>Theme</dt>
<dd>${bg.theme}</dd>
<dt>Variant</dt>
<dd>${bg.variant}</dd>
<dt>Mood</dt>
<dd>${bg.mood || 'Not specified'}</dd>
<dt>Colors</dt>
<dd style="display: flex; gap: 0.5rem;">
${(bg.colors || ['#1a1a2e', '#16213e', '#0f3460', '#e94560']).map(c =>
`<span style="width: 24px; height: 24px; background: ${c}; border-radius: 4px; border: 1px solid var(--border);"></span>`
).join('')}
</dd>
<dt>Tags</dt>
<dd>${(bg.tags || []).join(', ') || 'None'}</dd>
<dt>Description</dt>
<dd>${bg.description || 'No description available'}</dd>
</dl>
<h4 style="margin-bottom: 0.5rem;">Usage Code</h4>
<div class="code-block">GAME_CONFIG = {
assets: {
background: {
theme: "${bg.theme}",
variant: "${bg.variant}",
animated: true,
effects: ["particles"]
}
}
};</div>
<button class="copy-btn" onclick="AssetBrowser.copyCode(this, 'background', '${bg.theme}', '${bg.variant}')">📋 Copy Code</button>
`;
}
function renderAvatarPreview(item) {
const dims = {
'HEAD_ONLY': { w: 64, h: 64 },
'HEAD_AND_SHOULDERS': { w: 64, h: 96 },
'UPPER_BODY': { w: 64, h: 128 },
'FULL_BODY': { w: 64, h: 160 }
};
const d = dims[item.mode];
return `
<div class="preview-canvas-container" style="aspect-ratio: 1;">
<canvas id="avatarPreviewCanvas" width="${d.w * 3}" height="${d.h * 3}" style="image-rendering: pixelated;"></canvas>
</div>
<div class="avatar-builder">
<div class="avatar-option">
<label>Face Shape</label>
<select id="faceShape" onchange="AssetBrowser.updateAvatarPreview()">
<option value="round">Round</option>
<option value="oval">Oval</option>
<option value="square">Square</option>
<option value="heart">Heart</option>
</select>
</div>
<div class="avatar-option">
<label>Skin Tone</label>
<select id="skinTone" onchange="AssetBrowser.updateAvatarPreview()">
<option value="light">Light</option>
<option value="medium">Medium</option>
<option value="tan">Tan</option>
<option value="dark">Dark</option>
</select>
</div>
<div class="avatar-option">
<label>Hair Style</label>
<select id="hairStyle" onchange="AssetBrowser.updateAvatarPreview()">
<option value="short">Short</option>
<option value="medium">Medium</option>
<option value="long">Long</option>
<option value="curly">Curly</option>
<option value="spiky">Spiky</option>
<option value="ponytail">Ponytail</option>
</select>
</div>
<div class="avatar-option">
<label>Hair Color</label>
<select id="hairColor" onchange="AssetBrowser.updateAvatarPreview()">
<option value="#3d2314">Brown</option>
<option value="#1a1a1a">Black</option>
<option value="#d4a574">Blonde</option>
<option value="#8b4513">Auburn</option>
<option value="#ff6b6b">Red</option>
<option value="#6366f1">Purple</option>
</select>
</div>
</div>
<h4 style="margin-bottom: 0.5rem;">Animation</h4>
<div class="animation-select" id="animationSelect">
<button class="anim-btn active" data-anim="idle">Idle</button>
<button class="anim-btn" data-anim="walk">Walk</button>
<button class="anim-btn" data-anim="run">Run</button>
<button class="anim-btn" data-anim="jump">Jump</button>
<button class="anim-btn" data-anim="hurt">Hurt</button>
<button class="anim-btn" data-anim="celebrate">Celebrate</button>
</div>
<dl class="preview-meta" style="margin-top: 1.5rem;">
<dt>Render Mode</dt>
<dd>${item.mode.replace(/_/g, ' ')}</dd>
<dt>Dimensions</dt>
<dd>${d.w} × ${d.h} pixels</dd>
<dt>Description</dt>
<dd>${getModeDescription(item.mode)}</dd>
</dl>
<h4 style="margin-bottom: 0.5rem;">Usage Code</h4>
<div class="code-block">GAME_CONFIG = {
assets: {
avatarContext: {
mode: "${item.mode}",
context: "platformer-standard",
showAccessories: true
}
}
};</div>
<button class="copy-btn" onclick="AssetBrowser.copyCode(this, 'avatar', '${item.mode}')">📋 Copy Code</button>
`;
}
function renderContextPreview(ctx) {
return `
<div class="preview-canvas-container">
<canvas id="contextPreviewCanvas" width="300" height="300"></canvas>
</div>
<h4 style="margin-bottom: 0.5rem;">Animation</h4>
<div class="animation-select" id="contextAnimSelect">
<button class="anim-btn active" data-anim="idle">Idle</button>
<button class="anim-btn" data-anim="move">Move</button>
<button class="anim-btn" data-anim="action">Action</button>
</div>
<dl class="preview-meta" style="margin-top: 1.5rem;">
<dt>Context ID</dt>
<dd>${ctx.id}</dd>
<dt>Name</dt>
<dd>${ctx.name || ctx.id}</dd>
<dt>Category</dt>
<dd>${ctx.category}</dd>
<dt>Avatar Mode</dt>
<dd>${ctx.avatarMode}</dd>
<dt>Best For</dt>
<dd>${ctx.bestFor || 'General gameplay'}</dd>
<dt>Tags</dt>
<dd>${(ctx.tags || []).join(', ') || 'None'}</dd>
</dl>
<h4 style="margin-bottom: 0.5rem;">Usage Code</h4>
<div class="code-block">GAME_CONFIG = {
assets: {
avatarContext: {
mode: "${ctx.avatarMode}",
context: "${ctx.id}",
showAccessories: true
}
}
};</div>
<button class="copy-btn" onclick="AssetBrowser.copyCode(this, 'context', '${ctx.id}', '${ctx.avatarMode}')">📋 Copy Code</button>
`;
}
function renderPresetPreview(preset) {
return `
<div class="preset-previews" style="border-radius: 0.5rem; overflow: hidden; margin-bottom: 1.5rem;">
<div class="preset-preview-item">
<canvas id="presetBgCanvas" width="200" height="150"></canvas>
<div class="preset-preview-label">Background</div>
</div>
<div class="preset-preview-item" style="display: flex; align-items: center; justify-content: center;">
<canvas id="presetAvatarCanvas" width="64" height="160" style="image-rendering: pixelated;"></canvas>
<div class="preset-preview-label">Avatar</div>
</div>
<div class="preset-preview-item" style="display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, ${getMoodColor(preset.music?.mood || 'Epic')}, ${getMoodColorDark(preset.music?.mood || 'Epic')});">
<span style="font-size: 2.5rem;">🎵</span>
<div class="preset-preview-label">Music</div>
</div>
</div>
<dl class="preview-meta">
<dt>Preset Name</dt>
<dd>${preset.name}</dd>
<dt>Game Style</dt>
<dd>${preset.style}</dd>
<dt>Music</dt>
<dd>${preset.music?.mood || 'N/A'} (Energy: ${preset.music?.energy || 'N/A'})</dd>
<dt>Background</dt>
<dd>${preset.background?.theme || 'N/A'} / ${preset.background?.variant || 'N/A'}</dd>
<dt>Context</dt>
<dd>${preset.context?.id || 'N/A'}</dd>
<dt>Description</dt>
<dd>${preset.description || 'No description'}</dd>
</dl>
<h4 style="margin-bottom: 0.5rem;">Full Configuration</h4>
<div class="code-block">${JSON.stringify({
assets: {
music: preset.music,
background: preset.background,
avatarContext: preset.context
}
}, null, 2)}</div>
<button class="copy-btn" onclick="AssetBrowser.copyPreset('${preset.name}')">📋 Copy Preset Config</button>
`;
}
// ─── Helper Functions ────────────────────────────────────────
function getMoodColor(mood) {
const colors = {
'Epic': '#6366f1',
'Chill': '#06b6d4',
'Intense': '#ef4444',
'Playful': '#f59e0b',
'Mysterious': '#8b5cf6',
'Heroic': '#3b82f6',
'Quirky': '#ec4899',
'Ambient': '#14b8a6'
};
return colors[mood] || '#6366f1';
}
function getMoodColorDark(mood) {
const colors = {
'Epic': '#4338ca',
'Chill': '#0891b2',
'Intense': '#b91c1c',
'Playful': '#d97706',
'Mysterious': '#6d28d9',
'Heroic': '#1d4ed8',
'Quirky': '#be185d',
'Ambient': '#0d9488'
};
return colors[mood] || '#4338ca';
}
function getModeDescription(mode) {
const descs = {
'HEAD_ONLY': 'Best for cockpit views, maze games, and flappy-style games where only the face is visible.',
'HEAD_AND_SHOULDERS': 'Ideal for driving games, submarines, and seated vehicle contexts.',
'UPPER_BODY': 'Perfect for hoverboard, jetpack, and waist-up action games.',
'FULL_BODY': 'Standard for platformers, RPGs, sports games, and full-character adventures.'
};
return descs[mode] || '';
}
// ─── UI Functions ────────────────────────────────────────────
function renderSidebar() {
const config = tabConfigs[currentTab];
let html = `
<div class="filter-section">
<h3>Search</h3>
<input type="text" class="search-input" placeholder="Search ${config.title.toLowerCase()}..."
value="${activeFilters.search || ''}"
oninput="AssetBrowser.setFilter('search', this.value)">
</div>
`;
config.filters.forEach(filter => {
html += `
<div class="filter-section">
<h3>${filter.label}</h3>
<div class="filter-group">
<span class="filter-chip ${!activeFilters[filter.key] ? 'active' : ''}"
onclick="AssetBrowser.setFilter('${filter.key}', null)">All</span>
${filter.options.map(opt => `
<span class="filter-chip ${activeFilters[filter.key] === opt ? 'active' : ''}"
onclick="AssetBrowser.setFilter('${filter.key}', '${opt}')">${opt}</span>
`).join('')}
</div>
</div>
`;
});
// Random generator for certain tabs
if (['music', 'backgrounds', 'presets'].includes(currentTab)) {
html += `
<div class="random-generator">
<h3>✨ Random Pick</h3>
<p>Can't decide? Let us choose!</p>
<button class="random-btn" onclick="AssetBrowser.pickRandom()">Surprise Me</button>
</div>
`;
}
sidebar.innerHTML = html;
}
function renderContent() {
const config = tabConfigs[currentTab];
const data = config.getData();
let html = `
<div class="content-header">
<h2>${config.title}</h2>
<span class="result-count">${data.length} items</span>
</div>
<div class="grid" id="assetGrid"></div>
`;
content.innerHTML = html;
const grid = document.getElementById('assetGrid');
data.forEach(item => {
grid.appendChild(config.renderCard(item));
});
}
function openPreview(type, asset) {
selectedAsset = { type, data: asset };
const config = tabConfigs[type];
previewTitle.textContent = asset.title || asset.name || asset.id || asset.mode || 'Asset Details';
previewContent.innerHTML = config.renderPreview(asset);
previewPanel.classList.add('open');
overlay.classList.add('visible');
// Initialize preview-specific features
if (type === 'backgrounds') {
startBackgroundAnimation(asset);
} else if (type === 'avatars') {
startAvatarAnimation(asset);
} else if (type === 'contexts') {
startContextAnimation(asset);
} else if (type === 'presets') {
renderPresetPreviews(asset);
}
}
function closePreview() {
previewPanel.classList.remove('open');
overlay.classList.remove('visible');
stopAllAnimations();
selectedAsset = null;
}
// ─── Animation & Preview Logic ───────────────────────────────
let bgAnimating = true;
let currentAvatarAnim = 'idle';
let avatarAnimTime = 0;
function startBackgroundAnimation(bg) {
const canvas = document.getElementById('bgPreviewCanvas');
if (!canvas || !window.BackgroundEngine) return;
const ctx = canvas.getContext('2d');
let time = 0;
function animate() {
if (!bgAnimating || !document.getElementById('bgPreviewCanvas')) return;
time += 16;
window.BackgroundEngine.render(ctx, bg.theme, bg.variant, canvas.width, canvas.height, time);
animationFrame = requestAnimationFrame(animate);
}
animate();
}
function startAvatarAnimation(item) {
const canvas = document.getElementById('avatarPreviewCanvas');
if (!canvas || !window.AvatarSystem) return;
const ctx = canvas.getContext('2d');
// Setup animation buttons
document.querySelectorAll('#animationSelect .anim-btn').forEach(btn => {
btn.onclick = () => {
document.querySelectorAll('#animationSelect .anim-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentAvatarAnim = btn.dataset.anim;
};
});
function animate() {
if (!document.getElementById('avatarPreviewCanvas')) return;
avatarAnimTime += 16;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.scale(3, 3);
const avatar = getAvatarFromForm();
if (window.AvatarSystem.render) {
window.AvatarSystem.render(ctx, avatar, item.mode, currentAvatarAnim, avatarAnimTime);
}
ctx.restore();
animationFrame = requestAnimationFrame(animate);
}
animate();
}
function startContextAnimation(ctxData) {
const canvas = document.getElementById('contextPreviewCanvas');
if (!canvas || !window.ContextRenderer) return;
const ctx = canvas.getContext('2d');
let time = 0;
let currentContextAnim = 'idle';
// Setup animation buttons
document.querySelectorAll('#contextAnimSelect .anim-btn').forEach(btn => {
btn.onclick = () => {
document.querySelectorAll('#contextAnimSelect .anim-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentContextAnim = btn.dataset.anim;
};
});
function animate() {
if (!document.getElementById('contextPreviewCanvas')) return;
time += 16;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw simple background
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const avatar = {
faceShape: 'round',
skinTone: 'medium',
hairStyle: 'short',
hairColor: '#3d2314',
eyeShape: 'round',
eyeColor: '#4a7c59'
};
if (window.ContextRenderer.render) {
window.ContextRenderer.render(ctx, ctxData.id, avatar, 150, 150, currentContextAnim, time);
}
animationFrame = requestAnimationFrame(animate);
}
animate();
}
function renderPresetPreviews(preset) {
// Background preview
const bgCanvas = document.getElementById('presetBgCanvas');
if (bgCanvas && window.BackgroundEngine && preset.background) {
const ctx = bgCanvas.getContext('2d');
window.BackgroundEngine.render(ctx, preset.background.theme, preset.background.variant, 200, 150, 0);
}
// Avatar preview
const avatarCanvas = document.getElementById('presetAvatarCanvas');
if (avatarCanvas && window.AvatarSystem) {
const ctx = avatarCanvas.getContext('2d');
const avatar = {
faceShape: 'round',
skinTone: 'medium',
hairStyle: 'short',
hairColor: '#3d2314',
eyeShape: 'round',
eyeColor: '#4a7c59'
};
if (window.AvatarSystem.render) {
window.AvatarSystem.render(ctx, avatar, preset.context?.mode || 'FULL_BODY', 'idle', 0);
}
}
}
function stopAllAnimations() {
if (animationFrame) {
cancelAnimationFrame(animationFrame);
animationFrame = null;
}
stopMusic();
}
function getAvatarFromForm() {
return {
faceShape: document.getElementById('faceShape')?.value || 'round',
skinTone: document.getElementById('skinTone')?.value || 'medium',
hairStyle: document.getElementById('hairStyle')?.value || 'short',
hairColor: document.getElementById('hairColor')?.value || '#3d2314',
eyeShape: 'round',
eyeColor: '#4a7c59',
noseShape: 'small',
mouthShape: 'smile'
};
}
// ─── Music Playback ──────────────────────────────────────────
function toggleMusic(songId) {
const btn = document.getElementById('playBtn');
if (!btn) return;
if (currentMusic) {
stopMusic();
btn.textContent = '▶';
btn.classList.remove('playing');
} else {
playMusic(songId);
btn.textContent = '⏹';
btn.classList.add('playing');
}
}
function playMusic(songId) {
if (!window.MusicEngine || !window.MusicEngine.play) {
console.log('Music playback not available');
return;
}
try {
currentMusic = window.MusicEngine.play(songId);
} catch (e) {
console.error('Failed to play music:', e);
}
}
function stopMusic() {
if (currentMusic && window.MusicEngine && window.MusicEngine.stop) {
window.MusicEngine.stop();
}
currentMusic = null;
}
function setVolume(value) {
if (window.MusicEngine && window.MusicEngine.setVolume) {
window.MusicEngine.setVolume(value / 100);
}
}
// ─── Clipboard ───────────────────────────────────────────────
function copyCode(btn, type, ...args) {
let code = '';
if (type === 'music') {
const songId = args[0];
code = `assets: {\n music: {\n id: "${songId}",\n enabled: true\n }\n}`;
} else if (type === 'background') {
const [theme, variant] = args;
code = `assets: {\n background: {\n theme: "${theme}",\n variant: "${variant}",\n animated: true\n }\n}`;
} else if (type === 'avatar') {
const mode = args[0];
code = `assets: {\n avatarContext: {\n mode: "${mode}",\n showAccessories: true\n }\n}`;
} else if (type === 'context') {
const [contextId, mode] = args;
code = `assets: {\n avatarContext: {\n mode: "${mode}",\n context: "${contextId}",\n showAccessories: true\n }\n}`;
}
navigator.clipboard.writeText(code).then(() => {
btn.textContent = '✓ Copied!';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = '📋 Copy Code';
btn.classList.remove('copied');
}, 2000);
});
}
function copyPreset(presetName) {
const presets = window.AssetPresets?.getAll() || [];
const preset = presets.find(p => p.name === presetName);
if (!preset) return;
const code = JSON.stringify({
assets: {
music: preset.music,
background: preset.background,
avatarContext: preset.context
}
}, null, 2);
navigator.clipboard.writeText(code).then(() => {
const btn = document.querySelector('.copy-btn');
if (btn) {
btn.textContent = '✓ Copied!';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = '📋 Copy Preset Config';
btn.classList.remove('copied');
}, 2000);
}
});
}
// ─── Random Pick ─────────────────────────────────────────────
function pickRandom() {
const config = tabConfigs[currentTab];
const data = config.getData();
if (data.length === 0) return;
const randomItem = data[Math.floor(Math.random() * data.length)];
openPreview(currentTab, randomItem);
}
// ─── Public API ──────────────────────────────────────────────
function setFilter(key, value) {
if (value === null || value === '') {
delete activeFilters[key];
} else {
activeFilters[key] = value;
}
renderContent();
// Update active chip
if (key !== 'search') {
renderSidebar();
}
}
function switchTab(tab) {
currentTab = tab;
activeFilters = {};
document.querySelectorAll('.tab').forEach(t => {
t.classList.toggle('active', t.dataset.tab === tab);
});
renderSidebar();
renderContent();
}
function toggleBgAnimation(enabled) {
bgAnimating = enabled;
if (enabled && selectedAsset?.type === 'backgrounds') {
startBackgroundAnimation(selectedAsset.data);
}
}
function updateAvatarPreview() {
// Avatar preview updates automatically via animation loop
}
// ─── Initialize ──────────────────────────────────────────────
function init() {
// Tab click handlers
document.querySelectorAll('.tab').forEach(tab => {
tab.onclick = () => switchTab(tab.dataset.tab);
});
// Close preview handlers
document.getElementById('closePreview').onclick = closePreview;
overlay.onclick = closePreview;
// Keyboard shortcuts
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closePreview();
});
// Initial render
renderSidebar();
renderContent();
}
// Wait for assets to load
window.addEventListener('load', () => {
setTimeout(init, 100);
});
// Expose public API
return {
setFilter,
switchTab,
toggleMusic,
setVolume,
copyCode,
copyPreset,
pickRandom,
toggleBgAnimation,
updateAvatarPreview
};
})();
</script>
</body>
</html>