83 lines
2.8 KiB
Swift
83 lines
2.8 KiB
Swift
import SwiftUI
|
|
|
|
struct SkipButton: View {
|
|
let backward: Bool
|
|
let action: () -> Void
|
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
@State private var skips = 0
|
|
|
|
var body: some View {
|
|
Button {
|
|
if !reduceMotion {
|
|
skips += 1
|
|
}
|
|
action()
|
|
} label: {
|
|
SkipGlyph(skips: skips).scaleEffect(x: backward ? -1 : 1)
|
|
}
|
|
.accessibilityLabel(backward ? "Previous" : "Next")
|
|
}
|
|
}
|
|
|
|
private struct SkipGlyph: View {
|
|
let skips: Int
|
|
|
|
private static let step = 0.455
|
|
private nonisolated static let base = UnitPoint(x: 0.078, y: 0.5)
|
|
private nonisolated static let tip = UnitPoint(x: 0.45, y: 0.5)
|
|
|
|
var body: some View {
|
|
Image(systemName: "forward.fill")
|
|
.hidden()
|
|
.overlay {
|
|
GeometryReader { proxy in
|
|
let shift = proxy.size.width * Self.step
|
|
Color.clear.keyframeAnimator(initialValue: 0.0, trigger: skips) { _, time in
|
|
Self.triangles(at: time, shift: shift)
|
|
} keyframes: { _ in
|
|
LinearKeyframe(0.75, duration: 0.75)
|
|
MoveKeyframe(0)
|
|
}
|
|
}
|
|
}
|
|
.transaction { $0.animation = nil }
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
private nonisolated static func triangles(at time: Double, shift: CGFloat) -> some View {
|
|
let travel = springCurve(time, omega: 12.8, damping: 0.68)
|
|
let fade = min(max(time - 0.035, 0) / 0.09, 1)
|
|
return ZStack(alignment: .topLeading) {
|
|
triangle
|
|
.scaleEffect(springCurve(time - 0.035, omega: 12.4, damping: 0.68), anchor: base)
|
|
.opacity(time > 0 ? 0.6 + 0.4 * min(time / 0.26, 1) : 1)
|
|
.offset(x: max(travel - 1, 0) * shift)
|
|
triangle
|
|
.offset(x: travel * shift)
|
|
triangle
|
|
.scaleEffect(1 - fade, anchor: tip)
|
|
.opacity(1 - 0.5 * fade)
|
|
.offset(x: shift)
|
|
}
|
|
}
|
|
|
|
private nonisolated static var triangle: some View {
|
|
Image(systemName: "forward.fill").mask { JunctionMask() }
|
|
}
|
|
}
|
|
|
|
private nonisolated struct JunctionMask: Shape {
|
|
func path(in rect: CGRect) -> Path {
|
|
let junction = rect.width * 0.53
|
|
let taper = rect.width * 0.04
|
|
return Path { path in
|
|
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
|
|
path.addLine(to: CGPoint(x: junction - taper, y: rect.minY))
|
|
path.addLine(to: CGPoint(x: junction, y: rect.midY))
|
|
path.addLine(to: CGPoint(x: junction - taper, y: rect.maxY))
|
|
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
|
|
path.closeSubpath()
|
|
}
|
|
}
|
|
}
|