Skip to content

Commit

Permalink
feat: option for circle animation shape
Browse files Browse the repository at this point in the history
  • Loading branch information
peterklingelhofer committed Mar 19, 2023
1 parent 923c3e8 commit 8d7667d
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 24 deletions.
4 changes: 4 additions & 0 deletions exhale/exhale.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
0208F33B29C75C0900CC6FAC /* types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0208F33A29C75C0900CC6FAC /* types.swift */; };
020C89CD29C2B05500720231 /* exhaleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020C89CC29C2B05500720231 /* exhaleApp.swift */; };
020C89CF29C2B05500720231 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020C89CE29C2B05500720231 /* ContentView.swift */; };
020C89D129C2B05700720231 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 020C89D029C2B05700720231 /* Assets.xcassets */; };
Expand Down Expand Up @@ -38,6 +39,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
0208F33A29C75C0900CC6FAC /* types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = types.swift; sourceTree = "<group>"; };
020C89C929C2B05500720231 /* exhale.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = exhale.app; sourceTree = BUILT_PRODUCTS_DIR; };
020C89CC29C2B05500720231 /* exhaleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = exhaleApp.swift; sourceTree = "<group>"; };
020C89CE29C2B05500720231 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -112,6 +114,7 @@
020C8A0129C3307100720231 /* SettingsView.swift */,
020C8A0329C3323F00720231 /* SettingsModel.swift */,
02775BB529C6311E00584F4B /* CustomMenu.swift */,
0208F33A29C75C0900CC6FAC /* types.swift */,
);
path = exhale;
sourceTree = "<group>";
Expand Down Expand Up @@ -277,6 +280,7 @@
02775BB629C6311E00584F4B /* CustomMenu.swift in Sources */,
020C89CF29C2B05500720231 /* ContentView.swift in Sources */,
020C89CD29C2B05500720231 /* exhaleApp.swift in Sources */,
0208F33B29C75C0900CC6FAC /* types.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
3 changes: 2 additions & 1 deletion exhale/exhale/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
exhaleDuration: Binding(get: { self.settingsModel.exhaleDuration }, set: { self.settingsModel.exhaleDuration = $0 }),
postExhaleHoldDuration: Binding(get: { self.settingsModel.postExhaleHoldDuration }, set: { self.settingsModel.postExhaleHoldDuration = $0 }),
drift: Binding(get: { self.settingsModel.drift }, set: { self.settingsModel.drift = $0 }),
overlayOpacity: Binding(get: { self.settingsModel.overlayOpacity }, set: { self.settingsModel.overlayOpacity = $0 })
overlayOpacity: Binding(get: { self.settingsModel.overlayOpacity }, set: { self.settingsModel.overlayOpacity = $0 }),
shape: Binding<AnimationShape>(get: { self.settingsModel.shape }, set: { self.settingsModel.shape = $0 })
).environmentObject(settingsModel))

settingsWindow.title = "Preferences"
Expand Down
47 changes: 25 additions & 22 deletions exhale/exhale/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,31 @@ struct ContentView: View {
@State private var showSettings = false
@State private var cycleCount: Int = 0

var maxCircleScale: CGFloat {
guard let screen = NSScreen.main else { return 1.0 }
let screenWidth = screen.frame.width
let screenHeight = screen.frame.height
let maxDimension = max(screenWidth, screenHeight)
return maxDimension / min(screenWidth, screenHeight)
}

var body: some View {
ZStack {
GeometryReader { geometry in
ZStack {
settingsModel.backgroundColor.edgesIgnoringSafeArea(.all)
Rectangle()
.fill(settingsModel.overlayColor)
.frame(height: animationProgress * geometry.size.height)
.position(x: geometry.size.width / 2, y: geometry.size.height - (animationProgress * geometry.size.height) / 2)

if settingsModel.shape == .rectangle {
Rectangle()
.fill(settingsModel.overlayColor)
.frame(height: animationProgress * geometry.size.height)
.position(x: geometry.size.width / 2, y: geometry.size.height - (animationProgress * geometry.size.height) / 2)
} else {
Circle()
.fill(settingsModel.overlayColor)
.frame(width: min(geometry.size.width, geometry.size.height) * animationProgress * maxCircleScale, height: min(geometry.size.width, geometry.size.height) * animationProgress * maxCircleScale)
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
}
}
}
.edgesIgnoringSafeArea(.all)
Expand All @@ -32,7 +48,8 @@ struct ContentView: View {
exhaleDuration: $settingsModel.exhaleDuration,
postExhaleHoldDuration: $settingsModel.postExhaleHoldDuration,
drift: $settingsModel.drift,
overlayOpacity: $overlayOpacity
overlayOpacity: $overlayOpacity,
shape: Binding<AnimationShape>(get: { self.settingsModel.shape }, set: { self.settingsModel.shape = $0 })
)
}
}
Expand All @@ -51,6 +68,9 @@ struct ContentView: View {
withAnimation(.linear(duration: duration)) {
breathingPhase = .inhale
animationProgress = 1.0
if settingsModel.shape == .circle {
animationProgress = 1
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
holdAfterInhale()
Expand Down Expand Up @@ -88,23 +108,6 @@ struct ContentView: View {
}
}

enum BreathingPhase {
case inhale, holdAfterInhale, exhale, holdAfterExhale

func duration(settingsModel: SettingsModel) -> TimeInterval {
switch self {
case .inhale:
return settingsModel.inhaleDuration
case .holdAfterInhale:
return settingsModel.postInhaleHoldDuration
case .exhale:
return settingsModel.exhaleDuration
case .holdAfterExhale:
return settingsModel.postExhaleHoldDuration
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
Expand Down
3 changes: 2 additions & 1 deletion exhale/exhale/SettingsModel.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SettingsModel.swift
// SettingsModel.swift
import SwiftUI

class SettingsModel: ObservableObject {
Expand All @@ -14,4 +14,5 @@ class SettingsModel: ObservableObject {
@Published var drift: Double = 1.0
@Published var overlayOpacity: Double = 0.1
@Published var backgroundColor: Color = Color.black
@Published var shape: AnimationShape = .rectangle
}
8 changes: 8 additions & 0 deletions exhale/exhale/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ struct SettingsView: View {
@Binding var postExhaleHoldDuration: Double
@Binding var drift: Double
@Binding var overlayOpacity: Double
@Binding var shape: AnimationShape

func createNumberFormatter(minimumValue: Double, maximumValue: Double? = nil) -> NumberFormatter {
let formatter = NumberFormatter()
Expand Down Expand Up @@ -81,6 +82,13 @@ struct SettingsView: View {
TextFieldWithValidation(title: "Drift", value: $drift, formatter: createNumberFormatter(minimumValue: 0.5), minimumValue: 0.5)

TextFieldWithValidation(title: "Overlay Opacity", value: $overlayOpacity, formatter: createNumberFormatter(minimumValue: 0, maximumValue: 1), minimumValue: 0)

Picker("Shape", selection: $shape) {
ForEach(AnimationShape.allCases, id: \.self) { shape in
Text(shape.rawValue).tag(shape)
}
}
.pickerStyle(MenuPickerStyle())
}
.foregroundColor(.white)
.padding(.horizontal)
Expand Down
26 changes: 26 additions & 0 deletions exhale/exhale/types.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// types.swift
import SwiftUI

enum BreathingPhase {
case inhale, holdAfterInhale, exhale, holdAfterExhale

func duration(settingsModel: SettingsModel) -> TimeInterval {
switch self {
case .inhale:
return settingsModel.inhaleDuration
case .holdAfterInhale:
return settingsModel.postInhaleHoldDuration
case .exhale:
return settingsModel.exhaleDuration
case .holdAfterExhale:
return settingsModel.postExhaleHoldDuration
}
}
}

enum AnimationShape: String, CaseIterable, Identifiable {
case rectangle = "Rectangle"
case circle = "Circle"

var id: String { self.rawValue }
}

0 comments on commit 8d7667d

Please sign in to comment.