Skip to content

Commit 90df975

Browse files
authored
basic support for CSS animations (#27)
* wip * wip * basic "push-to-DOM" animation mechanism * 'tis a bit tricky * animation combining fix * fixed multi-transaction and combining animations * fixed embedded build
1 parent 0b5c8ce commit 90df975

24 files changed

+1164
-171
lines changed

Examples/Basic/Sources/App/AnimationsView.swift

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,45 @@ import _ElementaryMath
44
@View
55
struct AnimationsView {
66
@State var angle: Double = 0
7+
@State var isBallFading: Bool = false
8+
@State var isOffset: Bool = false
9+
@State var isRotated: Bool = false
710

811
var content: some View {
912

1013
div {
11-
AnimatedView(angle: angle)
12-
button { "Animate" }
13-
.onClick { _ in
14-
withAnimation(.bouncy(duration: 1)) {
15-
angle += 1
14+
AnimatedView(angle: angle, isBallFading: isBallFading)
15+
div(.style(["display": "flex", "flex-direction": "row", "gap": "10px"])) {
16+
button { "Animate" }
17+
.onClick { _ in
18+
withAnimation(.smooth) {
19+
angle += 1
20+
isBallFading.toggle()
21+
}
1622
}
17-
}
23+
Square(color: "blue")
24+
.rotationEffect(.degrees(0))
25+
.rotationEffect(.radians(angle), anchor: .topTrailing)
26+
Square(color: "red")
27+
.rotationEffect(.degrees(isRotated ? 360 : 0))
28+
.offset(x: isOffset ? 100 : 0)
29+
.onClick { _ in
30+
withAnimation(.bouncy(duration: 3)) {
31+
isOffset.toggle()
32+
}
33+
withAnimation(.easeIn(duration: 1).delay(1)) {
34+
isRotated.toggle()
35+
}
36+
}
37+
}
1838
}
1939
}
2040
}
2141

2242
@View
2343
struct AnimatedView {
2444
var angle: Double
45+
var isBallFading: Bool
2546

2647
let size = 100.0
2748
var x: Double { size * (1 - cos(angle)) }
@@ -37,6 +58,7 @@ struct AnimatedView {
3758
"position": "relative",
3859
])
3960
)
61+
.opacity(isBallFading ? 0.1 : 1)
4062
}.attributes(
4163
.style([
4264
"height": "\(2 * size + 10)px",
@@ -47,6 +69,22 @@ struct AnimatedView {
4769
}
4870
}
4971

72+
@View
73+
struct Square {
74+
var color: String
75+
76+
var content: some View {
77+
span {}
78+
.attributes(
79+
.style([
80+
"background": color,
81+
"height": "20px",
82+
"width": "20px",
83+
])
84+
)
85+
}
86+
}
87+
5088
extension AnimatedView: Animatable {
5189
var animatableValue: Double {
5290
get { angle }

Sources/ElementaryDOM/Animation/AnimatedValue.swift

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,25 +81,25 @@ struct AnimatedValue<Value: AnimatableVectorConvertible>: ~Copyable {
8181
context: &context
8282
)
8383

84-
self.currentAnimationValue = Value(animationBase + animatedVector)
85-
8684
if let finishedAnimationIndex {
8785
removeAnimations(upThrough: finishedAnimationIndex)
8886
}
8987

90-
#if DEBUG
91-
if !isAnimating {
92-
assert(self.currentAnimationValue == self.currentTarget)
93-
assert(self.animationBase == self.currentTarget.animatableVector)
88+
if isAnimating {
89+
self.currentAnimationValue = Value(animationBase + animatedVector)
90+
} else {
91+
// NOTE: avoid floating point weirdness
92+
self.currentAnimationValue = self.currentTarget
93+
self.animationBase = self.currentTarget.animatableVector
9494
}
95-
#endif
9695
}
9796

9897
// TODO: figure out the shape for this
9998
func peekFutureValues(_ times: StrideThrough<Double>) -> [Value] {
10099
var results: [Value] = []
101100
var contextCopy = context
102101
var runningAnimations = runningAnimations[...]
102+
var base = animationBase
103103

104104
results.reserveCapacity(times.underestimatedCount)
105105

@@ -110,12 +110,19 @@ struct AnimatedValue<Value: AnimatableVectorConvertible>: ~Copyable {
110110
context: &contextCopy
111111
)
112112

113-
results.append(Value(self.animationBase + animatedVector))
114-
115113
if let completedIndex {
116-
//TODO: update base
114+
for i in runningAnimations.startIndex...completedIndex {
115+
base += runningAnimations[i].target
116+
}
117117
runningAnimations = runningAnimations[(completedIndex + 1)...]
118118
}
119+
120+
if runningAnimations.isEmpty {
121+
results.append(self.currentTarget)
122+
break
123+
}
124+
125+
results.append(Value(base + animatedVector))
119126
}
120127
return results
121128
}
@@ -138,6 +145,7 @@ private func calculateAnimationAtTime<AnimationList>(
138145
) -> (animatedVector: AnimatableVector, finishedAnimationIndex: AnimationList.Index?)
139146
where AnimationList: Collection<RunningAnimation> {
140147
guard runningAnimations.count > 1 else {
148+
assert(runningAnimations.first != nil, "Running animations should not be empty")
141149
if let vector = runningAnimations.first!.animate(time: time, context: &context, additionalVector: nil) {
142150
return (vector, nil)
143151
} else {
@@ -161,7 +169,7 @@ where AnimationList: Collection<RunningAnimation> {
161169
carryOverVector = runningAnimation.target - vector
162170
} else {
163171
finishedAnimationIndex = index
164-
totalAnimationVector += runningAnimation.target
172+
//totalAnimationVector = zero
165173
carryOverVector = zero
166174
}
167175

@@ -170,3 +178,17 @@ where AnimationList: Collection<RunningAnimation> {
170178

171179
return (totalAnimationVector, finishedAnimationIndex)
172180
}
181+
182+
internal extension AnimatedValue {
183+
mutating func setValueAndReturnIfAnimationWasStarted(_ value: Value, context: borrowing _RenderContext) -> Bool {
184+
let wasAnimating = isAnimating
185+
186+
if let animation = context.transaction?.animation {
187+
self.animate(to: value, animation: AnimationInstance(startTime: context.currentFrameTime, animation: animation))
188+
} else {
189+
self.setValue(value)
190+
}
191+
192+
return isAnimating && !wasAnimating
193+
}
194+
}

Sources/ElementaryDOM/Animation/Transaction.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public func withTransaction<Result, Failure>(
2626
defer {
2727
Transaction._current = previous
2828
}
29+
logTrace("withTransaction \(transaction._id)")
2930
return try body()
3031
}
3132

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
extension DOM {
2+
public struct Animation {
3+
let _cancel: () -> Void
4+
let _update: (KeyframeEffect) -> Void
5+
6+
func cancel() {
7+
_cancel()
8+
}
9+
10+
func update(_ effect: KeyframeEffect) {
11+
_update(effect)
12+
}
13+
}
14+
}
15+
16+
extension DOM.Animation {
17+
public enum CompositeOperation: Sendable {
18+
case replace
19+
case add
20+
case accumulate
21+
}
22+
23+
public struct KeyframeEffect {
24+
var property: String
25+
var values: [String]
26+
var duration: Int // milliseconds
27+
var composite: CompositeOperation
28+
}
29+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
extension DOM {
2+
public struct Node {
3+
let ref: AnyObject
4+
}
5+
6+
public struct Event {
7+
let ref: AnyObject
8+
}
9+
10+
public struct EventSink {
11+
let ref: AnyObject
12+
}
13+
14+
public enum PropertyValue {
15+
case string(String)
16+
case number(Double)
17+
case boolean(Bool)
18+
case stringArray([String])
19+
case null
20+
case undefined
21+
}
22+
23+
public struct PropertyAccessor {
24+
let _get: () -> PropertyValue?
25+
let _set: (PropertyValue) -> Void
26+
27+
init(
28+
get: @escaping () -> PropertyValue?,
29+
set: @escaping (PropertyValue) -> Void
30+
) {
31+
self._get = get
32+
self._set = set
33+
}
34+
35+
func get() -> PropertyValue? {
36+
_get()
37+
}
38+
39+
func set(_ value: PropertyValue) {
40+
_set(value)
41+
}
42+
}
43+
44+
public struct StyleAccessor {
45+
let _get: () -> String
46+
let _set: (String) -> Void
47+
48+
init(
49+
get: @escaping () -> String,
50+
set: @escaping (String) -> Void
51+
) {
52+
self._get = get
53+
self._set = set
54+
}
55+
56+
func get() -> String {
57+
_get()
58+
}
59+
60+
func set(_ value: String) {
61+
_set(value)
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)