Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 220 additions & 28 deletions boringNotch/components/AnimatedFace.swift
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think generally it would be best practice to do the mouse tracking/sleep tracking logic in a separate file.

Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,58 @@
//
// Created by Harsh Vardhan Goswami on 04/08/24.
//

import SwiftUI

struct MinimalFaceFeatures: View {
@State private var isBlinking = false
@State var height:CGFloat = 20;
@State var width:CGFloat = 30;
@State private var mouseLocation: CGPoint = .zero
@State private var timer: Timer?
@State private var isSleeping = false
@State private var lastMouseMoveTime = Date()
@State private var yawnTimer: Timer?
@State var height: CGFloat = 20
@State var width: CGFloat = 32

var body: some View {
VStack(spacing: 4) { // Adjusted spacing to fit within 30x30
// Eyes
HStack(spacing: 4) { // Adjusted spacing to fit within 30x30
Eye(isBlinking: $isBlinking)
Eye(isBlinking: $isBlinking)
}

// Nose and mouth combined
VStack(spacing: 2) { // Adjusted spacing to fit within 30x30
// Nose
RoundedRectangle(cornerRadius: 2)
.fill(Color.white)
.frame(width: 3, height: 4)
ZStack {
VStack(spacing: 3) {
HStack(spacing: 8) {
MouseFollowingEye(mouseLocation: mouseLocation, isBlinking: $isBlinking, isSleeping: $isSleeping)
MouseFollowingEye(mouseLocation: mouseLocation, isBlinking: $isBlinking, isSleeping: $isSleeping)
}

// Mouth (happy)
GeometryReader { geometry in
Path { path in
let width = geometry.size.width
let height = geometry.size.height
path.move(to: CGPoint(x: 0, y: height / 2))
path.addQuadCurve(to: CGPoint(x: width, y: height / 2), control: CGPoint(x: width / 2, y: height))
if !isSleeping {
VStack(spacing: 1) {
RoundedRectangle(cornerRadius: 1)
.fill(Color.white)
.frame(width: 2, height: 3)

Path { path in
let width: CGFloat = 12
let height: CGFloat = 6
path.move(to: CGPoint(x: 0, y: height / 2))
path.addQuadCurve(to: CGPoint(x: width, y: height / 2), control: CGPoint(x: width / 2, y: height))
}
.stroke(Color.white, lineWidth: 1.5)
.frame(width: 12, height: 6)
}
.stroke(Color.white, lineWidth: 2)
}
.frame(width: 14, height: 10)
}

if isSleeping {
SleepingZZZ()
.offset(x: 12, y: -8)
}
}
.frame(width: self.width, height: self.height) // Maximum size of face
.frame(width: self.width, height: self.height)
.onAppear {
startBlinking()
startMouseTracking()
startSleepTracking()
}
.onDisappear {
stopMouseTracking()
stopSleepTracking()
}
}

Expand All @@ -57,6 +70,120 @@ struct MinimalFaceFeatures: View {
}
}
}

func startMouseTracking() {
timer = Timer.scheduledTimer(withTimeInterval: 1/60.0, repeats: true) { _ in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be very inefficient. I am pretty sure there are event monitors that would be more performant, but I don't know off the top of my head.

let screenMouseLocation = NSEvent.mouseLocation
if screenMouseLocation != mouseLocation {
lastMouseMoveTime = Date()
}
mouseLocation = screenMouseLocation
}
}

func stopMouseTracking() {
timer?.invalidate()
timer = nil
}

func startSleepTracking() {
yawnTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
let timeSinceLastMove = Date().timeIntervalSince(lastMouseMoveTime)
if timeSinceLastMove >= 10 && !isSleeping {
withAnimation(.easeInOut(duration: 0.5)) {
isSleeping = true
}
} else if timeSinceLastMove < 10 && isSleeping {
withAnimation(.easeInOut(duration: 0.5)) {
isSleeping = false
}
}
}
}

func stopSleepTracking() {
yawnTimer?.invalidate()
yawnTimer = nil
}
}

struct MouseFollowingEye: View {
let mouseLocation: CGPoint
@Binding var isBlinking: Bool
@Binding var isSleeping: Bool

private let eyeSize: CGFloat = 12
private let pupilSize: CGFloat = 4

var body: some View {
ZStack {
Circle()
.fill(Color.white)
.frame(width: eyeSize, height: eyeSize)
.offset(y: isSleeping ? eyeSize * 0.5 : 0)
.overlay(
Circle()
.offset(y: isSleeping ? eyeSize * 0.5 : 0)
.stroke(Color.gray, lineWidth: 1)
)

if !isBlinking {
Circle()
.fill(Color.black)
.frame(width: pupilSize, height: pupilSize)
.offset(pupilOffset())
.animation(.easeOut(duration: 0.1), value: mouseLocation)
}
}
.frame(width: eyeSize, height: eyeSize)
.scaleEffect(isBlinking ? CGSize(width: 1, height: 0.1) : CGSize(width: 1, height: 1))
.mask(
Rectangle()
.offset(y: isSleeping ? eyeSize * 0.5 : 0)
.frame(width: eyeSize, height: isSleeping ? eyeSize * 0.5 : eyeSize)
)
.animation(.easeInOut(duration: 0.1), value: isBlinking)
.animation(.easeInOut(duration: 0.5), value: isSleeping)
}

private func pupilOffset() -> CGSize {
let maxOffsetX = (eyeSize - pupilSize) / 2 - 1
let maxOffsetY = (eyeSize - pupilSize) / 2 - 1

if isSleeping {
return CGSize(width: -2, height: eyeSize * 0.5 + 2)
}

let screenWidth = NSScreen.main?.frame.width ?? 1920
let screenHeight = NSScreen.main?.frame.height ?? 1080
let screenCenter = CGPoint(x: screenWidth / 2, y: screenHeight / 2)

let dx = mouseLocation.x - screenCenter.x
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be based on the pupil location rather than the center of the screen?

let dy = mouseLocation.y - screenCenter.y
let distance = sqrt(dx * dx + dy * dy)

let veryCloseDistance = min(screenWidth, screenHeight) * 0.15
if distance < veryCloseDistance {
let crossIntensity = 1.0 - (distance / veryCloseDistance)
let crossOffset = maxOffsetX * 0.7 * crossIntensity
return CGSize(width: dx > 0 ? -crossOffset : crossOffset, height: maxOffsetY * 0.4)
}

let normalizedX = max(-1.0, min(1.0, dx / (screenWidth * 0.4)))
let pupilX = normalizedX * maxOffsetX

let notchY = screenHeight - 50

let pupilY: CGFloat
if mouseLocation.y > notchY {
let normalizedY = max(-1.0, min(0.01, -dy / (screenHeight * 0.1)))
pupilY = normalizedY * maxOffsetY + maxOffsetY * 0.6
} else {
pupilY = maxOffsetY * 0.8
}

return CGSize(width: pupilX, height: pupilY)
}
}

struct Eye: View {
Expand All @@ -66,17 +193,82 @@ struct Eye: View {
RoundedRectangle(cornerRadius: 10)
.fill(Color.white)
.frame(width: 4, height: isBlinking ? 1 : 4)
.frame(maxWidth: 15, maxHeight: 15) // Adjusted max size
.frame(maxWidth: 15, maxHeight: 15)
.animation(.easeInOut(duration: 0.1), value: isBlinking)
}
}

struct SleepingZZZ: View {
@State private var animationOffset1: CGFloat = 0
@State private var animationOffset2: CGFloat = 0
@State private var animationOffset3: CGFloat = 0
@State private var opacity1: Double = 1
@State private var opacity2: Double = 1
@State private var opacity3: Double = 1

var body: some View {
ZStack {
Text("z")
.font(.system(size: 8, weight: .bold))
.foregroundColor(.white)
.offset(x: -2, y: animationOffset1)
.opacity(opacity1)

Text("z")
.font(.system(size: 10, weight: .bold))
.foregroundColor(.white)
.offset(x: 2, y: animationOffset2)
.opacity(opacity2)

Text("Z")
.font(.system(size: 12, weight: .bold))
.foregroundColor(.white)
.offset(x: 6, y: animationOffset3)
.opacity(opacity3)
}
.onAppear {
startZZZAnimation()
}
}

private func startZZZAnimation() {
let duration = 3.0

Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { _ in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be possible to do this without timers? Not sure, but make sure to invalidate timers if they are necessary.

animationOffset1 = 0
opacity1 = 1
withAnimation(.linear(duration: duration)) {
animationOffset1 = -25
opacity1 = 0
}
}

Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { _ in
animationOffset2 = 0
opacity2 = 1
withAnimation(.linear(duration: duration)) {
animationOffset2 = -28
opacity2 = 0
}
}

Timer.scheduledTimer(withTimeInterval: 2.5, repeats: true) { _ in
animationOffset3 = 0
opacity3 = 1
withAnimation(.linear(duration: duration)) {
animationOffset3 = -30
opacity3 = 0
}
}
}
}

struct MinimalFaceFeatures_Previews: PreviewProvider {
static var previews: some View {
ZStack {
Color.black
MinimalFaceFeatures()
}
.previewLayout(.fixed(width: 60, height: 60)) // Adjusted preview size for better visibility
.previewLayout(.fixed(width: 60, height: 60))
}
}