Make cursor stay in boundary - javascript

I have a color-wheel picker (which I took a good chunk of code from this library). I'm trying to get the color-wheels cursor to not go out of boundaries. I don't want it going pass the gray border.
I can do the obvious and make the parent div: overflow:hidden, but that would just hide the cursor, it wouldn't make it not go pass the boundaries.
I think the relevant variable to edit, is one of the following (in the hsvMove function, starting on line 39):
var r = currentTargetHeight / 2,
x = e.pageX - startPoint.left - r,
y = e.pageY - startPoint.top - r
How can I get the cursor to not go past the boundaries?
JSFiddle
(function(window) {
"use strict"
// Some common use variables
var myColor = new Colors(),
startPoint,
currentTarget,
currentTargetHeight = 0,
PI = Math.PI,
PI2 = PI * 2;
/* ---------------------------------- */
/* ---- HSV-circle color picker ----- */
/* ---------------------------------- */
var colorDiskWrapper = document.getElementById('colorDiskWrapper'),
colorDiskCursor = document.getElementById('colorDiskCursor'),
colorDisk = document.getElementById('colorDisk');
var colorDiscRadius = colorDisk.offsetHeight / 2;
// Create Event Functions
var hsvDown = function(e) { // mouseDown callback
var target = e.target || e.srcElement;
if (e.preventDefault) e.preventDefault();
if (target === colorDiskCursor || target === colorDisk) {
currentTarget = target.parentNode;
} else
return;
startPoint = getOrigin(currentTarget);
currentTargetHeight = currentTarget.offsetHeight;
addEvent(window, 'mousemove', hsvMove);
hsvMove(e);
startRender();
},
hsvMove = function(e) { // mouseMove callback
var r = currentTargetHeight / 2,
x = e.pageX - startPoint.left - r,
y = e.pageY - startPoint.top - r,
h = 360 - ((Math.atan2(y, x) * 180 / PI) + (y < 0 ? 360 : 0)),
s = (Math.sqrt((x * x) + (y * y)) / r) * 100;
myColor.setColor({
h: h,
s: s
}, 'hsv');
},
renderHSVPicker = function(color) { // used in renderCallback of 'new ColorPicker'
var x = Math.cos(PI2 - color.hsv.h * PI2),
y = Math.sin(PI2 - color.hsv.h * PI2),
r = color.hsv.s * colorDiscRadius;
// Position the Cursor
colorDiskCursor.style.left = (x * r + colorDiscRadius) + 'px';
colorDiskCursor.style.top = (y * r + colorDiscRadius) + 'px';
};
addEvent(colorDiskWrapper, 'mousedown', hsvDown); // event delegation
addEvent(window, 'mouseup', function() {
removeEvent(window, 'mousemove', hsvMove);
stopRender();
});
var doRender = function(color) {
renderHSVPicker(color);
},
renderTimer,
startRender = function(oneTime) {
if (oneTime) { // only Colors is instanciated
doRender(myColor.colors);
} else {
renderTimer = window.setInterval(
function() {
doRender(myColor.colors);
// http://stackoverflow.com/questions/2940054/
}, 13); // 1000 / 60); // ~16.666 -> 60Hz or 60fps
}
},
stopRender = function() {
window.clearInterval(renderTimer);
};
/*
Function Helpers
*/
function getOrigin(elm) {
var box = (elm.getBoundingClientRect) ? elm.getBoundingClientRect() : {
top: 0,
left: 0
},
doc = elm && elm.ownerDocument,
body = doc.body,
win = doc.defaultView || doc.parentWindow || window,
docElem = doc.documentElement || body.parentNode,
clientTop = docElem.clientTop || body.clientTop || 0, // border on html or body or both
clientLeft = docElem.clientLeft || body.clientLeft || 0;
return {
left: box.left + (win.pageXOffset || docElem.scrollLeft) - clientLeft,
top: box.top + (win.pageYOffset || docElem.scrollTop) - clientTop
};
}
function addEvent(obj, type, func) {
addEvent.cache = addEvent.cache || {
_get: function(obj, type, func, checkOnly) {
var cache = addEvent.cache[type] || [];
for (var n = cache.length; n--;) {
if (obj === cache[n].obj && '' + func === '' + cache[n].func) {
func = cache[n].func;
if (!checkOnly) {
cache[n] = cache[n].obj = cache[n].func = null;
cache.splice(n, 1);
}
return func;
}
}
},
_set: function(obj, type, func) {
var cache = addEvent.cache[type] = addEvent.cache[type] || [];
if (addEvent.cache._get(obj, type, func, true)) {
return true;
} else {
cache.push({
func: func,
obj: obj
});
}
}
};
if (!func.name && addEvent.cache._set(obj, type, func) || typeof func !== 'function') {
return;
}
if (obj.addEventListener) obj.addEventListener(type, func, false);
else obj.attachEvent('on' + type, func);
}
function removeEvent(obj, type, func) {
if (typeof func !== 'function') return;
if (!func.name) {
func = addEvent.cache._get(obj, type, func) || func;
}
if (obj.removeEventListener) obj.removeEventListener(type, func, false);
else obj.detachEvent('on' + type, func);
}
})(window);
#colorDisk {
background-image: url("http://i.imgur.com/tX5NbWs.png");
width: 350px;
height: 350px;
background-repeat: no-repeat;
cursor: pointer;
}
#colorDiskCursor {
position: absolute;
border: 2px solid black;
border-radius: 50%;
width: 9px;
height: 9px;
cursor: pointer;
}
<div id="colorDiskWrapper">
<div id="colorDisk"></div>
<div id="colorDiskCursor"></div>
</div>
<script src="https://rawgit.com/PitPik/colorPicker/master/colors.js"></script>

Problem:
Let's start by outlining the algorithm, so we are all clear about what we are trying to do: with each mouse move/click, calculate the H and S values represented by the mouse position relative to the color disk (of a HSV color system). Periodically, render the disk cursor exactly on the position corresponding to the H and S values.
There are a few things we need to take note of:
The actual radius that we should use to calculate the color values (S in particular) is the radius of the color disk minus the radius of the cursor, because we want to prevent the cursor leaving the boundary of the color disk. In this case, we have 175px - 6.5px, which is 168.5px.
When rendering the cursor, we are setting its top-left position. We need to offset the position by its radius so that our "finger pointer" appears nicely in the middle of the cursor.
Solution:
With the above understanding, the solution is straightforward.
There are problems with your code, since you are using the entire radius of the color disk (175px), without accounting for the radius of the disk cursor (6.5px).
A few things you should also fix / consider in your code:
Your currentTargetHeight is the height of the wrapper (350px), which is then halved to derive the r. This looks wrong to me. You should not be concerned with the wrapper's dimension at all. Remove this variable from the code. The values we need to be concerned with should be colorDiskRadius and colorDiskCursorRadius.
Your colorDiscCursor is set to position: absolute, but its offset parent is not the wrapper, since wrapper is not a positioned element. Hence, the top-left position that we set for colorDiscCursor may be totally unpredictable, depending where its actual parent is on an actual page. To solve this, set wrapper to position: relative.
I notice that you are not setting box-sizing (defaults to content-box), which is why your cursor is actually 13px wide despite having width: 9px; likewise for height. I personally like to use box-sizing: border-box so that when I have to do pixel-accurate calculations, I just need to look at the actual width and height properties in CSS, without having to also refer to padding and border.
Minor issue: You sometimes use disc, and sometimes disk in your code. Try to standardise this for sanity's sake.
TL;DR
Here's the fiddle implementing 1, 2 and 4: https://jsfiddle.net/hrnn9w9k/4/
I didn't include 3 as it might not be your preference, but I strongly recommend it.

Related

Random movement of circles created by the script

