Skip to content

Commit ceb8474

Browse files
authored
improved animation control (#30)
* tighter grip on transactions * animation completion tracking * fixed animation tracking * more animation completion tracking * formatting * build with 6.2.1
1 parent d50d80d commit ceb8474

File tree

21 files changed

+504
-235
lines changed

21 files changed

+504
-235
lines changed

.github/workflows/ci-examples.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ jobs:
1919
strategy:
2020
matrix:
2121
swift:
22-
- image: swift:6.2
23-
wask-sdk-url: https://download.swift.org/swift-6.2-release/wasm/swift-6.2-RELEASE/swift-6.2-RELEASE_wasm.artifactbundle.tar.gz
24-
wasm-sdk-checksum: fe4e8648309fce86ea522e9e0d1dc48e82df6ba6e5743dbf0c53db8429fb5224
22+
- image: swift:6.2.1
23+
wask-sdk-url: https://download.swift.org/swift-6.2.1-release/wasm-sdk/swift-6.2.1-RELEASE/swift-6.2.1-RELEASE_wasm.artifactbundle.tar.gz
24+
wasm-sdk-checksum: 482b9f95462b87bedfafca94a092cf9ec4496671ca13b43745097122d20f18af
2525
examples:
2626
- Examples/Swiftle
2727
- Examples/Basic

.swift-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
6.2.0
1+
6.2.1

Examples/Basic/Sources/App/AnimationsView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ struct AnimationsView {
1515
div(.style(["display": "flex", "flex-direction": "row", "gap": "10px"])) {
1616
button { "Animate" }
1717
.onClick { _ in
18-
withAnimation(.smooth) {
18+
withAnimation(.bouncy) {
1919
angle += 1
2020
isBallFading.toggle()
2121
}

Examples/Basic/Sources/App/Views.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,16 @@ struct App {
2929
div {
3030
for _ in 0..<bindingViewCount {
3131
BindingsView()
32-
.transition(.fade)
32+
.transition(.fade, animation: .bouncy)
3333
}
3434
button { "Add bindings view" }
3535
.onClick { _ in
36-
withAnimation {
37-
bindingViewCount += 1
38-
}
36+
bindingViewCount += 1
3937
}
4038
button { "Remove bindings view" }
4139
.onClick { _ in
4240
guard bindingViewCount > 0 else { return }
43-
withAnimation {
44-
bindingViewCount -= 1
45-
}
41+
bindingViewCount -= 1
4642
}
4743
}
4844
hr()

Sources/ElementaryDOM/Animation/AnimatedValue.swift

Lines changed: 106 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,32 @@
1-
struct AnimationInstance {
2-
struct TrackingReference {
3-
let instanceID: AnimationTracker.InstanceID
4-
let tracker: AnimationTracker
5-
}
6-
7-
let startTime: Double
1+
private struct RunningAnimation {
2+
let trackedInstance: AnimationTracker.Instance?
83
let animation: Animation
9-
let trackingReference: TrackingReference?
4+
let startTime: Double
5+
let target: AnimatableVector
6+
var context: AnimationContext
7+
var hasLogicallyCompleted: Bool = false
108

11-
init(startTime: Double, animation: Animation, trackingReference: TrackingReference? = nil) {
12-
self.startTime = startTime
13-
self.animation = animation
14-
self.trackingReference = trackingReference
15-
}
9+
mutating func animate(time: Double, additionalVector: AnimatableVector?) -> (AnimatableVector, Bool)? {
10+
let result: AnimatableVector?
1611

17-
func reportLogicallyComplete() {
18-
trackingReference?.tracker.reportLogicallyComplete(trackingReference!.instanceID)
19-
}
12+
let before = context.isLogicallyComplete
2013

21-
func reportRemoved() {
22-
trackingReference?.tracker.reportRemoved(trackingReference!.instanceID)
23-
}
24-
}
25-
26-
private struct RunningAnimation {
27-
let instance: AnimationInstance
28-
let target: AnimatableVector
29-
30-
borrowing func animate(time: Double, context: inout AnimationContext, additionalVector: AnimatableVector?) -> AnimatableVector? {
3114
if let additionalVector {
32-
instance.animation.animate(value: target + additionalVector, time: time - instance.startTime, context: &context)
15+
result = animation.animate(value: target + additionalVector, time: time - startTime, context: &context)
3316
} else {
34-
instance.animation.animate(value: target, time: time - instance.startTime, context: &context)
17+
result = animation.animate(value: target, time: time - startTime, context: &context)
3518
}
19+
20+
return result.map { ($0, !before && context.isLogicallyComplete) }
21+
}
22+
23+
mutating func reportRemoved() {
24+
// TODO: maybe make this non-copyable and have a consuming remove func
25+
trackedInstance?.reportRemoved()
26+
}
27+
28+
mutating func reportLogicallyComplete() {
29+
trackedInstance?.reportLogicallyComplete()
3630
}
3731
}
3832

@@ -42,7 +36,6 @@ struct AnimatedValue<Value: AnimatableVectorConvertible>: ~Copyable {
4236
private var currentAnimationValue: Value
4337

4438
private var animationBase: AnimatableVector
45-
private var context: AnimationContext
4639

4740
var model: Value { borrowing get { currentTarget } }
4841
var presentation: Value { borrowing get { currentAnimationValue } }
@@ -52,50 +45,76 @@ struct AnimatedValue<Value: AnimatableVectorConvertible>: ~Copyable {
5245
self.animationBase = value.animatableVector
5346
self.currentTarget = value
5447
self.currentAnimationValue = value
55-
self.context = AnimationContext()
5648
}
5749

5850
mutating func setValue(_ value: Value) {
5951
self.animationBase = value.animatableVector
6052
self.currentTarget = value
6153
self.currentAnimationValue = value
54+
if !runningAnimations.isEmpty {
55+
removeAnimations(upThrough: runningAnimations.endIndex - 1, skipBaseUpdate: true)
56+
}
57+
}
6258

63-
removeAnimations(upThrough: runningAnimations.endIndex - 1, skipBaseUpdate: true)
59+
mutating func cancelAnimation() {
60+
guard isAnimating else { return }
61+
62+
// setting value cancels all animations
63+
setValue(currentTarget)
6464
}
6565

66-
mutating func animate(to value: Value, animation: AnimationInstance) {
67-
self.progressToTime(animation.startTime)
66+
mutating func animate(to value: Value, startTime: Double, animation: Animation, tracker: AnimationTracker? = nil) {
67+
68+
self.progressToTime(startTime)
6869
var animationTarget = value.animatableVector - currentTarget.animatableVector
70+
var context = AnimationContext()
6971

7072
if let previous = runningAnimations.last {
71-
let elapsedTime = animation.startTime - previous.instance.startTime
72-
let shouldMerge = animation.animation.shouldMerge(
73-
previous: previous.instance.animation,
73+
var previousContext = previous.context
74+
let elapsedTime = startTime - previous.startTime
75+
let shouldMerge = animation.shouldMerge(
76+
previous: previous.animation,
7477
value: previous.target,
7578
time: elapsedTime,
76-
context: &context
79+
context: &previousContext
7780
)
7881

7982
if shouldMerge {
8083
self.animationBase = currentAnimationValue.animatableVector
8184
self.removeAnimations(upThrough: runningAnimations.endIndex - 1, skipBaseUpdate: true)
8285
animationTarget = value.animatableVector - self.animationBase
86+
context = previousContext
87+
// FIXME: this feels very hacky....
88+
context.isLogicallyComplete = false
8389
}
8490
}
8591

8692
self.currentTarget = value
87-
runningAnimations.append(RunningAnimation(instance: animation, target: animationTarget))
93+
runningAnimations.append(
94+
RunningAnimation(
95+
trackedInstance: tracker?.addAnimation(),
96+
animation: animation,
97+
startTime: startTime,
98+
target: animationTarget,
99+
context: context
100+
)
101+
)
88102
}
89103

90104
mutating func progressToTime(_ time: Double) {
91105
guard isAnimating else { return }
92106

93-
let (animatedVector, finishedAnimationIndex) = calculateAnimationAtTime(
107+
let (animatedVector, completedIndexes, finishedAnimationIndex) = calculateAnimationAtTime(
94108
time,
95-
runningAnimations: runningAnimations,
96-
context: &context
109+
runningAnimations: &runningAnimations[...],
97110
)
98111

112+
// Report logical completion for any animations that became logically complete at this time.
113+
// This must happen here (mutating path), not inside calculateAnimationAtTime, so peeking stays non-mutating.
114+
for index in completedIndexes {
115+
runningAnimations[index].reportLogicallyComplete()
116+
}
117+
99118
if let finishedAnimationIndex {
100119
removeAnimations(upThrough: finishedAnimationIndex)
101120
}
@@ -109,42 +128,52 @@ struct AnimatedValue<Value: AnimatableVectorConvertible>: ~Copyable {
109128
}
110129
}
111130

112-
// TODO: figure out the shape for this
113-
func peekFutureValues(_ times: StrideThrough<Double>) -> [Value] {
131+
// TODO: this can't be the best shape of this function...
132+
func peekFutureValuesUnlessCompletedOrFinished(_ times: StrideThrough<Double>) -> [Value] {
114133
var results: [Value] = []
115-
var contextCopy = context
116134
var runningAnimations = runningAnimations[...]
117135
var base = animationBase
118136

119137
results.reserveCapacity(times.underestimatedCount)
120138

121139
for time in times {
122-
let (animatedVector, completedIndex) = calculateAnimationAtTime(
140+
var shouldBailEarly = false
141+
142+
let (animatedVector, completedIndexes, removedUpToIndex) = calculateAnimationAtTime(
123143
time,
124-
runningAnimations: runningAnimations,
125-
context: &contextCopy
144+
runningAnimations: &runningAnimations,
126145
)
127146

128-
if let completedIndex {
129-
for i in runningAnimations.startIndex...completedIndex {
147+
if !completedIndexes.isEmpty {
148+
shouldBailEarly = true
149+
}
150+
151+
if let removedUpToIndex {
152+
for i in runningAnimations.startIndex...removedUpToIndex {
130153
base += runningAnimations[i].target
131154
}
132-
runningAnimations = runningAnimations[(completedIndex + 1)...]
155+
runningAnimations = runningAnimations[(removedUpToIndex + 1)...]
156+
shouldBailEarly = true
133157
}
134158

135159
if runningAnimations.isEmpty {
136160
results.append(self.currentTarget)
137-
break
161+
shouldBailEarly = true
162+
} else {
163+
results.append(Value(base + animatedVector))
138164
}
139165

140-
results.append(Value(base + animatedVector))
166+
if shouldBailEarly {
167+
break
168+
}
141169
}
142170
return results
143171
}
144172

145173
private mutating func removeAnimations(upThrough index: Int, skipBaseUpdate: Bool = false) {
146174
for i in 0...index {
147-
runningAnimations[i].instance.reportRemoved()
175+
// NOTE: completion is triggered automatically on removal, no extra handling needed here
176+
runningAnimations[i].reportRemoved()
148177
if !skipBaseUpdate {
149178
self.animationBase += runningAnimations[i].target
150179
}
@@ -153,22 +182,22 @@ struct AnimatedValue<Value: AnimatableVectorConvertible>: ~Copyable {
153182
}
154183
}
155184

156-
private func calculateAnimationAtTime<AnimationList>(
185+
private func calculateAnimationAtTime(
157186
_ time: Double,
158-
runningAnimations: AnimationList,
159-
context: inout AnimationContext,
160-
) -> (animatedVector: AnimatableVector, finishedAnimationIndex: AnimationList.Index?)
161-
where AnimationList: Collection<RunningAnimation> {
187+
runningAnimations: inout ArraySlice<RunningAnimation>
188+
) -> (animatedVector: AnimatableVector, completedIndexes: [Int], finishedAnimationIndex: Int?) {
162189
guard runningAnimations.count > 1 else {
163190
assert(runningAnimations.first != nil, "Running animations should not be empty")
164-
if let vector = runningAnimations.first!.animate(time: time, context: &context, additionalVector: nil) {
165-
return (vector, nil)
191+
let index = runningAnimations.startIndex
192+
if let (vector, logicallyCompleted) = runningAnimations[index].animate(time: time, additionalVector: nil) {
193+
return (vector, logicallyCompleted ? [index] : [], nil)
166194
} else {
167-
return (runningAnimations.first!.target, runningAnimations.indices.first)
195+
return (runningAnimations[0].target, [], index)
168196
}
169197
}
170198

171-
var finishedAnimationIndex: AnimationList.Index?
199+
var finishedAnimationIndex: Int?
200+
var completedIndexes: [Int] = []
172201
var index = runningAnimations.startIndex
173202

174203
let zero = AnimatableVector.zero(runningAnimations.first!.target)
@@ -177,11 +206,12 @@ where AnimationList: Collection<RunningAnimation> {
177206
var carryOverVector = zero
178207

179208
while index < runningAnimations.endIndex {
180-
let runningAnimation = runningAnimations[index]
181-
182-
if let vector = runningAnimation.animate(time: time, context: &context, additionalVector: carryOverVector) {
209+
if let (vector, logicallyCompleted) = runningAnimations[index].animate(time: time, additionalVector: carryOverVector) {
183210
totalAnimationVector += vector
184-
carryOverVector = runningAnimation.target - vector
211+
carryOverVector = runningAnimations[index].target - vector
212+
if logicallyCompleted {
213+
completedIndexes.append(index)
214+
}
185215
} else {
186216
finishedAnimationIndex = index
187217
//totalAnimationVector = zero
@@ -191,7 +221,7 @@ where AnimationList: Collection<RunningAnimation> {
191221
runningAnimations.formIndex(after: &index)
192222
}
193223

194-
return (totalAnimationVector, finishedAnimationIndex)
224+
return (totalAnimationVector, completedIndexes, finishedAnimationIndex)
195225
}
196226

197227
internal extension AnimatedValue {
@@ -200,8 +230,15 @@ internal extension AnimatedValue {
200230

201231
let wasAnimating = isAnimating
202232

203-
if let animation = context.transaction?.newAnimation(at: context.currentFrameTime) {
204-
self.animate(to: value, animation: animation)
233+
if !context.transaction.disablesAnimation,
234+
let animation = context.transaction.animation
235+
{
236+
self.animate(
237+
to: value,
238+
startTime: context.currentFrameTime,
239+
animation: animation,
240+
tracker: context.transaction._animationTracker
241+
)
205242
} else {
206243
self.setValue(value)
207244
}

Sources/ElementaryDOM/Animation/Animation.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public extension Animation {
9292

9393
public struct AnimationContext {
9494
var initialVelocity: AnimatableVector?
95+
var isLogicallyComplete: Bool = false
9596
//TODO: provide container for custom values
9697
}
9798

0 commit comments

Comments
 (0)