feat: slight animation improvement
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user