Show editable segment window timeline
This commit is contained in:
@@ -368,18 +368,19 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
private func clipEditor(_ clip: GrowthLapseProjectClip) -> some View {
|
||||
let maxStart = max(0, clip.duration - clip.segmentLength)
|
||||
let liveClip = currentClip(id: clip.id) ?? clip
|
||||
let maxStart = max(0, liveClip.duration - liveClip.segmentLength)
|
||||
let startBinding = Binding<Double>(
|
||||
get: { currentClip(id: clip.id)?.segmentStart ?? clip.segmentStart },
|
||||
set: { processor.updateClip(clip, segmentStart: $0) }
|
||||
)
|
||||
|
||||
return VStack(alignment: .leading, spacing: 8) {
|
||||
ClipPreviewView(clip: currentClip(id: clip.id) ?? clip)
|
||||
ClipPreviewView(clip: liveClip)
|
||||
.frame(height: 180)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
|
||||
Text(clip.displayName)
|
||||
Text(liveClip.displayName)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.lineLimit(2)
|
||||
|
||||
@@ -391,7 +392,16 @@ struct ContentView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Slider(value: startBinding, in: 0...maxStart, step: 0.1)
|
||||
SegmentTimelineView(clip: liveClip)
|
||||
.frame(height: 42)
|
||||
.help("Blau ist das verwendete Segmentfenster. Der markierte rechte Bereich kann nicht als Startpunkt gewählt werden, weil das Segment sonst über das Videoende hinauslaufen würde.")
|
||||
|
||||
if maxStart > 0 {
|
||||
Slider(value: startBinding, in: 0...maxStart, step: 0.1)
|
||||
} else {
|
||||
Slider(value: .constant(0), in: 0...1)
|
||||
.disabled(true)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Button("-1 s") {
|
||||
@@ -401,7 +411,7 @@ struct ContentView: View {
|
||||
processor.updateClip(clip, segmentStart: startBinding.wrappedValue - 0.5)
|
||||
}
|
||||
Button("Zur Mitte") {
|
||||
processor.updateClip(clip, segmentStart: max(0, (clip.duration - clip.segmentLength) / 2 + settings.startOffset))
|
||||
processor.updateClip(clip, segmentStart: max(0, (liveClip.duration - liveClip.segmentLength) / 2 + settings.startOffset))
|
||||
}
|
||||
Button("+0,5 s") {
|
||||
processor.updateClip(clip, segmentStart: startBinding.wrappedValue + 0.5)
|
||||
@@ -410,7 +420,7 @@ struct ContentView: View {
|
||||
processor.updateClip(clip, segmentStart: startBinding.wrappedValue + 1)
|
||||
}
|
||||
}
|
||||
.disabled(processor.state.isRunning)
|
||||
.disabled(processor.state.isRunning || maxStart <= 0)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
}
|
||||
@@ -554,3 +564,64 @@ private struct ClipPreviewView: View {
|
||||
player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SegmentTimelineView: View {
|
||||
let clip: GrowthLapseProjectClip
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
GeometryReader { proxy in
|
||||
let width = max(1, proxy.size.width)
|
||||
let duration = max(0.001, clip.duration)
|
||||
let maxStart = max(0, clip.duration - clip.segmentLength)
|
||||
let startX = width * CGFloat(clamp(clip.segmentStart / duration))
|
||||
let segmentWidth = max(3, width * CGFloat(clamp(clip.segmentLength / duration)))
|
||||
let invalidStartX = width * CGFloat(clamp(maxStart / duration))
|
||||
let invalidWidth = max(0, width - invalidStartX)
|
||||
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color(nsColor: .separatorColor).opacity(0.35))
|
||||
|
||||
Rectangle()
|
||||
.fill(Color.orange.opacity(0.16))
|
||||
.frame(width: invalidWidth)
|
||||
.offset(x: invalidStartX)
|
||||
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color.accentColor.opacity(0.75))
|
||||
.frame(width: min(segmentWidth, width - startX))
|
||||
.offset(x: startX)
|
||||
|
||||
Rectangle()
|
||||
.fill(Color.accentColor)
|
||||
.frame(width: 2)
|
||||
.offset(x: startX)
|
||||
}
|
||||
}
|
||||
.frame(height: 14)
|
||||
|
||||
HStack {
|
||||
Text("0 s")
|
||||
Spacer()
|
||||
Text("Segment \(format(clip.segmentStart))-\(format(min(clip.duration, clip.segmentStart + clip.segmentLength))) s")
|
||||
Spacer()
|
||||
Text("\(format(clip.duration)) s")
|
||||
}
|
||||
.font(.caption2.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private func clamp(_ value: Double) -> Double {
|
||||
min(1, max(0, value))
|
||||
}
|
||||
|
||||
private func format(_ value: Double) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = .current
|
||||
formatter.minimumFractionDigits = value.rounded() == value ? 0 : 1
|
||||
formatter.maximumFractionDigits = 1
|
||||
return formatter.string(from: NSNumber(value: value)) ?? "\(value)"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user