This commit is contained in:
team3 committed 2026-09-30 00:59:59 +02:00
1 parent 7bf9667007
commit c589e389df
20 files changed
+1814 -29

No files matched your search

+274
View File
@@ -0,0 +1,274 @@
<script setup>
// Der Zeitstrahl der Aufnahme rund um die Marke. Ein Balken zeigt das
// Fenster des Clips; an seinen Enden zieht man Anfang und Ende, in der
// Mitte schiebt man das ganze Fenster. Zeiten in Sekunden relativ zur
// Marke: 0 ist der Moment, an dem C gedrückt wurde.
import { computed, ref } from 'vue'
const props = defineProps({
// Stücke der Aufnahme: [{ von, bis }] relativ zur Marke
segmente: { type: Array, default: () => [] },
maxVorher: { type: Number, default: 600 },
minDauer: { type: Number, default: 3 },
maxDauer: { type: Number, default: 180 },
// Uhrzeit der Marke, für die Beschriftung
marke: { type: String, default: '' },
})
const vorher = defineModel('vorher', { type: Number, default: 30 })
const dauer = defineModel('dauer', { type: Number, default: 35 })
const emit = defineEmits(['ende'])
// Der sichtbare Ausschnitt des Strahls, zoombar
const ZOOMS = [120, 300, 900, 1800, 7200, 14400]
const zoom = ref(1)
const mitte = ref(0)
const spanne = computed(() => ZOOMS[zoom.value])
const links = computed(() => mitte.value - spanne.value / 2)
const rechts = computed(() => mitte.value + spanne.value / 2)
const strahl = ref(null)
function prozent(t) {
return ((t - links.value) / spanne.value) * 100
}
const start = computed(() => -vorher.value)
const ende = computed(() => -vorher.value + dauer.value)
// Wo die Aufnahme überhaupt Material hat
const grenzen = computed(() => {
if (!props.segmente.length) return { von: -props.maxVorher, bis: props.maxDauer }
return {
von: Math.min(...props.segmente.map((s) => s.von)),
bis: Math.max(...props.segmente.map((s) => s.bis)),
}
})
function zoomen(schritt) {
zoom.value = Math.max(0, Math.min(ZOOMS.length - 1, zoom.value + schritt))
mitte.value = (start.value + ende.value) / 2
}
// Beschriftung: runde Schritte, etwa sechs bis acht Striche
const striche = computed(() => {
const schritt = [10, 30, 60, 300, 900, 1800][zoom.value]
const aus = []
for (let t = Math.ceil(links.value / schritt) * schritt; t <= rechts.value; t += schritt) {
aus.push({ t, text: uhrzeit(t) })
}
return aus
})
function uhrzeit(t) {
if (!props.marke) return `${t >= 0 ? '+' : '−'}${Math.abs(t)} s`
const d = new Date(new Date(props.marke).getTime() + t * 1000)
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
}
// ── Ziehen ────────────────────────────────────────────────────────────
let griff = null
let ausgang = null
function anfassen(e, was) {
griff = was
ausgang = { x: e.clientX, vorher: vorher.value, dauer: dauer.value }
e.currentTarget.setPointerCapture(e.pointerId)
}
function ziehen(e) {
if (!griff || !strahl.value) return
const dt = ((e.clientX - ausgang.x) / strahl.value.clientWidth) * spanne.value
const g = grenzen.value
if (griff === 'links') {
// Der Anfang wandert, das Ende bleibt stehen
const altesEnde = -ausgang.vorher + ausgang.dauer
let neuerStart = -ausgang.vorher + dt
neuerStart = Math.max(g.von, -props.maxVorher, neuerStart)
neuerStart = Math.min(
altesEnde - props.minDauer,
Math.max(altesEnde - props.maxDauer, neuerStart),
)
vorher.value = Math.round(-neuerStart)
dauer.value = Math.round(altesEnde - neuerStart)
} else if (griff === 'rechts') {
let neuesEnde = -ausgang.vorher + ausgang.dauer + dt
neuesEnde = Math.min(g.bis, neuesEnde)
const d = Math.max(props.minDauer, Math.min(props.maxDauer, neuesEnde + ausgang.vorher))
dauer.value = Math.round(d)
} else {
// Das ganze Fenster verschieben
let neuerStart = -ausgang.vorher + dt
neuerStart = Math.max(g.von, -props.maxVorher, Math.min(g.bis - ausgang.dauer, neuerStart))
vorher.value = Math.round(-neuerStart)
}
}
function loslassen(e) {
if (!griff) return
griff = null
e.currentTarget.releasePointerCapture?.(e.pointerId)
emit('ende')
}
</script>
<template>
<div class="trimmer">
<div class="kopf">
<span class="fenster">
{{ uhrzeit(start) }} – {{ uhrzeit(ende) }} · <b>{{ dauer }} s</b>
</span>
<span class="zoom">
<button type="button" :disabled="zoom === 0" title="Näher" @click="zoomen(-1)">+</button>
<button
type="button"
:disabled="zoom === ZOOMS.length - 1"
title="Weiter"
@click="zoomen(1)"
>
−
</button>
</span>
</div>
<div
ref="strahl"
class="strahl"
@pointermove="ziehen"
@pointerup="loslassen"
@pointercancel="loslassen"
>
<!-- Wo Aufnahme liegt -->
<span
v-for="(s, i) in segmente"
:key="i"
class="aufnahme"
:style="{ left: prozent(s.von) + '%', width: prozent(s.bis) - prozent(s.von) + '%' }"
></span>
<!-- Die Marke: der Moment, an dem C gedrückt wurde -->
<span class="marke" :style="{ left: prozent(0) + '%' }" title="Marke"></span>
<!-- Das Fenster des Clips mit seinen Griffen -->
<span
class="bereich"
:style="{ left: prozent(start) + '%', width: prozent(ende) - prozent(start) + '%' }"
@pointerdown="anfassen($event, 'mitte')"
>
<span class="griff links" @pointerdown.stop="anfassen($event, 'links')"></span>
<span class="griff rechts" @pointerdown.stop="anfassen($event, 'rechts')"></span>
</span>
<span v-for="s in striche" :key="s.t" class="strich" :style="{ left: prozent(s.t) + '%' }">
<span class="text">{{ s.text }}</span>
</span>
</div>
</div>
</template>
<style scoped>
.trimmer {
display: flex;
flex-direction: column;
gap: 0.4rem;
user-select: none;
}
.kopf {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--muted);
font-size: 0.82rem;
font-variant-numeric: tabular-nums;
}
.fenster b {
color: var(--text);
}
.zoom button {
width: 1.7rem;
height: 1.7rem;
color: var(--text);
border: 1px solid var(--line);
border-radius: 6px;
}
.zoom button:disabled {
opacity: 0.4;
}
.strahl {
position: relative;
height: 3.4rem;
overflow: hidden;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 8px;
touch-action: none;
}
.aufnahme {
position: absolute;
top: 0.4rem;
height: 1.6rem;
background: rgba(255, 255, 255, 0.1);
}
.marke {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
margin-left: -1px;
background: #f5c518;
}
.bereich {
position: absolute;
top: 0.4rem;
height: 1.6rem;
background: rgba(229, 9, 20, 0.35);
border: 2px solid var(--accent);
border-radius: 4px;
cursor: grab;
}
.griff {
position: absolute;
top: -2px;
bottom: -2px;
width: 0.7rem;
background: var(--accent);
cursor: ew-resize;
}
.griff.links {
left: -2px;
border-radius: 4px 0 0 4px;
}
.griff.rechts {
right: -2px;
border-radius: 0 4px 4px 0;
}
.strich {
position: absolute;
top: 2.2rem;
bottom: 0;
width: 1px;
background: var(--line);
pointer-events: none;
}
.text {
position: absolute;
top: 0.3rem;
left: 0.25rem;
color: var(--muted);
font-size: 0.68rem;
white-space: nowrap;
}
</style>