Skip to content

Commit

Permalink
Add AnimatedArtworkView
Browse files Browse the repository at this point in the history
  • Loading branch information
rudrankriyam committed Oct 14, 2024
1 parent 6cd2b22 commit dcd1665
Showing 1 changed file with 147 additions and 0 deletions.
147 changes: 147 additions & 0 deletions Sources/MusadoraKit/Views/AnimatedArtworkView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import SwiftUI
import MusicKit

/// A view that displays an animated artwork with a dynamic mesh gradient background.
///
/// This view uses the current queue entry's artwork to create a visually appealing
/// animated background. It extracts dominant colors from the artwork and uses them
/// to generate a mesh gradient that animates over time.
///
/// - Important: This view is available on iOS 18, macOS 15, watchOS 11, tvOS 18, and visionOS 2 or later.
@available(iOS 18.0, macOS 15.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
public struct AnimatedArtworkView: View {
/// The music player queue to observe for current entry changes.
@ObservedObject private var queue: MusicPlayer.Queue

/// The current entry in the music player queue.
@State private var currentEntry: MusicPlayer.Queue.Entry?

/// The animation phase used for the gradient animation.
@State private var phase: Double = 0

/// The dominant colors extracted from the artwork.
@State private var dominantColors: [Color] = []

/// Random offset for X-axis to add variation to the gradient.
@State private var randomOffsetX: Float = 0

/// Random offset for Y-axis to add variation to the gradient.
@State private var randomOffsetY: Float = 0

/// The artwork to display, if provided directly.
var artwork: MusicKit.Artwork?

/// The width of the artwork image to fetch.
private let width: Int

/// The height of the artwork image to fetch.
private let height: Int

/// The points used to define the mesh gradient.
@State private var points: [SIMD2<Float>] = [
SIMD2<Float>(0.000, 0.000),
SIMD2<Float>(0.333, 0.000),
SIMD2<Float>(0.667, 0.000),
SIMD2<Float>(1.000, 0.000),
SIMD2<Float>(0.000, 0.333),
SIMD2<Float>(0.789, 0.234),
SIMD2<Float>(0.456, 0.901),
SIMD2<Float>(1.000, 0.333),
SIMD2<Float>(0.000, 0.667),
SIMD2<Float>(0.321, 0.567),
SIMD2<Float>(0.765, 0.123),
SIMD2<Float>(1.000, 0.667),
SIMD2<Float>(0.000, 1.000),
SIMD2<Float>(0.333, 1.000),
SIMD2<Float>(0.667, 1.000),
SIMD2<Float>(1.000, 1.000)
]

/// Initializes a new instance of `AnimatedArtworkView`.
///
/// - Parameters:
/// - queue: The music player queue to observe.
/// - artwork: An optional artwork to display instead of the queue's current entry artwork.
/// - width: The width of the artwork image to fetch. Defaults to 300.
/// - height: The height of the artwork image to fetch. Defaults to 300.
public init(queue: MusicPlayer.Queue, artwork: MusicKit.Artwork? = nil, width: Int = 300, height: Int = 300) {
self.queue = queue
self.artwork = artwork
self.width = width
self.height = height
}

public var body: some View {
TimelineView(.animation) { timeline in
MeshGradient(width: 4, height: 4, points: gradientContent(for: timeline.date), colors: dominantColors, smoothsColors: true)
}
.ignoresSafeArea()
.onAppear(perform: generateRandomOffsets)
.task {
await extractColors()
}
.onChange(of: queue.currentEntry) { _, _ in
Task {
await extractColors()
}
}
}

/// Extracts the dominant colors from the artwork.
private func extractColors() async {
do {
let colors: [Color]
if let artwork = self.artwork {
colors = try await artwork.fetchColors(width: width, height: height, numberOfColors: 16)
} else if case .song(let song) = queue.currentEntry?.item,
let songArtwork = song.artwork {
colors = try await songArtwork.fetchColors(width: width, height: height, numberOfColors: 16)
} else {
colors = []
}

await MainActor.run {
self.dominantColors = colors
}
} catch {
print("Error extracting colors: \(error)")
}
}

/// Generates the gradient content for a given date.
///
/// - Parameter date: The current date used for animation.
/// - Returns: An array of SIMD2<Float> points representing the gradient positions.
private func gradientContent(for date: Date) -> [SIMD2<Float>] {
let phase = CGFloat(date.timeIntervalSince1970) / 3.0

var animatedPositions = points

// Animate edge points
animatedPositions[1].x = Float(0.33 + 0.1 * cos(phase * 0.7)) // Top edge
animatedPositions[2].x = Float(0.67 - 0.1 * cos(phase * 0.8)) // Top edge
animatedPositions[4].y = Float(0.33 + 0.1 * cos(phase * 0.9)) // Left edge
animatedPositions[7].y = Float(0.37 - 0.1 * cos(phase * 0.6)) // Left edge
animatedPositions[11].y = Float(0.67 - 0.1 * cos(phase * 1.2)) // Bottom edge
animatedPositions[13].x = Float(0.33 + 0.1 * cos(phase * 1.3)) // Right edge
animatedPositions[14].x = Float(0.67 - 0.1 * cos(phase * 1.4)) // Right edge

// Animate inner points
animatedPositions[5].x = Float(0.33 + 0.15 * cos(phase * 0.8))
animatedPositions[5].y = Float(0.33 + 0.15 * cos(phase * 0.9))
animatedPositions[6].x = Float(0.67 - 0.15 * cos(phase * 1.0))
animatedPositions[6].y = Float(0.33 + 0.15 * cos(phase * 1.1))
animatedPositions[9].x = Float(0.33 + 0.15 * cos(phase * 1.2))
animatedPositions[9].y = Float(0.67 - 0.15 * cos(phase * 1.3))
animatedPositions[10].x = Float(0.67 - 0.15 * cos(phase * 1.4))
animatedPositions[10].y = Float(0.67 - 0.15 * cos(phase * 1.5))

return animatedPositions
}

/// Generates random offsets for the gradient animation.
private func generateRandomOffsets() {
randomOffsetX = Float.random(in: -0.1...0.1)
randomOffsetY = Float.random(in: -0.1...0.1)
}
}

0 comments on commit dcd1665

Please sign in to comment.