Skip to content

Instantly share code, notes, and snippets.

@futuraprime
Last active August 29, 2015 14:08
Show Gist options
  • Save futuraprime/782017ea7492dac90bf5 to your computer and use it in GitHub Desktop.
Save futuraprime/782017ea7492dac90bf5 to your computer and use it in GitHub Desktop.
Bach face plant // source http://jsbin.com/yatigi
/*!
* Bowser - a browser detector
* https://github.com/ded/bowser
* MIT License | (c) Dustin Diaz 2014
*/
!function (name, definition) {
if (typeof module != 'undefined' && module.exports) module.exports['browser'] = definition()
else if (typeof define == 'function' && define.amd) define(definition)
else this[name] = definition()
}('bowser', function () {
/**
* See useragents.js for examples of navigator.userAgent
*/
var t = true
function detect(ua) {
function getFirstMatch(regex) {
var match = ua.match(regex);
return (match && match.length > 1 && match[1]) || '';
}
var iosdevice = getFirstMatch(/(ipod|iphone|ipad)/i).toLowerCase()
, likeAndroid = /like android/i.test(ua)
, android = !likeAndroid && /android/i.test(ua)
, versionIdentifier = getFirstMatch(/version\/(\d+(\.\d+)?)/i)
, tablet = /tablet/i.test(ua)
, mobile = !tablet && /[^-]mobi/i.test(ua)
, result
if (/opera|opr/i.test(ua)) {
result = {
name: 'Opera'
, opera: t
, version: versionIdentifier || getFirstMatch(/(?:opera|opr)[\s\/](\d+(\.\d+)?)/i)
}
}
else if (/windows phone/i.test(ua)) {
result = {
name: 'Windows Phone'
, windowsphone: t
, msie: t
, version: getFirstMatch(/iemobile\/(\d+(\.\d+)?)/i)
}
}
else if (/msie|trident/i.test(ua)) {
result = {
name: 'Internet Explorer'
, msie: t
, version: getFirstMatch(/(?:msie |rv:)(\d+(\.\d+)?)/i)
}
}
else if (/chrome|crios|crmo/i.test(ua)) {
result = {
name: 'Chrome'
, chrome: t
, version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
}
}
else if (iosdevice) {
result = {
name : iosdevice == 'iphone' ? 'iPhone' : iosdevice == 'ipad' ? 'iPad' : 'iPod'
}
// WTF: version is not part of user agent in web apps
if (versionIdentifier) {
result.version = versionIdentifier
}
}
else if (/sailfish/i.test(ua)) {
result = {
name: 'Sailfish'
, sailfish: t
, version: getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i)
}
}
else if (/seamonkey\//i.test(ua)) {
result = {
name: 'SeaMonkey'
, seamonkey: t
, version: getFirstMatch(/seamonkey\/(\d+(\.\d+)?)/i)
}
}
else if (/firefox|iceweasel/i.test(ua)) {
result = {
name: 'Firefox'
, firefox: t
, version: getFirstMatch(/(?:firefox|iceweasel)[ \/](\d+(\.\d+)?)/i)
}
if (/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(ua)) {
result.firefoxos = t
}
}
else if (/silk/i.test(ua)) {
result = {
name: 'Amazon Silk'
, silk: t
, version : getFirstMatch(/silk\/(\d+(\.\d+)?)/i)
}
}
else if (android) {
result = {
name: 'Android'
, version: versionIdentifier
}
}
else if (/phantom/i.test(ua)) {
result = {
name: 'PhantomJS'
, phantom: t
, version: getFirstMatch(/phantomjs\/(\d+(\.\d+)?)/i)
}
}
else if (/blackberry|\bbb\d+/i.test(ua) || /rim\stablet/i.test(ua)) {
result = {
name: 'BlackBerry'
, blackberry: t
, version: versionIdentifier || getFirstMatch(/blackberry[\d]+\/(\d+(\.\d+)?)/i)
}
}
else if (/(web|hpw)os/i.test(ua)) {
result = {
name: 'WebOS'
, webos: t
, version: versionIdentifier || getFirstMatch(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)
};
/touchpad\//i.test(ua) && (result.touchpad = t)
}
else if (/bada/i.test(ua)) {
result = {
name: 'Bada'
, bada: t
, version: getFirstMatch(/dolfin\/(\d+(\.\d+)?)/i)
};
}
else if (/tizen/i.test(ua)) {
result = {
name: 'Tizen'
, tizen: t
, version: getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i) || versionIdentifier
};
}
else if (/safari/i.test(ua)) {
result = {
name: 'Safari'
, safari: t
, version: versionIdentifier
}
}
else result = {}
// set webkit or gecko flag for browsers based on these engines
if (/(apple)?webkit/i.test(ua)) {
result.name = result.name || "Webkit"
result.webkit = t
if (!result.version && versionIdentifier) {
result.version = versionIdentifier
}
} else if (!result.opera && /gecko\//i.test(ua)) {
result.name = result.name || "Gecko"
result.gecko = t
result.version = result.version || getFirstMatch(/gecko\/(\d+(\.\d+)?)/i)
}
// set OS flags for platforms that have multiple browsers
if (android || result.silk) {
result.android = t
} else if (iosdevice) {
result[iosdevice] = t
result.ios = t
}
// OS version extraction
var osVersion = '';
if (iosdevice) {
osVersion = getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i);
osVersion = osVersion.replace(/[_\s]/g, '.');
} else if (android) {
osVersion = getFirstMatch(/android[ \/-](\d+(\.\d+)*)/i);
} else if (result.windowsphone) {
osVersion = getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i);
} else if (result.webos) {
osVersion = getFirstMatch(/(?:web|hpw)os\/(\d+(\.\d+)*)/i);
} else if (result.blackberry) {
osVersion = getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i);
} else if (result.bada) {
osVersion = getFirstMatch(/bada\/(\d+(\.\d+)*)/i);
} else if (result.tizen) {
osVersion = getFirstMatch(/tizen[\/\s](\d+(\.\d+)*)/i);
}
if (osVersion) {
result.osversion = osVersion;
}
// device type extraction
var osMajorVersion = osVersion.split('.')[0];
if (tablet || iosdevice == 'ipad' || (android && (osMajorVersion == 3 || (osMajorVersion == 4 && !mobile))) || result.silk) {
result.tablet = t
} else if (mobile || iosdevice == 'iphone' || iosdevice == 'ipod' || android || result.blackberry || result.webos || result.bada) {
result.mobile = t
}
// Graded Browser Support
// http://developer.yahoo.com/yui/articles/gbs
if ((result.msie && result.version >= 10) ||
(result.chrome && result.version >= 20) ||
(result.firefox && result.version >= 20.0) ||
(result.safari && result.version >= 6) ||
(result.opera && result.version >= 10.0) ||
(result.ios && result.osversion && result.osversion.split(".")[0] >= 6) ||
(result.blackberry && result.version >= 10.1)
) {
result.a = t;
}
else if ((result.msie && result.version < 10) ||
(result.chrome && result.version < 20) ||
(result.firefox && result.version < 20.0) ||
(result.safari && result.version < 6) ||
(result.opera && result.version < 10.0) ||
(result.ios && result.osversion && result.osversion.split(".")[0] < 6)
) {
result.c = t
} else result.x = t
return result
}
var bowser = detect(typeof navigator !== 'undefined' ? navigator.userAgent : '')
/*
* Set our detect method to the main bowser object so we can
* reuse it to test other user agents.
* This is needed to implement future tests.
*/
bowser._detect = detect;
return bowser
});
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="I'll Be Bach" />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>I'll Be Bach</title>
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<!-- for generating hash-based names for files-->
<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/md5.js"></script>
<!-- browser sniffing -->
<script src="bowser.js"></script>
<!-- Finite State Machine -->
<script src="machina.js"></script>
<!-- Gotham Font -->
<link rel="stylesheet" href="//media.wnyc.org/static/gotham/176205/1D6F0D9D0E2D9044E.css" type="text/css">
<!-- Icon font -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
<style>
body {
background-color: black;
color: white;
}
body, #trigger, #save, #cancel {
font-family: 'Gotham SSm A', 'Gotham SSm B', Helvetica, Arial, sans-serif;
}
h1, h2, h3 {
font-weight: normal;
font-family: 'Gotham A', 'Gotham B', Helvetica, Arial, sans-serif;
}
.header, .buttons {
text-align: center;
}
.buttons {
margin-top: 10px;
}
#picture {
display: block;
margin: 10px auto;
border: 1px solid #222;
}
#trigger,#save,#cancel {
border-radius: 30px;
border: none;
outline: none;
cursor: pointer;
background-color: #aaa;
color: black;
padding: 8px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
text-decoration: none;
-webkit-transition: background-color 250ms ease-out;
-o-transition: background-color 250ms ease-out;
transition: background-color 250ms ease-out;
text-shadow: 1px 1px 0 rgba(255,255,255,0.5);
text-transform: uppercase;
font-weight: 600;
}
#trigger:hover, #save:hover, #cancel:hover {
background-color: white;
text-decoration: none;
}
#trigger {
width: 200px;
}
#trigger:before, #save:before, #cancel:before {
font-family: FontAwesome;
display: block;
margin-bottom: 3px;
font-size: 18px;
font-weight: initial;
}
#trigger:before {
font-size: 25px;
}
#cancel, #save {
width: 40px;
height: 40px;
font-size: 18px;
line-height: 24px; /* 40 - 8 - 8 */
vertical-align: 10px;
}
.notifier {
position: absolute;
display: none;
top: 0;
padding: 10px;
background-color: black;
}
.links {
text-align: center;
margin-top: 5px;
}
.links a {
color: white;
}
</style>
</head>
<body>
<div class="notifier" id="notifier">
Activate your webcam!
</div>
<!-- <div class="header">
<img src="wqxr.svg" alt="WQXR" height="48">
</div>
-->
<canvas id="picture" width="480" height="600"></canvas>
<div class="buttons">
<a id="save" style="display:none;" class="fa fa-save"></a>
<a id="trigger" class="fa fa-camera">Snap my Bach</a>
<a id="cancel" style="display:none;" class="fa fa-refresh"></a>
</div>
<div class="links">
<a href="http://www.wqxr.org/">Visit WQXR.org</a> |
<a href="http://www.wqxr.org/mobile">Get the mobile app</a>
</div>
<script src="jsbin.yatigi.js"></script>
<!-- Google Analytics -->
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-283599-23']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ga, s);
})();
</script>
<!-- End Google Analytics -->
</body>
</html>
var video = document.createElement('video');
var canvas = document.getElementById('picture');
var ctx = canvas.getContext('2d');
// polyfill from https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement.toBlob
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function (callback, type, quality) {
var binStr = atob( this.toDataURL(type, quality).split(',')[1] ),
len = binStr.length,
arr = new Uint8Array(len);
for (var i=0; i<len; i++ ) {
arr[i] = binStr.charCodeAt(i);
}
callback( new Blob( [arr], {type: type || 'image/png'} ) );
}
});
}
var bachURL = './bach.png';
var bach = new Image();
bach.src = bachURL;
var bach_ready = false;
bach.onload = function() {
bach_ready = true;
};
var videoOpts = { 'video' : true };
ctx.textAlign = 'center';
ctx.font = '14px "Gotham SSm A", "Gotham SSm B", Helvetica, Arial, sans-serif';
var trigger = document.getElementById('trigger');
var cancel = document.getElementById('cancel');
var save = document.getElementById('save');
var notifier = document.getElementById('notifier');
var snapURL, snap = new Image();
var startTime = null;
var currentTime = null;
var flash = null;
// this function takes care of text on the display
var writeMessage;
function buttonDisplay(triggerDisplay, cancelDisplay, saveDisplay) {
trigger.style.display = triggerDisplay ? 'inline-block' : 'none';
cancel.style.display = cancelDisplay ? 'inline-block' : 'none';
save.style.display = saveDisplay ? 'inline-block' : 'none';
}
// we need an FSM here
var fsm = new machina.Fsm({
initialize : function() {
},
// _save is a utility function that uploads the file...
_save : function() {
var fd = new FormData();
var self = this;
canvas.toBlob(function(blob) {
fd.append("1279_3067-answer", blob, self.filename);
var request = new XMLHttpRequest();
request.open(
"POST",
"http://www.wqxr.org/crowdsourcing/bach-face/api/submit_survey_questions_json/"
);
request.send(fd);
}, 'image/png');
this.outURL = 'https://media2.wnyc.org/i/raw/1/'+this.filename;
},
initialState : 'initializing',
states : {
'initializing' : {
_onEnter : function() {
var self = this;
var errorCallback = function(error) {
self.transition('denied_access')
};
buttonDisplay();
writeMessage = function() {
ctx.fillText('Please allow us to', 270, 232);
ctx.fillText('access your camera', 270, 250);
}
if(bowser.mobile || bowser.tablet) {
return this.transition('mobile');
}
notifier.style.display = 'block';
if(bowser.chrome) {
notifier.style.right = '50px';
notifier.innerHTML = 'Activate your camera! <i class="fa fa-arrow-up"></i>';
} else if(bowser.firefox) {
notifier.style.left = '40px';
notifier.innerHTML = '<i class="fa fa-arrow-up"></i> Activate your camera!';
}
// kick off the video, if we can...
if(navigator.getUserMedia) {
navigator.getUserMedia(videoOpts, function(stream) {
self.transition('viewing');
video.src = stream;
video.play();
}, errorCallback);
} else if(navigator.webkitGetUserMedia) {
navigator.webkitGetUserMedia(videoOpts, function(stream) {
self.transition('viewing');
video.src = window.webkitURL.createObjectURL(stream);
video.play();
}, errorCallback);
} else if(navigator.mozGetUserMedia) {
navigator.mozGetUserMedia(videoOpts, function(stream){
self.transition('viewing');
video.src = window.URL.createObjectURL(stream);
video.play();
}, errorCallback);
} else {
self.transition('unsupported');
}
}
},
'unsupported' : {
_onEnter : function() {
notifier.style.display = 'none'
buttonDisplay();
writeMessage = function() {
ctx.fillText('Your browser won\'t let', 270, 250-18*2);
ctx.fillText('you be Bach today.', 270, 250-18);
ctx.fillText('(Please try again in', 270, 250+18);
ctx.fillText('Chrome or Firefox.)', 270, 250+18*2);
}
}
},
'mobile' : {
_onEnter : function() {
notifier.style.display = 'none';
buttonDisplay();
writeMessage = function() {
ctx.fillText('Unfortunately, you', 270, 250-18*3);
ctx.fillText('cannot be Bach', 270, 250-18*2);
ctx.fillText('on a mobile device.', 270, 250-18);
ctx.fillText('(Please try again in', 270, 250+18);
ctx.fillText('Chrome or Firefox', 270, 250+18*2);
ctx.fillText('on your computer.)', 270, 250+18*3)
}
}
},
'denied_access' : {
_onEnter : function() {
buttonDisplay();
notifier.style.display = 'block';
if(bowser.chrome) {
notifier.innerHTML = '<img src="chrome_icon.png" style="margin:5px 0 -5px;"/> Please reset your webcam settings.';
notifier.style.right = '30px';
} else if(bowser.firefox) {
notifier.style.left = '0px';
notifier.innerHTML = 'You\'ve disabled your webcam for this page.';
}
writeMessage = function() {
ctx.fillText('You have decided not', 270, 250-18*3);
ctx.fillText('to be Bach today.', 270, 250-18*2);
ctx.fillText('You\'ll need to allow', 270, 250+18*0);
ctx.fillText('access to your webcam', 270, 250+18*1);
ctx.fillText('for us to bring you Bach.', 270, 250+18*2);
}
}
},
'viewing' : {
_onEnter : function() {
notifier.style.display = 'none';
buttonDisplay(true);
trigger.classList.add('fa-camera', 'fa-refresh');
trigger.classList.remove('fa-save');
trigger.innerHTML = "Snap my Bach";
writeMessage = function() {
ctx.fillText('Starting the camera...', 270, 250);
}
},
trigger : function () {
this.transition('snapped');
snapURL = canvas.toDataURL('image/png');
snap.src = snapURL;
flash = currentTime;
}
},
'snapped' : {
_onEnter : function() {
notifier.style.display = 'none';
buttonDisplay(true, true, true);
trigger.classList.remove('fa-camera', 'fa-refresh');
trigger.classList.add('fa-save');
trigger.innerHTML = "Save my Bach";
save.style.visibility = 'hidden';
cancel.style.visibility = 'visible';
var url = this.savedUrl = canvas.toDataURL('image/png');
this.filename = CryptoJS.MD5(url).toString() + String(+new Date()) + '.png';
},
cancel : function() {
this.transition('viewing');
},
// this tweets the image
// trigger : function() {
// this._save();
// var url = this.outURL;
// window.open(
// 'https://twitter.com/share?via=WQXR&hashtags=illbebach&url='+url,
// 'twitter',
// 'height=400,width=600,left=10,top=10,menubar=no,location=no,status=no'
// );
// },
// this offers the image for download
trigger : function(evt) {
trigger.href = this.savedUrl;
trigger.download = 'illbebach.png';
this.transition('saved');
}
},
'saved' : {
_onEnter : function() {
notifier.style.display = 'none';
buttonDisplay(true, true, true);
trigger.classList.remove('fa-camera', 'fa-save');
trigger.classList.add('fa-refresh');
trigger.innerHTML = "Another Bach?";
save.style.visibility = 'visible';
cancel.style.visibility = 'hidden';
this.filename = CryptoJS.MD5(this.savedUrl).toString() + String(+new Date()) + '.png';
},
save : function() {
save.href = this.savedUrl;
save.download = 'illbebach.png';
this.transition('saved');
},
trigger : function() {
// weirdly, these have to be here, in the event handler...
trigger.removeAttribute('href');
trigger.removeAttribute('download');
this.transition('viewing');
}
}
}
});
trigger.addEventListener('click', function() {
fsm.handle('trigger');
});
cancel.addEventListener('click', function() {
fsm.handle('cancel');
});
save.addEventListener('click', function() {
fsm.handle('save');
});
var flashTime = 250; // ms
function step(timestamp) {
currentTime = timestamp;
if(startTime === null) { startTime = timestamp; }
if(['snapped', 'saved'].indexOf(fsm.state) > -1) {
ctx.drawImage(snap, 0, 0, canvas.width, canvas.height);
if((timestamp - flash) < (flashTime * 2)) {
ctx.fillStyle = 'rgba(255,255,255,'+Math.min(1, 1 + (1 - (timestamp - flash)/flashTime))+')';
ctx.fillRect(0,0,canvas.width, canvas.height);
}
} else {
ctx.fillStyle = 'black';
ctx.fillRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = 'white';
writeMessage();
ctx.save();
ctx.translate(canvas.width, 0);
ctx.scale(-1, 1);
ctx.drawImage(video, 0, 50, canvas.width, canvas.width * video.videoHeight / video.videoWidth );
ctx.restore();
if(bach_ready) {
ctx.drawImage(bach, 0, 0, canvas.width, canvas.width * bach.height / bach.width);
}
}
window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);
/**
* machina - A library for creating powerful and flexible finite state machines. Loosely inspired by Erlang/OTP's gen_fsm behavior.
* Author: Jim Cowart (http://freshbrewedcode.com/jimcowart)
* Version: v0.4.0-1
* Url: http://machina-js.org/
* License(s): MIT, GPL
*/
(function (root, factory) {
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["lodash"], function (_) {
return factory(_, root);
});
} else if (typeof module === "object" && module.exports) {
// Node, or CommonJS-Like environments
module.exports = factory(require("lodash"));
} else {
// Browser globals
root.machina = factory(root._, root);
}
}(this, function (_, global, undefined) {
var slice = [].slice;
var NEXT_TRANSITION = "transition";
var NEXT_HANDLER = "handler";
var HANDLING = "handling";
var HANDLED = "handled";
var NO_HANDLER = "nohandler";
var TRANSITION = "transition";
var INVALID_STATE = "invalidstate";
var DEFERRED = "deferred";
var NEW_FSM = "newfsm";
var utils = {
makeFsmNamespace: (function () {
var machinaCount = 0;
return function () {
return "fsm." + machinaCount++;
};
})(),
getDefaultOptions: function () {
return {
initialState: "uninitialized",
eventListeners: {
"*": []
},
states: {},
eventQueue: [],
namespace: utils.makeFsmNamespace(),
targetReplayState: "",
state: undefined,
priorState: undefined,
_priorAction: "",
_currentAction: ""
};
}
};
if (!_.deepExtend) {
var behavior = {
"*": function (obj, sourcePropKey, sourcePropVal) {
obj[sourcePropKey] = sourcePropVal;
},
"object": function (obj, sourcePropKey, sourcePropVal) {
obj[sourcePropKey] = deepExtend({}, obj[sourcePropKey] || {}, sourcePropVal);
},
"array": function (obj, sourcePropKey, sourcePropVal) {
obj[sourcePropKey] = [];
_.each(sourcePropVal, function (item, idx) {
behavior[getHandlerName(item)](obj[sourcePropKey], idx, item);
}, this);
}
},
getActualType = function (val) {
if (_.isArray(val)) {
return "array";
}
if (_.isDate(val)) {
return "date";
}
if (_.isRegExp(val)) {
return "regex";
}
return typeof val;
},
getHandlerName = function (val) {
var propType = getActualType(val);
return behavior[propType] ? propType : "*";
},
deepExtend = function (obj) {
_.each(slice.call(arguments, 1), function (source) {
_.each(source, function (sourcePropVal, sourcePropKey) {
behavior[getHandlerName(sourcePropVal)](obj, sourcePropKey, sourcePropVal);
});
});
return obj;
};
_.mixin({
deepExtend: deepExtend
});
}
var Fsm = function (options) {
_.extend(this, options);
_.defaults(this, utils.getDefaultOptions());
this.initialize.apply(this, arguments);
machina.emit(NEW_FSM, this);
if (this.initialState) {
this.transition(this.initialState);
}
};
_.extend(Fsm.prototype, {
initialize: function () {},
emit: function (eventName) {
var args = arguments;
if (this.eventListeners["*"]) {
_.each(this.eventListeners["*"], function (callback) {
try {
callback.apply(this, slice.call(args, 0));
} catch (exception) {
if (console && typeof console.log !== "undefined") {
console.log(exception.toString());
}
}
}, this);
}
if (this.eventListeners[eventName]) {
_.each(this.eventListeners[eventName], function (callback) {
try {
callback.apply(this, slice.call(args, 1));
} catch (exception) {
if (console && typeof console.log !== "undefined") {
console.log(exception.toString());
}
}
}, this);
}
},
handle: function (inputType) {
if (!this.inExitHandler) {
var states = this.states,
current = this.state,
args = slice.call(arguments, 0),
handlerName, handler, catchAll, action;
this.currentActionArgs = args;
if (states[current][inputType] || states[current]["*"] || this["*"]) {
handlerName = states[current][inputType] ? inputType : "*";
catchAll = handlerName === "*";
if (states[current][handlerName]) {
handler = states[current][handlerName];
action = current + "." + handlerName;
} else {
handler = this["*"];
action = "*";
}
if (!this._currentAction) this._currentAction = action;
this.emit.call(this, HANDLING, {
inputType: inputType,
args: args.slice(1)
});
if (_.isFunction(handler)) handler = handler.apply(this, catchAll ? args : args.slice(1));
if (_.isString(handler)) this.transition(handler);
this.emit.call(this, HANDLED, {
inputType: inputType,
args: args.slice(1)
});
this._priorAction = this._currentAction;
this._currentAction = "";
this.processQueue(NEXT_HANDLER);
} else {
this.emit.call(this, NO_HANDLER, {
inputType: inputType,
args: args.slice(1)
});
}
this.currentActionArgs = undefined;
}
},
transition: function (newState) {
if (!this.inExitHandler && newState !== this.state) {
var curState = this.state;
if (this.states[newState]) {
if (curState && this.states[curState] && this.states[curState]._onExit) {
this.inExitHandler = true;
this.states[curState]._onExit.call(this);
this.inExitHandler = false;
}
this.targetReplayState = newState;
this.priorState = curState;
this.state = newState;
this.emit.call(this, TRANSITION, {
fromState: this.priorState,
action: this._currentAction,
toState: newState
});
if (this.states[newState]._onEnter) {
this.states[newState]._onEnter.call(this);
}
if (this.targetReplayState === newState) {
this.processQueue(NEXT_TRANSITION);
}
return;
}
this.emit.call(this, INVALID_STATE, {
state: this.state,
attemptedState: newState
});
}
},
processQueue: function (type) {
var filterFn = type === NEXT_TRANSITION ?
function (item) {
return item.type === NEXT_TRANSITION && ((!item.untilState) || (item.untilState === this.state));
} : function (item) {
return item.type === NEXT_HANDLER;
};
var toProcess = _.filter(this.eventQueue, filterFn, this);
this.eventQueue = _.difference(this.eventQueue, toProcess);
_.each(toProcess, function (item) {
this.handle.apply(this, item.args);
}, this);
},
clearQueue: function (type, name) {
if (!type) {
this.eventQueue = [];
} else {
var filter;
if (type === NEXT_TRANSITION) {
filter = function (evnt) {
return (evnt.type === NEXT_TRANSITION && (name ? evnt.untilState === name : true));
};
} else if (type === NEXT_HANDLER) {
filter = function (evnt) {
return evnt.type === NEXT_HANDLER;
};
}
this.eventQueue = _.filter(this.eventQueue, filter);
}
},
deferUntilTransition: function (stateName) {
if (this.currentActionArgs) {
var queued = {
type: NEXT_TRANSITION,
untilState: stateName,
args: this.currentActionArgs
};
this.eventQueue.push(queued);
this.emit.call(this, DEFERRED, {
state: this.state,
queuedArgs: queued
});
}
},
deferUntilNextHandler: function () {
if (this.currentActionArgs) {
var queued = {
type: NEXT_HANDLER,
args: this.currentActionArgs
};
this.eventQueue.push(queued);
this.emit.call(this, DEFERRED, {
state: this.state,
queuedArgs: queued
});
}
},
on: function (eventName, callback) {
var self = this;
if (!self.eventListeners[eventName]) {
self.eventListeners[eventName] = [];
}
self.eventListeners[eventName].push(callback);
return {
eventName: eventName,
callback: callback,
off: function () {
self.off(eventName, callback);
}
};
},
off: function (eventName, callback) {
if (!eventName) {
this.eventListeners = {};
} else {
if (this.eventListeners[eventName]) {
if (callback) {
this.eventListeners[eventName] = _.without(this.eventListeners[eventName], callback);
} else {
this.eventListeners[eventName] = [];
}
}
}
}
});
Fsm.prototype.trigger = Fsm.prototype.emit;
// _machKeys are members we want to track across the prototype chain of an extended FSM constructor
// Since we want to eventually merge the aggregate of those values onto the instance so that FSMs
// that share the same extended prototype won't share state *on* those prototypes.
var _machKeys = ["states", "initialState"];
var inherits = function (parent, protoProps, staticProps) {
var fsm; // placeholder for instance constructor
var machObj = {}; // object used to hold initialState & states from prototype for instance-level merging
var ctor = function () {}; // placeholder ctor function used to insert level in prototype chain
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor.
if (protoProps && protoProps.hasOwnProperty('constructor')) {
fsm = protoProps.constructor;
} else {
// The default machina constructor (when using inheritance) creates a
// deep copy of the states/initialState values from the prototype and
// extends them over the instance so that they'll be instance-level.
// If an options arg (args[0]) is passed in, a states or intialState
// value will be preferred over any data pulled up from the prototype.
fsm = function () {
var args = slice.call(arguments, 0);
args[0] = args[0] || {};
var blendedState;
var instanceStates = args[0].states || {};
blendedState = _.deepExtend(_.cloneDeep(machObj), {
states: instanceStates
});
blendedState.initialState = args[0].initialState || this.initialState;
_.extend(args[0], blendedState);
parent.apply(this, args);
};
}
// Inherit class (static) properties from parent.
_.deepExtend(fsm, parent);
// Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function.
ctor.prototype = parent.prototype;
fsm.prototype = new ctor();
// Add prototype properties (instance properties) to the subclass,
// if supplied.
if (protoProps) {
_.extend(fsm.prototype, protoProps);
_.deepExtend(machObj, _.transform(protoProps, function (accum, val, key) {
if (_machKeys.indexOf(key) !== -1) {
accum[key] = val;
}
}));
}
// Add static properties to the constructor function, if supplied.
if (staticProps) {
_.deepExtend(fsm, staticProps);
}
// Correctly set child's `prototype.constructor`.
fsm.prototype.constructor = fsm;
// Set a convenience property in case the parent's prototype is needed later.
fsm.__super__ = parent.prototype;
return fsm;
};
// The self-propagating extend function that Backbone classes use.
Fsm.extend = function (protoProps, classProps) {
var fsm = inherits(this, protoProps, classProps);
fsm.extend = this.extend;
return fsm;
};
var machina = {
Fsm: Fsm,
utils: utils,
on: function (eventName, callback) {
if (!this.eventListeners[eventName]) {
this.eventListeners[eventName] = [];
}
this.eventListeners[eventName].push(callback);
return callback;
},
off: function (eventName, callback) {
if (this.eventListeners[eventName]) {
this.eventListeners[eventName] = _.without(this.eventListeners[eventName], callback);
}
},
trigger: function (eventName) {
var i = 0,
len, args = arguments,
listeners = this.eventListeners[eventName] || [];
if (listeners && listeners.length) {
_.each(listeners, function (callback) {
callback.apply(null, slice.call(args, 1));
});
}
},
eventListeners: {
newFsm: []
}
};
machina.emit = machina.trigger;
return machina;
}));
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment