KaisarCode

Scripting 4 freeDOM



JavaScript Rotate Dial

by KaisarCode

In the last post we were talking about CSS rotation and the library that leads to the topic we'll discuss now.

So, if you don't remember this:

ball

Maybe you have to go back one post.

The Spin! (at last)

So, let's see the code... and then we'll talk.

JS
var kcRotateDial=function(elem){
	var output={};
	//Preventing elem to being selected on IE
	if(document.all && !window.opera) elem.setAttribute("unselectable","on");
	//Public Properties
	output.rad=0;
	output.deg=0;
	output.per=0;
	output.fullRad=0;
	output.fullDeg=0;
	output.fullPer=0;
	output.spin=0;
	output.clock=false;
	//Private properties
	var drag=false;
	var pos=[];
	var size=[];
	var axis=[];
	var cursor=[];
	var rad=0;
	var lastRad=0;
	var lastPer=0;
	var lastFullRad=0;
	var maxRad=6.283185307179586;
	var maxDeg=360;
	var maxPer=100;
	var Dx=[];
	var Dy=[];
	var dummy;
	//Public Methods
	output.onchange=function(){};
	//Private Methods
	function preventDefault(e){
		//prevent event's default action
		if(window.event) e=window.event;
		if(e.preventDefault){e.preventDefault()}else{e.returnValue=false};
	}
	function getPos(elem){
		//get the position [left,top] relative to whole document
		var tmp=elem;
		var left=tmp.offsetLeft;
		var top=tmp.offsetTop;
		while (tmp=tmp.offsetParent) left += tmp.offsetLeft;
		tmp=elem;
		while(tmp=tmp.offsetParent) top+=tmp.offsetTop;
		return [left,top];
	}
	function getSize(elem){
		//return the size [width,height] of the element
		return [elem.offsetWidth,elem.offsetHeight];
	}
	function getAxis(elem){
		//return the center point [left,top] of the element
		return [getPos(elem)[0]+getSize(elem)[0]/2,getPos(elem)[1]+getSize(elem)[1]/2];
	}
	function getCursorPos(e){
		//return the cursor's position [x,y]
		var cursorPos;
		if(window.event) e=window.event;
		if(e.clientX) cursorPos=[e.clientX,e.clientY];
		if(e.pageX) cursorPos=[e.pageX,e.pageY];
		try{if(e.targetTouches[0]) cursorPos=[e.targetTouches[0].pageX,e.targetTouches[0].pageY];}catch(err){};
		return cursorPos;
	}
	function getAngle(e){
		//getting rotation angle by Arc Tangent 2
		var rad;
		pos=getPos(elem);
		size=getSize(elem);
		axis=getAxis(elem);
		cursor=getCursorPos(e);
		try{rad=Math.atan2(cursor[1]-axis[1],cursor[0]-axis[0])}catch(err){};
		//correct the 90° of difference starting from the Y axis of the element
		rad+=maxRad/4;
		//transform opposite angle negative value, to possitive
		if(rad<0) rad+=maxRad;
		return rad;
	}
	function setDrag(e,bool){
		//set or unset the drag flag
		if(bool){
			preventDefault(e);
			rad=getAngle(e);
			drag=true;
		}else{
			drag=false;
		}
	}
	function rotate(e){
		//Rotate the element
		if(drag){
			//setting control variables
			var cursorRad;
			var relativeRad;
			var rotationRad;
			cursorRad=getAngle(e);
			relativeRad=cursorRad-rad;
			var rotationRad=lastRad+relativeRad;
			if(isNaN(rotationRad)) rotationRad=lastRad;
			if(rotationRad<0) rotationRad=maxRad;
			if(rotationRad>maxRad) rotationRad=0;
			
			rad=cursorRad;
			
			//applying rotation to element
			elem.style.transform="rotate("+rotationRad+"rad)";
			elem.style.MozTransform="rotate("+rotationRad+"rad)";
			elem.style.WebkitTransform="rotate("+rotationRad+"rad)";
			elem.style.OTransform="rotate("+rotationRad+"rad)";
			elem.style.MsTransform="rotate("+rotationRad+"rad)";
			
			//rotation Matrix for IExplorer
			var iecos = Math.cos(cursorRad);
			var iesin = Math.sin(cursorRad);
			Dx[0]=-(size[0]/2)*iecos + (size[1]/2)*iesin + (size[0]/2);
			Dx[1]=-(size[0]/2)*iesin - (size[1]/2)*iecos + (size[1]/2);
			elem.style.filter  ="progid:DXImageTransform.Microsoft.Matrix(M11="+iecos+", M12="+-iesin+", M21="+iesin+", M22="+iecos+", Dx="+Dx[0]+", Dy="+Dx[1]+", SizingMethod=auto expand)";
			elem.style.msFilter="progid:DXImageTransform.Microsoft.Matrix(M11="+iecos+", M12="+-iesin+", M21="+iesin+", M22="+iecos+", Dx="+Dx[0]+", Dy="+Dx[1]+", SizingMethod=auto expand)";

			//assigning values to public properties
			output.rad=rotationRad;
			output.deg=maxDeg*output.rad/(2*Math.PI);
			output.per=(output.rad*maxPer)/maxRad;
			
			if((lastPer<=100 && lastPer>=60) && (output.per>=0 && output.per<=30)) output.spin++;
			if((lastPer<=30 && lastPer>=0) && (output.per>=60 && output.per<=100)) output.spin--;
			
			output.fullRad=output.rad+(maxRad*output.spin);
			output.fullDeg=output.deg+(maxDeg*output.spin);
			output.fullPer=output.per+(maxPer*output.spin);
			
			if(lastFullRadoutput.fullRad) output.clock=false;
			
			lastRad=rotationRad;
			lastPer=output.per;
			lastFullRad=output.fullRad;
			output.onchange();
		}
	}
	//Listen events
	elem.onmousedown=function(e){setDrag(e,true);}
	document.onmouseup=function(e){setDrag(e,false);}
	document.onmousemove=function(e){rotate(e);}
	try{elem.addEventListener('touchstart',function(e){setDrag(e,true);})}catch(err){}
	try{document.addEventListener('touchend',function(e){setDrag(e,false);})}catch(err){}
	try{document.addEventListener('touchmove',function(e){rotate(e)})}catch(err){}
	
	//Fixing black box issue on IE9
	dummy=document.createElement("div");
	dummy.innerHTML='';
	if(dummy.getElementsByTagName("br").length==1) elem.style.filter="none";
	delete dummy;
	
	//Output
	return output;
}

