Files
website/src/routes/models/+page.svelte
T
2025-11-28 06:17:25 +00:00

206 lines
4.8 KiB
Svelte

<script lang="ts">
import TerminalTUI from '$lib/components/TerminalTUI.svelte';
import type { TerminalLine } from '$lib/components/tui/types';
import ModelViewer from '$lib/components/ModelViewer.svelte';
import { user, models as configModels } from '$lib/config';
import { getPageSpeedMultiplier, getPageAutoscroll } from '$lib';
import { themeColors } from '$lib/stores/theme';
import Icon from '@iconify/svelte';
const speed = getPageSpeedMultiplier('models');
const autoscroll = getPageAutoscroll('models');
// Build GLB model list from config (`models`) while preserving metadata.
// Keep `name`, `path`, `thumbnail`, and `description` for richer UI use.
const glbModels = (configModels && configModels.length > 0)
? configModels
.map(m => ({
name: m.name,
path: m.modelUrl ?? m.downloadUrl ?? m.thumbnail ?? '',
thumbnail: m.thumbnail ?? '',
description: m.description ?? ''
}))
.filter(m => m.path)
: [
{ name: 'Bake Potato', path: '/models/BakePotato.glb', thumbnail: '/models/BakePotato-thumb.png', description: 'A fun baked-potato 3D model.' },
{ name: 'Soda Can', path: '/models/Soda.glb', thumbnail: '/models/Soda-thumb.png', description: 'A realistic soda can model.' }
];
let selectedModel = $state(glbModels[0] ?? { name: '', path: '', thumbnail: '', description: '' });
let showViewer = $state(false);
function selectModel(model: typeof glbModels[0]) {
selectedModel = model;
showViewer = true;
}
// Build the terminal lines for the models page
const lines: TerminalLine[] = [
{ type: 'command', content: 'ls ~/models/' },
{ type: 'blank', content: '' },
{ type: 'header', content: '(&primary,bold)3D Models(&)' },
{ type: 'output', content: `(&muted)A collection of 3D models created for games, visualization, and fun.(&)` },
{ type: 'blank', content: '' },
{ type: 'divider', content: 'VIEWER' },
{ type: 'blank', content: '' },
// Interactive model buttons
...glbModels.map(model => ({
type: 'button' as const,
content: model.name,
icon: 'mdi:cube-scan',
style: 'primary' as const,
action: () => selectModel(model)
})),
{ type: 'blank', content: '' },
{ type: 'success', content: `(&success)${glbModels.length} models available(&)` }
];
</script>
<svelte:head>
<title>3D Models | {user.name}</title>
<meta name="description" content="3D Models portfolio - Characters, Environments, Props and more" />
</svelte:head>
<div class="models-page">
<div
class="models-layout"
class:viewer-open={showViewer}
style="--theme-bg: {$themeColors.background};"
>
<!-- Terminal Section -->
<div class="terminal-section">
<TerminalTUI {lines} title="~/models" interactive={true} {speed} {autoscroll} />
</div>
<!-- 3D Viewer Section -->
{#if showViewer}
<div class="viewer-section">
<div class="viewer-tabs">
{#each glbModels as model}
<button
class="viewer-tab"
class:active={selectedModel.path === model.path}
onclick={() => selectModel(model)}
>
<Icon icon="mdi:cube-outline" width="14" />
{model.name}
</button>
{/each}
<button class="viewer-tab close" onclick={() => showViewer = false}>
<Icon icon="mdi:close" width="14" />
</button>
</div>
<ModelViewer
modelPath={selectedModel.path}
modelName={selectedModel.name}
class="model-viewer-container"
/>
</div>
{/if}
</div>
</div>
<style>
.models-page {
min-height: calc(100vh - 60px);
padding: 2rem 1rem;
}
.models-layout {
display: flex;
gap: 1.5rem;
max-width: 1600px;
margin: 0 auto;
transition: all 0.3s ease;
}
.terminal-section {
flex: 1;
min-width: 0;
}
.viewer-section {
width: 500px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 0;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.viewer-tabs {
display: flex;
background: var(--theme-bg);
border-radius: 8px 8px 0 0;
overflow: hidden;
}
.viewer-tab {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 1rem;
background: transparent;
border: none;
color: inherit;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
cursor: pointer;
opacity: 0.6;
transition: all 0.2s ease;
}
.viewer-tab:hover {
opacity: 0.9;
background: rgba(255, 255, 255, 0.05);
}
.viewer-tab.active {
opacity: 1;
background: rgba(255, 255, 255, 0.1);
}
.viewer-tab.close {
margin-left: auto;
opacity: 0.5;
}
.viewer-tab.close:hover {
opacity: 1;
color: #f38ba8;
}
:global(.model-viewer-container) {
flex: 1;
min-height: 400px;
border-radius: 0 0 8px 8px;
}
@media (max-width: 1024px) {
.models-layout {
flex-direction: column;
}
.viewer-section {
width: 100%;
order: -1;
}
}
</style>