This commit is contained in:
th committed 2026-04-19 17:53:36 +02:00
commit c6863f2bfc
34 files changed
+6909

No files matched your search

+262
View File
@@ -0,0 +1,262 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import {
PhExport,
PhGearSix,
PhPause,
PhPlay,
PhSkipBack,
PhSparkle,
PhStop,
} from '@phosphor-icons/vue'
import AssetSidebar from './components/AssetSidebar.vue'
import CanvasStage from './components/CanvasStage.vue'
import DocumentSettings from './components/DocumentSettings.vue'
import ExportDialog from './components/ExportDialog.vue'
import PropertiesPanel from './components/PropertiesPanel.vue'
import Timeline from './components/Timeline.vue'
import { usePlaybackLoop } from './composables/usePlaybackLoop'
import { useSceneStore } from './stores/scene'
const settingsOpen = ref(false)
const exportOpen = ref(false)
usePlaybackLoop()
const store = useSceneStore()
function rewind() {
store.setPlayhead(0)
}
function stop() {
store.pause()
}
function isEditable(el: EventTarget | null): boolean {
if (!(el instanceof HTMLElement)) return false
const tag = el.tagName
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true
if (el.isContentEditable) return true
return false
}
function onKeyDown(e: KeyboardEvent) {
if (e.code !== 'Space') return
if (isEditable(e.target)) return
e.preventDefault()
store.togglePlay()
}
onMounted(() => document.addEventListener('keydown', onKeyDown))
onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
</script>
<template>
<div class="app">
<header class="topbar">
<div class="brand-area">
<span class="brand">
<PhSparkle :size="16" weight="fill" class="brand-icon" />
Animatr
</span>
<span class="hint">PoC</span>
</div>
<div class="transport">
<button class="tbtn" title="Zum Anfang" @click="rewind">
<PhSkipBack :size="14" weight="fill" />
</button>
<button
class="tbtn primary"
:title="store.playing ? 'Pause (Space)' : 'Play (Space)'"
@click="store.togglePlay()"
>
<PhPause v-if="store.playing" :size="14" weight="fill" />
<PhPlay v-else :size="14" weight="fill" />
</button>
<button class="tbtn" title="Stop" @click="stop">
<PhStop :size="14" weight="fill" />
</button>
<span class="time">{{ store.playhead.toFixed(2) }}s / {{ store.scene.duration }}s</span>
</div>
<div class="topbar-right">
<button class="icon-btn" title="Document Settings" @click="settingsOpen = true">
<PhGearSix :size="16" />
</button>
<button class="export-btn" title="Exportieren" @click="exportOpen = true">
<PhExport :size="14" weight="bold" />
Export
</button>
</div>
</header>
<aside class="sidebar"><AssetSidebar /></aside>
<main class="stage"><CanvasStage /></main>
<aside class="properties"><PropertiesPanel /></aside>
<footer class="timeline"><Timeline /></footer>
<DocumentSettings v-model="settingsOpen" />
<ExportDialog v-model="exportOpen" />
</div>
</template>
<style scoped>
.app {
display: grid;
grid-template-columns: 260px 1fr 300px;
grid-template-rows: 48px 1fr 220px;
grid-template-areas:
'top top top'
'side stage props'
'time time time';
height: 100vh;
width: 100vw;
}
.topbar {
grid-area: top;
display: flex;
align-items: center;
padding: 0 16px;
background: var(--panel);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow-sm);
}
.brand-area {
flex: 1 1 0;
display: flex;
gap: 10px;
align-items: center;
min-width: 0;
}
.brand {
font-weight: 600;
font-size: 14px;
letter-spacing: -0.01em;
display: inline-flex;
align-items: center;
gap: 6px;
}
.brand-icon {
color: var(--accent);
}
.hint {
color: var(--text-dim);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 2px 6px;
border: 1px solid var(--border);
border-radius: 10px;
}
.transport {
display: flex;
gap: 4px;
align-items: center;
}
.tbtn {
width: 32px;
height: 30px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid var(--border);
color: var(--text-dim-strong);
border-radius: var(--radius-sm);
}
.tbtn:hover:not(:disabled) {
background: var(--panel-alt);
color: var(--text);
}
.tbtn.primary {
background: var(--accent-soft);
border-color: var(--accent);
color: var(--accent);
}
.tbtn.primary:hover:not(:disabled) {
background: var(--accent-soft-strong);
color: var(--accent);
}
.time {
margin-left: 10px;
font-variant-numeric: tabular-nums;
color: var(--text-dim);
font-size: 11px;
min-width: 92px;
text-align: center;
}
.topbar-right {
flex: 1 1 0;
display: flex;
justify-content: flex-end;
gap: 6px;
}
.export-btn {
padding: 6px 12px;
border: 1px solid var(--accent);
color: var(--accent);
background: var(--accent-soft);
border-radius: var(--radius-sm);
font-size: 12px;
font-weight: 500;
}
.export-btn:hover:not(:disabled) {
background: var(--accent-soft-strong);
}
.icon-btn {
width: 30px;
height: 30px;
padding: 0;
justify-content: center;
background: transparent;
border: 1px solid var(--border);
color: var(--text-dim);
}
.icon-btn:hover:not(:disabled) {
background: var(--panel-alt);
color: var(--text);
}
.sidebar {
grid-area: side;
background: var(--panel);
border-right: 1px solid var(--border);
overflow: auto;
}
.stage {
grid-area: stage;
background: var(--bg);
overflow: hidden;
min-width: 0;
min-height: 0;
}
.properties {
grid-area: props;
background: var(--panel);
border-left: 1px solid var(--border);
overflow: auto;
}
.timeline {
grid-area: time;
background: var(--panel);
border-top: 1px solid var(--border);
overflow: hidden;
}
</style>