diff --git a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift index 77fe179..8da6261 100644 --- a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift +++ b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift @@ -882,6 +882,7 @@ struct MinimalisticAlbumArtView: View { .clipShape(RoundedRectangle(cornerRadius: 12)) .matchedGeometryEffect(id: "albumArt", in: albumArtNamespace) .albumArtFlip(angle: musicManager.flipAngle) + .parallax3D(magnitude: 12) } .buttonStyle(PlainButtonStyle()) .opacity(musicManager.isPlaying ? 1 : 0.4) diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index 3e7001f..3174032 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -69,6 +69,7 @@ struct AlbumArtView: View { appIconOverlay } .albumArtFlip(angle: musicManager.flipAngle) + .parallax3D(magnitude: 12) } .buttonStyle(PlainButtonStyle()) .scaleEffect(musicManager.isPlaying ? 1 : 0.85) diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index b13dea7..d3aaaad 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1453,6 +1453,8 @@ struct Media: View { .disabled(!enableSneakPeek) Defaults.Toggle("Enable lyrics", key: .enableLyrics) .settingsHighlight(id: highlightID("Enable lyrics")) + Defaults.Toggle("Enable album art parallax effect", key: .enableParallaxEffect) + .settingsHighlight(id: highlightID("Enable album art parallax effect")) Picker("Sneak Peek Style", selection: $sneakPeekStyles){ ForEach(SneakPeekStyle.allCases) { style in Text(style.rawValue).tag(style) diff --git a/DynamicIsland/extensions/View+Parallax3D.swift b/DynamicIsland/extensions/View+Parallax3D.swift new file mode 100644 index 0000000..efc1782 --- /dev/null +++ b/DynamicIsland/extensions/View+Parallax3D.swift @@ -0,0 +1,70 @@ +// +// View+Parallax3D.swift +// DynamicIsland +// +// Created for 3D Parallax Effect +// + +import SwiftUI + +import Defaults + +struct ParallaxMotionModifier: ViewModifier { + var magnitude: Double + + @Default(.enableParallaxEffect) var enableParallaxEffect + @State private var offset: CGSize = .zero + @State private var isHovering = false + + func body(content: Content) -> some View { + if !enableParallaxEffect { + content + } else { + content + .overlay( + GeometryReader { proxy in + Color.clear + .onContinuousHover { phase in + switch phase { + case .active(let location): + let width = proxy.size.width + let height = proxy.size.height + + // Normalize -1 to 1 + // (0,0) is top left, so we want center (width/2, height/2) to be (0,0) + let x = (location.x / width) * 2 - 1 + let y = (location.y / height) * 2 - 1 + + // Animate the transition to the new hover position + withAnimation(.interactiveSpring(response: 0.1, dampingFraction: 0.5)) { + offset = CGSize(width: x, height: y) + isHovering = true + } + case .ended: + // Return to center when hover ends + withAnimation(.spring(response: 0.35, dampingFraction: 0.6)) { + offset = .zero + isHovering = false + } + } + } + } + ) + .rotation3DEffect( + .degrees(offset.height * magnitude), // Y movement rotates around X axis + axis: (x: 1, y: 0, z: 0) + ) + .rotation3DEffect( + .degrees(offset.width * -magnitude), // X movement rotates around Y axis (inverted to look naturally) + axis: (x: 0, y: 1, z: 0) + ) + .scaleEffect(isHovering ? 1.02 : 1.0) // Subtle scale up on hover + } + } +} + +extension View { + func parallax3D(magnitude: Double = 10) -> some View { + modifier(ParallaxMotionModifier(magnitude: magnitude)) + } +} diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 4d6dc3d..f223294 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -442,6 +442,7 @@ extension Defaults.Keys { static let sneakPeekStyles = Key("sneakPeekStyles", default: .standard) static let showSneakPeekOnTrackChange = Key("showSneakPeekOnTrackChange", default: true) static let enableFullscreenMediaDetection = Key("enableFullscreenMediaDetection", default: true) + static let enableParallaxEffect = Key("enableParallaxEffect", default: true) static let waitInterval = Key("waitInterval", default: 3) static let showShuffleAndRepeat = Key("showShuffleAndRepeat", default: true) static let showMediaOutputControl = Key("showMediaOutputControl", default: false)