🐮 "Is it my cow?" Elm game Postmortem - Part 3 - Animation
This is the continuation post of game postmortem blog post series you can find the complete list of this series posts below:
"Is it my cow?" Elm game postmortem series:
- The idea and core problem
- The cow without legs
- Animation (currently reading)
- coming soon...
🎬 Animation
It's been a while since last blog post about my Elm game. But to remind you, previously, we've been able to generate some random cows and spread them on our game scene so they don't overlap. Our cows were maximally simplified to a picture of a head + ellipsis with a random black pattern. What they are missing though are legs. The reason that I've postponed that part of the cow's body to the later stage was the fact that I wanted to add a little spice to my game. An extras that is not so essential in my game mechanics but should improve the visual aesthetics a bit. That thing was the animation. The idea was to convert statically placed cows into cows that walk around the stage from time to time.
💭 3 ways for animation
I've been considering a choice between 3 approaches to the animation issue. Two of them are probably well-known to you, at least to a some point, but the 3rd one may be actually surprising as it seems not so popular as the previous ones these days.
🎨 Approach 1 - CSS
This is something that is default these days for the frontend developer. CSS has two options that can put your DOM elements in motion:
transition
property which makes it able to smoothen the transition between two different values of some CSS property for instancetransform
'stranslate
function can be used together withtransition
to move smoothly some DOM element from one place to another.- CSS Animations - a nice technique that enables to define an animation through a series of
@keyframes
. Then such animation can be applied to a specific DOM element.
🎬 Approach 2 - onAnimationFrame
Although CSS is nice as in many cases it's easier to define the expected motion in a declarative manner ("I want it to behave this way") and push all of the hard work ("How it should be achieved?") to the browser's engine. But sometimes the convenient abstractions like this one get not so convenient. Especially in cases where animation must be customized or adapt to some logic on the fly. In such cases, you will probably need a more control over the specific steps of the animation so for instance you can control the speed of the moving element dynamically with respect to some other elements in the game that may interact with the first one somehow. In other words, not all animations can be preplanned.
When you want to control some animation programmatically from a JavaScript running in a web browser perspective, you can do that through setInterval
time-tick events. That was the way for instance the jQuery was doing animations in the past since the CSS features were not so rich to support transitions in those years. This gave an opportunity to control the DOM state on each frame (predefined time interval tick) which resulted some animation in the end user's web browser. However, the problem with the setInterval
approach is that the code of the JS callback function that controls the DOM may be executed in inappropriate moment, like when browser is not ready to rerender the visible content due to some other actions that have a higher priority. This results an animation that is no longer guaranteed to run smooth and can sometime freeze between the frames.
Fortunately, to solve this problem, web browsers these days have some nice built-in function called requestAnimationFrame
. This function enables you to trigger some rendering when browser decides is best to do so. In Elm, we've got onAnimationFrame
subscription which hooks into that function and makes it usable in TEA (The Elm Architecture) terms.
🧟 Approach 3 - SMIL
This one may surprise you because is SVG specific, it's specs are a bit old and it was even staged as deprecated once. Synchronized Multimedia Integration Language which SMIL stands for.
📦 Let's try some package!
An Elm community recommended me a nice package that does the animation job nicely and seems that have well-designed API, namely: elm-animator. At least one other Elm Game Jam participant (and also an organiser by the way) used it successfully in his Garden of Eels game. I've decided to give it a shot. It took me some time to get used to a nice concept of Timeline which this package offers in practice. It was a bit of work to introduce an animation to my code at that stage, but because of Elm static type nature + delightful compiler that guides you though refactor nicely I've managed to get a simple proof of concept where a random cow slides from point A to B smoothly (with no walking animation yet).
However, during that development I had some thoughts. First of all, is it beneficial to introduce a new abstraction into my game where an animation wasn't the core feature of the game as it's concept is - let's say - more static? By default, elm-animator
controls the animation frames through onAnimationFrame
subscription. This approach meant that more messages (that come into the update
function) will appear during the debugging sessions. What is nice though is that elm-animator
gives you the other way to control the animation in which only one message will have to be sent, precisely when animation should start only thanks to a CSS. This seems reasonable but again, some other thing bothered me...
Beside performing some nice smooth movement of a cow from place A to a place B, cow has to manifest somehow that it's not just magically moved from one place to another. In other words, we have to animate the cow walking state somehow. But how to approach this problem with the our Timeline
abstraction? Should we animate each of the cow leg independently? If so, isn't the code that is the core game idea going to be overshadowed by animation boilerplate? I mean, the animation should be just one of extras that is a secret ingredient of my game, but it should manage to be playable still without this bonus at the first place. This lead me to decide to go some other way and unfortunately to remove elm-animator
from my game 😢 But I hope to give it a shot in some other project in a future maybe as I still think that this is a nice package that is worth to give it another try in different circumstances 🙂
💁♂️ Custom approach
Because of the stuff that bothered me when I used a 3rd party library. I've decided to code the animation in my own way as I like to have as much direct control over a things as I can.
How to approach the smooth movement of an Html (Svg to be exact) element from one place to another in a straight line? Of course first what comes to the head of the today's Frontend Developer is just a simple CSS transition + translation property combination. So we can implement this easily using Elm's raw Html package through just a style
attribute. This problem was easy, however the next part of the animation will be a bit trickier...
How to animate cow legs? I've watched several movies on YouTube how cows walk, and dang... This seems that is not an easy task to represent this accurately 😅 Just check it out for yourself if you don't believe me 😁 But, of course, as my game is not a real cow simulator and our cow graphics are very simplified we don't have to recreate real cow walking animation precisely, we can make it more in a cartoony style where we just move the two legs in the front of the player in the one direction synchronously and the two others from the back in the opposite direction at the same time just like this:
So to make our question a bit more specific, how to animate cow legs in a way where they don't move though a straight path but through a more curved one?
For a strange reason I knew from the start that SVG has some preexisting mechanism to achieve that. Okay, not as strange reason... to be honest I've just seen once that one of my coworker made somehow SVG perform some nice animations that were doing something more than just simple CSS transitions and there was no JavaScript behind this at all. So, I've Googled and came across SMIL in MDN. What is nice about it is that it enables you to animate movement of a SVG object through a predefined path that has the same syntax as a SVG path. This means it's possible to just draw a path in some vector graphics program like my favourite LibreOffice Draw or software like Inkscape, just export it to a SVG file, and just extract the one of the path
element props (namely d
). My case was a bit simple, I knew that I just need a curved line but I've drawn a curved line in a drawing software anyway just to make it simpler to figure out the numbers that can be parametrised though the code more easily. In other words, I didn't have to figure out the documentation of those commands too deep to ensure which command parameter corresponds to the height of the curve, and which one corresponds to a ending position coordinates etc. as it was easier for me to figure out this from the number values of my curved line sample.
So, in terms of the code, I've extracted cow leg animation into separate function and some 2 small helper functions that helped me in generating the animation path
property:
viewLeg : ( Float, Float ) -> LegAnimation -> Svg msg
viewLeg ( x, y ) animation =
let
cy =
y + cowBodyRY * 0.7
in
Svg.g []
[ Svg.ellipse
[ Svg.Attributes.ry <| String.fromFloat (cowBodyRY * 0.8)
, Svg.Attributes.rx <| String.fromFloat (cowBodyRY * 0.15)
, Svg.Attributes.cx <| String.fromFloat x
, Svg.Attributes.cy <| String.fromFloat cy
, Svg.Attributes.style "fill: white; stroke: black"
]
(case animation of
GoLeft delay ->
[ Svg.animateMotion
[ Svg.Attributes.path <|
pathCmds
[ ( 'M', [ ( 0, 0 ) ] )
, ( 'C', [ ( 0, 0 ), ( -cowStepSize, -cowStepSize ), ( -cowStepSize, 0 ) ] )
, ( 'L', [ ( 0, 0 ) ] )
]
, Svg.Attributes.dur "1s"
, Svg.Attributes.repeatCount "indefinite"
, Svg.Attributes.begin (String.fromFloat delay ++ "s")
]
[]
]
_ ->
[]
)
]
pathCmds : List ( Char, List ( Float, Float ) ) -> String
pathCmds cmds =
cmds
|> List.map (\( cmd, points ) -> pathCmd cmd points)
|> String.join " "
pathCmd : Char -> List ( Float, Float ) -> String
pathCmd cmd points =
String.fromChar cmd
++ (points
|> List.map (\( x, y ) -> String.fromFloat x ++ "," ++ String.fromFloat y)
|> String.join " "
)
where LegAnimation
has 2 possible states:
type LegAnimation
= Stand
| GoLeft Float
The float in GoLeft
state makes it possible to delay the animation for the pair of legs on the one side of the cow:
rightLegAnim : LegAnimation
rightLegAnim =
case action of
Idle ->
Stand
Walk _ ->
GoLeft 0
leftLegAnim : LegAnimation
leftLegAnim =
case action of
Idle ->
Stand
Walk _ ->
GoLeft 0.5
Cool! We have a cow marching in place "to the left" and we can trigger this animation by just putting the cow model into Walk
state. But how to move it to a particular place at the same time? Here's where CSS's transition
and transform
properties mix can be helpful! We can put these SVG attributes to move our cow from one place to another in a nice smooth way controlled by a web browser:
[ Svg.Attributes.transform <|
"translate("
++ String.fromFloat targetX
++ " "
++ String.fromFloat targetY
++ ")"
, Html.Attributes.style "transition" "linear 3s"
]
Side note: Interesting thing about the SMIL is that it has been deprecated once as I've mentioned already. That's because for a while there was a promising alternative on the horizon called CSS motion path. However, it seems that it didn't catched up, so legacy SMIL is still the only option to make browser safely (cross-compatiblenessly) do the job of animating some DOM elements that follow a predefined path declaratively (without JavaScript code).
But going back to our implementation, there is still an issue to be resolved. We have to synchronise the walking animation with that CSS transition so the walking cow animation is played until cow reaches the target position, but it should stop right after the target position is reached. Because we don't have control over the transition animation as it is calculated and performed by a Web Browser itself, we have to perform some small trick to detect the moment of cow reaching the target. Namely, we need to subscribe to transitionend
event, so let's do that with some small help of a helper function as this event is not builtin into Elm Html library:
onTransitionEnd msg =
Html.Events.on "transitionend" (Json.Decode.succeed msg)
Our cow view function will look like this:
view : Cow -> Svg Msg
view ((Cow { patches, targetPosition, size, action }) as model) =
let
( targetX, targetY ) =
targetPosition
in
Svg.g
[ Svg.Attributes.transform <|
"translate("
++ String.fromFloat targetX
++ " "
++ String.fromFloat targetY
++ ")"
, onTransitionEnd ReachedTarget
, Html.Attributes.style "transition" "linear 3s"
]
[ viewStatic model
]
So, when cow reaches the target position (ReachedTarget
msg), we can just change it state again to the Idle
in update function:
update : Msg -> Cow -> Cow
update msg (Cow state) =
case msg of
ReachedTarget ->
Cow { state | action = Idle }
_ ->
Cow state
We are almost there in terms of the animation. It could be considered as complete... but only in the case if cows would walk always to the left... So, how about cow walking to the right? To be honest I've been a bit lazy here and I just flipped the cow through the Y-axis:
Part of the cow view function:
[ Svg.g
-- Flip cow when walking right
(if action == Walk Right then
[ Svg.Attributes.transform <| "scale(-1 1) translate(" ++ String.fromFloat -cowWidth ++ " 0)"
]
else
[]
)
[ cowBodyClipPath
, viewLeg ( cowBodyCX - cowBodyRX * 0.8, cowBodyCY * 0.9 ) rightLegAnim
, viewLeg ( cowBodyCX + cowBodyRX * 0.6, cowBodyCY * 0.9 ) rightLegAnim
, cowBody
, viewLeg ( cowBodyCX - cowBodyRX * 0.6, cowBodyCY ) leftLegAnim
, viewLeg ( cowBodyCX + cowBodyRX * 0.8, cowBodyCY ) leftLegAnim
, cowHead
]
]
This is against the laws of physics and so, but hey, this is cartoon-like game where physics can be altered 😉
🐛 Animation bug
Worth noting though is that there is some bug that I haven't fixed so far. Sometimes the transitionend
event may be not emitted, for instance when browser tab has been changed in the meantime or user have changed the game state which results rendering some other scene. In such cases the transition may get cancelled. This could rarely manifest with cow walking in place in some circumstances. To fix that, I would probably have to handle the tranansitioncancel
event.
⏩ To be continued...
This is the third post of the series of the blog posts where I tell a story with some details how I developed my game. Next time we'll be polishing our game by i.a. adding some music and implementing some increasing difficulty mechanism.
Stay tuned!