Update: Album art flip

This commit is contained in:
Hariharan Mudaliar 2026-03-08 18:06:57 +05:30
commit 2db3fc9aaf
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E

View file

@ -137,7 +137,9 @@ class MusicManager: ObservableObject {
private var lastArtworkBundleIdentifier: String? = nil private var lastArtworkBundleIdentifier: String? = nil
@Published var flipAngle: Double = 0 @Published var flipAngle: Double = 0
@Published var lastFlipDirection: SkipDirection = .forward
private let flipAnimationDuration: TimeInterval = 0.45 private let flipAnimationDuration: TimeInterval = 0.45
private var flipCooldownActive: Bool = false
@Published var isTransitioning: Bool = false @Published var isTransitioning: Bool = false
private var transitionWorkItem: DispatchWorkItem? private var transitionWorkItem: DispatchWorkItem?
@ -380,8 +382,22 @@ class MusicManager: ObservableObject {
} }
private func triggerFlipAnimation() { private func triggerFlipAnimation() {
// Debounce: rapid metadata updates (title, artwork, bundle arriving
// separately for one track change) should only produce a single flip.
guard !flipCooldownActive else { return }
flipCooldownActive = true
// Direction: positive rotation = next (page turn forward),
// negative rotation = previous (page turn backward).
let delta: Double = lastFlipDirection == .forward ? 180 : -180
withAnimation(.easeInOut(duration: flipAnimationDuration)) { withAnimation(.easeInOut(duration: flipAnimationDuration)) {
flipAngle += 180 flipAngle += delta
}
// Reset cooldown after the animation completes so the next
// genuine track change can flip again.
DispatchQueue.main.asyncAfter(deadline: .now() + flipAnimationDuration + 0.15) { [weak self] in
self?.flipCooldownActive = false
} }
} }
@ -624,8 +640,10 @@ class MusicManager: ObservableObject {
switch behavior { switch behavior {
case .track: case .track:
if direction == .forward { if direction == .forward {
lastFlipDirection = .forward
nextTrack() nextTrack()
} else { } else {
lastFlipDirection = .backward
previousTrack() previousTrack()
} }
case .tenSecond: case .tenSecond:
@ -932,24 +950,29 @@ extension MusicManager {
private struct AlbumArtFlipModifier: ViewModifier { private struct AlbumArtFlipModifier: ViewModifier {
let angle: Double let angle: Double
private var normalizedAngle: Double {
var value = angle.truncatingRemainder(dividingBy: 360)
if value < 0 { value += 360 }
return value
}
private var mirrorScale: CGFloat {
(normalizedAngle > 90 && normalizedAngle < 270) ? -1 : 1
}
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.rotation3DEffect( .rotation3DEffect(
.degrees(angle), .degrees(angle),
axis: (x: 0, y: 1, z: 0), axis: (x: 0, y: 1, z: 0),
perspective: 0.65 anchor: .center,
anchorZ: 0,
perspective: 0.5
) )
.scaleEffect(x: mirrorScale, y: 1) // Counter-rotate the content so the image never appears mirrored.
// At odd multiples of 180° the 3D rotation mirrors along X;
// applying an opposite scaleEffect cancels that out.
.scaleEffect(x: cosineSign(for: angle), y: 1)
}
/// Returns +1 when the front face is showing, −1 when the back face is showing.
private func cosineSign(for degrees: Double) -> CGFloat {
let cos = Darwin.cos(degrees * .pi / 180)
// Use a small tolerance to avoid flickering exactly at 90°/270°.
if cos > 0.001 { return 1 }
if cos < -0.001 { return -1 }
// At the exact edge, prefer the side we're animating toward.
return degrees.truncatingRemainder(dividingBy: 360) >= 0 ? -1 : 1
} }
} }