Implementing an FSM in javascript: Machina JS

view code

use the 'a' and 'd' keys the move the bird!

Overview

top

Machina.js is a JavaScript framework for highly customizable finite state machines.

In this demo, I'll be using Machina.js to create a finite state machine for the movements of a bird

You will need to include scripts for both machina and lodash

for lodash, use the cdn that is shown in the html section of my code

machina doesn't currently have a cdn, but can be downloaded from their github

Start planning out the state machine by drawing a diagram of how it should behave given different inputs in each state

diagram of the state machine showing states: shuffling, standing, jumping, landing and flying
diagram for the state machine

for simplicity, I'll only be implementing some of the states, specifically: standing, shuffling left, and shuffling right

diagram for states of shuffling and standing only
diagram of standing and shuffling states

HTML

top
            
<head>  
  <script src="https://cdn.jsdelivr.net/lodash/3.10.1/lodash.min.js"></script>
  <script src="https://code.jquery.com/jquery-3.1.1.js" 
             integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA="   
            crossorigin="anonymous"></script>
  <script src="external-sources/machina.js"></script>
</head>          
<body>
  <!-- wrapper for animation  -->
  <div class="wrapper">
      <div id="line">
        <!-- curve for the powerline (this will work with any inline path) -->
        <svg viewBox="0 0 560 64.06">
          <path id="powerline" d="M0,31.67C181.33,100.33,418,45.33,560,0"/>
        </svg>
        <!-- bird -->
        <div id="bird">
            <div></div>
        </div>
      </div>
  </div>
</body>
            
            

SCSS

top

:root {
  //create super nutty css variables
  //demo: http://codepen.io/jasesmith/pen/dpwjra
  --position-x: 0;
  --position-y: 0;
  --angle: -7deg;
  --cycle-state: 'paused';
}
#line{
  position:absolute;
  width: 100%;
  top: calc(100% - 17.2vw);
  height: auto;
  #bird {
    position: absolute;
    top: 45%;
    left: 0px;
    width:  8vw;
    height: 8vw;
    animation: move 0.5s infinite ease-in-out forwards;
    animation-play-state: paused;
    z-index:10;
    transform-origin: bottom center;
    transform: rotate(0deg);
    div {
      position: absolute;
      width: 100%;
      height: 100%;
      background-image: url("../images/raven.png");
      background-size: auto 100%;
      background-position: 0% 0%;
      animation: cycleFrames 0.5s infinite steps(3,start);
      animation-play-state: paused;
      transform: rotate(var(--angle));  
    }
  }
  path {
    fill: none;
  }
}

//contains code demo
.wrapper {
  width: 100%;
  height: 100vh;
  position: relative;
  background-image: url("../images/background-grain.jpg");
  background-size: 100% auto;
  background-position: center bottom;
  background-repeat: no-repeat;
}

                

Javascript

top
              

/* -----------------------------
  
         the state machine

  -----------------------------*/

var $bird = $("#bird"),
    $birdBefore = $('#bird div'),
    path = document.getElementById("powerline");

//create a new state machine
var bird = new machina.Fsm({
    namespace: "bird",
  //any local variables and functions you'd like for your state machine
    initialize: function (){
        _keyspressed = {
            'left':false, //a key
            'right':false //d key
        };
        var initialPosition = newton($(window).width()*0.8, path);
        $bird.css({'top': (initialPosition -0.9*$bird.width()) + 'px', 'left':($(window).width()*0.8-0.5*$bird.width()) + 'px'}); 
        //this is also where I think i'll set up my event listeners for the animations
        //also, initialize play-state
    },
    //this tells the state machine which state to start in
  initialState: "standing",
  //this is where you include all the different states you like to have
  //in this example, this will be the different actions the bird will be doing
  //standing, shuffling left, and shuffling right
  states: {
    //inside each state, you give it which inputs it will handle, and how
    //the inputs for this example will be the arrow keys (well, actually, aswd)
    //machina also has builtin events for states you can use
    // _onEnter ---- lets you define actions to take upon entering a state
    //_onExit   ---- lets you define actions to take upen exiting a state
    standing: {
      //this state will react to both left and right keydown
            _onEnter: function() {
                //switch to standing animation (if there is one)
            },
      keydownleft: function() {
                this.transition("shufflingLeft");
            },
      keydownright: function() {
                this.transition("shufflingRight");
            }
    },
    shufflingLeft: {
      _onEnter: function () {
        //start animation
                //use jquery animate for moving div, and css animation for sprite loops?
        //after one cycle is complete, check if key is up
        //if it is, pass the keyup action you've defined
                startShuffling($bird, "left");
      },
            keydownright: function() {
                //instead of switching directly, emit event and add to queue?
                this.deferAndTransition("standing");
            },
            keyupleft: function() {
                console.log("kepupleft");
                if (_keyspressed.right) {
                    this.handle("keydownright");
                } else {
                    this.transition("standing");
                }
            },
            _onExit: function (){
                stopShuffling($bird, "left");
            }
    },
        shuffleLeftInterrupt: {

        },
    shufflingRight: {
      _onEnter: function () {
                //start animation
                //use jquery animate for moving div, and css animation for sprite loops?
                //after one cycle is complete, check if key is up
                //if it is, pass the keyup action you've defined
                startShuffling($bird, "right")
            },
            keydownleft: function() {
                //this.clearQueue();
                this.deferAndTransition("standing");
            },
            keyupright: function() {
                //this.clearQueue();
                if (_keyspressed.left) {
                    this.handle("keydownleft");
                } else {
                    this.transition("standing");
                }
            },
            _onExit: function (){
                stopShuffling($bird, "right");
            }
    }
  },
  //wrappers to make calls prettier
  leftdown: function () {
    if (!_keyspressed.left) {
            _keyspressed.left = true;
            this.handle("keydownleft");
        }
  },
    leftup: function () {
        if (_keyspressed.left) {
            _keyspressed.left = false;
            this.handle("keyupleft");
        }
    },
  rightdown: function () {
    if (!_keyspressed.right) {
            _keyspressed.right = true;
            this.handle("keydownright");
        }
  },
    rightup: function () {
        if (_keyspressed.right) {
            _keyspressed.right = false;
            this.handle("keyupright");
        }
    }
});

