ifsm
Version:
a jQuery State Machine (FSM / HSM) to design and manage javascript web user interfaces, simulators, games...
626 lines (559 loc) • 17.9 kB
HTML
<html>
<head>
<title>iFSM in action! a Finite State Machine for jQuery</title>
<script type="text/javascript" src="../extlib/jquery-3.2.0.min.js"></script>
<script type="text/javascript" src="../extlib/jquery.dorequesttimeout.js"></script>
<script type="text/javascript" src="../extlib/jquery.attrchange.js"></script>
<script type="text/javascript" src="../extlib/jquery.mousewheel.js"></script>
<script type="text/javascript" src="../extlib/jquery.touchSwipe.js"></script>
<script type="text/javascript" src="../iFSM.js"></script>
<style type="text/css">
html {
font-family: Helvetica, Arial, sans-serif;
}
body {
padding: 0 10px;
}
pre {
font-size: 12px;
background-color: black;
color: green;
}
#cursor,#cursor2,#cursor3 {
position: absolute;
width:30px;
height:50px;
background-color: #AAA;
margin:10px 0px;
}
#cursor.dragged,#cursor2.dragged,#cursor3.dragged {
background-color: #CCC;
}
#cursorContainer,#cursorContainer2,#cursorContainer3{
position: relative;
width:100%;
height:70px;
background-color: #404040;
margin: 10px 0px;
}
#cursorContainer.dragged,#cursorContainer2.dragged,#cursorContainer3.dragged {
background-color: #505050;
}
#status,#status2,#status3 {
margin: 10px;
}
</style>
<script type="text/javascript" id="iFSMscript">
$(document).ready(function() {
$('#cursorContainer').iFSM(aSliderAnimation, {
sticky:false,cursor:$('#cursor')
, alertObject:$( "#status" )
, debugAnimation:false
, debugInfo:$( "#status" )
});
$('#cursor').iFSM(aCursorAnimation);
$('#cursorContainer2').iFSM(aSliderAnimation, {
sticky:false,cursor:$('#cursor2')
, steps:10
, alertObject:$( "#status2" )
, debugAnimation:false
, debugInfo:$( "#status2" )
});
$('#cursor2').iFSM(aCursorAnimation);
$('#cursorContainer3').iFSM(aSliderAnimation, {
sticky:true
, cursor:$('#cursor3')
, steps:51
, alertObject:$( "#status3" )
, debugAnimation:false
, debugInfo:$( "#status3" )
});
$('#cursor3').iFSM(aCursorAnimation);
var debug=false;
$( "#status,#status2,#status3" ).on(
{
"slideCursorMoveStart": function(e,posX) {
if (debug)
$(this).html($(this).html()+ 'slideCursorMoveStart!'+ "<br>-------------<br>");
else
$(this).html('slideCursorMoveStart!'+ "<br>-------------<br>");
if (debug) this.logConsole( 'slideCursorMoveStart!' );
},
"slideCursorMoveStop": function(e,posX) {
$(this).html( $(this).html()+'slideCursorMoveStop!' +"<br>-------------<br>");
if (debug) this.logConsole( 'slideCursorMoveStop!' );
},
"slideCursorMove": function(e,posX) {
//$(this).html( $(this).html()+'slideCursorMove:'+posX +"<br>-------------<br>");
if (debug) this.logConsole( 'slideCursorMove:'+posX );
},
"slideCursorMoveToStep": function(e,dataStep) {
$(this).html( $(this).html()+ 'slideCursorMoveToStep:'+dataStep.posX + ' - ' + dataStep.step+' - '+dataStep.cursor+"<br>-------------<br>");
if (debug) this.logConsole( 'slideCursorMoveToStep:'+dataStep.posX + ' - ' + dataStep.step );
}
});
});
var docMachine= {
DefaultState : {
mouseup:{
init_function:function(){
this.opts.slider.trigger('mouseup');
},
},
mousemove:
{
init_function:function(p,e){
this.opts.slider.trigger('mousemove',e);
},
},
},
};
var windowMachine= {
DefaultState : {
resize:
{
init_function:function(p,e){
this.opts.slider.trigger('resize',e);
},
},
},
};
/**
* States to handle the click on buttons
*/
var aSliderAnimation = {
waitToDragCursor: //in order to let the animation ends after its 500ms and to not cumulate multiple clicks
{
enterState:
{
init_function: function() {
//init basic values of the slider
this.opts['sliderWidth'] = this.myUIObject.outerWidth();
this.opts['sliderMaxCursorPosX'] = this.myUIObject.outerWidth()-this.opts.cursor.outerWidth();
this.opts['sliderHeight'] = this.myUIObject.outerHeight();
this.opts['sliderLeft'] = this.myUIObject.offset().left;
this.opts['sliderTop'] = this.myUIObject.offset().top;
if (!this.opts.mouseWheelFactor)
{
if (this.opts.steps)
this.opts.myMouseWheelFactor = this.opts['sliderMaxCursorPosX'] / (this.opts.steps);
else
this.opts.myMouseWheelFactor = this.opts.cursor.width()*2;
}
}
},
resize:
{
init_function: function(p,e) {
//alert('coucou');
},
how_process_event:{delay:200},
propagate_event:['enterState','reinitCursor'],
},
reinitCursor:
{
next_state:'moveCursorToFinal',
},
mousedown:
{
init_function: function(p,e,eExt) {
this.logConsole('waitToDragCursor:mousedown',true);
if (eExt) e=eExt;
this.opts.eventPosition = {};
this.opts.eventPosition['positionX'] = this.computeCursorPositionXFromDocToSlider(e.pageX);
//this.opts['eventPosition']['positionY'] = this.computeCursorPositionYFromDocToSlider(e.pageY);;
this.logConsole( 'waitToDragCursor:mousedown:pageX:'+e.pageX );
},
next_state: 'initDragCursor',
},
mousewheel:
{
init_function: function(p,e) {
var delta = e.deltaY;
delta *= this.opts.myMouseWheelFactor;
e.pageX=this.opts.cursor.offset().left+delta;
this.trigger('mousedown',e);
this.trigger('mousemove',e);
this.logConsole( 'waitToDragCursor:mousewheel:pageX:'+e.pageX );
},
propagate_event:'mouseWheelUp',
},
swipestart:
{
init_function:function(p,e,data){
this.logConsole( 'waitToDragCursor:swipestart:start');
if (!data || !data.fingerData || !data.fingerData[0].end) return;
e={pageX:data.fingerData[0].end.x,pageY:data.fingerData[0].end.y};
this.trigger('mousedown',e);
}
}
},
initDragCursor:
{
enterState:
{
init_function: function() {
},
},
mousemove:
{
init_function: function(p,e,eExt) {
if (eExt && eExt.pageX) e=eExt;//comes from fsm...
this.myUIObject.addClass('dragged');
//position the cursor where we have clicked
this.opts.cursor.trigger('setLeftX',this.computeCursorPositionXFromSliderToDoc(this.opts.eventPosition.positionX));
this.opts.cursor.trigger('startDrag');
this.opts.targetPosition = 100 * this.opts.eventPosition.positionX/this.opts['sliderMaxCursorPosX'];
this.opts.alertObject.trigger('slideCursorMoveStart');
this.logConsole( 'initDragCursor:mousemove:pageX:'+e.pageX );
},
propagate_event: true,
next_state: 'dragCursor',
},
//~clic
mouseup:
{
init_function: function(p,e,eExt) {
if (eExt && eExt.pageX) e=eExt;//comes from fsm...
if ( (e.pageX - this.opts.cursor.offset().left) > 0)
//cursor on the right
e.pageX = this.opts.cursor.offset().left + this.opts.myMouseWheelFactor;
else
//cursor on the left
e.pageX = this.opts.cursor.offset().left - this.opts.myMouseWheelFactor;
this.trigger('mousemove',e);
this.opts.alertObject.trigger('slideCursorMoveStart');
this.logConsole( 'initDragCursor:mouseup:pageX:'+e.pageX );
},
next_state: 'dragCursor',
propagate_event:'mouseWheelUp',
},
swipemove:
{
init_function:function(p,e,data){
if (!data || !data.fingerData || !data.fingerData[0].end) return;
e={pageX:data.fingerData[0].end.x,pageY:data.fingerData[0].end.y};
this.trigger('mousemove',e);
this.logConsole( 'initDragCursor:swipemove:pageX:'+e.pageX );
}
},
swipecancel:'swipeend',
swipeend:
{
init_function:function(p,e,data){
if (!data || !data.fingerData || !data.fingerData[0].end) return;
e={pageX:data.fingerData[0].end.x,pageY:data.fingerData[0].end.y};
this.trigger('mouseup',e);
this.logConsole( 'initDragCursor:swipeend:pageX:'+e.pageX );
}
},
},
dragCursor:
{
mouseup:
{
init_function: function() {
this.myUIObject.removeClass('dragged');
this.opts.cursor.trigger('stopDrag');
this.logConsole( 'dragCursor:mouseup');
},
next_state: 'moveCursorToFinal',
},
mousemove:
{
init_function: function(p,e,eExt) {
if (eExt && eExt.pageX) e=eExt;//comes from fsm...
if (e.pageX) this.opts.nextCursorPosition = this.computeCursorPositionXFromDocToSlider(e.pageX);
else return; //curious that there is no pageX
if (!this.opts.sticky)
{
this.opts.cursor.trigger('setLeftX',this.computeCursorPositionXFromSliderToDoc(this.opts.nextCursorPosition));
}
this.logConsole( 'dragCursor:mousemove:pageX:'+e.pageX );
},
propagate_event:'setCursorPosition',
},
mouseWheelUp:
{
propagate_event:'mouseup',
how_process_event :{delay:20},
},
swipemove:
{
init_function:function(p,e,data){
if (!data || !data.fingerData || !data.fingerData[0].end) return;
e={pageX:data.fingerData[0].end.x,pageY:data.fingerData[0].end.y};
this.trigger('mousemove',e);
this.logConsole( 'dragCursor:swipemove:pageX:'+e.pageX );
}
},
swipecancel:'swipeend',
swipeend:
{
init_function:function(p,e,data){
if (!data || !data.fingerData || !data.fingerData[0].end) return;
e={pageX:data.fingerData[0].end.x,pageY:data.fingerData[0].end.y};
this.trigger('mouseup',e);
this.logConsole( 'dragCursor:swipeend:pageX:'+e.pageX );
}
},
setCursorPosition:
{
init_function: function() {
var targetPosition;
var stepFound = 0;
var cursorPosX=this.opts.nextCursorPosition;
var cursorPosXNormalized = 100 * cursorPosX/this.opts['sliderMaxCursorPosX'];
if (this.opts.steps)
{
var OneStep = 100 / (this.opts.steps - 1);
targetPosition = 0;
var found = false;
do{
if (
(cursorPosXNormalized >= (targetPosition - OneStep/2) )
&& (cursorPosXNormalized <= (targetPosition + OneStep/2))
)
found = true;
else
{
targetPosition += OneStep;
stepFound ++;
}
}
while(!found && targetPosition < 100);
if (targetPosition != this.opts.targetPosition)
{
this.opts.alertObject.trigger('slideCursorMoveToStep',{posX:targetPosition/100,step:stepFound,cursor:cursorPosXNormalized});
this.opts.targetPosition = targetPosition;
}
if (this.opts.sticky)
{
var offsetX = targetPosition * this.opts['sliderMaxCursorPosX'] / 100;
this.opts.cursor.trigger('setLeftX',this.computeCursorPositionXFromSliderToDoc(offsetX));
}
}
this.opts.alertObject.trigger('slideCursorMove',cursorPosXNormalized/100);
},
},
},
moveCursorToFinal:
{
enterState:
{
init_function: function() {
var targetPosition;
var stepFound=0;
var cursorPosX=this.computeCursorPositionXFromSliderToDoc(this.opts.cursor.offset().left-this.opts.sliderLeft+this.opts.cursor.outerWidth()/2);
var cursorPosXNormalized = 100 * cursorPosX/this.opts['sliderMaxCursorPosX'];
if (this.opts.steps)
{
var OneStep = 100 / (this.opts.steps - 1);
targetPosition = 0;
var found = false;
do{
if (
(cursorPosXNormalized >= (targetPosition - OneStep/2) )
&& (cursorPosXNormalized <= (targetPosition + OneStep/2))
)
found = true;
else
{
targetPosition += OneStep;
stepFound ++;
}
}
while(!found && targetPosition < 100);
var offsetX = targetPosition * this.opts['sliderMaxCursorPosX'] / 100;
this.opts.cursor.trigger('setLeftX',this.computeCursorPositionXFromSliderToDoc(offsetX));
this.opts.alertObject.trigger('slideCursorMoveToStep',{posX:targetPosition/100,step:stepFound,cursor:cursorPosXNormalized});
}
else
{
targetPosition = cursorPosXNormalized;
}
this.opts.alertObject.trigger('slideCursorMove',targetPosition/100);
this.opts.alertObject.trigger('slideCursorMoveStop');
},
next_state: 'waitToDragCursor',
}
},
DefaultState:
{
start:
{
init_function: function() {
//to manage when the events are outside the slider
$(document).iFSM(docMachine,{slider:this.myUIObject});
$(window).iFSM(windowMachine,{slider:this.myUIObject});
this.myUIObject.swipe({
//Generic swipe handler for all directions
swipeStatus:function(event, phase, direction, distance, duration, fingerCount, fingerData) {
$(this).trigger('doSwipe',
{ phase:phase
, direction:direction
, distance:distance
, duration:duration
, fingerCount:fingerCount
, fingerData:fingerData}
);
},
//Default is 75px, set to 0 for demo so any distance triggers swipe
threshold:0
});
var sliderOpts=this.opts;
this.computeCursorPositionXFromDocToSlider = function (aPosXinDocument)
{
var posX = aPosXinDocument-sliderOpts.sliderLeft-sliderOpts.cursor.outerWidth()/2;
posX = Math.max(Math.min(posX,sliderOpts.sliderMaxCursorPosX),0);
//limit the position of the cursor between 0 and slider width
return posX;
};
this.computeCursorPositionXFromSliderToDoc = function (aPosXinSlider)
{
var posX = aPosXinSlider;
return posX;
};
this.logConsole = function (aMsg,reset)
{
if (!reset) reset = false;
if (this.opts.debugAnimation)
{
console.log(aMsg);
if (this.opts.debugInfo)
{
if (reset)
sliderOpts.debugInfo.html("<br>"+aMsg+"<br>");
else
sliderOpts.debugInfo.html(sliderOpts.debugInfo.html()+"<br>"+aMsg+"<br>");
}
}
};
},
next_state: 'waitToDragCursor'
},
doSwipe:
{
init_function:function(p,e,data){
if (!data || !data.fingerData || !data.fingerData[0].end) return;
this.trigger('swipe'+data.phase,data);//trigger event swipestart, swipemove, swipeend, swipecancel
this.logConsole( 'DefaultState('+this.currentState+'):swipe:data.phase:'+data.phase );
}
}
}
};
/**
* States to handle the click on buttons
*/
var aCursorAnimation = {
waitToBeDragged: //in order to let the animation ends after its 500ms and to not cumulate multiple clicks
{
startDrag:'mousedown',
mousedown:
{
init_function: function(p,e) {
this.myUIObject.addClass('dragged');
},
next_state: 'dragged',
UI_event_bubble:true,
},
},
dragged: //in order to let the animation ends after its 500ms and to not cumulate multiple clicks
{
stopDrag:'mouseup',
mouseup:
{
init_function: function() {
this.myUIObject.removeClass('dragged');
},
next_state: 'waitToBeDragged',
UI_event_bubble:true,
},
},
DefaultState:
{
start:
{
init_function: function() {
this.opts['sliderWidth'] = this.myUIObject.outerWidth();
this.opts['sliderHeight'] = this.myUIObject.outerHeight();
},
next_state: 'waitToBeDragged'
},
setLeftX:
{
//posX considered at the middle of the object
init_function: function(p,e,posX) {
this.myUIObject.animate({
left: parseFloat(posX),
},0);
if (this.opts.debugAnimation) $('#status').html('posX:'+posX);
},
},
}
}
</script>
</head>
<body style="margin:100px;">
<h1>A (not so) simple slide scale driven with iFSM </h1>
<p>This example shows an horizontal slider made up of a cursor and a background where the cursor moves.<p>
<p>Steps may be defined where the cursor should be set when the mouse button is released. </p>
<p>Steps may be "sticky" meaning that the cursor will move step by step when the mouse moves...</p>
<p>Events that are sent to any listening objects are :</p>
<ul>
<li>"slideCursorMoveStart": cursor starts to move (mousedown)</li>
<li>"slideCursorMoveStop": cursor was released (mouseup)</li>
<li>"slideCursorMove": cursor moves along the slider</li>
<li>"slideCursorMoveToStep": cursor is on a step of the slider</li>
</ul>
<p>
the cursor reacts to:
<ul>
<li>the click of the mouse on the left or the right of the cursor</li>
<li>the mouse move on the cursor or the slider</li>
<li>the 'swipe' of your finger on mobile</li>
<li>the 'wheel' of the mouse</li>
</ul>
</p>
<h2>Slide free to move...</h2>
<div id="cursorContainer">
<div id="cursor"></div>
</div>
<div id="status"></div>
<h2>Slide with 10 steps (0 to 9) with no "sticky steps" when moving the mouse</h2>
<div id="cursorContainer2">
<div id="cursor2"></div>
</div>
<div id="status2"></div>
<h2>Slide with 50 steps with "sticky steps"</h2>
<div id="cursorContainer3">
<div id="cursor3"></div>
</div>
<div id="status3"></div>
<br>
<br>
<br>
<pre>
<script>
function escapeHtml(text) {
var map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, function(m) {
return map[m];
});
}
document.write(escapeHtml($('#iFSMscript').html()))
</script>
</pre>
<p>
provided by <a href="http://www.intersel.fr">Intersel</a>
</p>
</pre>
</body>
</html>