fix: proper scaling of animations
This commit is contained in:
@@ -140,14 +140,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
case .blinkTriggered:
|
case .blinkTriggered:
|
||||||
|
let sizePercentage = settingsManager?.settings.subtleReminderSizePercentage ?? 15.0
|
||||||
contentView = AnyView(
|
contentView = AnyView(
|
||||||
BlinkReminderView { [weak self] in
|
BlinkReminderView(sizePercentage: sizePercentage) { [weak self] in
|
||||||
self?.timerEngine?.dismissReminder()
|
self?.timerEngine?.dismissReminder()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
case .postureTriggered:
|
case .postureTriggered:
|
||||||
|
let sizePercentage = settingsManager?.settings.subtleReminderSizePercentage ?? 10.0
|
||||||
contentView = AnyView(
|
contentView = AnyView(
|
||||||
PostureReminderView { [weak self] in
|
PostureReminderView(sizePercentage: sizePercentage) { [weak self] in
|
||||||
self?.timerEngine?.dismissReminder()
|
self?.timerEngine?.dismissReminder()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ struct AppSettings: Codable, Equatable, Hashable {
|
|||||||
var userTimers: [UserTimer]
|
var userTimers: [UserTimer]
|
||||||
|
|
||||||
// UI and display settings
|
// UI and display settings
|
||||||
var subtleReminderSizePercentage: Double // 2-35% of screen width
|
var subtleReminderSizePercentage: Double // 0.5-25% of screen width
|
||||||
|
|
||||||
// App state and behavior
|
// App state and behavior
|
||||||
var hasCompletedOnboarding: Bool
|
var hasCompletedOnboarding: Bool
|
||||||
@@ -29,10 +29,13 @@ struct AppSettings: Codable, Equatable, Hashable {
|
|||||||
var playSounds: Bool
|
var playSounds: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
lookAwayTimer: TimerConfiguration = TimerConfiguration(enabled: true, intervalSeconds: 20 * 60),
|
lookAwayTimer: TimerConfiguration = TimerConfiguration(
|
||||||
|
enabled: true, intervalSeconds: 20 * 60),
|
||||||
lookAwayCountdownSeconds: Int = 20,
|
lookAwayCountdownSeconds: Int = 20,
|
||||||
blinkTimer: TimerConfiguration = TimerConfiguration(enabled: false, intervalSeconds: 7 * 60),
|
blinkTimer: TimerConfiguration = TimerConfiguration(
|
||||||
postureTimer: TimerConfiguration = TimerConfiguration(enabled: true, intervalSeconds: 30 * 60),
|
enabled: false, intervalSeconds: 7 * 60),
|
||||||
|
postureTimer: TimerConfiguration = TimerConfiguration(
|
||||||
|
enabled: true, intervalSeconds: 30 * 60),
|
||||||
userTimers: [UserTimer] = [],
|
userTimers: [UserTimer] = [],
|
||||||
subtleReminderSizePercentage: Double = 5.0,
|
subtleReminderSizePercentage: Double = 5.0,
|
||||||
hasCompletedOnboarding: Bool = false,
|
hasCompletedOnboarding: Bool = false,
|
||||||
@@ -66,14 +69,12 @@ struct AppSettings: Codable, Equatable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: AppSettings, rhs: AppSettings) -> Bool {
|
static func == (lhs: AppSettings, rhs: AppSettings) -> Bool {
|
||||||
lhs.lookAwayTimer == rhs.lookAwayTimer &&
|
lhs.lookAwayTimer == rhs.lookAwayTimer
|
||||||
lhs.lookAwayCountdownSeconds == rhs.lookAwayCountdownSeconds &&
|
&& lhs.lookAwayCountdownSeconds == rhs.lookAwayCountdownSeconds
|
||||||
lhs.blinkTimer == rhs.blinkTimer &&
|
&& lhs.blinkTimer == rhs.blinkTimer && lhs.postureTimer == rhs.postureTimer
|
||||||
lhs.postureTimer == rhs.postureTimer &&
|
&& lhs.userTimers == rhs.userTimers
|
||||||
lhs.userTimers == rhs.userTimers &&
|
&& lhs.subtleReminderSizePercentage == rhs.subtleReminderSizePercentage
|
||||||
lhs.subtleReminderSizePercentage == rhs.subtleReminderSizePercentage &&
|
&& lhs.hasCompletedOnboarding == rhs.hasCompletedOnboarding
|
||||||
lhs.hasCompletedOnboarding == rhs.hasCompletedOnboarding &&
|
&& lhs.launchAtLogin == rhs.launchAtLogin && lhs.playSounds == rhs.playSounds
|
||||||
lhs.launchAtLogin == rhs.launchAtLogin &&
|
|
||||||
lhs.playSounds == rhs.playSounds
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,62 +5,48 @@
|
|||||||
// Created by Mike Freno on 1/8/26.
|
// Created by Mike Freno on 1/8/26.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import Lottie
|
import Lottie
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct LottieView: NSViewRepresentable {
|
struct GazeLottieView: View {
|
||||||
let animationName: String
|
let animationName: String
|
||||||
let loopMode: LottieLoopMode
|
let loopMode: LottieLoopMode
|
||||||
let animationSpeed: CGFloat
|
let animationSpeed: CGFloat
|
||||||
|
let onAnimationFinish: ((Bool) -> Void)?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
animationName: String,
|
animationName: String,
|
||||||
loopMode: LottieLoopMode = .playOnce,
|
loopMode: LottieLoopMode = .playOnce,
|
||||||
animationSpeed: CGFloat = 1.0
|
animationSpeed: CGFloat = 1.0,
|
||||||
|
onAnimationFinish: ((Bool) -> Void)? = nil
|
||||||
) {
|
) {
|
||||||
self.animationName = animationName
|
self.animationName = animationName
|
||||||
self.loopMode = loopMode
|
self.loopMode = loopMode
|
||||||
self.animationSpeed = animationSpeed
|
self.animationSpeed = animationSpeed
|
||||||
|
self.onAnimationFinish = onAnimationFinish
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeNSView(context: Context) -> LottieAnimationView {
|
var body: some View {
|
||||||
let animationView = LottieAnimationView()
|
|
||||||
animationView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
if let animation = LottieAnimation.named(animationName) {
|
if let animation = LottieAnimation.named(animationName) {
|
||||||
animationView.animation = animation
|
LottieView(animation: animation)
|
||||||
animationView.loopMode = loopMode
|
.playing(.fromProgress(nil, toProgress: 1, loopMode: loopMode))
|
||||||
animationView.animationSpeed = animationSpeed
|
.animationSpeed(animationSpeed)
|
||||||
animationView.backgroundBehavior = .pauseAndRestore
|
.animationDidFinish { completed in
|
||||||
animationView.play()
|
onAnimationFinish?(completed)
|
||||||
}
|
}
|
||||||
|
|
||||||
return animationView
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateNSView(_ nsView: LottieAnimationView, context: Context) {
|
|
||||||
guard nsView.animation == nil || nsView.isAnimationPlaying == false else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let animation = LottieAnimation.named(animationName) {
|
|
||||||
nsView.animation = animation
|
|
||||||
nsView.loopMode = loopMode
|
|
||||||
nsView.animationSpeed = animationSpeed
|
|
||||||
nsView.play()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Lottie Preview") {
|
#Preview("Lottie Preview") {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
LottieView(animationName: "blink")
|
GazeLottieView(animationName: "blink")
|
||||||
.frame(width: 200, height: 200)
|
.frame(width: 200, height: 200)
|
||||||
|
|
||||||
LottieView(animationName: "look-away", loopMode: .loop)
|
GazeLottieView(animationName: "look-away", loopMode: .loop)
|
||||||
.frame(width: 200, height: 200)
|
.frame(width: 200, height: 200)
|
||||||
|
|
||||||
LottieView(animationName: "posture")
|
GazeLottieView(animationName: "posture")
|
||||||
.frame(width: 200, height: 200)
|
.frame(width: 200, height: 200)
|
||||||
}
|
}
|
||||||
.frame(width: 600, height: 800)
|
.frame(width: 600, height: 800)
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ struct SettingsOnboardingView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Slider(
|
Slider(
|
||||||
value: $subtleReminderSizePercentage,
|
value: $subtleReminderSizePercentage,
|
||||||
in: 2...35,
|
in: 0.5...25,
|
||||||
step: 1
|
step: 0.5
|
||||||
)
|
)
|
||||||
Text("\(Int(subtleReminderSizePercentage))%")
|
Text("\(String(format: "%.1f", subtleReminderSizePercentage))%")
|
||||||
.frame(width: 50, alignment: .trailing)
|
.frame(width: 50, alignment: .trailing)
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,8 @@ struct SettingsOnboardingView: View {
|
|||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.glassEffect(.regular.tint(.orange).interactive(), in: .rect(cornerRadius: 10))
|
.glassEffect(
|
||||||
|
.regular.tint(.orange).interactive(), in: .rect(cornerRadius: 10))
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,29 +5,43 @@
|
|||||||
// Created by Mike Freno on 1/7/26.
|
// Created by Mike Freno on 1/7/26.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import Lottie
|
import Lottie
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct BlinkReminderView: View {
|
struct BlinkReminderView: View {
|
||||||
|
let sizePercentage: Double
|
||||||
var onDismiss: () -> Void
|
var onDismiss: () -> Void
|
||||||
|
|
||||||
@State private var opacity: Double = 0
|
@State private var opacity: Double = 0
|
||||||
@State private var scale: CGFloat = 0
|
@State private var scale: CGFloat = 0
|
||||||
|
@State private var shouldShowAnimation = false
|
||||||
|
|
||||||
private let screenHeight = NSScreen.main?.frame.height ?? 800
|
private let screenHeight = NSScreen.main?.frame.height ?? 800
|
||||||
private let screenWidth = NSScreen.main?.frame.width ?? 1200
|
private let screenWidth = NSScreen.main?.frame.width ?? 1200
|
||||||
|
|
||||||
// For now, we'll use hardcoded size but leave framework for configuration
|
private var baseSize: CGFloat {
|
||||||
// In a real implementation, this would be passed in from SettingsManager
|
screenWidth * (sizePercentage / 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
LottieView(
|
if shouldShowAnimation {
|
||||||
animationName: AnimationAsset.blink.fileName,
|
GazeLottieView(
|
||||||
loopMode: .playOnce,
|
animationName: AnimationAsset.blink.fileName,
|
||||||
animationSpeed: 1.0
|
loopMode: .playOnce,
|
||||||
)
|
animationSpeed: 1.0,
|
||||||
.frame(width: scale, height: scale)
|
onAnimationFinish: { completed in
|
||||||
.shadow(color: .black.opacity(0.2), radius: 5, x: 0, y: 2)
|
if completed {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
|
fadeOut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.frame(width: baseSize, height: baseSize)
|
||||||
|
.scaleEffect(scale)
|
||||||
|
.shadow(color: .black.opacity(0.2), radius: 5, x: 0, y: 2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.opacity(opacity)
|
.opacity(opacity)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
@@ -38,22 +52,20 @@ struct BlinkReminderView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func startAnimation() {
|
private func startAnimation() {
|
||||||
// Fade in and grow
|
|
||||||
withAnimation(.easeOut(duration: 0.3)) {
|
withAnimation(.easeOut(duration: 0.3)) {
|
||||||
opacity = 1.0
|
opacity = 1.0
|
||||||
scale = screenWidth * 0.15
|
scale = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animation duration (2 seconds for double blink) + hold time
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.3) {
|
shouldShowAnimation = true
|
||||||
fadeOut()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fadeOut() {
|
private func fadeOut() {
|
||||||
withAnimation(.easeOut(duration: 0.3)) {
|
withAnimation(.easeOut(duration: 0.3)) {
|
||||||
opacity = 0
|
opacity = 0
|
||||||
scale = screenWidth * 0.1
|
scale = 0.7
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
@@ -63,11 +75,11 @@ struct BlinkReminderView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Blink Reminder") {
|
#Preview("Blink Reminder") {
|
||||||
BlinkReminderView(onDismiss: {})
|
BlinkReminderView(sizePercentage: 15.0, onDismiss: {})
|
||||||
.frame(width: 800, height: 600)
|
.frame(width: 800, height: 600)
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Blink Reminder") {
|
#Preview("Blink Reminder") {
|
||||||
BlinkReminderView(onDismiss: {})
|
BlinkReminderView(sizePercentage: 15.0, onDismiss: {})
|
||||||
.frame(width: 800, height: 600)
|
.frame(width: 800, height: 600)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
// Created by Mike Freno on 1/7/26.
|
// Created by Mike Freno on 1/7/26.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import Lottie
|
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import Lottie
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct LookAwayReminderView: View {
|
struct LookAwayReminderView: View {
|
||||||
let countdownSeconds: Int
|
let countdownSeconds: Int
|
||||||
@@ -38,7 +38,7 @@ struct LookAwayReminderView: View {
|
|||||||
.font(.system(size: 28))
|
.font(.system(size: 28))
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.white.opacity(0.9))
|
||||||
|
|
||||||
LottieView(
|
GazeLottieView(
|
||||||
animationName: AnimationAsset.lookAway.fileName,
|
animationName: AnimationAsset.lookAway.fileName,
|
||||||
loopMode: .loop,
|
loopMode: .loop,
|
||||||
animationSpeed: 0.75
|
animationSpeed: 0.75
|
||||||
@@ -116,10 +116,10 @@ struct LookAwayReminderView: View {
|
|||||||
|
|
||||||
private func setupKeyMonitor() {
|
private func setupKeyMonitor() {
|
||||||
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
|
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
|
||||||
if event.keyCode == 53 { // ESC key
|
if event.keyCode == 53 { // ESC key
|
||||||
dismiss()
|
dismiss()
|
||||||
return nil
|
return nil
|
||||||
} else if event.keyCode == 49 { // Space key
|
} else if event.keyCode == 49 { // Space key
|
||||||
dismiss()
|
dismiss()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PostureReminderView: View {
|
struct PostureReminderView: View {
|
||||||
|
let sizePercentage: Double
|
||||||
var onDismiss: () -> Void
|
var onDismiss: () -> Void
|
||||||
|
|
||||||
@State private var scale: CGFloat = 0
|
@State private var scale: CGFloat = 0
|
||||||
@@ -34,17 +35,17 @@ struct PostureReminderView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func startAnimation() {
|
private func startAnimation() {
|
||||||
// Phase 1: Fade in + Grow to 10% screen width
|
// Phase 1: Fade in + Grow to configured size
|
||||||
withAnimation(.easeOut(duration: 0.4)) {
|
withAnimation(.easeOut(duration: 0.4)) {
|
||||||
opacity = 1.0
|
opacity = 1.0
|
||||||
scale = screenWidth * 0.1
|
scale = screenWidth * (sizePercentage / 100.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: Hold
|
// Phase 2: Hold
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4 + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4 + 0.5) {
|
||||||
// Phase 3: Shrink to 5%
|
// Phase 3: Shrink to half the configured size
|
||||||
withAnimation(.easeInOut(duration: 0.3)) {
|
withAnimation(.easeInOut(duration: 0.3)) {
|
||||||
scale = screenWidth * 0.05
|
scale = screenWidth * (sizePercentage / 100.0) * 0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 4: Shoot upward
|
// Phase 4: Shoot upward
|
||||||
@@ -64,6 +65,6 @@ struct PostureReminderView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Posture Reminder") {
|
#Preview("Posture Reminder") {
|
||||||
PostureReminderView(onDismiss: {})
|
PostureReminderView(sizePercentage: 10.0, onDismiss: {})
|
||||||
.frame(width: 800, height: 600)
|
.frame(width: 800, height: 600)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user