I have a function that craeates divs with a circle.
Now they are all created and appear at the beginning of the page and go further in order.
Next, I need each circle to appear in a random place. I did this.
Now I need all of them to move randomly across the entire page, I have difficulties with this.
Here is an example of how everything works for one element that is already on the page.
https://jsfiddle.net/quej8wko/
But when I add this code, all my created circles don't move.
I get an error:
"message": "Uncaught TypeError: Cannot set properties of null (setting 'willChange')",
This is probably due to the fact that initially there are no circles on the page. How can I connect the code so that all created circles move?
//creating circles
var widthHeight = 40; // <-- circle width
var margin = 20; // <-- margin - is it necessary ?
var delta = widthHeight + margin;
function createDiv(id, color) {
let div = document.createElement('div');
var currentTop = 0;
var documentHeight = document.documentElement.clientHeight;
var documentWidth = document.documentElement.clientWidth;
div.setAttribute('class', id);
if (color === undefined) {
let colors = ['#35def2', '#35f242', '#b2f235', '#f2ad35', '#f24735', '#3554f2', '#8535f2', '#eb35f2', '#f2359b', '#f23547'];
div.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
}
else {
div.style.backgroundColor = color;
}
div.classList.add("circle");
div.classList.add("animation");
// Get the random positions minus the delta
currentTop = Math.floor(Math.random() * documentHeight) - delta;
currentLeft = Math.floor(Math.random() * documentWidth) - delta;
// Keep the positions between -20px and the current positions
var limitedTop = Math.max(margin * -1, currentTop);
var limitedLeft = Math.max(margin * -1, currentLeft);
div.style.top = limitedTop + "px";
div.style.left = limitedLeft + "px";
document.body.appendChild(div);
}
let i = 0;
const oneSecond = 1000;
setInterval(() => {
i += 1;
createDiv(`circle${i}`)
}, oneSecond);
//move circles
function RandomObjectMover(obj, container) {
this.$object = obj;
this.$container = container;
this.container_is_window = container === window;
this.pixels_per_second = 250;
this.current_position = { x: 0, y: 0 };
this.is_running = false;
}
// Set the speed of movement in Pixels per Second.
RandomObjectMover.prototype.setSpeed = function(pxPerSec) {
this.pixels_per_second = pxPerSec;
}
RandomObjectMover.prototype._getContainerDimensions = function() {
if (this.$container === window) {
return { 'height' : this.$container.innerHeight, 'width' : this.$container.innerWidth };
} else {
return { 'height' : this.$container.clientHeight, 'width' : this.$container.clientWidth };
}
}
RandomObjectMover.prototype._generateNewPosition = function() {
// Get container dimensions minus div size
var containerSize = this._getContainerDimensions();
var availableHeight = containerSize.height - this.$object.clientHeight;
var availableWidth = containerSize.width - this.$object.clientHeight;
// Pick a random place in the space
var y = Math.floor(Math.random() * availableHeight);
var x = Math.floor(Math.random() * availableWidth);
return { x: x, y: y };
}
RandomObjectMover.prototype._calcDelta = function(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
var dist = Math.sqrt( dx*dx + dy*dy );
return dist;
}
RandomObjectMover.prototype._moveOnce = function() {
// Pick a new spot on the page
var next = this._generateNewPosition();
// How far do we have to move?
var delta = this._calcDelta(this.current_position, next);
// Speed of this transition, rounded to 2DP
var speed = Math.round((delta / this.pixels_per_second) * 100) / 100;
//console.log(this.current_position, next, delta, speed);
this.$object.style.transition='transform '+speed+'s linear';
this.$object.style.transform='translate3d('+next.x+'px, '+next.y+'px, 0)';
// Save this new position ready for the next call.
this.current_position = next;
};
RandomObjectMover.prototype.start = function() {
if (this.is_running) {
return;
}
// Make sure our object has the right css set
this.$object.willChange = 'transform';
this.$object.pointerEvents = 'auto';
this.boundEvent = this._moveOnce.bind(this)
// Bind callback to keep things moving
this.$object.addEventListener('transitionend', this.boundEvent);
// Start it moving
this._moveOnce();
this.is_running = true;
}
RandomObjectMover.prototype.stop = function() {
if (!this.is_running) {
return;
}
this.$object.removeEventListener('transitionend', this.boundEvent);
this.is_running = false;
}
// Init it
var x = new RandomObjectMover(document.querySelector(".circle"), window);
// Start it off
x.start();
.circle {
clip-path: circle(50%);
height: 40px;
width: 40px;
margin: 20px;
position: absolute;
}
I have modified the snippet which works as you expected.
There was a mistake where you were initializing and creating the object instance only once and none of the div elements that you created inside the setInterval function never got Instantiated.
I think you are just starting out with JavaScript with this sample project.
Below are few suggestions:
Learn to debug the code. You should be using dev tools by making use of debugger statement where it takes you to the source code to analyze the variable scope and stack during the runtime. console.log also helps in few situations.
I could see a lot of confusing naming convention (You have named the create div parameter as id but creating a div class using that id)
Try using ES6 features (class syntax is really good when writing OOP in JS although it's just a syntactic sugar for prototype)
//creating circles
var widthHeight = 40; // <-- circle width
var margin = 20; // <-- margin - is it necessary ?
var delta = widthHeight + margin;
function createAndInitializeDivObject(id, color) {
let div = document.createElement('div');
var currentTop = 0;
var documentHeight = document.documentElement.clientHeight;
var documentWidth = document.documentElement.clientWidth;
div.setAttribute('class', id);
if (color === undefined) {
let colors = ['#35def2', '#35f242', '#b2f235', '#f2ad35', '#f24735', '#3554f2', '#8535f2', '#eb35f2', '#f2359b', '#f23547'];
div.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
}
else {
div.style.backgroundColor = color;
}
div.classList.add("circle");
div.classList.add("animation");
// Get the random positions minus the delta
currentTop = Math.floor(Math.random() * documentHeight) - delta;
currentLeft = Math.floor(Math.random() * documentWidth) - delta;
// Keep the positions between -20px and the current positions
var limitedTop = Math.max(margin * -1, currentTop);
var limitedLeft = Math.max(margin * -1, currentLeft);
div.style.top = limitedTop + "px";
div.style.left = limitedLeft + "px";
document.body.appendChild(div);
var x = new RandomObjectMover(document.querySelector(`.${id}`), window);
x.start();
}
let i = 0;
const oneSecond = 1000;
setInterval(() => {
i += 1;
createAndInitializeDivObject(`circle${i}`)
}, oneSecond);
//move circles
function RandomObjectMover(obj, container) {
this.$object = obj;
this.$container = container;
this.container_is_window = container === window;
this.pixels_per_second = 250;
this.current_position = { x: 0, y: 0 };
this.is_running = false;
}
// Set the speed of movement in Pixels per Second.
RandomObjectMover.prototype.setSpeed = function(pxPerSec) {
this.pixels_per_second = pxPerSec;
}
RandomObjectMover.prototype._getContainerDimensions = function() {
if (this.$container === window) {
return { 'height' : this.$container.innerHeight, 'width' : this.$container.innerWidth };
} else {
return { 'height' : this.$container.clientHeight, 'width' : this.$container.clientWidth };
}
}
RandomObjectMover.prototype._generateNewPosition = function() {
// Get container dimensions minus div size
var containerSize = this._getContainerDimensions();
var availableHeight = containerSize.height - this.$object.clientHeight;
var availableWidth = containerSize.width - this.$object.clientHeight;
// Pick a random place in the space
var y = Math.floor(Math.random() * availableHeight);
var x = Math.floor(Math.random() * availableWidth);
return { x: x, y: y };
}
RandomObjectMover.prototype._calcDelta = function(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
var dist = Math.sqrt( dx*dx + dy*dy );
return dist;
}
RandomObjectMover.prototype._moveOnce = function() {
// Pick a new spot on the page
var next = this._generateNewPosition();
// How far do we have to move?
var delta = this._calcDelta(this.current_position, next);
// Speed of this transition, rounded to 2DP
var speed = Math.round((delta / this.pixels_per_second) * 100) / 100;
//console.log(this.current_position, next, delta, speed);
this.$object.style.transition='transform '+speed+'s linear';
this.$object.style.transform='translate3d('+next.x+'px, '+next.y+'px, 0)';
// Save this new position ready for the next call.
this.current_position = next;
};
RandomObjectMover.prototype.start = function() {
if (this.is_running) {
return;
}
// Make sure our object has the right css set
this.$object.willChange = 'transform';
this.$object.pointerEvents = 'auto';
this.boundEvent = this._moveOnce.bind(this)
// Bind callback to keep things moving
this.$object.addEventListener('transitionend', this.boundEvent);
// Start it moving
this._moveOnce();
this.is_running = true;
}
RandomObjectMover.prototype.stop = function() {
if (!this.is_running) {
return;
}
this.$object.removeEventListener('transitionend', this.boundEvent);
this.is_running = false;
}
// Init it
var x = new RandomObjectMover(document.querySelector(".circle"), window);
// Start it off
x.start();
.circle {
width: 35px;
height: 35px;
border-radius: 35px;
background-color: #ffffff;
border: 3px solid purple;
position: absolute;
}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="circle"></div>
<script src="app.js"></script>
</body>
</html>

canvas not responding to touch when inside a div on ios safari

I'm trying to get a nice knob/dial that can be rotated to send a value back to the server via websockets.
The basic premise works well, I got the code from the web.
I am trying to modify the code so that I get a prettier knob. I've been successful by placing the canvas inside a couple of divs which display static images, while the canvas rotates a translucent image in response to mouse/touch events.
The additions that I made to the code work well on the desktop (I'm running Firefox 45.0.2) but do not work at all on an iPad (Safari, iOS 9.3.5) and only partially on an iPhone (iOS 10.2.1)
On the iPhone, the knob rotates in the opposite direction to that expected, and often only horizontal movement will start the knob rotating.
I'm not using (nor do I want to use) any libraries such as jquery.
The code below will work as is. However, removing the comment marks in the body section will cause the problems I indicated.
(Oh and to forestall any comments, the black background and odd text colour is just there to so that you can see the translucent element without the static backgrounds)
I'm not at all experienced with jscript and can only just manage to follow what the code is doing at the moment. (one of the reasons I don't want to use additional libraries)
I suspect that the problem lies with how the touch event coordinates are interpreted, but I can't test them in any way.
Any help or suggestions would be appreciated.
HTML Code:
<!DOCTYPE html>
<html>
<head>
<title>Stepper example</title>
<meta name="viewport" content="width=device-width, initial-scale=0.7">
<style>
body {
text-align: center; background-color: black;
color: red
}
.container{
position: relative;
background: url(step_background.png);
width: 480px;
height: 480px;
margin: auto;
z-index:1;
}
.knob{
position: relative;
top: 59px;
background: url(knob_bg.png);
width: 362px;
height:362px;
margin:auto;
z-index:2;
}
#stepper{
position: relative;
}
</style>
<script>
var MIN_TOUCH_RADIUS = 20;
var MAX_TOUCH_RADIUS = 200;
var CANVAS_WIDTH = 362, CANVAS_HEIGHT = 362;
var PIVOT_X = 181, PIVOT_Y = 181;
var plate_angle = 0;
var plate_img = new Image();
var click_state = 0;
var last_angle_pos = 0;
var mouse_xyra = {x:0, y:0, r:0.0, a:0.0};
var ws;
plate_img.src = "knob_fg.png";
function init() {
var stepper = document.getElementById("stepper");
var ctx = stepper.getContext("2d");
stepper.width = CANVAS_WIDTH;
stepper.height = CANVAS_HEIGHT;
stepper.addEventListener("touchstart", mouse_down);
stepper.addEventListener("touchend", mouse_up);
stepper.addEventListener("touchmove", mouse_move);
stepper.addEventListener("mousedown", mouse_down);
stepper.addEventListener("mouseup", mouse_up);
stepper.addEventListener("mousemove", mouse_move);
ctx.translate(PIVOT_X, PIVOT_Y);
rotate_plate(plate_angle);
}
function connect_onclick() {
if(ws == null) {
ws = new WebSocket('ws://'+ window.location.hostname + ':81/', ['arduino']);
document.getElementById("ws_state").innerHTML = "CONNECTING";
ws.onopen = ws_onopen;
ws.onclose = ws_onclose;
ws.onmessage = ws_onmessage;
ws.onerror = function(){ alert("websocket error " + this.url) };
}
else
ws.close();
}
function ws_onopen() {
document.getElementById("ws_state").innerHTML = "<font color='blue'>CONNECTED</font>";
document.getElementById("bt_connect").innerHTML = "Disconnect";
rotate_plate(plate_angle);
}
function ws_onclose() {
document.getElementById("ws_state").innerHTML = "<font color='gray'>CLOSED</font>";
document.getElementById("bt_connect").innerHTML = "Connect";
ws.onopen = null;
ws.onclose = null;
ws.onmessage = null;
ws = null;
rotate_plate(plate_angle);
}
function ws_onmessage(e_msg) {
e_msg = e_msg || window.event; // MessageEvent
plate_angle = Number(e_msg.data);
rotate_plate(plate_angle);
//alert("msg : " + e_msg.data);
}
function rotate_plate(angle) {
var stepper = document.getElementById("stepper");
var ctx = stepper.getContext("2d");
ctx.clearRect(-PIVOT_X, -PIVOT_Y, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.rotate(-angle / 180 * Math.PI);
ctx.drawImage(plate_img, -PIVOT_X, -PIVOT_Y);
ctx.rotate(angle / 180 * Math.PI);
/*
Currently, the angle displayed and sent as a message appears to be set such that movement in a clockwise direction
reports a negative number. Needs to be looked at, probably by changing "angle.toFixed" to "-angle.toFixed"
*/
if(ws && (ws.readyState == 1))
ws.send(plate_angle.toFixed(4) + "\r\n");
ws_angle = document.getElementById("ws_angle");
ws_angle.innerHTML = angle.toFixed(1);
}
function check_update_xyra(event, mouse_xyra) {
var x, y, r, a;
var min_r, max_r, width;
if(event.touches) {
var touches = event.touches;
x = (touches[0].pageX - touches[0].target.offsetLeft) - PIVOT_X;
y = PIVOT_Y - (touches[0].pageY - touches[0].target.offsetTop);
}
else {
x = event.offsetX - PIVOT_X;
y = PIVOT_Y - event.offsetY;
}
/* cartesian to polar coordinate conversion */
r = Math.sqrt(x * x + y * y);
a = Math.atan2(y, x);
mouse_xyra.x = x;
mouse_xyra.y = y;
mouse_xyra.r = r;
mouse_xyra.a = a;
if((r >= MIN_TOUCH_RADIUS) && (r <= MAX_TOUCH_RADIUS))
return true;
else
return false;
}
function mouse_down(event) {
if(event.target == stepper)
event.preventDefault();
if(event.touches && (event.touches.length > 1))
click_state = event.touches.length;
if(click_state > 1)
return;
if(check_update_xyra(event, mouse_xyra)) {
click_state = 1;
last_angle_pos = mouse_xyra.a / Math.PI * 180.0;
}
}
function mouse_up() {
click_state = 0;
}
function mouse_move(event) {
var angle_pos, angle_offset;
if(event.touches && (event.touches.length > 1))
click_state = event.touches.length;
if(!click_state || (click_state > 1))
return;
if(!check_update_xyra(event, mouse_xyra)) {
click_state = 0;
return;
}
event.preventDefault();
angle_pos = mouse_xyra.a / Math.PI * 180.0;
if(angle_pos < 0.0)
angle_pos = angle_pos + 360.0;
angle_offset = angle_pos - last_angle_pos;
last_angle_pos = angle_pos;
if(angle_offset > 180.0)
angle_offset = -360.0 + angle_offset;
else
if(angle_offset < -180.0)
angle_offset = 360 + angle_offset;
plate_angle += angle_offset;
rotate_plate(plate_angle);
}
window.onload = init;
</script>
</head>
<body>
<h2>
Smart Expansion / Stepper Motor<br><br>
Angle <font id="ws_angle" color="blue">0</font><br><br>
<!--
<div class="container">
<div class="knob">
-->
<canvas id="stepper"></canvas>
<!--
</div>
</div>
-->
<br><br>
WebSocket <font id="ws_state" color="gray">CLOSED</font>
</h2>
<p><button id="bt_connect" type="button" onclick="connect_onclick();">Connect</button></p>
</body>
</html>
I might need to add an additional comment to give the link to the backgound image
knob_bg.png
knob_fg.png
So, after managing to find out how to debug html on an ios device via firefox on windows, I have managed to find out what was causing my code to fail.
The problem was in the function check_update_xyra(event, mouse_xyra)
Specifically the lines :
x = (touches[0].pageX - touches[0].target.offsetLeft) - PIVOT_X;
y = PIVOT_Y - (touches[0].pageY - touches[0].target.offsetTop);
The target.offsetxxx was returning a value of 0. This made the radian value (r) to be out of bounds which caused the function to return false, or in the case of the iPhone caused the touch event to behave strangely.
The reason for for the offsets coming back as 0 was because I did not factor in the fact that they provided the offset from the targets parent only, not the document as a whole.
I managed to fix this by adding some code to add the offsets for all parent elements then used that sum to calculate new x and y coordinates.
My code change follows.
However, if anyone has a more elegant method of calculating the offsets, I would appreciate it.
Cheers...
function check_update_xyra(event, mouse_xyra) {
var x, y, r, a;
var tgtoffleft = 0;
var tgtofftop = 0;
var min_r, max_r, width;
if(event.touches) {
var touches = event.touches;
// Bit of code to calculate the actual Left and Top offsets by adding offsets
// of each parent back through the hierarchy
var tgt = event.touches[0].target;
while (tgt) {
tgtoffleft = tgtoffleft + tgt.offsetLeft;
tgtofftop = tgtofftop + tgt.offsetTop;
tgt = tgt.offsetParent;
}
// x = (touches[0].pageX - touches[0].target.offsetLeft) - PIVOT_X;
// y = PIVOT_Y - (touches[0].pageY - touches[0].target.offsetTop);
x = (touches[0].pageX - tgtoffleft) - PIVOT_X;
y = PIVOT_Y - (touches[0].pageY - tgtofftop);
}
else {
x = event.offsetX - PIVOT_X;
y = PIVOT_Y - event.offsetY;
}
/* cartesian to polar coordinate conversion */
r = Math.sqrt(x * x + y * y);
a = Math.atan2(y, x);
mouse_xyra.x = x;
mouse_xyra.y = y;
mouse_xyra.r = r;
mouse_xyra.a = a;
if((r >= MIN_TOUCH_RADIUS) && (r <= MAX_TOUCH_RADIUS))
return true;
else
return false;
}

Jump/scroll to #id within a scroll element [duplicate]

I have a div that has overflow: scroll and I have some elements inside the DIV that are hidden. On click of a button on the page, I want to make the DIV scroll to a particular element inside the DIV.
How do I achieve this?
You need to read the offsetTop property of the div you need to scroll to and then set that offset to the scrollTop property of the container div. Bind this function the event you want to :
function scrollToElementD(){
var topPos = document.getElementById('inner-element').offsetTop;
document.getElementById('container').scrollTop = topPos-10;
}
div {
height: 200px;
width: 100px;
border: 1px solid black;
overflow: auto;
}
p {
height: 80px;
background: blue;
}
#inner-element {
background: red;
}
<div id="container"><p>A</p><p>B</p><p>C</p><p id="inner-element">D</p><p>E</p><p>F</p></div>
<button onclick="scrollToElementD()">SCROLL TO D</button>
function scrollToElementD(){
var topPos = document.getElementById('inner-element').offsetTop;
document.getElementById('container').scrollTop = topPos-10;
}
Fiddle : http://jsfiddle.net/p3kar5bb/322/ (courtesy #rofrischmann)
Just improved it by setting a smooth auto scrolling inside a list contained in a div
https://codepen.io/rebosante/pen/eENYBv
var topPos = elem.offsetTop
document.getElementById('mybutton').onclick = function () {
console.log('click')
scrollTo(document.getElementById('container'), topPos-10, 600);
}
function scrollTo(element, to, duration) {
var start = element.scrollTop,
change = to - start,
currentTime = 0,
increment = 20;
var animateScroll = function(){
currentTime += increment;
var val = Math.easeInOutQuad(currentTime, start, change, duration);
element.scrollTop = val;
if(currentTime < duration) {
setTimeout(animateScroll, increment);
}
};
animateScroll();
}
//t = current time
//b = start value
//c = change in value
//d = duration
Math.easeInOutQuad = function (t, b, c, d) {
t /= d/2;
if (t < 1) return c/2*t*t + b;
t--;
return -c/2 * (t*(t-2) - 1) + b;
};
I guess it may help someone :)
Here's a simple pure JavaScript solution that works for a target Number (value for scrollTop), target DOM element, or some special String cases:
/**
* target - target to scroll to (DOM element, scrollTop Number, 'top', or 'bottom'
* containerEl - DOM element for the container with scrollbars
*/
var scrollToTarget = function(target, containerEl) {
// Moved up here for readability:
var isElement = target && target.nodeType === 1,
isNumber = Object.prototype.toString.call(target) === '[object Number]';
if (isElement) {
containerEl.scrollTop = target.offsetTop;
} else if (isNumber) {
containerEl.scrollTop = target;
} else if (target === 'bottom') {
containerEl.scrollTop = containerEl.scrollHeight - containerEl.offsetHeight;
} else if (target === 'top') {
containerEl.scrollTop = 0;
}
};
And here are some examples of usage:
// Scroll to the top
var scrollableDiv = document.getElementById('scrollable_div');
scrollToTarget('top', scrollableDiv);
or
// Scroll to 200px from the top
var scrollableDiv = document.getElementById('scrollable_div');
scrollToTarget(200, scrollableDiv);
or
// Scroll to targetElement
var scrollableDiv = document.getElementById('scrollable_div');
var targetElement= document.getElementById('target_element');
scrollToTarget(targetElement, scrollableDiv);
You need a ref to the div you wish to scroll to inner-div and a ref to the scrollable div scrollable-div:
const scrollToDiv = () => {
const innerDivPos = document.getElementById('inner-div').offsetTop
document
.getElementById('scrollable-div')
.scrollTo({ top: innerDivPos, behavior: 'smooth' })
}
<div id="scrollable-div" style="height:100px; overflow-y:auto;">
<button type="button" style="margin-bottom:500px" onclick="scrollToDiv()">Scroll To Div</button>
<div id="inner-div">Inner Div</div>
</div>

Overlapping range inputs. On click change input with closest value

I have two overlapping range inputs, this creates a multi range input effect.
I want it so that whenever a click is made on either of these, the input with the closest value to the newly clicked value, is changed. Not entirely sure how to go about this.
How could I do this?
(function() {
"use strict";
var supportsMultiple = self.HTMLInputElement && "valueLow" in HTMLInputElement.prototype;
var descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
self.multirange = function(input) {
if (supportsMultiple || input.classList.contains("multirange")) {
return;
}
var values = input.getAttribute("value").split(",");
var max = +input.max || 100;
var ghost = input.cloneNode();
input.classList.add("multirange", "original");
ghost.classList.add("multirange", "ghost");
input.value = values[0] || max / 2;
ghost.value = values[1] || max / 2;
input.parentNode.insertBefore(ghost, input.nextSibling);
Object.defineProperty(input, "originalValue", descriptor.get ? descriptor : {
// Dang you Safari >:(
get: function() {
return this.value;
},
set: function(v) {
this.value = v;
}
});
Object.defineProperties(input, {
valueLow: {
get: function() {
return Math.min(this.originalValue, ghost.value);
},
set: function(v) {
this.originalValue = v;
},
enumerable: true
},
valueHigh: {
get: function() {
return Math.max(this.originalValue, ghost.value);
},
set: function(v) {
ghost.value = v;
},
enumerable: true
}
});
if (descriptor.get) {
// Again, fuck you Safari
Object.defineProperty(input, "value", {
get: function() {
return this.valueLow + "," + this.valueHigh;
},
set: function(v) {
var values = v.split(",");
this.valueLow = values[0];
this.valueHigh = values[1];
},
enumerable: true
});
}
function update() {
ghost.style.setProperty("--low", input.valueLow * 100 / max + 1 + "%");
ghost.style.setProperty("--high", input.valueHigh * 100 / max - 1 + "%");
}
input.addEventListener("input", update);
ghost.addEventListener("input", update);
update();
}
multirange.init = function() {
Array.from(document.querySelectorAll("input[type=range][multiple]:not(.multirange)")).forEach(multirange);
}
if (document.readyState == "loading") {
document.addEventListener("DOMContentLoaded", multirange.init);
} else {
multirange.init();
}
})();
#supports (--css: variables) {
input[type="range"].multirange {
-webkit-appearance: none;
padding: 0;
margin: 0;
display: inline-block;
vertical-align: top;
width: 250px;
margin-top: 50px;
margin-left: 50px;
background: lightblue;
}
input[type="range"].multirange.original {
position: absolute;
}
input[type="range"].multirange.original::-webkit-slider-thumb {
position: relative;
z-index: 2;
}
input[type="range"].multirange.original::-moz-range-thumb {
transform: scale(1);
/* FF doesn't apply position it seems */
G z-index: 1;
}
input[type="range"].multirange::-moz-range-track {
border-color: transparent;
/* needed to switch FF to "styleable" control */
}
input[type="range"].multirange.ghost {
position: relative;
background: var(--track-background);
--track-background: linear-gradient(to right, transparent var(--low), var(--range-color) 0, var(--range-color) var(--high), transparent 0) no-repeat 0 45% / 100% 40%;
--range-color: hsl(190, 80%, 40%);
}
input[type="range"].multirange.ghost::-webkit-slider-runnable-track {
background: var(--track-background);
}
input[type="range"].multirange.ghost::-moz-range-track {
background: var(--track-background);
}
}
<input type="range" multiple value="10,80" />
You'll have to capture a mouse event on the element and calculate how close it is to the high marker vs. the low marker and decide which one to update based on that. Also, because these are two stacked input elements, you'll probably have to pass the event to the low range input manually.
Here's my go at creating such a function:
function passClick(evt) {
// Are the ghost and input elements inverted? (ghost is lower range)
var isInverted = input.valueLow == ghost.value;
// Find the horizontal position that was clicked (as a percentage of the element's width)
var clickPoint = evt.offsetX / this.offsetWidth;
// Map the percentage to a value in the range (note, assumes a min value of 0)
var clickValue = max * clickPoint;
// Get the distance to both high and low values in the range
var highDiff = Math.abs(input.valueHigh - clickValue);
var lowDiff = Math.abs(input.valueLow - clickValue);
if (lowDiff < highDiff && !isInverted || (isInverted && lowDiff > highDiff)) {
// The low value is closer to the click point than the high value
// We should update the low value input
var passEvent = new MouseEvent("mousedown", {screenX: evt.screenX, clientX: evt.clientX});
// Pass a new event to the low "input" element (which is obscured by the
// higher "ghost" element, and doesn't get mouse events outside the drag handle
input.dispatchEvent(passEvent);
// The higher "ghost" element should not respond to this event
evt.preventDefault();
return false;
}
else {
console.log("move ghost");
// The high value is closer to the click point than the low value
// The default behavior is appropriate, so do nuthin
}
}
ghost.addEventListener("mousedown", passClick);
I put this code immediately above the input.addEventListener("input", update); line in your sample, and it seems to work. See my fiddle.
Some provisos though:
I only tested in Chrome. IE might have some trouble based on how I replicated the event. It may use a mechanism other than dispatchEvent... like fireEvent or something.
Initially I coded it assuming that the "ghost" element always kept track of the high range. I've since updated things to invert the event dispatching when the ghost element has the lower value--but I sped through it.
Here's something simple you could use. Although you might want to customize the style.
I am altering the z-index of the slider element based upon its proximity to the cursor.
JSFiddle
HTML
<input id='a' type='range' />
<input id='b' type='range' />
<label role='info'></label>
JS
var a = document.getElementById('a');
var b = document.getElementById('b');
a.onmousemove = function(e) {
MouseMove.call(a, e);
};
b.onmousemove = function(e) {
MouseMove.call(b, e);
};
var MouseMove = function(eventArg)
{
var max = parseInt(a.max),
min = parseInt(a.min),
diff = max - min,
clickPoint = eventArg.offsetX / a.offsetWidth,
clickPointVal = parseInt(diff * clickPoint) + min;
/* absolute distance from respective slider values */
var da = Math.abs(a.value - clickPointVal),
db = Math.abs(b.value - clickPointVal);
// Making the two sliders appear above one another only when no mouse button is pressed, this condition may be removed at will
if (!eventArg.buttons)
{
if (da < db)
{
a.style.zIndex = 2;
b.style.zIndex = 1;
}
else if (db < da)
{
b.style.zIndex = 2;
a.style.zIndex = 1;
}
}
document.querySelector('label').innerHTML = 'Red: ' + a.value + ', Green: ' + b.value + ', X: ' + eventArg.clientX;
}
CSS
input {
margin: 0px;
position: absolute;
left: 0px;
}
label {
display: inline-block;
margin-top: 100px;
}
#a {
z-index: 2;
}
#b {
z-index: 1;
}