Okay, I'm an asshole. I know.

But, really, it's not the big deal. Let's understand...

How?

First of all, this function returns an object with some properties and methods in which you can rely to take advantage of some cool data (Remember that this has to have some real use, not just a spinny crap).

In fact, I did this because I needed a dial wheel, like in the old radios, to create a real simulator for a piece of hardware, and actually make it change values in each of the artifact's different modes.

And, what you see... I mean, you could also create it as a JavaScript pseudoclass and instantiate it really easy, but let's use it just like this for now, to be simpler (You can allways fork me on github, pwn my code, and make me feel like a dumb):

JS
var elem=document.getElementById("spin-ball");
var dial=kcRotateDial(elem);

Pretty easy implementation right? Now, let's see what this shit does:

  • First of all, I make sure that the rotational element is not selectable as a crappy text or whatever, because that brings problems when you need to control the dial (And even more on IExplorer). Also, I've put this line at the beginning... just to get rid of that shit as quickly as I can.
  • Next I set up the public properties that I'll return as output.
    I'll talk about this properties now, because if not... I'll forget:
    (Remember that "dial" is the variable that now contains the output)
dial.rad
The spin value in radians - from 0 to 6.283185307179586 (approximated full radian value... equivalent to 360°).
dial.deg
The spin value in degrees - from 0 to 360° (or 359.99999999...°).
dial.per
The spin value in percentage - from 0 to 100 (same 9999999999 decimals... don't bother me.).
dial.fullRad
The full spin value in radians - infinite possitive or negative, that depends on the amount of spins.
dial.fullDeg
Same, but in degrees.
dial.fullPer
Same, but in percentage.
dial.spin
Amount of spins clockwise. (If you spin backwards, you'll have a negative integer).
dial.clock
Am I spinning clockwise or not? - Boolean

And then we have one method, onchange... a pseudoevent that is fired when the dial is moved, and it updates all the mentioned values.

dial.onchange=function(){
	console.log(dial.deg);
}

Okay, keep going.

Many of the internal functions that you'll find here are just to find some trivial values.

The preventDefault, getPos, getSize and other functions are here just because I'm not using JQuery; but if you can translate this function to that kind of frameworks, you'll get rid of them and the code will be shorter. So, I'll not stop to explain those functions, maybe I'll write about them in other posts.

But there are two important ones here that I can't omit: getAxis and getAngle.

Both of them are too important. Okay, the others are important too, because without them the code would not work at all =P. But this two are directly related to the trigonometric calculations needed "to make my balls spin".

getAxis
This one returns the central pivot point around which the element will rotate. And its value is calculated taking the position of the element and the half of its size.
getAngle
This function uses the previous functions to retrieve the rotational angle, in radians, taking the cursor position, or the position of your finger on screen, and the central axis of the element. (Thanks programming for the Arc Tangent 2 ...).

The next function, setDrag, just handles when we can drag , or spin the wheel. And we will use it later.

Rotation

Now we've arrived to the tasty part. the rotation itself.

At this point, we already have all what we need to spin the shit out.

So, we prepare some variables with the angle, the cursor position, the angle of the previous spin event, the angle relative to the change between the last position and this one, and we finally have the true and bugless value to rotate the element using the mouse or the touch.

Next, we just apply te CSS properties. Much like when we did it in the previous post.

After that, we populate the properties that we discussed before, and we trigger the onchange pseudoevent.

At the bottom, we listen for the element's events of mousedown, up, move, touchstart, and so on... to let the magic happen.

Uff... done

So, that's how it works, and at the end, you can just use this library in the simplest way I've mentioned before, you're not forced to know what the hell this does. But it's important to know what the hell are YOU doing.

I hope this helps, and see you next time.

Downloads

kc-dial.zip

Github repository

I've got a Github repository here, with more info if you like, and some observations. You should check it out.