wip
This commit is contained in:
236
Shared/DemoAnimationManager.swift
Normal file
236
Shared/DemoAnimationManager.swift
Normal file
@@ -0,0 +1,236 @@
|
||||
//
|
||||
// DemoAnimationManager.swift
|
||||
// Feels
|
||||
//
|
||||
// Manages demo animation mode for promotional videos.
|
||||
// Animates filling mood entries from top-left to bottom-right.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
/// Manages demo animation state for promotional video recording
|
||||
@MainActor
|
||||
final class DemoAnimationManager: ObservableObject {
|
||||
static let shared = DemoAnimationManager()
|
||||
|
||||
/// Whether demo mode is active
|
||||
@Published var isDemoMode: Bool = false
|
||||
|
||||
/// Whether the animation has started (after the 3-second delay)
|
||||
@Published var animationStarted: Bool = false
|
||||
|
||||
/// Current animation progress (0.0 to 1.0)
|
||||
@Published var animationProgress: Double = 0.0
|
||||
|
||||
/// The delay before starting the fill animation (in seconds)
|
||||
let startDelay: TimeInterval = 3.0
|
||||
|
||||
/// Total animation duration for filling all cells in one month
|
||||
let monthAnimationDuration: TimeInterval = 1.5
|
||||
|
||||
/// Delay between months
|
||||
let monthDelay: TimeInterval = 0.3
|
||||
|
||||
/// Delay between each cell in year view
|
||||
let yearCellDelay: TimeInterval = 0.05
|
||||
|
||||
/// Delay between month columns in year view
|
||||
let yearMonthDelay: TimeInterval = 0.1
|
||||
|
||||
/// Timer for animation progress
|
||||
private var animationTimer: Timer?
|
||||
private var startTime: Date?
|
||||
|
||||
private init() {}
|
||||
|
||||
/// Start demo mode - will begin animation after delay
|
||||
func startDemoMode() {
|
||||
isDemoMode = true
|
||||
animationStarted = false
|
||||
animationProgress = 0.0
|
||||
|
||||
// Start animation after delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + startDelay) { [weak self] in
|
||||
self?.beginAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
/// Restart demo mode animation (for switching between tabs)
|
||||
func restartAnimation() {
|
||||
animationTimer?.invalidate()
|
||||
animationTimer = nil
|
||||
animationStarted = false
|
||||
animationProgress = 0.0
|
||||
startTime = nil
|
||||
|
||||
// Start animation after delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + startDelay) { [weak self] in
|
||||
self?.beginAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop demo mode and reset
|
||||
func stopDemoMode() {
|
||||
isDemoMode = false
|
||||
animationStarted = false
|
||||
animationProgress = 0.0
|
||||
animationTimer?.invalidate()
|
||||
animationTimer = nil
|
||||
startTime = nil
|
||||
}
|
||||
|
||||
/// Begin the fill animation
|
||||
private func beginAnimation() {
|
||||
guard isDemoMode else { return }
|
||||
|
||||
animationStarted = true
|
||||
startTime = Date()
|
||||
|
||||
// Update progress at 60fps
|
||||
animationTimer = Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.updateProgress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update animation progress - runs continuously for multi-month animation
|
||||
private func updateProgress() {
|
||||
guard let startTime = startTime else { return }
|
||||
|
||||
let elapsed = Date().timeIntervalSince(startTime)
|
||||
// Progress goes from 0 to a large number (we use elapsed time directly)
|
||||
animationProgress = elapsed
|
||||
|
||||
// Stop after a reasonable max time (300 seconds for year view with 0.65s per cell)
|
||||
if elapsed >= 300.0 {
|
||||
animationTimer?.invalidate()
|
||||
animationTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the animation time for a specific month (0-indexed from most recent)
|
||||
/// MonthView shows months in reverse chronological order, so monthIndex 0 is the current month
|
||||
func monthStartTime(monthIndex: Int) -> Double {
|
||||
return Double(monthIndex) * (monthAnimationDuration + monthDelay)
|
||||
}
|
||||
|
||||
/// Check if a cell should be visible based on current animation progress
|
||||
/// For MonthView: animates cells within each month from top-left to bottom-right
|
||||
/// - Parameters:
|
||||
/// - row: Row index within the month grid
|
||||
/// - column: Column index within the month grid
|
||||
/// - totalRows: Total number of rows in this month
|
||||
/// - totalColumns: Total number of columns (7 for weekdays)
|
||||
/// - monthIndex: Which month this is (0 = most recent/current month)
|
||||
/// - Returns: True if cell should be visible
|
||||
func isCellVisible(row: Int, column: Int, totalRows: Int, totalColumns: Int, monthIndex: Int = 0) -> Bool {
|
||||
guard animationStarted else { return false }
|
||||
|
||||
// Calculate when this month's animation starts
|
||||
let monthStart = monthStartTime(monthIndex: monthIndex)
|
||||
|
||||
// Check if we've reached this month yet
|
||||
if animationProgress < monthStart {
|
||||
return false
|
||||
}
|
||||
|
||||
// Calculate position within this month (top-left to bottom-right)
|
||||
let totalCells = Double(totalRows * totalColumns)
|
||||
let cellIndex = Double(row * totalColumns + column)
|
||||
let normalizedPosition = cellIndex / max(1, totalCells - 1)
|
||||
|
||||
// Calculate when this specific cell should appear
|
||||
let cellDelay = monthStart + (normalizedPosition * monthAnimationDuration)
|
||||
|
||||
return animationProgress >= cellDelay
|
||||
}
|
||||
|
||||
/// Check if a cell should be visible for YearView
|
||||
/// For YearView: animates column by column (month by month) within each year
|
||||
/// Each cell waits for the previous cell to complete before starting
|
||||
func isCellVisibleForYear(row: Int, column: Int, totalRows: Int, totalColumns: Int, yearIndex: Int = 0) -> Bool {
|
||||
guard animationStarted else { return false }
|
||||
|
||||
// Calculate total cells in previous years
|
||||
let cellsPerYear = 31 * 12 // approximate max cells per year
|
||||
let yearStart = Double(yearIndex) * (Double(cellsPerYear) * yearCellDelay + yearMonthDelay)
|
||||
|
||||
if animationProgress < yearStart {
|
||||
return false
|
||||
}
|
||||
|
||||
// Calculate cell position: column-major order (month by month, then day within month)
|
||||
// Each month column has up to 31 days
|
||||
let cellsBeforeThisMonth = column * 31
|
||||
let cellIndex = cellsBeforeThisMonth + row
|
||||
|
||||
// Add month delay between columns
|
||||
let monthDelays = Double(column) * yearMonthDelay
|
||||
|
||||
// Each cell starts after the previous cell's delay
|
||||
let cellStart = yearStart + Double(cellIndex) * yearCellDelay + monthDelays
|
||||
|
||||
return animationProgress >= cellStart
|
||||
}
|
||||
|
||||
/// Calculate the percentage of cells visible for a month (0.0 to 1.0)
|
||||
/// Used for animating charts in demo mode
|
||||
func visiblePercentageForMonth(totalCells: Int, monthIndex: Int) -> Double {
|
||||
guard animationStarted else { return 0.0 }
|
||||
|
||||
let monthStart = monthStartTime(monthIndex: monthIndex)
|
||||
|
||||
// Before this month starts
|
||||
if animationProgress < monthStart {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Calculate how far through the month animation we are
|
||||
let progressInMonth = animationProgress - monthStart
|
||||
let percentage = min(1.0, progressInMonth / monthAnimationDuration)
|
||||
|
||||
return percentage
|
||||
}
|
||||
|
||||
/// Calculate the percentage of cells visible for a year (0.0 to 1.0)
|
||||
/// Used for animating charts in year view demo mode
|
||||
func visiblePercentageForYear(totalCells: Int, yearIndex: Int) -> Double {
|
||||
guard animationStarted else { return 0.0 }
|
||||
|
||||
// Calculate when this year's animation starts
|
||||
let cellsPerYear = 31 * 12
|
||||
let yearStart = Double(yearIndex) * (Double(cellsPerYear) * yearCellDelay + yearMonthDelay)
|
||||
|
||||
// Before this year starts
|
||||
if animationProgress < yearStart {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Total year duration based on cell delays and month delays
|
||||
let yearDuration = Double(cellsPerYear) * yearCellDelay + 12.0 * yearMonthDelay
|
||||
let progressInYear = animationProgress - yearStart
|
||||
let percentage = min(1.0, progressInYear / yearDuration)
|
||||
|
||||
return percentage
|
||||
}
|
||||
|
||||
/// Generate a random mood biased towards positive values
|
||||
/// Distribution: Great 35%, Good 30%, Average 20%, Bad 10%, Horrible 5%
|
||||
static func randomPositiveMood() -> Mood {
|
||||
let random = Double.random(in: 0...1)
|
||||
switch random {
|
||||
case 0..<0.35:
|
||||
return .great
|
||||
case 0.35..<0.65:
|
||||
return .good
|
||||
case 0.65..<0.85:
|
||||
return .average
|
||||
case 0.85..<0.95:
|
||||
return .bad
|
||||
default:
|
||||
return .horrible
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user