Raphael.js drag with scale causes weird jumping behavior

I'm trying to resize/scale an image using Raphael.js's built in drag method, but I'm getting some weird behavior.
Here is the jsfiddle: http://jsfiddle.net/charleshimmer/5pdyy/1/
Use the right or bottom right corner to resize the image. You will see some weird behavior with it jumping and skipping using the scale method. Anybody have any idea why?
I can get it to resize smoothing by updating the image's width and height, but then the aspect ratio is off. Using image.scale, the aspect ratio is maintained, but then it jumps all over the place.
HTML
<html>
<head>
<title>Photo Test</title>
</head>
<body>
<div id="editor"></div>
<img id="image"
src="http://www.pyrblu.com/assets/launchpad_resources/demo.jpg"
style="display:none"
>
</body>
</html>
CSS
svg
{
border: 1px solid red;
background:#fff;
border-radius: 45px;
}
JavaScript
var Editor = {},
ctFactor = 7;
// create Raphael canvas
Editor.paper = Raphael('editor', 582, 514.8);
// wait for image to load
$("#image").load(function(){
Editor.image = Editor.paper.image("http://www.pyrblu.com/assets/launchpad_resources/demo.jpg", 25, 25, 282, 465.2);
Editor.image.drag(Editor.dragging, Editor.dragStart, Editor.dragEnd);
Editor.image.ready = true;
Editor.image.mousemove(function (e) {
// only do this if the user isn't currently moving / resizing image
if( ! this.ready){
return;
}
var side = Editor.sideDection(e, this);
// if the user's mouse is along the edge we want resize
if(side){
Editor.image.state = 'resizable';
}
// else it's towards the middle and we want to move
else{
Editor.image.state = 'movable';
}
var cursor = (side) ? side + '-resize' : 'move';
this.attr('cursor', cursor);
});
});
Editor.sideDection = function(event, ct){
// check north side
var directions = {
n: Math.abs(event.offsetY - ct.attr('y')) <= ctFactor,
s: Math.abs(event.offsetY - (ct.attr('height') + ct.attr('y'))) <= ctFactor,
e: Math.abs(event.offsetX - (ct.attr('width') + ct.attr('x'))) <= ctFactor,
w: Math.abs(event.offsetX - ct.attr('x')) <= ctFactor
},
side = '';
// loop through all 4 sides and concate the ones that are true
for(var key in directions) {
if(directions.hasOwnProperty(key)){
if(directions[key]){
side = side + key;
}
}
}
return side;
};
Editor.dragStart = function () {
console.log('at start');
// grab original x, y coords
this.ox = this.attr("x");
this.oy = this.attr("y");
// toggle user is doing something
// so other actions are blocked
this.ready = false;
this.animate({opacity: .65}, 500, ">");
};
Editor.dragging = function (dx, dy, x, y, e) {
console.log('at dragging');
if(this.state === 'movable'){
// this does the actual moving of the object
this.attr({x: this.ox + dx, y: this.oy + dy});
}
// we are resizing then
else{
var diff = (x - this.ox) - this.attr('width'),
xratio = 1 + diff / this.attr('width'),
yratio = 1 + diff / this.attr('height');
console.log('diff: ', diff, 'xratio: ', xratio, 'yratio: ', yratio);
//resize image, update both height and width to keep aspect ratio
// this.attr({
// 'width': this.attr('width') * xratio,
// 'height': this.attr('height') * yratio
// });
this.scale(xratio, xratio, 0, 0);
//console.log('h: ', this.attr('height'), 'w: ', this.attr('width'), 'r', this.attr('width') / this.attr('height'));
}
};
Editor.dragEnd = function () {
this.ready = true;
this.animate({opacity: 1}, 500, ">");
};

Categories