1
0
Fork 0
muzika-gromche/Frontend/src/components/timeline/Timeline.vue

234 lines
8.6 KiB
Vue

<script setup lang="ts">
import type { UseOptionalWidgetStateReturn } from '@/lib/useOptionalWidgetState'
import type { UseZoomAxis } from '@/lib/useZoomAxis'
import { useElementBounding, useScroll } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { useId, useTemplateRef, watch } from 'vue'
import ZoomSlider from '@/components/library/ZoomSlider.vue'
import ScrollSync from '@/components/scrollsync/ScrollSync.vue'
import TimelineHeader from '@/components/timeline/header/TimelineHeader.vue'
import Playhead from '@/components/timeline/Playhead.vue'
import { onInputKeyStroke } from '@/lib/onInputKeyStroke'
import { useTimelineScrubbing } from '@/lib/useTimelineScrubbing'
import { useVeiwportWheel } from '@/lib/useVeiwportWheel'
import { bindTwoWay, toPx } from '@/lib/vue'
import { useTimelineStore } from '@/store/TimelineStore'
import TimelineMarkers from './markers/TimelineMarkers.vue'
import TimelineTrackHeader from './TimelineTrackHeader.vue'
import TimelineTrackView from './TimelineTrackView.vue'
const {
rightSidebar,
} = defineProps<{
rightSidebar: UseOptionalWidgetStateReturn
}>()
const timeline = useTimelineStore()
const {
headerHeight,
sidebarWidth,
viewportScrollOffsetTop,
viewportScrollOffsetLeft,
contentWidthIncludingEmptySpacePx,
visibleTracks,
} = storeToRefs(timeline)
// nested composable marked with markRaw
const viewportZoomHorizontal = timeline.viewportZoomHorizontal as any as UseZoomAxis
const viewportZoomVertical = timeline.viewportZoomVertical as any as UseZoomAxis
const timelineScrollGroup = useId()
const timelineRootElement = useTemplateRef('timelineRootElement')
const timelineScrollView = useTemplateRef<InstanceType<typeof ScrollSync>>('timelineScrollView')
const timelineScrollViewBounding = useElementBounding(timelineScrollView)
watch(timelineScrollViewBounding.width, (value) => {
timeline.viewportWidth = value
})
watch(timelineScrollViewBounding.height, (value) => {
timeline.viewportHeight = value
})
const {
arrivedState: timelineScrollViewArrivedState,
x: timelineScrollViewOffsetLeft,
y: timelineScrollViewOffsetTop,
} = useScroll(() => timelineScrollView.value?.$el)
bindTwoWay(timelineScrollViewOffsetTop, viewportScrollOffsetTop)
bindTwoWay(timelineScrollViewOffsetLeft, viewportScrollOffsetLeft)
useVeiwportWheel(timelineRootElement, {
axisHorizontal: viewportZoomHorizontal,
axisVertical: viewportZoomVertical,
scrollOffsetLeft: timelineScrollViewOffsetLeft,
})
// Shift+Z - reset zoom
onInputKeyStroke(event => event.shiftKey && (event.key === 'Z' || event.key === 'z'), (event) => {
timeline.zoomToggleBetweenWholeAndLoop()
event.preventDefault()
})
const scrubbing = useTemplateRef('scrubbing')
useTimelineScrubbing(scrubbing)
</script>
<template>
<div
ref="timelineRootElement" class="tw:w-full tw:grid tw:gap-0" :style="{
'grid-template-columns': `${toPx(sidebarWidth)} 1fr ${rightSidebar.visible.value ? rightSidebar.width.string.value : ''}`,
'grid-template-rows': `${toPx(headerHeight)} 1fr`,
}"
>
<!-- top left corner, contains zoom controls -->
<div
class="toolbar-background tw:max-w-full tw:flex tw:flex-row tw:flex-nowrap tw:items-center"
style="grid-row: 1; grid-column: 1; border-right: var(--view-separator-border); border-bottom: var(--view-separator-border);"
>
<ZoomSlider :axis="viewportZoomHorizontal" class="tw:flex-1" />
</div>
<!-- left sidebar with timeline track names -->
<ScrollSync
:group="timelineScrollGroup" :vertical="true" class="toolbar-background scrollbar-none"
style="grid-row: 2; grid-column: 1; border-right: var(--view-separator-border);"
>
<template v-for="timelineTrack in visibleTracks" :key="timelineTrack.name">
<TimelineTrackHeader :timeline-track />
</template>
</ScrollSync>
<!-- header with timestamps -->
<ScrollSync
:group="timelineScrollGroup" :horizontal="true" class="timeline-background scrollbar-none tw:relative"
style="grid-row: 1; grid-column: 2; border-bottom: var(--view-separator-border);"
>
<div ref="scrubbing" class="tw:relative tw:h-full" :style="{ width: contentWidthIncludingEmptySpacePx }">
<TimelineHeader />
</div>
<!-- <Playhead :positionSeconds="timeline.playheadPosition"> -->
<!-- <Timestamp :seconds="cursorPositionSeconds ?? 0" :beats="cursorPositionBeats ?? 0" v-if="isDragging" /> -->
<!-- </Playhead> -->
</ScrollSync>
<!-- timeline content -->
<ScrollSync
ref="timelineScrollView" :group="timelineScrollGroup" :horizontal="true" :vertical="true"
class="tw:size-full timeline-background tw:relative" style="grid-row: 2; grid-column: 2;"
>
<!-- timeline content wrapper for good measure -->
<div
class="tw:relative tw:overflow-hidden tw:min-h-full"
:style="{ width: timeline.contentWidthIncludingEmptySpacePx }"
>
<!-- timeline markers -->
<TimelineMarkers />
<!-- timeline tracks -->
<div>
<template v-for="timelineTrack in visibleTracks" :key="timelineTrack.name">
<TimelineTrackView :timeline-track />
</template>
</div>
</div>
</ScrollSync>
<!-- horizontal bars of scroll shadow, on top of sidebar and content, but under playhead -->
<div class="tw:size-full tw:relative tw:pointer-events-none" style="grid-row: 2; grid-column: 1 / 3;">
<div
class="tw:absolute tw:top-0 tw:left-0 tw:h-0 tw:w-full"
:class="{ 'tw:invisible': timelineScrollViewArrivedState.top }"
>
<div class="tw:h-4 tw:w-full shadow-bottom" />
</div>
<div
class="tw:absolute tw:bottom-4 tw:left-0 tw:h-0 tw:w-full"
:class="{ 'tw:invisible': timelineScrollViewArrivedState.bottom }"
>
<div class="tw:h-4 tw:w-full shadow-top" />
</div>
</div>
<!-- playhead -->
<ScrollSync
:group="timelineScrollGroup" :horizontal="true" class="tw:size-full tw:pointer-events-none"
style="grid-row: 1 / 3; grid-column: 2;"
>
<div
class="tw:h-full tw:relative tw:overflow-hidden"
:style="{ width: timeline.contentWidthIncludingEmptySpacePx }"
>
<!-- actuals playback position -->
<Playhead :position-seconds="timeline.playheadPosition" :knob="true">
<!-- <Timestamp :seconds="cursorPositionSeconds ?? 0" :beats="cursorPositionBeats ?? 0" v-if="isDragging" /> -->
</Playhead>
</div>
</ScrollSync>
<!-- cursor on hover -->
<!-- <Playhead :position="cursorPosition" :timelineWidth="timelineWidth" :knob="false"
:hidden="cursorPositionSeconds === null || isDragging">
<Timestamp :seconds="cursorPositionSeconds ?? 0" :beats="cursorPositionBeats ?? 0" />
</Playhead> -->
<!-- vertical bars of scroll shadow, on top of header, content AND playhead -->
<div class="tw:size-full tw:relative tw:pointer-events-none" style="grid-row: 1 / -1; grid-column: 2;">
<div
class="tw:absolute tw:top-0 tw:left-0 tw:w-0 tw:h-full"
:class="{ 'tw:invisible': timelineScrollViewArrivedState.left }"
>
<div class="tw:w-4 tw:h-full shadow-right" />
</div>
<div
class="tw:absolute tw:top-0 tw:right-4 tw:w-0 tw:h-full"
:class="{ 'tw:invisible': timelineScrollViewArrivedState.right }"
>
<div class="tw:w-4 tw:h-full shadow-left" />
</div>
</div>
<!-- empty cell at the top right -->
<div
v-if="rightSidebar.visible.value" class="toolbar-background"
style="grid-row: 1; grid-column: 3; border-bottom: var(--view-separator-border); border-left: var(--view-separator-border);"
/>
<!-- right sidebar with vertical zoom slider -->
<div
v-if="rightSidebar.visible.value"
class="toolbar-background tw:size-full tw:min-h-0 tw:py-2 tw:flex tw:flex-col tw:items-center"
style="grid-row: 2; grid-column: 3; border-left: var(--view-separator-border);"
>
<ZoomSlider :axis="viewportZoomVertical" orientation="vertical" class="tw:w-full tw:min-h-0" />
</div>
</div>
</template>
<style scoped>
.shadow-top,
.shadow-right,
.shadow-bottom,
.shadow-left {
--shadow-darkest: rgba(0, 0, 0, 0.3);
}
.shadow-top {
background-image: linear-gradient(to top, var(--shadow-darkest) 0%, rgba(0, 0, 0, .0) 100%);
}
.shadow-right {
background-image: linear-gradient(to right, var(--shadow-darkest) 0%, rgba(0, 0, 0, .0) 100%);
}
.shadow-bottom {
background-image: linear-gradient(to bottom, var(--shadow-darkest) 0%, rgba(0, 0, 0, .0) 100%);
}
.shadow-left {
background-image: linear-gradient(to left, var(--shadow-darkest) 0%, rgba(0, 0, 0, .0) 100%);
}
</style>