1843 lines
65 KiB
HTML
1843 lines
65 KiB
HTML
<!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">×</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>
|