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
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="300px" height="72px" viewBox="0 0 300 72" enable-background="new 0 0 300 72" xml:space="preserve">
<g>
<path fill="#00ADEE" d="M134.818,32.69v-0.115c0-11.499-9.153-20.707-21.51-20.707S91.686,21.191,91.686,32.69v0.115
c0,11.497,9.153,20.708,21.509,20.708c0.569,0,1.124-0.04,1.679-0.079c1.136,2.171,2.708,4.211,4.743,5.968
c2.518,2.181,5.749,3.835,9.5,4.722l6.149-9.121c-4.934,0.403-8.065-2.38-9.834-5.025C131.183,46.236,134.818,39.891,134.818,32.69
M123.491,32.805c0,5.776-4.062,10.697-10.183,10.697c-6.063,0-10.239-5.034-10.239-10.812v-0.115
c0-5.777,4.061-10.696,10.125-10.696c6.121,0,10.297,5.033,10.297,10.811V32.805z"/>
<polyline fill="#0066B2" points="34.35,53.185 43.973,53.185 51.945,30.236 59.973,53.185 69.597,53.185 82.921,13.041
71.476,13.041 64.528,36.103 56.842,12.926 47.333,12.926 39.645,36.103 32.698,13.041 21.025,13.041 34.35,53.185 "/>
<polyline fill="#0066B2" points="146.4,52.9 158.812,52.9 166.5,40.886 174.131,52.9 186.83,52.9 173.162,32.516 186.26,13.041
173.846,13.041 166.729,24.257 159.668,13.041 146.97,13.041 160.066,32.628 146.4,52.9 "/>
<path fill="#0066B2" d="M235.377,26.764v-0.114c0-3.928-1.193-7.004-3.529-9.282c-2.676-2.733-6.891-4.326-12.982-4.326h-18.848
V52.9h11.047V40.828h4.84l8.027,12.072h12.758l-9.512-13.895C232.133,36.899,235.377,32.856,235.377,26.764 M224.275,27.446
c0,2.905-2.164,4.728-5.922,4.728h-7.289v-9.567h7.232c3.699,0,5.979,1.595,5.979,4.727V27.446z"/>
<rect x="272.969" y="7.877" fill="#DCDCDC" width="6.006" height="51.76"/>
<path fill="#DCDCDC" d="M256.613,30.98c1.914,0,3.465-1.55,3.465-3.464c0-1.913-1.551-3.463-3.465-3.463
c-1.912,0-3.463,1.551-3.463,3.463C253.15,29.43,254.701,30.98,256.613,30.98"/>
<path fill="#DCDCDC" d="M256.613,43.462c1.914,0,3.465-1.551,3.465-3.465c0-1.911-1.551-3.463-3.465-3.463
c-1.912,0-3.463,1.552-3.463,3.463C253.15,41.911,254.701,43.462,256.613,43.462"/>
<rect x="266.107" y="7.877" fill="#DCDCDC" width="2.174" height="51.76"/>
</g>
</svg>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment