diff --git a/resources/scripts/engine/animations/keyframe.js b/resources/scripts/engine/animations/keyframe.js new file mode 100644 index 0000000..b28071a --- /dev/null +++ b/resources/scripts/engine/animations/keyframe.js @@ -0,0 +1,142 @@ +Engine.Animations.Keyframe = class Keyframe { + constructor(sprite, keyframes) { + this.sprite = sprite; + // The key-frames array contains objects with the properties of the + // sprite at the current time-point, e.g. width of 100 and height of 200 + this.keyframes = keyframes; + this.age = 0; + this.frame = 0; + // This flag determines what's gonna happen to the animation once + // it's finished playing + this.repetitionMode = "none"; + this.lastKeyframe = _.last(keyframes); + this.lastFrame = this.lastKeyframe.frame; + + // These are the properties which we can animate + this.animables = [ + "x", "y", "width", "height", "opacity" + ]; + + // Set a map whose keys represent animatable properties and values represent an array + // with relevant key-frames to its belonging property + this.trimmedKeyframes = this.animables.reduce((trimmedKeyframes, key) => { + trimmedKeyframes[key] = keyframes.filter(keyframe => keyframe[key] != null); + return trimmedKeyframes; + }, {}); + + // Set initial properties on sprite based on initial key-frame + _.each(keyframes[0], (value, key) => { + if (this.animables.includes(key)) sprite[key] = value; + }); + } + + draw(context, offsetX, offsetY) { + this.sprite.draw(context, offsetX, offsetY); + } + + update(span) { + if (!this.playing) return; + + this.age += span; + + switch (this.repetitionMode) { + // After one cycle animation would stop + case "none": + this.frame += span; + + if (this.frame > this.lastFrame) { + this.frame = this.lastFrame; + this.playing = false; + } + + break; + + // Once finished, replay from the beginning + case "cyclic": + this.frame = this.age % this.lastFrame; + break; + + // Once finished, play backwards, and so on + case "full": + this.frame = this.age % this.lastFrame; + let animationComplete = (this.age / this.lastFrame) % 2 >= 1; + if (animationComplete) this.frame = this.lastFrame - this.frame; + break; + } + + // Update sprite properties based on given key-frame's easing mode + this.animables.forEach(key => { + let motion = this.getKeyframeMotion(key); + + if (motion) + this.sprite[key] = this.calculateRelativeValue(motion, key); + }); + } + + play() { + this.playing = true; + } + + pause() { + this.playing = false; + } + + // Gets motion for current refresh + getKeyframeMotion(key) { + let keyframes = this.trimmedKeyframes[key]; + + // If no key-frames defined, motion is idle + if (keyframes == null) return; + // If there is only one key frame, motion is idle + if (keyframes.length < 2) return; + // If last frame reached, motion is idle + if (this.frame > _.last(keyframes).frame) return; + + let start = this.findStartKeyframe(keyframes); + let end = this.findEndKeyframe(keyframes); + let ratio = this.getKeyframesRatio(start, end); + + return { start, end, ratio }; + } + + // Gets the movement ratio + getKeyframesRatio(start, end) { + return (this.frame - start.frame) / (end.frame - start.frame); + } + + // Get property end value based on current frame + findEndKeyframe(keyframes) { + return _.find(keyframes, keyframe => + keyframe.frame >= (this.frame || 1) + ); + } + + // Get property start value based on current frame + findStartKeyframe(keyframes) { + let resultIndex; + + keyframes.some((keyframe, currIndex) => { + if (keyframe.frame >= (this.frame || 1)) { + resultIndex = currIndex; + return true; + } + }); + + return keyframes[resultIndex - 1]; + } + + // Get a recalculated property value relative to provided easing mode + calculateRelativeValue(motion, key) { + let a = motion.start[key]; + let b = motion.end[key]; + let r = motion.ratio; + let easing = r > 0 ? motion.start.easing : motion.end.easing; + + switch (easing) { + case "in": r = Math.sin((r * Math.PI) / 2); break; + case "out": r = Math.cos((r * Math.PI) / 2); break; + } + + return ((b - a) * r) + a; + } +}; \ No newline at end of file diff --git a/views/game.html b/views/game.html index 6f5cc3a..5703eff 100644 --- a/views/game.html +++ b/views/game.html @@ -10,6 +10,7 @@ +