Horay Horay - 4 months ago 14
CSS Question

Make cursor stay in boundary

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>




Answer

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:

  1. 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.

  2. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.