init
This commit is contained in:
commit
c6863f2bfc
34 files changed
+6909
No files matched your search
+262
@@ -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>
|
||||
Reference in new issue
Block a user