I'm trying to create a few interactive rotary knobs like those you could find in vintage TVs and radios. I'm using KnobKnob.js to allow the user to rotate. It sometimes behaves weirdly, but given the paucity of plugins available for this particular need and my lack of time, I decided to give it a go anyway.
What I'd like to do is change the source of a video depending on the angle of rotation of the rectangular part of the knob, or <div id="centralKnob"> in my code, to replicate the behavior of a Channels knob. The user should also be able to just click the surrounding links to do that.
So, I thought the best way to do that would be to change the source based on the current rotation angle of the knob. As I'm pretty new to JS/jQuery, I had no idea how to get the angle of an element, so I looked around and found this article. What they do:
var el = document.getElementById("centralKnob");
var st = window.getComputedStyle(el, null);
var tr = st.getPropertyValue("-webkit-transform") ||
st.getPropertyValue("-moz-transform") ||
st.getPropertyValue("-ms-transform") ||
st.getPropertyValue("-o-transform") ||
st.getPropertyValue("transform") ||
"Either no transform set, or browser doesn't do getComputedStyle";
var values = tr.split('(')[1];
values = values.split(')')[0];
values = values.split(',');
var values = tr.split('(')[1];
whatever = tr.split(')')[0];
whatever = tr.split(',');
var a = values[0];
var b = values[1];
var c = values[2];
var d = values[3];
var angle = Math.round(Math.asin(b) * (180/Math.PI));
This should give the current angle of the knob.
But how do I make it so that the angle is recalculated every time the user changes it and the video source gets changed whenever the knob is positioned between X(deg) and Y(deg)?
Thanks in advance. I also welcome any suggestion or advice as to how to make the whole thing work better, especially how to allow the user to rotate the knob smoothly. Also, I know that rotary dials and knobs aren't such a good idea in webdesign and aren't the best thing to control with a mouse, but this is more of a technical challenge than an actual public website.
I made something like this in the past.
http://jsfiddle.net/gTDdp/16/
(With some tweaking it should be possible, to create a rotating knob.)
I added touch events to for mobile devices
var dragging = false
$(function() {
init();
var target = $('#target')
var offset_x = -256;
var offset_y = 356;
var angle = 0;
var startAngle;
var slices = 8;
var sliceAngle = 360 / slices;
target.mousedown(function(e) {
var mouse_x = e.pageX;
var mouse_y = e.pageY;
var radians = Math.atan2(mouse_x - offset_x, mouse_y - offset_y);
dragging = true
startAngle = ((radians * (180 / Math.PI) * -1) + 90) - angle;
})
$(document).mouseup(function() {
dragging = false;
var slice = (angle + (sliceAngle/2)) / sliceAngle;
if(slice < 0){
slice = 12 + slice;
}else if(slice > 12){
slice = 12 - slice;
}
// Move circle in perfect position
angle = (Math.floor(slice) * (360 / slices));
target.css('-moz-transform', 'rotate(' + angle + 'deg)');
target.css('-moz-transform-origin', '50% 50%');
target.css('-webkit-transform', 'rotate(' + angle + 'deg)');
target.css('-webkit-transform-origin', '50%, 50%');
target.css('-o-transform', 'rotate(' + angle + 'deg)');
target.css('-o-transform-origin', '50% 50%');
target.css('-ms-transform', 'rotate(' + angle + 'deg)');
target.css('-ms-transform-origin', '50% 50%');
alert(Math.floor(slice));
})
$(document).mousemove(function(e) {
if (dragging) {
var mouse_x = e.pageX;
var mouse_y = e.pageY;
var radians = Math.atan2(mouse_x - offset_x, mouse_y - offset_y);
var degree = (radians * (180 / Math.PI) * -1) + 90;
angle = degree - startAngle;
if(angle < 0){
angle = 360 + angle;
}else if(angle > 360){
angle = 360 - angle;
}
target.css('-moz-transform', 'rotate(' + angle + 'deg)');
target.css('-moz-transform-origin', '50% 50%');
target.css('-webkit-transform', 'rotate(' + angle + 'deg)');
target.css('-webkit-transform-origin', '50%, 50%');
target.css('-o-transform', 'rotate(' + angle + 'deg)');
target.css('-o-transform-origin', '50% 50%');
target.css('-ms-transform', 'rotate(' + angle + 'deg)');
target.css('-ms-transform-origin', '50% 50%');
}
})
})
function touchHandler(event)
{
var touches = event.changedTouches,
first = touches[0],
type = "";
switch(event.type)
{
case "touchstart": type = "mousedown"; break;
case "touchmove": type="mousemove"; break;
case "touchend": type="mouseup"; break;
default: return;
}
var simulatedEvent = document.createEvent("MouseEvent");
simulatedEvent.initMouseEvent(type, true, true, window, 1,
first.screenX, first.screenY,
first.clientX, first.clientY, false,
false, false, false, 0/*left*/, null);
first.target.dispatchEvent(simulatedEvent);
event.preventDefault();
}
function init()
{
document.addEventListener("touchstart", touchHandler, true);
document.addEventListener("touchmove", touchHandler, true);
document.addEventListener("touchend", touchHandler, true);
document.addEventListener("touchcancel", touchHandler, true);
}
Related
In this jsFiddle I have a Raphael JS grid with a rectangle that I need to move snapped to a grid. The rectangle has four red handles in each corner, and when it moves the handles move as well.
So far I can move the rectangle around (that works fine) but when I try to move it to the left the rectangle flickers and bounces back and forth. Could the problem be that I'm setting the x rectangle coordinate inside the drag move function? Any ideas will be greatly appreciated.
var move = function(dx, dy) {
rect.attr("x", ox + dx);
rect.attr("y", oy + dy);
leftTop.attr("x", ox1 + dx);
leftTop.attr("y", oy1 + dy);
rightTop.attr("x", ox2 + dx);
rightTop.attr("y", oy2 + dy);
leftBottom.attr("x", ox3 + dx);
leftBottom.attr("y", oy3 + dy);
rightBottom.attr("x", ox4 + dx);
rightBottom.attr("y", oy4 + dy);
if ((dx - lastdx) < 0)
seeMoveLeft(rect, leftTop, rightTop,
leftBottom, rightBottom);
lastdx = dx;
};
var up = function() {};
rect.drag(move, start, up);
var seeMoveLeft = function (rect, leftTop, rightTop,
leftBottom, rightBottom){
var left = rect.attr('x');
// find next left grid
var found = false;
var min = left - 40;
for (var i=left; i>=min; i--){
if (i % 40 == 0) {
found = true;
break;
}
}
if (found) {
var diff = left - i;
rect.attr('x', i);
var lt = leftTop.attr('x');
leftTop.attr('x', lt - diff);
var rt = rightTop.attr('x');
rightTop.attr('x', rt - diff);
var lb = leftBottom.attr('x');
leftBottom.attr('x', lb - diff);
var rb = rightBottom.attr('x');
rightBottom.attr('x', rb - diff);
}
}
The problem is that you set the new x of rect here:
rect.attr("x", ox + dx);
Then you modify it here:
if ((dx - lastdx) < 0)
seeMoveLeft(rect, leftTop, rightTop,
leftBottom, rightBottom);
But since move function is based on the mousemove event, sometimes, you'll get the same dx value 2 times in a row. This is normal with mousemove event, especially if you move slow. You can try it below, each time the clientX stays the same, the window will turn green.
var lastClientX;
window.addEventListener('mousemove', (e) => {
if (lastClientX && e.clientX === lastClientX) {
document.body.style.backgroundColor = 'green';
} else {
document.body.style.backgroundColor = 'yellow';
}
lastClientX = e.clientX;
})
body {
width: 100%;
height 100%;
}
What happens in your case when you have 2 consecutive identical dx is that x - lastdx won't validate and so the position won't be adjusted and will stay at rect.attr("x", ox + dx);. So first move event, the position is adjusted to the grid with seeMoveLeft, next, the position changes but isn't adjusted because dx is not smaller than lastdx, it's equal. Hence the flicker you see.
Simplest way to correct, would be to skip the positionning if dx is the same as lastdx. Like this:
if(lastdx !== dx){
rect.attr("x", ox + dx);
leftTop.attr("x", ox1 + dx);
rightTop.attr("x", ox2 + dx);
leftBottom.attr("x", ox3 + dx);
rightBottom.attr("x", ox4 + dx);
}
https://jsfiddle.net/ce7sh9ov/1/
I have created a circle slider using JavaScript, and I need it to act right if it gets zoomed in or out.
My issue is when the circle slider gets zoomed in eg. (zoom: 0.5) , the mouse event listener for the slider will not act probably.
This issue only happen if I set the the zoom property to less or bigger than 1 .
You can try and see the differences: https://jsfiddle.net/mqgfxkjf/8/
Change:
<div style="zoom: 1.0">
To:
<div style="zoom: 0.5">
And you will find that it's not acting right while moving the slider to all-directions.
Tested on Chrome
You have to scale the mouse position accordingly to the zoom value.
Let's say you have set zoom value to 0.5, you will have to scale the mouse position (x and y) with the same value. So in order to fix this exact problem, you can do something as simple as just dividing by the scale value: var mPos = {x: (e.clientX / 0.5) - elPos.x, y: (e.clientY / 0.5) - elPos.y };.
I highly suggest that you let the script handle the scale / zoom value so that you can set it as a variable in your script. I.e. something like this:
(function () {
var scaleValue = 0.5;
addZoom(scaleValue);
var $container = $('#container');
var $slider = $('#slider');
var sliderW2 = $slider.width()/2;
var sliderH2 = $slider.height()/2;
var radius = 200;
var deg = 0;
var elP = $('#container').offset();
var elPos = { x: elP.left, y: elP.top};
var X = 0, Y = 0;
var mdown = false;
$('#container')
.mousedown(function (e) { mdown = true; })
.mouseup(function (e) { mdown = false; })
.mousemove(function (e) {
if (mdown) {
var mPos = {x: (e.clientX / scaleValue) - elPos.x, y: (e.clientY / scaleValue) - elPos.y };
var atan = Math.atan2(mPos.x-radius, mPos.y-radius);
deg = -atan/(Math.PI/180) + 180; // final (0-360 positive) degrees from mouse position
X = Math.round(radius* Math.sin(deg*Math.PI/180));
Y = Math.round(radius* -Math.cos(deg*Math.PI/180));
$slider.css({ left: X+radius-sliderW2, top: Y+radius-sliderH2 });
// AND FINALLY apply exact degrees to ball rotation
$slider.css({ WebkitTransform: 'rotate(' + deg + 'deg)'});
$slider.css({ '-moz-transform': 'rotate(' + deg + 'deg)'});
//
// PRINT DEGREES
$('#value').html('angle deg= '+deg);
}
});
})();
function addZoom(scaleValue) {
$('#zoom-container').css('zoom', scaleValue);
}
Fiddle:
https://jsfiddle.net/mqgfxkjf/10/
I have a project with a circle that, when clicked, rotates to a predefined position. It is almost there, but the last requirement is that it always rotates clockwise to the marker. I just can't seem to figure out how to get the right value so that when i set css transform:rotate(Xdeg), it will always go clockwise. Keeping the angle between 0 and 360 would also be a plus for another piece of this, but not necessary.
See this fiddle, javascript below as well Rotation
$(function () {
$('body').on('click', '#graph1', function (e) {
console.log('********************');
//get mouse position relative to div and center of div for polar origin
var pos = getMousePosAndCenter(e, 'graph1');
//get the current degrees of rotation from the css
var currentRotationDegrees = getCSSRotation('#graph1');
console.log('CSS Rotation Value: ' + currentRotationDegrees);
//current rotation in radians
var currentRotationRadians = radians(currentRotationDegrees);
//radians where clicked
var clickRadiansFromZero = Math.atan2(pos.y - pos.originY, pos.x - pos.originX);
//degrees the click is offset from 0 origin
var offsetDegrees = degrees(clickRadiansFromZero);
//how many degrees to rotate in css to put the mouse click at 0
var degreesToZero;
if (offsetDegrees >= 0)
degreesToZero = currentRotationDegrees - Math.abs(offsetDegrees);
else
degreesToZero = currentRotationDegrees + Math.abs(offsetDegrees);
console.log("Degrees to Zero: " + degreesToZero);
//distance in pixels from origin
var distance = calculateDistance(pos.originX, pos.originY, pos.x, pos.y);
console.log("Distance From Origin(px): " + distance);
$('#graph1').css('transform','rotate(' + degreesToZero + 'deg)')
});
});
function getMousePosAndCenter(e, id) {
var rect = document.getElementById(id).getBoundingClientRect();
return {
x: (((e.clientX - rect.left) / rect.width) * rect.width) + 0.5 << 0,
y: (((e.clientY - rect.top) / rect.height) * rect.height) + 0.5 << 0,
originY: (rect.height / 2),
originX: (rect.width / 2)
};
}
function radians(degrees) {
return degrees * Math.PI / 180;
};
function degrees(radians) {
return radians * 180 / Math.PI;
};
function calculateDistance(originX, originY, mouseX, mouseY) {
return Math.floor(Math.sqrt(Math.pow(mouseX - originX, 2) + Math.pow(mouseY - originY, 2)));
}
function getCSSRotation(id) {
var matrix = $(id).css('transform');
var values = matrix.split('(')[1],
values = values.split(')')[0],
values = values.split(',');
var a = values[0];
var b = values[1];
var c = values[2];
var d = values[3];
var cssRotation = degrees(Math.atan2(b, a));
return cssRotation;
}
Think out of the box:
We can CSS3 rotate an element with transform to i.e: 720° ...
it will make 2 clockwise turns. (OK, in our UI it can only do max a 359 turn but let's follow the math)
If we than animate it to 810°... it just means that it'll do a 90° clockwise move!
So all we need to do is always increase a degree variable to insanity!
HEY! If at some point you want to keep track of the current normalized 0-360 degree...
you can always retrieve that value doing ourCurrentInsanelyHighDegree % 360 = UIdegrees
Here's a jsBin demo
and this is all the JS you need.
function getCSSRotation( $el ) {
var matrix = $el.css('transform'),
v = matrix.split('(')[1].split(')')[0].split(','),
rds = Math.atan2(v[1], v[0]);
return rds*180/Math.PI <<0; // Degrees
}
var $EL = $("#graph1"),
w = $EL.width(),
r = w/2, // Radius
x = parseInt($EL.css("left"), 10),
y = parseInt($EL.css("top"), 10),
d = getCSSRotation( $EL ); // Initial degree (ONLY ONCE!)
$EL.on("click", function(e){
var mx = e.clientX-x-r, // Click coord X
my = e.clientY-y-r, // Click coord Y
rds = Math.atan2(-my, -mx), // Radians
md = (rds*180/Math.PI<<0) + 180; // Mouse Degrees
d += (360-md); // always increment to insanity!!
$(this).css({transform:"rotate("+ d +"deg)"});
});
#graph1 {
position:absolute;
top:10px; left:30px;
width:200px; height:200px;
background:url(//placehold.it/200x200&text=IMAGE);
transition:transform 2s ease;
transform:rotate(30deg);
transform-origin:50% 50%;
border-radius:50%;
}
#marker {
position: absolute;
top:110px;
left:230px;
border-top:1px solid black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="graph1"></div>
<div id="marker">Wherever you click, it rotates to here</div>
UPDATE:
Figuring it would be easy to do, I found it a little harder than I thought. The other answer with jQuery.animate works, but animate doesn't have the fluid framerate that css animation does (it runs on the GPU).
Here's a modified fiddle with a CSS solution: http://jsfiddle.net/2g17cjuL/2/
Keeping the angle between 0 and 360 would also be a plus
You cannot keep going forward (ie rotating by a positive number) and keep the rotation positive, however, in my fiddle offsetDegrees (the number of degrees additional rotated), or the remainder of totalDegreesdivided by 360 should give you what you need to use elsewhere.
Requrement: That it always rotates clockwise.
One thing: If you use CSS transitions, it'll calculate the shortest route for you. You want a bit more control over rotational direction, so I commented out the transition:transform 1s ease; in your CSS because we'll control this manually.
JAVASCRIPT
I borrowed this JQuery function and modified it so we can feed it a starting angle, and ending angle and it'll animate #graph1 for us. (Read the link to change duration, easing, and to use the complete callback)
$.fn.animateRotate = function(angle, start, duration, easing, complete) {
var args = $.speed(duration, easing, complete);
var step = args.step;
return this.each(function(i, e) {
args.complete = $.proxy(args.complete, e);
args.step = function(now) {
$.style(e, 'transform', 'rotate(' + now + 'deg)');
if (step) return step.apply(e, arguments);
};
$({deg: start}).animate({deg: angle}, args);
});
};
I also modified your JQuery so it won't rotate counter-clockwise: when currentRotationDegrees is greater than degreesToZero, it'll subtract 360, and then use this new value as the starting position for `animateRotate().
if(currentRotationDegrees > degreesToZero){
currentRotationDegrees -= 360;
}
$('#graph1').animateRotate(degreesToZero, currentRotationDegrees);
Here it is in action.
http://jsfiddle.net/q4nad31t/1/
We got 3 points: start, end and mail.
The mail image, moves in a curved line from the start and end point, this is done by jQuery animate.
Now the next step is to make the mail image rotate while the animation is running. So at the start point and end point it would be rotated 0 degrees, but while animating, it should rotate facing the path of the animation. (see the image)
What I have tried:
JSFiddle
// Init dom elements
var $start = $('#start');
var $end = $('#end');
var $mail = $('#mail');
// Get position coordinates
var startPos = $start.position();
var endPos = $end.Position();
// Angle calculation
var getAngle = function(currX, currY, endX, endY) {
var angle = Math.atan2(currX - endX, currY - endY) * (180 / Math.PI);
if (angle < 0) {
angle = Math.abs(angle);
} else {
angle = 360 - angle;
}
return angle;
};
// Mail angle
var getMailAngle = function() {
var currPos = $mail.position();
var endPos = $end.position();
return getAngle(currPos.left, currPos.top, endPos.left, endPos.top);
};
// Animate
$mail.animate({top: endPos.top, left: endPos.left}, {
specialEasing: {left: "easeInSine", top: "linear"},
// Executed each "animation" frame, so we rotate here.
step: function() {
var angle = getMailAngle();
$(this).css('transform', 'rotate(' + angle + 'deg'));
}
});
But the code above is not correct, the angle doesn't face up when started / ended, I have very little experience with geometry math, so I really appreciate help for the rotating calculations.
First off, you need to use an easing animation that starts and ends with the same "angle". If you look at the different easing options, swing, easeInOutQuad and easeInOutSine are some of the valid options.
To calculate an approximation of the angle, you can look at the mail icon's current position and its next position (in the next animation frame). To get a good approximation you need to "manually" calculate the current and next position using the easing function. This also means you need to control the animation manually.
Here's a code snippet, and you can also see it on JSFiddle.
// Init dom elements
var $start = $('#start');
var $end = $('#end');
var $mail = $('#mail');
// Get position coordinates
var startPos = $start.offset();
var endPos = $end.offset();
// Angle calculation
var getAngle = function(currX, currY, endX, endY) {
var angle = Math.atan2(currX - endX, currY - endY) * (180 / Math.PI);
if (angle < 0) {
angle = Math.abs(angle);
} else {
angle = 360 - angle;
}
return angle;
};
// Animate
var maxframe = 1000;
$({frame: 0}).animate({frame: maxframe}, {
easing: "linear",
duration: 1000,
// Executed each "animation" frame, so we rotate here.
step: function() {
var easing = $.easing.easeInOutQuad;
var left = easing(0, this.frame, startPos.left, endPos.left - startPos.left, maxframe);
var leftNext = easing(0, this.frame+1, startPos.left, endPos.left - startPos.left, maxframe);
var top = startPos.top + (endPos.top - startPos.top) * this.frame / maxframe;
var topNext = startPos.top + (endPos.top - startPos.top) * (this.frame + 1) / maxframe;
var angle = getAngle(left, top, leftNext, topNext);
$mail.offset({left: left, top: top});
$mail.css('transform', 'rotate(' + angle + 'deg)');
},
// Set the final position
complete: function() {
$mail.offset($end.offset());
$mail.css('transform', '');
}
});
I'm playing around with Three.js and WebGL and can't quite get the controls the way I want. I chose to try to "roll my own" controls since Three.js's FirstPersonControls do not use pointer lock.
Anyway, I took most of my code from the built-in FirstPersonControls, converted it to use pointer lock (movementX instead of pageX - offset), but I am having trouble smoothing the look motion.
Here is my onMouseMove (using originalEvent since it is a jquery event):
onMouseMove: function(e) {
if(!document.pointerLockElement) return;
var moveX = e.originalEvent.movementX ||
e.originalEvent.mozMovementX ||
e.originalEvent.webkitMovementX ||
0,
moveY = e.originalEvent.movementY ||
e.originalEvent.mozMovementY ||
e.originalEvent.webkitMovementY ||
0;
//Update the mouse movement for coming frames
this.mouseMovementX = moveX;
this.mouseMovementY = moveY;
}
And my Controls.update() (called on each animation frame, with the THREE.Clock delta):
update: function(delta) {
if(this.freeze) {
return;
}
//movement, works fine
if(this.moveForward) this.camera.translateZ(-(actualMoveSpeed + this.autoSpeedFactor));
if(this.moveBackward) this.camera.translateZ(actualMoveSpeed);
if(this.moveLeft) this.camera.translateX(-actualMoveSpeed);
if(this.moveRight) this.camera.translateX(actualMoveSpeed);
/////////
//ISSUES ARE WITH THIS CODE:
/////////
//look movement, really jumpy
this.lon += this.mouseMovementX;
this.lat -= this.mouseMovementY;
this.lat = Math.max(-85, Math.min(85, this.lat));
this.phi = (90 - this.lat) * Math.PI / 180;
this.theta = this.lon * Math.PI / 180;
this.target.x = this.camera.position.x + 100 * Math.sin(this.phi) * Math.cos(this.theta);
this.target.y = this.camera.position.y + 100 * Math.cos(this.phi);
this.target.z = this.camera.position.z + 100 * Math.sin(this.phi) * Math.sin(this.theta);
this.camera.lookAt(this.target);
}
This code does work, but moving the camera is jumpy as the mouse moves around. I could really use some help figuring out how to smooth it.
You can see what I mean by "jumpy" here. I'm new to Three.js, WebGL, and just 3D in general so any help is appreciated.
Thanks,
-Chad
EDIT After working with #przemo_li, here is the working code he came up with:
onMouseMove: function(e) {
if(!document.pointerLockElement) return;
var moveX = e.originalEvent.movementX ||
e.originalEvent.mozMovementX ||
e.originalEvent.webkitMovementX ||
0,
moveY = e.originalEvent.movementY ||
e.originalEvent.mozMovementY ||
e.originalEvent.webkitMovementY ||
0;
//Update the initial coords on mouse move
this.mouseMovementX += moveX; //aggregate mouse movements as a total delta delta
this.mouseMovementY += moveY;
},
update: function(delta) {
if(this.freeze) {
return;
}
//movement
if(this.moveForward) this.camera.translateZ(-(actualMoveSpeed + this.autoSpeedFactor));
if(this.moveBackward) this.camera.translateZ(actualMoveSpeed);
if(this.moveLeft) this.camera.translateX(-actualMoveSpeed);
if(this.moveRight) this.camera.translateX(actualMoveSpeed);
//look movement
this.lon += this.mouseMovementX;
this.lat -= this.mouseMovementY;
this.mouseMovementX = 0; //reset mouse deltas to 0 each rendered frame
this.mouseMovementY = 0;
this.phi = (90 - this.lat) * Math.PI / 180;
this.theta = this.lon * Math.PI / 180;
if(this.constrainVertical) {
this.phi = THREE.Math.mapLinear(this.phi, 0, Math.PI, this.verticalMin, this.verticalMax);
}
this.target.x = this.camera.position.x + 100 * Math.sin(this.phi) * Math.cos(this.theta);
this.target.y = this.camera.position.y + 100 * Math.cos(this.phi);
this.target.z = this.camera.position.z + 100 * Math.sin(this.phi) * Math.sin(this.theta);
this.camera.lookAt(this.target);
}
'Official' version just added: https://github.com/mrdoob/three.js/blob/master/examples/js/controls/PointerLockControls.js
1)Constraints?
In your code you limit mouse X movement to -|+ 85 Its unlikely that such constraint is needed.
2)Aggregate all events that happen during frame
In your code you override mouse movement with each new event. So if you get 3 events during frame only most recent will be stored.
Add those movements. Than after rendering frame you can clear count. And start gathering events again.