Skip to content

Commit 6ce4287

Browse files
committed
Step 3.8: Create a key-frame animation engine
1 parent 513c2c4 commit 6ce4287

File tree

2 files changed

+143
-0
lines changed

2 files changed

+143
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
Engine.Animations.Keyframe = class Keyframe {
2+
constructor(sprite, keyframes) {
3+
this.sprite = sprite;
4+
// The key-frames array contains objects with the properties of the
5+
// sprite at the current time-point, e.g. width of 100 and height of 200
6+
this.keyframes = keyframes;
7+
this.age = 0;
8+
this.frame = 0;
9+
// This flag determines what's gonna happen to the animation once
10+
// it's finished playing
11+
this.repetitionMode = "none";
12+
this.lastKeyframe = _.last(keyframes);
13+
this.lastFrame = this.lastKeyframe.frame;
14+
15+
// These are the properties which we can animate
16+
this.animables = [
17+
"x", "y", "width", "height", "opacity"
18+
];
19+
20+
// Set a map whose keys represent animatable properties and values represent an array
21+
// with relevant key-frames to its belonging property
22+
this.trimmedKeyframes = this.animables.reduce((trimmedKeyframes, key) => {
23+
trimmedKeyframes[key] = keyframes.filter(keyframe => keyframe[key] != null);
24+
return trimmedKeyframes;
25+
}, {});
26+
27+
// Set initial properties on sprite based on initial key-frame
28+
_.each(keyframes[0], (value, key) => {
29+
if (this.animables.includes(key)) sprite[key] = value;
30+
});
31+
}
32+
33+
draw(context, offsetX, offsetY) {
34+
this.sprite.draw(context, offsetX, offsetY);
35+
}
36+
37+
update(span) {
38+
if (!this.playing) return;
39+
40+
this.age += span;
41+
42+
switch (this.repetitionMode) {
43+
// After one cycle animation would stop
44+
case "none":
45+
this.frame += span;
46+
47+
if (this.frame > this.lastFrame) {
48+
this.frame = this.lastFrame;
49+
this.playing = false;
50+
}
51+
52+
break;
53+
54+
// Once finished, replay from the beginning
55+
case "cyclic":
56+
this.frame = this.age % this.lastFrame;
57+
break;
58+
59+
// Once finished, play backwards, and so on
60+
case "full":
61+
this.frame = this.age % this.lastFrame;
62+
let animationComplete = (this.age / this.lastFrame) % 2 >= 1;
63+
if (animationComplete) this.frame = this.lastFrame - this.frame;
64+
break;
65+
}
66+
67+
// Update sprite properties based on given key-frame's easing mode
68+
this.animables.forEach(key => {
69+
let motion = this.getKeyframeMotion(key);
70+
71+
if (motion)
72+
this.sprite[key] = this.calculateRelativeValue(motion, key);
73+
});
74+
}
75+
76+
play() {
77+
this.playing = true;
78+
}
79+
80+
pause() {
81+
this.playing = false;
82+
}
83+
84+
// Gets motion for current refresh
85+
getKeyframeMotion(key) {
86+
let keyframes = this.trimmedKeyframes[key];
87+
88+
// If no key-frames defined, motion is idle
89+
if (keyframes == null) return;
90+
// If there is only one key frame, motion is idle
91+
if (keyframes.length < 2) return;
92+
// If last frame reached, motion is idle
93+
if (this.frame > _.last(keyframes).frame) return;
94+
95+
let start = this.findStartKeyframe(keyframes);
96+
let end = this.findEndKeyframe(keyframes);
97+
let ratio = this.getKeyframesRatio(start, end);
98+
99+
return { start, end, ratio };
100+
}
101+
102+
// Gets the movement ratio
103+
getKeyframesRatio(start, end) {
104+
return (this.frame - start.frame) / (end.frame - start.frame);
105+
}
106+
107+
// Get property end value based on current frame
108+
findEndKeyframe(keyframes) {
109+
return _.find(keyframes, keyframe =>
110+
keyframe.frame >= (this.frame || 1)
111+
);
112+
}
113+
114+
// Get property start value based on current frame
115+
findStartKeyframe(keyframes) {
116+
let resultIndex;
117+
118+
keyframes.some((keyframe, currIndex) => {
119+
if (keyframe.frame >= (this.frame || 1)) {
120+
resultIndex = currIndex;
121+
return true;
122+
}
123+
});
124+
125+
return keyframes[resultIndex - 1];
126+
}
127+
128+
// Get a recalculated property value relative to provided easing mode
129+
calculateRelativeValue(motion, key) {
130+
let a = motion.start[key];
131+
let b = motion.end[key];
132+
let r = motion.ratio;
133+
let easing = r > 0 ? motion.start.easing : motion.end.easing;
134+
135+
switch (easing) {
136+
case "in": r = Math.sin((r * Math.PI) / 2); break;
137+
case "out": r = Math.cos((r * Math.PI) / 2); break;
138+
}
139+
140+
return ((b - a) * r) + a;
141+
}
142+
};

views/game.html

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<!-- Scripts -->
1111
<script type="text/javascript" src="/scripts/namespaces.js"></script>
1212
<script type="text/javascript" src="/scripts/engine/sprite.js"></script>
13+
<script type="text/javascript" src="/scripts/engine/animations/keyframe.js"></script>
1314
<script type="text/javascript" src="/scripts/engine/key_states.js"></script>
1415
<script type="text/javascript" src="/scripts/engine/layer.js"></script>
1516
<script type="text/javascript" src="/scripts/engine/screen.js"></script>

0 commit comments

Comments
 (0)