📜 ⬆️ ⬇️

We make a rotational regulator.

With this topic, I continue the series of articles on writing all sorts of goodies for MooTools. Today we will create a rotary control in pure JavaScript — a control that is often used in programs that work with sound to adjust volume or balance. Something like this:

Sample

Since it is impossible to rotate images using JavaScript, we will act differently than we would have done in a flash. To accomplish the task we need two images:

Elements
')
The idea is to place the indicator on the substrate and rotate it in accordance with the position of the mouse cursor. Mathematics is useful for calculating the position of the indicator. Do not be afraid, we will not have to solve the wave equation, let's recall a little school trigonometry and the polar coordinate system.

So, the polar coordinate system determines the position of a point by two quantities: the angle of rotation of the radius vector relative to the polar axis φ and the length of the radius vector r. For a better understanding, take a look at the diagram below.

Schema 1

It is a pity that the browser cannot visualize a point in such a coordinate system — it works with the Cartesian system. But this is not a problem, because we can easily go from the polar system to Cartesian using the formula:

x = r⋅cos(φ)
y = r⋅sin(φ)

Coorinates x and y will be substituted into the left and top CSS properties of the indicator. It should be noted that in the diagram, the y axis is directed upwards. In the browser, it has the opposite direction. We will take this into account when we change the coordinates of the indicator.

Ok, we figured it out. But if r is initially known (we know the size of the image), then how can we determine the angle φ from the position of the mouse cursor? Again, school trigonometry comes to the rescue. If you look at the following diagram, you can see that the vector from the pole to the point of the cursor position with its projections forms a right triangle.

Schema 2

The legs in this triangle will be the coordinates of the cursor. From here we can find the tangent of the angle φ as the ratio of the opposite leg to the adjacent one:

tg(φ) = y / x

The angle φ from here is found as arctangent:

φ = arctg(tg(φ)) = arctg(y / x)

From mathematics this is probably all. Now let's do coding. In HTML, everything is extremely simple:
<div id="Container">
<div id="Indicator"></div>
</div>

CSS:
#Container
{
position: relative;
background-image: url('./images/rheostat.png');
width: 64px;
height: 64px;
}

#Indicator
{
position: absolute;
background-image: url('./images/indicator.png');
width: 4px;
height: 4px;
visibility: hidden;
}


JavaScript code is written using the MooTools framework:
var Rheostat = new Class({
Implements: [Events, Options],

// .
options: {
radius: 27,
minValue: 0,
maxValue: 100
},

// .
deg2rad: Math.PI / 180,
rad2deg: 180 / Math.PI,

// , , .
captured: false ,

//
initialize: function (container, indicator, options){
this .setOptions(options);
this .indicator = $(indicator);
this .container = $(container);

// .
this .indicator.fade( 'show' );

// .
this .container.addEvent( 'mousedown' , this .captureMouse.bind( this ));
document .addEvents({
'mousemove' : this .updateAngle.bind( this ),
'mouseup' : this .releaseMouse.bind( this )
});

// ,
// .
var containerSize = this .container.getSize();
var indicatorSize = this .indicator.getSize();
this .offset = {
x: Math.floor(containerSize.x / 2) - Math.floor(indicatorSize.x / 2),
y: Math.floor(containerSize.y / 2) - Math.floor(indicatorSize.y / 2)
};

this .angle = 0;
this .updateIndicatorPosition();
},

// , .
captureMouse: function (){
this .captured = true ;
},

// .
releaseMouse: function (){
this .captured = false ;
},

// .
updateAngle: function (e){
if ( this .captured)
{
var containerPosition = this .container.getPosition();

// .
// mouseLeft 0.1 , .
var mouseLeft = e.client.x - this .offset.x - containerPosition.x + 0.1;
var mouseTop = this .offset.y - e.client.y + containerPosition.y;

// (.. Math.atan() ,
// ).
this .angle = Math.atan(mouseTop / mouseLeft) * this .rad2deg;

// .. -90 +90,
// 180.
// .
if (mouseLeft < 0)
this .angle += 180;

// , 0 360 .
if ( this .angle < 0)
this .angle += 360;

// .
var value = Math.floor(( this .options.maxValue - this .options.minValue) * this .angle / 360 + this .options.minValue);
this .fireEvent( 'valueChanged' , value)
this .updateIndicatorPosition();
}
},

updateIndicatorPosition: function (){
// Math.cos() Math.sin().
var radAngle = this .angle * this .deg2rad
var left = this .options.radius * Math.cos(radAngle) + this .offset.x;

// "-". y .
var top = - this .options.radius * Math.sin(radAngle) + this .offset.y;

// .
this .indicator.setStyle( 'left' , left);
this .indicator.setStyle( 'top' , top);
}
});

