Skip to content

Commit

Permalink
feat: [swift] color transition for constant fill type
Browse files Browse the repository at this point in the history
  • Loading branch information
peterklingelhofer committed Apr 11, 2023
1 parent cba0e95 commit a7d6ec9
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 31 deletions.
4 changes: 2 additions & 2 deletions swift/exhale.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.1.1;
MARKETING_VERSION = 1.1.2;
PRODUCT_BUNDLE_IDENTIFIER = peterklingelhofer.exhale;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand All @@ -476,7 +476,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.1.1;
MARKETING_VERSION = 1.1.2;
PRODUCT_BUNDLE_IDENTIFIER = peterklingelhofer.exhale;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down
84 changes: 59 additions & 25 deletions swift/exhale/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,54 @@
// ContentView.swift
import SwiftUI

extension Color {
func interpolate(to color: Color, fraction: Double) -> Color {
let fromComponents = self.cgColor?.components ?? [0, 0, 0, 0]
let toComponents = color.cgColor?.components ?? [0, 0, 0, 0]

let red = CGFloat(fromComponents[0] + (toComponents[0] - fromComponents[0]) * CGFloat(fraction))
let green = CGFloat(fromComponents[1] + (toComponents[1] - fromComponents[1]) * CGFloat(fraction))
let blue = CGFloat(fromComponents[2] + (toComponents[2] - fromComponents[2]) * CGFloat(fraction))
let alpha = CGFloat(fromComponents[3] + (toComponents[3] - fromComponents[3]) * CGFloat(fraction))

return Color(red: red, green: green, blue: blue, opacity: alpha)
}
}

extension Shape {
func conditionalFill<S1: ShapeStyle, S2: ShapeStyle>(_ condition: Bool, ifTrue: S1, ifFalse: S2) -> some View {
Group {
if condition {
self.fill(ifTrue)
@ViewBuilder
func colorTransitionFill(settingsModel: SettingsModel, animationProgress: CGFloat, breathingPhase: BreathingPhase, endRadius: CGFloat = 0) -> some View {
let isInhalePhase = breathingPhase == .inhale || breathingPhase == .holdAfterInhale
let lastColor = isInhalePhase ? settingsModel.inhaleColor : settingsModel.exhaleColor
let nextColor = isInhalePhase ? settingsModel.exhaleColor : settingsModel.inhaleColor
let startingColor = isInhalePhase ? settingsModel.exhaleColor : settingsModel.inhaleColor
let transitionFraction = breathingPhase == .exhale ? Double(1 - animationProgress) : Double(animationProgress)
let finalColor = settingsModel.colorTransitionEnabled ? startingColor.interpolate(to: nextColor, fraction: transitionFraction) : lastColor

if settingsModel.colorFillType != .constant {
if settingsModel.shape == .rectangle {
let gradient = LinearGradient(
gradient: Gradient(colors: [finalColor, settingsModel.backgroundColor]),
startPoint: .top,
endPoint: .bottom
)
self.fill(gradient)
} else {
self.fill(ifFalse)
let gradient = RadialGradient(
gradient: Gradient(colors: [settingsModel.backgroundColor, finalColor]),
center: .center,
startRadius: 0,
endRadius: endRadius
)
self.fill(gradient)
}
} else {
self.fill(finalColor)
}
}
}


struct ContentView: View {
@EnvironmentObject var settingsModel: SettingsModel
@State private var animationProgress: CGFloat = 0
Expand All @@ -30,28 +66,26 @@ struct ContentView: View {
}

var body: some View {
ZStack {
GeometryReader { geometry in
ZStack {
settingsModel.backgroundColor.edgesIgnoringSafeArea(.all)
if settingsModel.shape == .rectangle {
let fillColor = breathingPhase == .inhale || breathingPhase == .holdAfterInhale ? settingsModel.inhaleColor : settingsModel.exhaleColor
let gradient = LinearGradient(gradient: Gradient(colors: [fillColor, settingsModel.backgroundColor]), startPoint: .top, endPoint: .bottom)
Rectangle()
.conditionalFill(settingsModel.colorFillType == .linear, ifTrue: gradient, ifFalse: fillColor)
.frame(height: animationProgress * geometry.size.height)
.position(x: geometry.size.width / 2, y: geometry.size.height - (animationProgress * geometry.size.height) / 2)
} else {
let fillColor = breathingPhase == .inhale || breathingPhase == .holdAfterInhale ? settingsModel.inhaleColor : settingsModel.exhaleColor
let gradient = RadialGradient(gradient: Gradient(colors: [settingsModel.backgroundColor, fillColor]), center: .center, startRadius: 0, endRadius: (min(geometry.size.width, geometry.size.height) * animationProgress * maxCircleScale) / 2)
Circle()
.conditionalFill(settingsModel.colorFillType == .linear, ifTrue: gradient, ifFalse: fillColor)
.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)
ZStack {
GeometryReader { geometry in
ZStack {
settingsModel.backgroundColor.edgesIgnoringSafeArea(.all)

if settingsModel.shape == .rectangle {
Rectangle()
.colorTransitionFill(settingsModel: settingsModel, animationProgress: animationProgress, breathingPhase: breathingPhase)
.frame(height: geometry.size.height)
.scaleEffect(y: animationProgress, anchor: .bottom)
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
} else {
Circle()
.colorTransitionFill(settingsModel: settingsModel, animationProgress: animationProgress, breathingPhase: breathingPhase, endRadius: (min(geometry.size.width, geometry.size.height) * animationProgress * maxCircleScale) / 2)
.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)
.edgesIgnoringSafeArea(.all)

if showSettings {
SettingsView(
Expand Down
3 changes: 2 additions & 1 deletion swift/exhale/SettingsModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ class SettingsModel: ObservableObject {
@Published var drift: Double = 1.0
@Published var overlayOpacity: Double = 0.1
@Published var shape: AnimationShape = .rectangle
@Published var animationMode: AnimationMode = .linear
@Published var animationMode: AnimationMode = .sinusoidal
@Published var colorTransitionEnabled: Bool = false
}
16 changes: 13 additions & 3 deletions swift/exhale/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,21 @@ struct SettingsView: View {
.frame(alignment: .trailing)
}

HStack {
Text("Color Transition")
.frame(width: labelWidth, alignment: .leading)

Toggle(isOn: $settingsModel.colorTransitionEnabled) {
Text("")
.frame(alignment: .trailing)
}
}

HStack {
Text("Gradient Type")
.frame(width: labelWidth, alignment: .leading)

Picker("Gradient Type", selection: $colorFillType) {
Picker("", selection: $colorFillType) {
ForEach(ColorFillType.allCases) { type in
Text(type.rawValue).tag(type)
}
Expand All @@ -119,7 +129,7 @@ struct SettingsView: View {
Text("Shape")
.frame(width: labelWidth, alignment: .leading)

Picker("Shape", selection: $shape) {
Picker("", selection: $shape) {
ForEach(AnimationShape.allCases, id: \.self) { shape in
Text(shape.rawValue).tag(shape)
}
Expand All @@ -133,7 +143,7 @@ struct SettingsView: View {
Text("Animation Mode")
.frame(width: labelWidth, alignment: .leading)

Picker("Animation Mode", selection: $animationMode) {
Picker("", selection: $animationMode) {
ForEach(AnimationMode.allCases) { mode in
Text(mode.rawValue).tag(mode)
}
Expand Down

0 comments on commit a7d6ec9

Please sign in to comment.