//once the state machine is set up, here's where we'll be using it
$(document).keydown(function(e) {
  e.preventDefault(); 
    switch(e.which) {
        //controls for animation
        
        case 65: // a (left)
        //this is where we'd like our bird to shuffle left
        bird.leftdown();
        
        break;
        case 68: // d (right)
        //this is where we'd like our bird to shuffle right
        bird.rightdown();
        break;

        default: return; 
    }

});
$(document).keyup(function(e) {
  e.preventDefault(); 
    switch(e.which) {
        //controls for animation

        case 65: // a (left)
        //user releases the left key
        //bird should complete current cycle of animation, 
        //then go to standing then right if right key is down
        bird.leftup();
        break;
        case 68: // d (right)
        //user releases the right key
        //bird should complete current cycle of animation, 
        //then go to standing then left if left key is down
        bird.rightup();
        break;

        default: return; 
    }
});

/* ------------------------------------------------
  
      these are all for the animation of the bird

  -------------------------------------------------*/

//given x, find y of given curve
function newton(x, path) {
    //pick a starting point
    //try halfway between x value and total length
    //since length(x) >= x
    var scale = $(path).parent('svg').width()/path.getBBox().width;
     x /= scale;
    var start = x,
        end = path.getTotalLength(),
        testlength,
        testpoint;

    //algorithm part
    //edge cases: at end of path, at start of path
    while(true) {
      //try middle of current bounding region
      testlength = Math.floor((start + end) / 2);
      testpoint = path.getPointAtLength(testlength);
      if (testlength === end || testlength === start) {
          break;
      } else if (testpoint.x > x) {
        //guess was too high
        end = testlength;

      } else if (testpoint.x < x) {
        //guess was too low
        start = testlength;
      } else {
        //guess is correct
        break;
      }
    }
    return testpoint.y*scale;
}

//get position to shuffle to
function shuffle(x, step, path, direction) {
    //move set distance (step) in specified direction
    //parameters:
    //x: current x position
    //step: distance to shift
    //path: curve to move along
    //direction: which way to move along curve
    //returns: 
    //{x,y}: new position on curve
    var newposition = {};
    newposition.x = x;
    //console.log("x is: " + x);
    if (direction==="left") {
      //new x value
      newposition.x -= step;
      //new y value
    } else if (direction==="right") {
      //new x value
      newposition.x += step;
      
    }
    newposition.y = newton(newposition.x, path);
    return newposition;
}

//start the shuffling animation in the specified direction
function startShuffling($thing, direction){
    $thing.off("webkitAnimationIteration");
    $birdBefore.off("webkitAnimationIteration");
    //get next position on curve
    var position = shuffle($thing.offset().left+ 0.5*$thing.width(), 0.5*$thing.width(), path, direction);
    //find the slope of the curve at the birds current position
    var angle, opposite, adjacent;
    opposite = newton($thing.offset().left, path) - newton($thing.offset().left + $thing.width(), path);
    adjacent = $thing.width();
    angle = -1*Math.atan(opposite/adjacent)/Math.PI*180;
    //set the css variables
    document.documentElement.style.setProperty('--position-x', position.x-0.5*$thing.width())
    document.documentElement.style.setProperty('--position-y', position.y - 0.9*$thing.width())
    document.documentElement.style.setProperty('--angle', angle + 'deg');
    //start the animations
    $thing.css('animation-play-state','running');
    $birdBefore.css('animation-play-state','running');
    //get and set next animation end points after an iteration
    $thing.on("webkitAnimationIteration", function(e) {
        var animName = e.originalEvent.animationName;
        if (animName === "move") {
           $thing.css({'top': position.y -0.9*$thing.width(), 'left':position.x-0.5*$thing.width()}); 
        }
        //sets angle of bird
        opposite = newton($thing.offset().left, path) - newton($thing.offset().left + $thing.width(), path);
        adjacent = $thing.width();
        angle = -1*Math.atan(opposite/adjacent)/Math.PI*180;
        position = shuffle($thing.offset().left+0.5*$thing.width(), 0.5*$thing.width(), path, direction);
        //set the css variables
        document.documentElement.style.setProperty('--position-x', position.x-0.5*$thing.width());
        document.documentElement.style.setProperty('--position-y', position.y - 0.9*$thing.width());
        document.documentElement.style.setProperty('--angle', angle + 'deg');
    });
}

//turns off the animations after they've completed their current iteration
function stopShuffling($thing, position){
    //calls at start of iteration (except first one, kind of like fence post issue)
    $thing.on("webkitAnimationIteration", function(e) {
        var animName = e.originalEvent.animationName;
        if (animName === "move") {
           $thing.css({'top': position.y - 0.9*$thing.width(), 'left':position.x-0.5*$thing.width()});  
        }
        $thing.css('animation-play-state','paused');
    });
    $birdBefore.on("webkitAnimationIteration", function(e) {
        $birdBefore.css('animation-play-state','paused');
    });
}