Fast prototyping of apps using Sketch and d3.js, please refer to the the explanatory post.
-
-
Save joshcarr/11057134 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
body { | |
width: 960px; | |
padding-top: 40px; | |
margin: auto; | |
position: relative; | |
} | |
svg { | |
width: 100%; | |
max-height: 400px; | |
} | |
</style> | |
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
<script src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script> | |
<script src="machina.js"></script> | |
<script type='text/javascript'> | |
(function () { | |
var svg, screenLayers; | |
function animateGrowingCircle(maxRadius, minRadius, maxLoop) { | |
var loopNumber = maxLoop ? maxLoop : 0; | |
var animateFunction = function () { | |
d3.select(this) | |
.attr('r', minRadius) | |
.transition() | |
.duration(1000) | |
.attr('r', maxRadius) | |
.transition() | |
.duration(1000) | |
.attr('r', minRadius) | |
.each('end', function () { | |
if(--loopNumber !== 0) { | |
d3.select(this).each(animateFunction); | |
} | |
}); | |
}; | |
return animateFunction; | |
} | |
function hideAllScreens() { | |
for (var key in screenLayers) { | |
if (screenLayers.hasOwnProperty(key)) { | |
screenLayers[key].style('display', 'none'); | |
} | |
} | |
} | |
function displayScreen(screenLayerName) { | |
screenLayers[screenLayerName].style('display', 'block'); | |
} | |
var fsm = new machina.Fsm({ | |
initialState: 'initState', | |
states: { | |
initState: { | |
domLoaded: function () { | |
d3.xml('SafeSignal.svg', 'image/svg+xml', function (error, data) { | |
d3.select('svg').remove(); | |
d3.select('body').node().appendChild(data.documentElement); | |
svg = d3.select('svg'); | |
svg.select('#iPhoneGloss') | |
.style('pointer-events', 'none'); | |
svg.select('#ConnectedCircle2').style('display', 'none'); | |
svg.select('#Annotations').style('display', 'none'); | |
screenLayers = { | |
'loading': svg.select('#LoadingScreen'), | |
'menu': svg.select('#MenuScreen'), | |
'main': svg.select('#MainScreen'), | |
'map': svg.select('#MapScreen') | |
}; | |
fsm.transition('loadingScreen'); | |
}); | |
} | |
}, | |
loadingScreen: { | |
_onEnter: function () { | |
hideAllScreens(); | |
var loadingScreen = screenLayers['loading']; | |
loadingScreen.style('display', 'block'); | |
var loadingIndicator = loadingScreen.select('#LoadingIndicator'); | |
var circles = loadingIndicator.selectAll('circle'); | |
circles | |
.datum(function () { | |
return {'x': +d3.select(this).attr('cx')}; | |
}) | |
.sort(function (a, b) { return a.x - b.x; }) | |
.each(function (d, i) { | |
var r = +d3.select(this).attr('r'); | |
d3.select(this) | |
.transition() | |
.delay(i*250) | |
.each(animateGrowingCircle(r, r/2)); | |
}); | |
setTimeout(function () { fsm.handle('finishedLoading'); }, 2000); | |
}, | |
'finishedLoading': function () { | |
var loadingScreen = screenLayers['loading']; | |
var loadingIndicator = loadingScreen.select('#LoadingIndicator'); | |
var circles = loadingIndicator.selectAll('circle'); | |
circles | |
.transition() | |
.attr('r', 10) | |
.style('opacity', 0); | |
var pos = d3.transform(loadingIndicator.attr('transform')).translate; | |
loadingIndicator | |
.transition() | |
.duration(1000) | |
.attr('transform', 'translate('+pos[0]+', '+(pos[1]+80)+')') | |
.style('opacity', 0) | |
.remove() | |
.each('end', function () { | |
fsm.transition('connectedScreen'); | |
}); | |
} | |
}, | |
connectedScreen: { | |
_onEnter: function () { | |
hideAllScreens(); | |
var mainScreen = screenLayers['main']; | |
var menuScreen = screenLayers['menu']; | |
var mapScreen = screenLayers['map']; | |
mainScreen.style('display', 'block'); | |
menuScreen.style('display', 'block'); | |
mapScreen.style('display', 'block'); | |
var annotations = svg.select('#Annotations') | |
.style('display', 'block') | |
.style('opacity', 0.0); | |
var annotationsTransition = annotations | |
.transition() | |
.duration(2000) | |
.style('opacity', 1.0); | |
annotationsTransition | |
.select('#ConnectedAnnotation') | |
.transition() | |
.delay(5000) | |
.duration(1000) | |
.style('opacity', 0.0); | |
var connectedCircle = mainScreen.select('#ConnectedCircle'); | |
var connectedCircle2 = mainScreen.select('#ConnectedCircle2'); | |
var r = +connectedCircle.attr('r'); | |
var r2 = +connectedCircle2.attr('r'); | |
connectedCircle | |
.each(animateGrowingCircle(r, r2, 0)); | |
var position = mapScreen.select('#Position'); | |
r = +position.attr('r'); | |
position.each(animateGrowingCircle(r, 2*r, 0)); | |
var menuElements = menuScreen.selectAll('#Icons,#SnipsLogo') | |
.style('opacity', 0.4); | |
var hideAnnotations = function () { | |
annotations.selectAll('#MenuAnnotation,#MapAnnotation,#ConnectedAnnotation') | |
.transition() | |
.duration(1000) | |
.style('opacity', 0.0) | |
.remove(); | |
}; | |
var menuShown = false; | |
var toggleMenu = function () { | |
menuShown = !menuShown; | |
if (menuShown) { | |
hideAnnotations(); | |
menuElements | |
.transition() | |
.duration(1000) | |
.style('opacity', 1.0); | |
mainScreen | |
.transition() | |
.attr('transform', 'translate(180, 0)'); | |
} | |
else { | |
menuElements | |
.transition() | |
.style('opacity', 0.4); | |
mainScreen | |
.transition() | |
.attr('transform', 'translate(0, 0)'); | |
} | |
}; | |
var mapShown = false; | |
var toggleMap = function () { | |
mapShown = !mapShown; | |
if (mapShown) { | |
hideAnnotations(); | |
mainScreen | |
.transition() | |
.duration(500) | |
.attr('transform', 'translate(-320, 0)'); | |
mapScreen | |
.transition() | |
.duration(500) | |
.attr('transform', 'translate(0, 0)'); | |
} | |
else { | |
mainScreen | |
.transition() | |
.duration(500) | |
.attr('transform', 'translate(0, 0)'); | |
mapScreen | |
.transition() | |
.duration(500) | |
.attr('transform', 'translate(320, 0)'); | |
} | |
}; | |
var menuToggle = mainScreen.select('#MenuToggle') | |
.style('pointer-events', 'all'); | |
menuToggle.on('click', toggleMenu); | |
menuScreen.on('click', toggleMenu); | |
var mapToggle = mainScreen.select('#MapToggle') | |
.style('pointer-events', 'all'); | |
mapToggle.on('click', toggleMap); | |
mapScreen.on('click', toggleMap); | |
var reloadBtn = annotations.select('#ReloadAnnotation') | |
.style('cursor', 'pointer') | |
.on('mouseenter', function () { | |
reloadBtn.select('circle').style('fill', '#F75C4C'); }) | |
.on('mouseleave', function () { | |
reloadBtn.select('circle').style('fill', '#D8D8D8'); }) | |
.on('click', function () { | |
fsm.transition('initState'); | |
fsm.handle('domLoaded'); | |
}); | |
} | |
} | |
} | |
}); | |
window.addEventListener('load', function () { | |
fsm.handle('domLoaded'); | |
}); | |
})(); | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
machina | |
Author: Jim Cowart (http://freshbrewedcode.com/jimcowart) | |
License: Dual licensed MIT (http://www.opensource.org/licenses/mit-license) & GPL (http://www.opensource.org/licenses/gpl-license) | |
Version 0.3.4 | |
*/ | |
(function ( root, factory ) { | |
if ( typeof module === "object" && module.exports ) { | |
// Node, or CommonJS-Like environments | |
module.exports = function ( _ ) { | |
_ = _ || require( 'underscore' ); | |
return factory( _ ); | |
}; | |
} else if ( typeof define === "function" && define.amd ) { | |
// AMD. Register as an anonymous module. | |
define( ["underscore"], function ( _ ) { | |
return factory( _, root ); | |
} ); | |
} 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 oldState; | |
if ( this.states[newState] ) { | |
this.targetReplayState = newState; | |
this.priorState = this.state; | |
this.state = newState; | |
oldState = this.priorState; | |
if ( this.states[oldState] && this.states[oldState]._onExit ) { | |
this.inExitHandler = true; | |
this.states[oldState]._onExit.call( this ); | |
this.inExitHandler = false; | |
} | |
this.emit.call( this, TRANSITION, { fromState: oldState, 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_TRANSITION, 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; | |
var ctor = function () {}; | |
var inherits = function ( parent, protoProps, staticProps ) { | |
var fsm; | |
// 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 { | |
fsm = function () { | |
parent.apply( this, arguments ); | |
}; | |
} | |
// 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 ) { | |
_.deepExtend( fsm.prototype, protoProps ); | |
} | |
// 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; | |
} )); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment