This commit is contained in:
Trey t
2026-01-29 11:17:20 -06:00
parent 810ac2d649
commit 8e0f69c29a
4 changed files with 790 additions and 66 deletions

View 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
}
}
}