An instance of this controller is constructed like this:
var rheostat = new Rheostat( 'Container' , 'Indicator' );

Use the value change event:
rheostat.addEvent( 'valueChanged' , function( value ){
// value .
});

That seems so far and everything. Later I will tell you how to use the mouse wheel to adjust and how to limit the indicator rotation on both sides. Suggestions for additional functionality are accepted.

UPD. Improved script that fixes many flaws:
var Rheostat = new Class({
Implements: [Events, Options],

// .
options: {
radius: 27,

// .
minValue: 0,
maxValue: 100,

// .
minAngle: 50,
maxAngle: 310,

// .
angleOffset: -90,

// ?
reversed: true
},

// .
deg2rad: Math.PI / 180,
rad2deg: 180 / Math.PI,

// , , .
captured: false ,

angle: 0,
mouseAngle: 0,
oldMouseAngle: 0,

// .
initialize: function (container, indicator, options){
this .setOptions(options);
this .indicator = $(indicator);
this .container = $(container);

// .
this .container.addEvents({
'mousedown' : this .captureMouse.bind( this ),
'mousewheel' : this .handleWheel.bind( this ),
});

document .addEvents({
'mousemove' : this .updateAngle.bind( this ),
'mouseup' : this .releaseMouse.bind( this )
});

// ,
// .
var containerSize = this .container.getSize();
var indicatorSize = this .indicator.getSize();
this .offset = {
x: Math.floor(containerSize.x / 2) - Math.floor(indicatorSize.x / 2),
y: Math.floor(containerSize.y / 2) - Math.floor(indicatorSize.y / 2)
};

// .
this .angle = this .options.minAngle + this .options.angleOffset;
this .updateIndicatorPosition();

// .
this .indicator.fade( 'hide' ).fade( 'in' );
},

// .
handleWheel: function (e){
// .
var wheelAngle = this .angle + e.wheel;
if ((wheelAngle >= this .options.minAngle) && (wheelAngle <= this .options.maxAngle)){
this .oldMouseAngle = this .mouseAngle = this .angle = wheelAngle;
this .updateIndicatorPosition();
}
},

// , .
captureMouse: function (e){
this .captured = true ;

// .
var mouseAngle = this .getMouseAngle(e);
if ((mouseAngle >= this .options.minAngle) && (mouseAngle <= this .options.maxAngle)){
this .oldMouseAngle = this .mouseAngle = this .angle = mouseAngle;
this .updateIndicatorPosition();
}
},

// .
releaseMouse: function (){
this .captured = false ;
},

// .
getMouseAngle: function (e){
var containerPosition = this .container.getPosition();

// .
// mouseLeft 0.1 , .
var mouseLeft = e.client.x - this .offset.x - containerPosition.x + 0.1;
var mouseTop = this .offset.y - e.client.y + containerPosition.y;

// (.. Math.atan() ,
// ).
var angle = Math.atan(mouseTop / mouseLeft) * this .rad2deg;

// .. -90 +90,
// 180.
// .
if (mouseLeft < 0)
angle += 180;

// , 0 360 .
if (angle < 0)
angle += 360;

return angle - this .options.angleOffset;
},

// .
updateAngle: function (e){
// ?
if ( this .captured){
var mouseAngle = this .getMouseAngle(e);

// .
var diffAngle = mouseAngle - this .oldMouseAngle;

// .
if (( this .angle + diffAngle >= this .options.minAngle) && ( this .angle + diffAngle <= this .options.maxAngle))
this .angle += diffAngle;

this .oldMouseAngle = this .mouseAngle = mouseAngle;
this .updateIndicatorPosition();
}
},

// .
updateValue: function (){
var value = Math.floor(
( this .options.maxValue - this .options.minValue + 1) *
( this .angle - this .options.minAngle) /
( this .options.maxAngle - this .options.minAngle)
);

// .
this .fireEvent( 'valueChanged' , ( this .options.reversed) ? this .options.maxValue - value : value);
},

// .
updateIndicatorPosition: function (){
// Math.cos() Math.sin().
var radAngle = ( this .angle + this .options.angleOffset) * this .deg2rad;
var left = this .options.radius * Math.cos(radAngle) + this .offset.x;

// "-". y .
var top = - this .options.radius * Math.sin(radAngle) + this .offset.y;

// .
this .indicator.setStyles({left: left, top: top});

// .
this .updateValue();
}
});

An example with adjustable font size .

Source: https://habr.com/ru/post/41894/


All Articles