Skip to content

Commit e7f88fc

Browse files
BohdanBohdan
authored andcommitted
Fixed rotation animation
1 parent de936a5 commit e7f88fc

File tree

4 files changed

+73
-37
lines changed

4 files changed

+73
-37
lines changed

LanguageFlag/Animations/Core/AnimationCoordinator.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ final class AnimationCoordinator {
1616
// Animation styles that need frame setup before animation
1717
private let stylesNeedingFrameSetup: Set<AnimationStyle> = [
1818
.slide, .scale, .pixelate, .bounce, .flip, .swing, .elastic,
19-
.hologram, .energyPortal, .digitalMaterialize, .liquidRipple, .inkDiffusion, .vhsGlitch
19+
.hologram, .energyPortal, .digitalMaterialize, .liquidRipple, .inkDiffusion, .vhsGlitch, .rotate
2020
]
2121

2222
// Animation styles that need frame reset after out animation
2323
private let stylesNeedingFrameReset: Set<AnimationStyle> = [
2424
.slide, .scale, .pixelate, .flip, .bounce, .swing,
25-
.hologram, .energyPortal, .digitalMaterialize, .liquidRipple, .inkDiffusion, .vhsGlitch
25+
.hologram, .energyPortal, .digitalMaterialize, .liquidRipple, .inkDiffusion, .vhsGlitch, .rotate
2626
]
2727

2828
// MARK: - Public Methods
@@ -191,8 +191,7 @@ private extension AnimationCoordinator {
191191
case .bounce:
192192
window.bounceOut(duration: duration, completion: completion)
193193
case .rotate:
194-
window.rotateOut(duration: duration)
195-
completion?()
194+
window.rotateOut(duration: duration, completion: completion)
196195
case .swing:
197196
window.swingOut(duration: duration, completion: completion)
198197
case .elastic:

LanguageFlag/Animations/Core/BaseWindowAnimation.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ class BaseWindowAnimation {
3939
// Fix: Reset contentView alpha & layer opacity that might be permanently stuck at 0 from FadeAnimation
4040
contentView.alphaValue = 1.0
4141
layer.opacity = 1.0
42+
43+
// Fix: Globally Sanitize Layer Geometry
44+
// Interrupted animations (like Bounce) can permanently orphan the anchorPoint
45+
// causing subsequent animations (like Rotate) to spin wildly off-axis.
46+
layer.transform = CATransform3DIdentity
47+
layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
48+
layer.position = CGPoint(x: layer.bounds.midX, y: layer.bounds.midY)
4249
}
4350

4451
window.orderFrontRegardless()

LanguageFlag/Animations/Transform/BounceAnimation.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ class BounceAnimation: BaseWindowAnimation, WindowAnimation {
3434

3535
animation.delegate = AnimationCompletionDelegate { finished in
3636
guard finished else { return }
37-
layer.anchorPoint = CGPoint(x: 0.0, y: 0.0)
38-
layer.frame = originalFrame
3937
completion?()
4038
}
4139

@@ -71,8 +69,6 @@ class BounceAnimation: BaseWindowAnimation, WindowAnimation {
7169

7270
animation.delegate = AnimationCompletionDelegate { finished in
7371
guard finished else { return }
74-
layer.anchorPoint = CGPoint(x: 0.0, y: 0.0)
75-
layer.frame = originalFrame
7672
completion?()
7773
}
7874

LanguageFlag/Animations/Transform/RotateAnimation.swift

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,52 @@ class RotateAnimation: BaseWindowAnimation, WindowAnimation {
55

66
// MARK: - WindowAnimation
77
func animateIn(window: NSWindow, duration: TimeInterval, completion: (() -> Void)?) {
8-
setupWindow(window)
9-
108
guard
119
let layer = prepareLayer(from: window),
1210
let contentView = window.contentView
1311
else {
1412
completion?()
1513
return
1614
}
15+
setupWindow(window)
16+
17+
let oldAnchor = layer.anchorPoint
18+
let oldPosition = layer.position
1719

18-
let originalFrame = layer.frame
1920
layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
20-
layer.frame = originalFrame
21+
layer.position = CGPoint(x: layer.bounds.midX, y: layer.bounds.midY)
2122

22-
CATransaction.begin()
23-
CATransaction.setAnimationDuration(duration)
24-
CATransaction.setAnimationTimingFunction(AnimationTiming.easeOut)
25-
CATransaction.setCompletionBlock {
26-
layer.transform = CATransform3DIdentity
27-
layer.anchorPoint = CGPoint(x: 0, y: 0)
28-
layer.frame = originalFrame
29-
completion?()
30-
}
23+
// Calculate the mathematically perfect scale down to ensure the diagonal
24+
// never clips the shortest dimension of the window bounds during the spin.
25+
let width = layer.bounds.width
26+
let height = layer.bounds.height
27+
let diagonal = hypot(width, height)
28+
let minDimension = min(width, height)
29+
// Add a slight 2% margin of safety
30+
let safeScale = (minDimension / diagonal) * 0.98
3131

3232
let rotateAnim = createAnimation(keyPath: "transform.rotation.z", from: CGFloat.pi * 2, to: 0.0, duration: duration)
33-
layer.add(rotateAnim, forKey: "rotate")
3433

35-
CATransaction.commit()
34+
let scaleAnim = CAKeyframeAnimation(keyPath: "transform.scale")
35+
scaleAnim.values = [1.0, safeScale, 1.0]
36+
scaleAnim.keyTimes = [0.0, 0.5, 1.0]
37+
scaleAnim.duration = duration
38+
scaleAnim.timingFunction = CAMediaTimingFunction(name: .easeOut)
39+
40+
let group = CAAnimationGroup()
41+
group.animations = [rotateAnim, scaleAnim]
42+
group.duration = duration
43+
group.fillMode = .forwards
44+
group.isRemovedOnCompletion = false
45+
46+
group.delegate = AnimationCompletionDelegate { [weak layer] finished in
47+
layer?.transform = CATransform3DIdentity
48+
layer?.anchorPoint = oldAnchor
49+
layer?.position = oldPosition
50+
completion?()
51+
}
52+
53+
layer.add(group, forKey: "rotateIn")
3654

3755
animateAlpha(contentView: contentView, from: 0.0, to: 1.0, duration: duration)
3856
}
@@ -46,25 +64,41 @@ class RotateAnimation: BaseWindowAnimation, WindowAnimation {
4664
return
4765
}
4866

49-
let originalFrame = layer.frame
67+
let oldAnchor = layer.anchorPoint
68+
let oldPosition = layer.position
69+
5070
layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
51-
layer.frame = originalFrame
71+
layer.position = CGPoint(x: layer.bounds.midX, y: layer.bounds.midY)
72+
73+
let width = layer.bounds.width
74+
let height = layer.bounds.height
75+
let diagonal = hypot(width, height)
76+
let minDimension = min(width, height)
77+
let safeScale = (minDimension / diagonal) * 0.98
5278

53-
CATransaction.begin()
54-
CATransaction.setAnimationDuration(duration)
55-
CATransaction.setAnimationTimingFunction(AnimationTiming.easeIn)
56-
CATransaction.setCompletionBlock {
57-
layer.transform = CATransform3DIdentity
58-
layer.anchorPoint = CGPoint(x: 0, y: 0)
59-
layer.frame = originalFrame
79+
let rotateAnim = createAnimation(keyPath: "transform.rotation.z", from: 0.0, to: CGFloat.pi * 2, duration: duration, timing: CAMediaTimingFunction(name: .easeIn))
80+
81+
let scaleAnim = CAKeyframeAnimation(keyPath: "transform.scale")
82+
scaleAnim.values = [1.0, safeScale, 1.0]
83+
scaleAnim.keyTimes = [0.0, 0.5, 1.0]
84+
scaleAnim.duration = duration
85+
scaleAnim.timingFunction = CAMediaTimingFunction(name: .easeIn)
86+
87+
let group = CAAnimationGroup()
88+
group.animations = [rotateAnim, scaleAnim]
89+
group.duration = duration
90+
group.fillMode = .forwards
91+
group.isRemovedOnCompletion = false
92+
93+
group.delegate = AnimationCompletionDelegate { [weak layer] finished in
94+
layer?.transform = CATransform3DIdentity
95+
layer?.anchorPoint = oldAnchor
96+
layer?.position = oldPosition
6097
completion?()
6198
}
6299

63-
let rotateAnim = createAnimation(keyPath: "transform.rotation.z", from: 0.0, to: CGFloat.pi * 2, duration: duration)
64-
layer.add(rotateAnim, forKey: "rotate")
65-
66-
CATransaction.commit()
100+
layer.add(group, forKey: "rotateOut")
67101

68-
animateAlpha(contentView: contentView, from: 1.0, to: 0.0, duration: duration)
102+
animateAlpha(contentView: contentView, from: 1.0, to: 0.0, duration: duration, timing: CAMediaTimingFunction(name: .easeIn))
69103
}
70104
}

0 commit comments

Comments
 (0)