Skip to content

Commit 7136fb8

Browse files
BohdanBohdan
authored andcommitted
Bump version to 1.4
1 parent e7f88fc commit 7136fb8

File tree

9 files changed

+129
-89
lines changed

9 files changed

+129
-89
lines changed

LanguageFlag/Animations/FilterBased/InkDiffusionAnimation.swift

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ class InkDiffusionAnimation: BaseWindowAnimation, WindowAnimation {
1515
return
1616
}
1717

18-
// Legacy: Morphology 10.0, Blur 15.0, Contrast 1.5
18+
// Legacy: Morphology 10.0, Blur 15.0
1919
let morphFilter = FilterBuilder.morphologyMaximum(radius: 10.0)
2020
let blurFilter = FilterBuilder.gaussianBlur(radius: 15.0)
21-
let colorFilter = FilterBuilder.colorControls(contrast: 1.5)
2221

23-
applyFilters([morphFilter, blurFilter, colorFilter], to: layer)
22+
applyFilters([morphFilter, blurFilter], to: layer)
2423

2524
CATransaction.begin()
2625
CATransaction.setAnimationDuration(duration)
@@ -29,9 +28,10 @@ class InkDiffusionAnimation: BaseWindowAnimation, WindowAnimation {
2928
let morphAnim = createAnimation(keyPath: "filters.CIMorphologyMaximum.inputRadius", from: 10.0, to: 0.0, duration: duration)
3029
morphAnim.fillMode = .forwards
3130
morphAnim.isRemovedOnCompletion = false
32-
morphAnim.delegate = AnimationCompletionDelegate { [weak self] finished in
33-
guard let self, finished else { return }
34-
self.clearFilters(from: layer)
31+
morphAnim.delegate = AnimationCompletionDelegate { finished in
32+
guard finished else { return }
33+
layer.filters = nil
34+
layer.removeAllAnimations()
3535
completion?()
3636
}
3737
layer.add(morphAnim, forKey: "morph")
@@ -41,12 +41,6 @@ class InkDiffusionAnimation: BaseWindowAnimation, WindowAnimation {
4141
blurAnim.isRemovedOnCompletion = false
4242
layer.add(blurAnim, forKey: "blur")
4343

44-
// Legacy: Animate contrast 1.5 -> 1.0
45-
let contrastAnim = createAnimation(keyPath: "filters.CIColorControls.inputContrast", from: 1.5, to: 1.0, duration: duration)
46-
contrastAnim.fillMode = .forwards
47-
contrastAnim.isRemovedOnCompletion = false
48-
layer.add(contrastAnim, forKey: "contrast")
49-
5044
CATransaction.commit()
5145

5246
animateAlpha(contentView: contentView, from: 0.0, to: 1.0, duration: duration)
@@ -61,12 +55,11 @@ class InkDiffusionAnimation: BaseWindowAnimation, WindowAnimation {
6155
return
6256
}
6357

64-
// Start: Normal (Contrast 1.0, Morph 0, Blur 0)
58+
// Start: Normal (Morph 0, Blur 0)
6559
let morphFilter = FilterBuilder.morphologyMaximum(radius: 0.0)
6660
let blurFilter = FilterBuilder.gaussianBlur(radius: 0.0)
67-
let colorFilter = FilterBuilder.colorControls(contrast: 1.0)
6861

69-
applyFilters([morphFilter, blurFilter, colorFilter], to: layer)
62+
applyFilters([morphFilter, blurFilter], to: layer)
7063

7164
CATransaction.begin()
7265
CATransaction.setAnimationDuration(duration)
@@ -75,9 +68,10 @@ class InkDiffusionAnimation: BaseWindowAnimation, WindowAnimation {
7568
let morphAnim = createAnimation(keyPath: "filters.CIMorphologyMaximum.inputRadius", from: 0.0, to: 10.0, duration: duration)
7669
morphAnim.fillMode = .forwards
7770
morphAnim.isRemovedOnCompletion = false
78-
morphAnim.delegate = AnimationCompletionDelegate { [weak self] finished in
79-
guard let self, finished else { return }
80-
self.clearFilters(from: layer)
71+
morphAnim.delegate = AnimationCompletionDelegate { finished in
72+
guard finished else { return }
73+
layer.filters = nil
74+
layer.removeAllAnimations()
8175
completion?()
8276
}
8377
layer.add(morphAnim, forKey: "morph")
@@ -87,12 +81,6 @@ class InkDiffusionAnimation: BaseWindowAnimation, WindowAnimation {
8781
blurAnim.isRemovedOnCompletion = false
8882
layer.add(blurAnim, forKey: "blur")
8983

90-
// Legacy: Animate contrast 1.0 -> 1.5
91-
let contrastAnim = createAnimation(keyPath: "filters.CIColorControls.inputContrast", from: 1.0, to: 1.5, duration: duration)
92-
contrastAnim.fillMode = .forwards
93-
contrastAnim.isRemovedOnCompletion = false
94-
layer.add(contrastAnim, forKey: "contrast")
95-
9684
CATransaction.commit()
9785

9886
animateAlpha(contentView: contentView, from: 1.0, to: 0.0, duration: duration)

LanguageFlag/Animations/FilterBased/LiquidRippleAnimation.swift

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,27 @@ class LiquidRippleAnimation: BaseWindowAnimation, WindowAnimation {
1616

1717
let center = CIVector(x: layer.bounds.midX, y: layer.bounds.midY)
1818

19-
// Use splash distortion filter
20-
let splashFilter = FilterBuilder.circleSplashDistortion(center: center, radius: 150.0)
21-
let colorFilter = FilterBuilder.colorControls(saturation: 1.3, brightness: 0.1)
19+
// Use bump distortion filter for transparent lens ripple
20+
let bumpFilter = FilterBuilder.bumpDistortion(center: center, radius: 150.0, scale: 0.5)
21+
let colorFilter = FilterBuilder.colorControls(saturation: 1.3, brightness: 0.0)
2222

23-
applyFilters([splashFilter, colorFilter], to: layer)
23+
applyFilters([bumpFilter, colorFilter], to: layer)
2424

2525
CATransaction.begin()
2626
CATransaction.setAnimationDuration(duration)
2727
CATransaction.setAnimationTimingFunction(AnimationTiming.easeOut)
2828

29-
// Animate splash radius (150 -> 0)
30-
let splashAnim = createAnimation(keyPath: "filters.CICircleSplashDistortion.inputRadius", from: 150.0, to: 0.0, duration: duration)
31-
splashAnim.fillMode = .forwards
32-
splashAnim.isRemovedOnCompletion = false
33-
splashAnim.delegate = AnimationCompletionDelegate { [weak self] finished in
34-
guard let self, finished else { return }
35-
self.clearFilters(from: layer)
29+
// Animate bump radius (150 -> 0)
30+
let bumpAnim = createAnimation(keyPath: "filters.CIBumpDistortion.inputRadius", from: 150.0, to: 0.0, duration: duration)
31+
bumpAnim.fillMode = .forwards
32+
bumpAnim.isRemovedOnCompletion = false
33+
bumpAnim.delegate = AnimationCompletionDelegate { finished in
34+
guard finished else { return }
35+
layer.filters = nil
36+
layer.removeAllAnimations()
3637
completion?()
3738
}
38-
layer.add(splashAnim, forKey: "splash")
39+
layer.add(bumpAnim, forKey: "bump")
3940

4041
CATransaction.commit()
4142

@@ -53,25 +54,26 @@ class LiquidRippleAnimation: BaseWindowAnimation, WindowAnimation {
5354

5455
let center = CIVector(x: layer.bounds.midX, y: layer.bounds.midY)
5556

56-
let splashFilter = FilterBuilder.circleSplashDistortion(center: center, radius: 0.0)
57+
let bumpFilter = FilterBuilder.bumpDistortion(center: center, radius: 0.0, scale: 0.5)
5758
let colorFilter = FilterBuilder.colorControls(saturation: 1.0, brightness: 0.0)
5859

59-
applyFilters([splashFilter, colorFilter], to: layer)
60+
applyFilters([bumpFilter, colorFilter], to: layer)
6061

6162
CATransaction.begin()
6263
CATransaction.setAnimationDuration(duration)
6364
CATransaction.setAnimationTimingFunction(AnimationTiming.easeIn)
6465

65-
// Animate splash radius (0 -> 150)
66-
let splashAnim = createAnimation(keyPath: "filters.CICircleSplashDistortion.inputRadius", from: 0.0, to: 150.0, duration: duration)
67-
splashAnim.fillMode = .forwards
68-
splashAnim.isRemovedOnCompletion = false
69-
splashAnim.delegate = AnimationCompletionDelegate { [weak self] finished in
70-
guard let self, finished else { return }
71-
self.clearFilters(from: layer)
66+
// Animate bump radius (0 -> 150)
67+
let bumpAnim = createAnimation(keyPath: "filters.CIBumpDistortion.inputRadius", from: 0.0, to: 150.0, duration: duration)
68+
bumpAnim.fillMode = .forwards
69+
bumpAnim.isRemovedOnCompletion = false
70+
bumpAnim.delegate = AnimationCompletionDelegate { finished in
71+
guard finished else { return }
72+
layer.filters = nil
73+
layer.removeAllAnimations()
7274
completion?()
7375
}
74-
layer.add(splashAnim, forKey: "splash")
76+
layer.add(bumpAnim, forKey: "bump")
7577

7678
CATransaction.commit()
7779

LanguageFlag/Animations/Helpers/FilterBuilder.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ enum FilterBuilder {
1313
guard let filter = CIFilter(name: "CIGaussianBlur") else {
1414
fatalError("Failed to create CIGaussianBlur filter")
1515
}
16+
1617
filter.setDefaults()
1718
filter.setValue(radius, forKey: kCIInputRadiusKey)
19+
1820
return filter
1921
}
2022

@@ -27,9 +29,11 @@ enum FilterBuilder {
2729
guard let filter = CIFilter(name: "CIMotionBlur") else {
2830
fatalError("Failed to create CIMotionBlur filter")
2931
}
32+
3033
filter.setDefaults()
3134
filter.setValue(radius, forKey: kCIInputRadiusKey)
3235
filter.setValue(angle, forKey: kCIInputAngleKey)
36+
3337
return filter
3438
}
3539

@@ -42,9 +46,11 @@ enum FilterBuilder {
4246
guard let filter = CIFilter(name: "CIZoomBlur") else {
4347
fatalError("Failed to create CIZoomBlur filter")
4448
}
49+
4550
filter.setDefaults()
4651
filter.setValue(amount, forKey: kCIInputAmountKey)
4752
filter.setValue(center, forKey: kCIInputCenterKey)
53+
4854
return filter
4955
}
5056

@@ -57,8 +63,10 @@ enum FilterBuilder {
5763
guard let filter = CIFilter(name: "CIPixellate") else {
5864
fatalError("Failed to create CIPixellate filter")
5965
}
66+
6067
filter.setDefaults()
6168
filter.setValue(scale, forKey: kCIInputScaleKey)
69+
6270
return filter
6371
}
6472

@@ -69,8 +77,10 @@ enum FilterBuilder {
6977
guard let filter = CIFilter(name: "CIColorPosterize") else {
7078
fatalError("Failed to create CIColorPosterize filter")
7179
}
80+
7281
filter.setDefaults()
7382
filter.setValue(levels, forKey: "inputLevels")
83+
7484
return filter
7585
}
7686

@@ -90,10 +100,12 @@ enum FilterBuilder {
90100
guard let filter = CIFilter(name: "CIColorControls") else {
91101
fatalError("Failed to create CIColorControls filter")
92102
}
103+
93104
filter.setDefaults()
94105
filter.setValue(saturation, forKey: "inputSaturation")
95106
filter.setValue(brightness, forKey: "inputBrightness")
96107
filter.setValue(contrast, forKey: "inputContrast")
108+
97109
return filter
98110
}
99111

@@ -104,8 +116,10 @@ enum FilterBuilder {
104116
guard let filter = CIFilter(name: "CIHueAdjust") else {
105117
fatalError("Failed to create CIHueAdjust filter")
106118
}
119+
107120
filter.setDefaults()
108121
filter.setValue(angle, forKey: "inputAngle")
122+
109123
return filter
110124
}
111125

@@ -121,10 +135,31 @@ enum FilterBuilder {
121135
guard let filter = CIFilter(name: "CITwirlDistortion") else {
122136
fatalError("Failed to create CITwirlDistortion filter")
123137
}
138+
124139
filter.setDefaults()
125140
filter.setValue(center, forKey: kCIInputCenterKey)
126141
filter.setValue(radius, forKey: kCIInputRadiusKey)
127142
filter.setValue(angle, forKey: kCIInputAngleKey)
143+
144+
return filter
145+
}
146+
147+
/// Creates a bump distortion filter (liquid lens effect)
148+
/// - Parameters:
149+
/// - center: Center point of the bump
150+
/// - radius: Bump radius
151+
/// - scale: Height/depth of the bump
152+
/// - Returns: Configured CIFilter
153+
static func bumpDistortion(center: CIVector, radius: CGFloat, scale: CGFloat) -> CIFilter {
154+
guard let filter = CIFilter(name: "CIBumpDistortion") else {
155+
fatalError("Failed to create CIBumpDistortion filter")
156+
}
157+
158+
filter.setDefaults()
159+
filter.setValue(center, forKey: kCIInputCenterKey)
160+
filter.setValue(radius, forKey: kCIInputRadiusKey)
161+
filter.setValue(scale, forKey: kCIInputScaleKey)
162+
128163
return filter
129164
}
130165

@@ -137,9 +172,11 @@ enum FilterBuilder {
137172
guard let filter = CIFilter(name: "CICircleSplashDistortion") else {
138173
fatalError("Failed to create CICircleSplashDistortion filter")
139174
}
175+
140176
filter.setDefaults()
141177
filter.setValue(center, forKey: kCIInputCenterKey)
142178
filter.setValue(radius, forKey: kCIInputRadiusKey)
179+
143180
return filter
144181
}
145182

@@ -152,8 +189,10 @@ enum FilterBuilder {
152189
guard let filter = CIFilter(name: "CIMorphologyMaximum") else {
153190
fatalError("Failed to create CIMorphologyMaximum filter")
154191
}
192+
155193
filter.setDefaults()
156194
filter.setValue(radius, forKey: kCIInputRadiusKey)
195+
157196
return filter
158197
}
159198

@@ -168,9 +207,11 @@ enum FilterBuilder {
168207
guard let filter = CIFilter(name: "CIBloom") else {
169208
fatalError("Failed to create CIBloom filter")
170209
}
210+
171211
filter.setDefaults()
172212
filter.setValue(intensity, forKey: kCIInputIntensityKey)
173213
filter.setValue(radius, forKey: kCIInputRadiusKey)
214+
174215
return filter
175216
}
176217
}

LanguageFlag/Stories/Preferences/Panes/AboutPreferencesPane.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,8 @@ struct AboutPreferencesPane: View {
2222
animatedGradientBackground
2323

2424
ScrollView {
25-
VStack(spacing: 24) {
26-
appInfoSection
27-
28-
Spacer()
29-
}
30-
.padding()
25+
appInfoSection
26+
.padding()
3127
}
3228
}
3329
}
@@ -91,6 +87,19 @@ private extension AboutPreferencesPane {
9187

9288
// Links
9389
linksSection
90+
.padding(.top, 4)
91+
92+
// Credits
93+
VStack(spacing: 4) {
94+
Text("Proudly made by Bohdan Bochkovskyi ❤️")
95+
.font(.caption)
96+
.foregroundColor(.primary)
97+
98+
Text("© \(String(Calendar.current.component(.year, from: Date())))")
99+
.font(.caption2)
100+
.foregroundColor(.secondary)
101+
}
102+
.padding(.top, 8)
94103
}
95104
.padding(.top, 20)
96105
}

LanguageFlag/Stories/Preferences/Panes/AppearancePreferencesPane.swift

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ struct AppearancePreferencesPane: View {
2828
Divider()
2929

3030
animationSpeedSection
31+
32+
Divider()
33+
34+
displayDurationSection
3135

3236
Divider()
3337

@@ -57,7 +61,7 @@ struct AppearancePreferencesPane: View {
5761
.accessibilityIdentifier("opacity_value")
5862
}
5963

60-
Text("Transparency of the indicator window")
64+
Text("Transparency of the language window")
6165
.font(.caption)
6266
.foregroundColor(.secondary)
6367
}
@@ -126,7 +130,31 @@ struct AppearancePreferencesPane: View {
126130
.accessibilityIdentifier("animation_duration_value")
127131
}
128132

129-
Text("Duration of show/hide animations")
133+
Text("How fast the animated entrance and exit effects play when the window appears or disappears.")
134+
.font(.caption)
135+
.foregroundColor(.secondary)
136+
}
137+
}
138+
139+
private let displayDurationSteps: [Double] = stride(from: 0.5, through: 5.0, by: 0.5).map { $0 }
140+
141+
private var displayDurationSection: some View {
142+
VStack(alignment: .leading, spacing: 8) {
143+
Text("Display Duration")
144+
.font(.headline)
145+
146+
HStack(alignment: .top, spacing: 8) {
147+
VStack(spacing: 2) {
148+
Slider(value: $preferences.displayDuration, in: 0.5...5.0, step: 0.5)
149+
150+
SliderTickLabels(labels: displayDurationSteps.map { String(format: "%.1f", $0) })
151+
}
152+
153+
Text(String(format: "%.1fs", preferences.displayDuration))
154+
.frame(width: 95, alignment: .trailing)
155+
}
156+
157+
Text("How long the language window stays visible on your screen before automatically fading away.")
130158
.font(.caption)
131159
.foregroundColor(.secondary)
132160
}

0 commit comments

Comments
 (0)