Skip to content

Instantly share code, notes, and snippets.

@maelp
Last active January 2, 2016 18:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save maelp/8344014 to your computer and use it in GitHub Desktop.
Save maelp/8344014 to your computer and use it in GitHub Desktop.
Fast interactive prototypes with Sketch and d3.js
<!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>
/**
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;
} ));
Display the source blob
Display the rendered blob
Raw
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