feat: slight animation improvement

This commit is contained in:
Michael Freno
2026-01-08 22:44:01 -05:00
parent 3fc49333c4
commit 587b300a3c
3 changed files with 58 additions and 54 deletions

View File

@@ -6,10 +6,10 @@
// //
import SwiftUI import SwiftUI
import Combine
struct AnimatedFaceView: View { struct AnimatedFaceView: View {
@State private var eyeOffset: CGSize = .zero @State private var eyeOffset: CGSize = .zero
@State private var animationStep = 0
let size: CGFloat let size: CGFloat
var body: some View { var body: some View {
@@ -38,32 +38,26 @@ struct AnimatedFaceView: View {
} }
private func startAnimation() { private func startAnimation() {
let sequence: [CGSize] = [ // Continuous eye movement animation using spring
.zero, // Center DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
CGSize(width: -15, height: 0), // Left animateEyeMovement()
.zero, // Center }
CGSize(width: 15, height: 0), // Right
.zero, // Center
CGSize(width: 0, height: -10), // Up
.zero // Center
]
animateSequence(sequence, index: 0)
} }
private func animateSequence(_ sequence: [CGSize], index: Int) { private func animateEyeMovement() {
guard index < sequence.count else { // Random, smooth eye movement using spring animation for natural effect
// Loop the animation let randomOffset = CGSize(
animateSequence(sequence, index: 0) width: CGFloat.random(in: -10...10),
return height: CGFloat.random(in: -5...5)
)
withAnimation(.spring(duration: 1.2, bounce: 0.2)) {
eyeOffset = randomOffset
} }
withAnimation(.easeInOut(duration: 0.8)) { // Schedule next animation
eyeOffset = sequence[index] DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
} animateEyeMovement()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
animateSequence(sequence, index: index + 1)
} }
} }
} }
@@ -107,4 +101,4 @@ struct Arc: Shape {
#Preview("Animated Face") { #Preview("Animated Face") {
AnimatedFaceView(size: 200) AnimatedFaceView(size: 200)
.frame(width: 400, height: 400) .frame(width: 400, height: 400)
} }

View File

@@ -12,23 +12,33 @@ struct BlinkReminderView: View {
@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 blinkState: BlinkState = .open @State private var blinkProgress: Double = 0
@State private var blinkCount = 0 @State private var blinkCount = 0
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
enum BlinkState {
case open
case closed
}
var body: some View { var body: some View {
VStack { VStack {
Image(systemName: blinkState == .open ? "eye.circle" : "eye.slash.circle") // Custom eye design for more polished look
.font(.system(size: scale)) ZStack {
.foregroundColor(.accentColor) // Eye outline
.shadow(color: .black.opacity(0.2), radius: 5, x: 0, y: 2) Circle()
.stroke(Color.accentColor, lineWidth: 4)
.frame(width: scale * 1.2, height: scale * 1.2)
// Iris
Circle()
.fill(Color.accentColor)
.frame(width: scale * 0.6, height: scale * 0.6)
// Pupil that moves with blink
Circle()
.fill(.black)
.frame(width: scale * 0.25, height: scale * 0.25)
.offset(y: blinkProgress * -scale * 0.1) // Vertical movement during blink
}
.shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4)
} }
.opacity(opacity) .opacity(opacity)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
@@ -39,10 +49,10 @@ struct BlinkReminderView: View {
} }
private func startAnimation() { private func startAnimation() {
// Fade in and grow // Fade in and grow with spring animation for natural feel
withAnimation(.easeOut(duration: 0.3)) { withAnimation(.spring(duration: 0.5, bounce: 0.2)) {
opacity = 1.0 opacity = 1.0
scale = screenWidth * 0.1 scale = screenWidth * 0.12
} }
// Start blinking after fade in // Start blinking after fade in
@@ -56,15 +66,15 @@ struct BlinkReminderView: View {
let pauseBetweenBlinks = 0.2 let pauseBetweenBlinks = 0.2
func blink() { func blink() {
// Close eyes // Close eyes with spring animation for natural movement
withAnimation(.linear(duration: blinkDuration)) { withAnimation(.spring(duration: blinkDuration, bounce: 0.0)) {
blinkState = .closed blinkProgress = 1.0
} }
// Open eyes // Open eyes
DispatchQueue.main.asyncAfter(deadline: .now() + blinkDuration) { DispatchQueue.main.asyncAfter(deadline: .now() + blinkDuration) {
withAnimation(.linear(duration: blinkDuration)) { withAnimation(.spring(duration: blinkDuration, bounce: 0.0)) {
blinkState = .open blinkProgress = 0.0
} }
blinkCount += 1 blinkCount += 1
@@ -75,7 +85,7 @@ struct BlinkReminderView: View {
blink() blink()
} }
} else { } else {
// Fade out after all blinks // Fade out after all blinks with smooth spring animation
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
fadeOut() fadeOut()
} }
@@ -87,12 +97,12 @@ struct BlinkReminderView: View {
} }
private func fadeOut() { private func fadeOut() {
withAnimation(.easeOut(duration: 0.3)) { withAnimation(.spring(duration: 0.5, bounce: 0.2)) {
opacity = 0 opacity = 0
scale = screenWidth * 0.05 scale = screenWidth * 0.08
} }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
onDismiss() onDismiss()
} }
} }

View File

@@ -9,19 +9,19 @@ import SwiftUI
struct PostureReminderView: View { struct PostureReminderView: View {
var onDismiss: () -> Void var onDismiss: () -> Void
@State private var scale: CGFloat = 0 @State private var scale: CGFloat = 0
@State private var yOffset: CGFloat = 0 @State private var yOffset: CGFloat = 0
@State private var opacity: Double = 0 @State private var opacity: Double = 0
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
var body: some View { var body: some View {
VStack { VStack {
Image(systemName: "arrow.up.circle.fill") Image(systemName: "arrow.up.circle.fill")
.font(.system(size: scale)) .font(.system(size: scale))
.foregroundColor(.black) .foregroundColor(.accentColor)
.shadow(color: .black.opacity(0.2), radius: 5, x: 0, y: 2) .shadow(color: .black.opacity(0.2), radius: 5, x: 0, y: 2)
} }
.opacity(opacity) .opacity(opacity)
@@ -32,28 +32,28 @@ struct PostureReminderView: View {
startAnimation() startAnimation()
} }
} }
private func startAnimation() { private func startAnimation() {
// Phase 1: Fade in + Grow to 10% screen width // Phase 1: Fade in + Grow to 10% screen width
withAnimation(.easeOut(duration: 0.4)) { withAnimation(.easeOut(duration: 0.4)) {
opacity = 1.0 opacity = 1.0
scale = screenWidth * 0.1 scale = screenWidth * 0.1
} }
// 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 5%
withAnimation(.easeInOut(duration: 0.3)) { withAnimation(.easeInOut(duration: 0.3)) {
scale = screenWidth * 0.05 scale = screenWidth * 0.05
} }
// Phase 4: Shoot upward // Phase 4: Shoot upward
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
withAnimation(.easeIn(duration: 0.4)) { withAnimation(.easeIn(duration: 0.4)) {
yOffset = -screenHeight yOffset = -screenHeight
opacity = 0 opacity = 0
} }
// Dismiss after animation // Dismiss after animation
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
onDismiss() onDismiss()