Javascript - Interactive Color Picker
| This tutorial builds on a number of other javascript articles on this site, including Javascript Objects - A Useful Example (which covers some basics about how objects work in javascript - and we will be using the color object created in that tutorial), Javascript - Working With Events (which covers how to work with events and event objects), and Javascript - Draggable Elements (which covers how to do dragging in javascript). So, if you are relatively new to javascript, I would suggest at least skimming those before you continue. |
Below, you can see the result of what we will create today. You can adjust the hue by dragging the arrows on the color bar, and can adjust saturation and value by dragging around the small circle in the gradient square. The hex, RGB, and HSV values are output in the textboxes on the right, as well as a div containing the color picked. You can also enter values in the hex, RGB and HSV text boxes, and the correct color will show up (and the arrows and circle will move to the correct position). What we are going to create today is not a package, but it could be made into one very easily. Play around a bit- I know you want to.
| Hex: | |
| Red: | |
| Green: | |
| Blue: | |
| Hue: | |
| Saturation: | |
| Value: |
Ok, now that you've had your fun, lets dive right in. Right below, we have the html required to create the color picker:
<div style="position:relative;height:286px;width:531px;
border:1px solid black;">
<div id="gradientBox" style="cursor:crosshair;top:15px;
position:absolute;left:15px;width:256px;height:256px;">
<img id="gradientImg"
style="display:block;width:256px;height:256px;"
src="/Color_Picker/color_picker_gradient.png" />
<img id="circle"
style="position:absolute;height:11px;width:11px;"
src="/Color_Picker/color_picker_circle.gif" />
</div>
<div id="hueBarDiv" style="position:absolute;left:310px;
width:35px;height:256px;top:15px;">
<img style="position:absolute;height:256px;
width:19px;left:8px;"
src="/Color_Picker/color_picker_bar.png" />
<img id="arrows" style="position:absolute;
height:9px;width:35px;left:0px;"
src="/Color_Picker/color_picker_arrows.gif" />
</div>
<div style="position:absolute;left:370px;width:145px;
height:256px;top:15px;">
<div style="position:absolute;border: 1px solid black;
height:50px;width:145px;top:0px;left:0px;">
<div id="quickColor" style="position:absolute;
height:50px;width:73px;top:0px;left:0px;"></div>
<div id="staticColor" style="position:absolute;
height:50px;width:72px;top:0px;left:73px;"></div>
</div>
<br />
<table width="100%" style="position:absolute;top:55px;">
<tr>
<td>Hex: </td>
<td>
<input size="8" type="text" id="hexBox"
onchange="hexBoxChanged();" />
</td>
</tr>
<tr>
<td>Red: </td>
<td>
<input size="8" type="text" id="redBox"
onchange="redBoxChanged();" />
</td>
</tr>
<tr>
<td>Green: </td>
<td>
<input size="8" type="text" id="greenBox"
onchange="greenBoxChanged();" />
</td>
</tr>
<tr>
<td>Blue: </td>
<td>
<input size="8" type="text" id="blueBox"
onchange="blueBoxChanged();" />
</td>
</tr>
<tr>
<td>Hue: </td>
<td>
<input size="8" type="text" id="hueBox"
onchange="hueBoxChanged();" />
</td>
</tr>
<tr>
<td>Saturation: </td>
<td>
<input size="8" type="text" id="saturationBox"
onchange="saturationBoxChanged();" />
</td>
</tr>
<tr>
<td>Value: </td>
<td>
<input size="8" type="text" id="valueBox"
onchange="valueBoxChanged();" />
</td>
</tr>
</table>
</div>
</div>
border:1px solid black;">
<div id="gradientBox" style="cursor:crosshair;top:15px;
position:absolute;left:15px;width:256px;height:256px;">
<img id="gradientImg"
style="display:block;width:256px;height:256px;"
src="/Color_Picker/color_picker_gradient.png" />
<img id="circle"
style="position:absolute;height:11px;width:11px;"
src="/Color_Picker/color_picker_circle.gif" />
</div>
<div id="hueBarDiv" style="position:absolute;left:310px;
width:35px;height:256px;top:15px;">
<img style="position:absolute;height:256px;
width:19px;left:8px;"
src="/Color_Picker/color_picker_bar.png" />
<img id="arrows" style="position:absolute;
height:9px;width:35px;left:0px;"
src="/Color_Picker/color_picker_arrows.gif" />
</div>
<div style="position:absolute;left:370px;width:145px;
height:256px;top:15px;">
<div style="position:absolute;border: 1px solid black;
height:50px;width:145px;top:0px;left:0px;">
<div id="quickColor" style="position:absolute;
height:50px;width:73px;top:0px;left:0px;"></div>
<div id="staticColor" style="position:absolute;
height:50px;width:72px;top:0px;left:73px;"></div>
</div>
<br />
<table width="100%" style="position:absolute;top:55px;">
<tr>
<td>Hex: </td>
<td>
<input size="8" type="text" id="hexBox"
onchange="hexBoxChanged();" />
</td>
</tr>
<tr>
<td>Red: </td>
<td>
<input size="8" type="text" id="redBox"
onchange="redBoxChanged();" />
</td>
</tr>
<tr>
<td>Green: </td>
<td>
<input size="8" type="text" id="greenBox"
onchange="greenBoxChanged();" />
</td>
</tr>
<tr>
<td>Blue: </td>
<td>
<input size="8" type="text" id="blueBox"
onchange="blueBoxChanged();" />
</td>
</tr>
<tr>
<td>Hue: </td>
<td>
<input size="8" type="text" id="hueBox"
onchange="hueBoxChanged();" />
</td>
</tr>
<tr>
<td>Saturation: </td>
<td>
<input size="8" type="text" id="saturationBox"
onchange="saturationBoxChanged();" />
</td>
</tr>
<tr>
<td>Value: </td>
<td>
<input size="8" type="text" id="valueBox"
onchange="valueBoxChanged();" />
</td>
</tr>
</table>
</div>
</div>
The stuff in the bottom half is really simple - it is just a table for the text boxes and their labels. Of course, each of them has a javascript function attached to their
onchange event - but we will go into what those function do later. For now, we want to take a look at the top part - the divs that define the gradient box and the hue bar. The gradient box is just a div with a 256x256 partially transparent image inside of it. This partially transparent image is what gives the gradient - the upper right corner is completely transparent, the bottom right is solid white, and the left is solid black. Setting the background color on this div creates the appearance of a saturation-value gradient in that background color. This div also contains the circle image that represents the selected point.The hue bar doesn't require any gradient tricks - it is just a standard hue bar, where the bottom is a hue of 0 and the top is a hue of 359. The arrows are a single image sitting on top of the bar, where the center of the arrow image is transparent. The other divs represent the color box on the upper right of the color picker - it is split into two divs, one which represents the current color that you are dragging over (
quickColor), and the other represents the last color that you picked (staticColor). When you finish a drag operation - either on the gradient or the bar - both divs will update to show the color that the drag operation completed on.So now that you have an idea of what is where in terms of the html elements, lets turn to the javascript. Below, we have the initialization code for the color picker:
fixGradientImg();
var currentColor = Colors.ColorFromRGB(64,128,128);
new dragObject("arrows", "hueBarDiv", arrowsLowBounds,
arrowsUpBounds, arrowsDown, arrowsMoved, endMovement);
new dragObject("circle", "gradientBox", circleLowBounds,
circleUpBounds, circleDown, circleMoved, endMovement);
colorChanged('box');
var currentColor = Colors.ColorFromRGB(64,128,128);
new dragObject("arrows", "hueBarDiv", arrowsLowBounds,
arrowsUpBounds, arrowsDown, arrowsMoved, endMovement);
new dragObject("circle", "gradientBox", circleLowBounds,
circleUpBounds, circleDown, circleMoved, endMovement);
colorChanged('box');
Obviously, not much of that makes sense at the moment, but we shall take a look one line at a time. The first line, the call to
fixGradientImage, is what makes this color picker work in Internet Explorer 6. IE6, as you probably know, does not support transparency on pngs by default - and so we have to apply this hack to get the gradient image to appear correctly. The function looks like the following:function fixGradientImg()
{
fixPNG(document.getElementById("gradientImg"));
}
function fixPNG(myImage)
{
if(!document.body.filters)
return;
var arVersion = navigator.appVersion.split("MSIE");
var version = parseFloat(arVersion[1]);
if(version <5.5 || version>= 7)
return;
var imgID = (myImage.id) ? "id='" + myImage.id + "' " : ""
var imgStyle = "display:inline-block;" + myImage.style.cssText
var strNewHTML = "<span " + imgID
+ " style=\"" + "width:" + myImage.width
+ "px; height:" + myImage.height
+ "px;" + imgStyle + ";"
+ "filter:progid:DXImageTransform."
+ "Microsoft.AlphaImageLoader"
+ "(src=\'" + myImage.src + "\', "
+ "sizingMethod='scale');\"></span>"
myImage.outerHTML = strNewHTML
}
{
fixPNG(document.getElementById("gradientImg"));
}
function fixPNG(myImage)
{
if(!document.body.filters)
return;
var arVersion = navigator.appVersion.split("MSIE");
var version = parseFloat(arVersion[1]);
if(version <5.5 || version>= 7)
return;
var imgID = (myImage.id) ? "id='" + myImage.id + "' " : ""
var imgStyle = "display:inline-block;" + myImage.style.cssText
var strNewHTML = "<span " + imgID
+ " style=\"" + "width:" + myImage.width
+ "px; height:" + myImage.height
+ "px;" + imgStyle + ";"
+ "filter:progid:DXImageTransform."
+ "Microsoft.AlphaImageLoader"
+ "(src=\'" + myImage.src + "\', "
+ "sizingMethod='scale');\"></span>"
myImage.outerHTML = strNewHTML
}
There is plenty of info on the web about what exactly this hack does in IE6, so I won't go over that here. Suffice it to say, where we had a opaque image, we now have a span with a partially transparent png.
The next line of initialization code (
var currentColor = Colors.ColorFromRGB(64,128,128);) sets up the global object that will always hold the currently selected color, and initializes it to a value smack dab in the middle of the spectrum. This is probably a good time to re-introduce the color object from Javascript Objects - A Useful Example. And here it is in all its glory:var Colors = new function()
{
this.ColorFromHSV = function(hue, sat, val)
{
var color = new Color();
color.SetHSV(hue,sat,val);
return color;
}
this.ColorFromRGB = function(r, g, b)
{
var color = new Color();
color.SetRGB(r,g,b);
return color;
}
this.ColorFromHex = function(hexStr)
{
var color = new Color();
color.SetHexString(hexStr);
return color;
}
function Color()
{
//Stored as values between 0 and 1
var red = 0;
var green = 0;
var blue = 0;
//Stored as values between 0 and 360
var hue = 0;
//Strored as values between 0 and 1
var saturation = 0;
var value = 0;
this.SetRGB = function(r, g, b)
{
if (isNaN(r) || isNaN(g) || isNaN(b))
return false;
r = r/255.0;
red = r> 1 ? 1 : r <0 ? 0 : r;
g = g/255.0;
green = g> 1 ? 1 : g <0 ? 0 : g;
b = b/255.0;
blue = b> 1 ? 1 : b <0 ? 0 : b;
calculateHSV();
return true;
}
this.Red = function()
{ return Math.round(red*255); }
this.Green = function()
{ return Math.round(green*255); }
this.Blue = function()
{ return Math.round(blue*255); }
this.SetHSV = function(h, s, v)
{
if (isNaN(h) || isNaN(s) || isNaN(v))
return false;
hue = (h>= 360) ? 359.99 : (h <0) ? 0 : h;
saturation = (s> 1) ? 1 : (s <0) ? 0 : s;
value = (v> 1) ? 1 : (v <0) ? 0 : v;
calculateRGB();
return true;
}
this.Hue = function()
{ return hue; }
this.Saturation = function()
{ return saturation; }
this.Value = function()
{ return value; }
this.SetHexString = function(hexString)
{
if(hexString == null || typeof(hexString) != "string")
return false;
if (hexString.substr(0, 1) == '#')
hexString = hexString.substr(1);
if(hexString.length != 6)
return false;
var r = parseInt(hexString.substr(0, 2), 16);
var g = parseInt(hexString.substr(2, 2), 16);
var b = parseInt(hexString.substr(4, 2), 16);
return this.SetRGB(r,g,b);
}
this.HexString = function()
{
var rStr = this.Red().toString(16);
if (rStr.length == 1)
rStr = '0' + rStr;
var gStr = this.Green().toString(16);
if (gStr.length == 1)
gStr = '0' + gStr;
var bStr = this.Blue().toString(16);
if (bStr.length == 1)
bStr = '0' + bStr;
return ('#' + rStr + gStr + bStr).toUpperCase();
}
this.Complement = function()
{
var newHue = (hue>= 180) ? hue - 180 : hue + 180;
var newVal = (value * (saturation - 1) + 1);
var newSat = (value*saturation) / newVal;
var newColor = new Color();
newColor.SetHSV(newHue, newSat, newVal);
return newColor;
}
function calculateHSV()
{
var max = Math.max(Math.max(red, green), blue);
var min = Math.min(Math.min(red, green), blue);
value = max;
saturation = 0;
if(max != 0)
saturation = 1 - min/max;
hue = 0;
if(min == max)
return;
var delta = (max - min);
if (red == max)
hue = (green - blue) / delta;
else if (green == max)
hue = 2 + ((blue - red) / delta);
else
hue = 4 + ((red - green) / delta);
hue = hue * 60;
if(hue <0)
hue += 360;
}
function calculateRGB()
{
red = value;
green = value;
blue = value;
if(value == 0 || saturation == 0)
return;
var tHue = (hue / 60);
var i = Math.floor(tHue);
var f = tHue - i;
var p = value * (1 - saturation);
var q = value * (1 - saturation * f);
var t = value * (1 - saturation * (1 - f));
switch(i)
{
case 0:
red = value; green = t; blue = p;
break;
case 1:
red = q; green = value; blue = p;
break;
case 2:
red = p; green = value; blue = t;
break;
case 3:
red = p; green = q; blue = value;
break;
case 4:
red = t; green = p; blue = value;
break;
default:
red = value; green = p; blue = q;
break;
}
}
}
}
();
{
this.ColorFromHSV = function(hue, sat, val)
{
var color = new Color();
color.SetHSV(hue,sat,val);
return color;
}
this.ColorFromRGB = function(r, g, b)
{
var color = new Color();
color.SetRGB(r,g,b);
return color;
}
this.ColorFromHex = function(hexStr)
{
var color = new Color();
color.SetHexString(hexStr);
return color;
}
function Color()
{
//Stored as values between 0 and 1
var red = 0;
var green = 0;
var blue = 0;
//Stored as values between 0 and 360
var hue = 0;
//Strored as values between 0 and 1
var saturation = 0;
var value = 0;
this.SetRGB = function(r, g, b)
{
if (isNaN(r) || isNaN(g) || isNaN(b))
return false;
r = r/255.0;
red = r> 1 ? 1 : r <0 ? 0 : r;
g = g/255.0;
green = g> 1 ? 1 : g <0 ? 0 : g;
b = b/255.0;
blue = b> 1 ? 1 : b <0 ? 0 : b;
calculateHSV();
return true;
}
this.Red = function()
{ return Math.round(red*255); }
this.Green = function()
{ return Math.round(green*255); }
this.Blue = function()
{ return Math.round(blue*255); }
this.SetHSV = function(h, s, v)
{
if (isNaN(h) || isNaN(s) || isNaN(v))
return false;
hue = (h>= 360) ? 359.99 : (h <0) ? 0 : h;
saturation = (s> 1) ? 1 : (s <0) ? 0 : s;
value = (v> 1) ? 1 : (v <0) ? 0 : v;
calculateRGB();
return true;
}
this.Hue = function()
{ return hue; }
this.Saturation = function()
{ return saturation; }
this.Value = function()
{ return value; }
this.SetHexString = function(hexString)
{
if(hexString == null || typeof(hexString) != "string")
return false;
if (hexString.substr(0, 1) == '#')
hexString = hexString.substr(1);
if(hexString.length != 6)
return false;
var r = parseInt(hexString.substr(0, 2), 16);
var g = parseInt(hexString.substr(2, 2), 16);
var b = parseInt(hexString.substr(4, 2), 16);
return this.SetRGB(r,g,b);
}
this.HexString = function()
{
var rStr = this.Red().toString(16);
if (rStr.length == 1)
rStr = '0' + rStr;
var gStr = this.Green().toString(16);
if (gStr.length == 1)
gStr = '0' + gStr;
var bStr = this.Blue().toString(16);
if (bStr.length == 1)
bStr = '0' + bStr;
return ('#' + rStr + gStr + bStr).toUpperCase();
}
this.Complement = function()
{
var newHue = (hue>= 180) ? hue - 180 : hue + 180;
var newVal = (value * (saturation - 1) + 1);
var newSat = (value*saturation) / newVal;
var newColor = new Color();
newColor.SetHSV(newHue, newSat, newVal);
return newColor;
}
function calculateHSV()
{
var max = Math.max(Math.max(red, green), blue);
var min = Math.min(Math.min(red, green), blue);
value = max;
saturation = 0;
if(max != 0)
saturation = 1 - min/max;
hue = 0;
if(min == max)
return;
var delta = (max - min);
if (red == max)
hue = (green - blue) / delta;
else if (green == max)
hue = 2 + ((blue - red) / delta);
else
hue = 4 + ((red - green) / delta);
hue = hue * 60;
if(hue <0)
hue += 360;
}
function calculateRGB()
{
red = value;
green = value;
blue = value;
if(value == 0 || saturation == 0)
return;
var tHue = (hue / 60);
var i = Math.floor(tHue);
var f = tHue - i;
var p = value * (1 - saturation);
var q = value * (1 - saturation * f);
var t = value * (1 - saturation * (1 - f));
switch(i)
{
case 0:
red = value; green = t; blue = p;
break;
case 1:
red = q; green = value; blue = p;
break;
case 2:
red = p; green = value; blue = t;
break;
case 3:
red = p; green = q; blue = value;
break;
case 4:
red = t; green = p; blue = value;
break;
default:
red = value; green = p; blue = q;
break;
}
}
}
}
();
This code, as explained in that previous tutorial, creates a static Colors object which can be used to create color objects (which can take RGB, HSV, or Hex and produce RGB, HSV, or Hex). I'm not going to go into detail on how it works here - if you want to know more about it, read the tutorial.
The next two lines in the initialization hook the two elements that need to be draggable - the circle and the arrows.
new dragObject("arrows", "hueBarDiv", arrowsLowBounds,
arrowsUpBounds, arrowsDown, arrowsMoved, endMovement);
new dragObject("circle", "gradientBox", circleLowBounds,
circleUpBounds, circleDown, circleMoved, endMovement);
arrowsUpBounds, arrowsDown, arrowsMoved, endMovement);
new dragObject("circle", "gradientBox", circleLowBounds,
circleUpBounds, circleDown, circleMoved, endMovement);
The first line here declares that the element
arrows is draggable and hueBarDiv is the handle. This means that if you click anywhere on the hueBarDiv, the arrows will start dragging. Then we give it a lower and an upper position bound for where we can drag the arrows, and three functions, a function to call at the beginning of dragging, one after each movement, and one when dragging completes. We set the same type of thing up in the second line, for the circle on the gradient image - but in this case the gradientBox is the drag handle - so if you click anywhere on the gradient image, you start dragging the circle.Just as a refresher, here is the main chunk of code behind
dragObj:function dragObject(element, attachElement,
lowerBound, upperBound, startCallback,
moveCallback, endCallback, attachLater)
{
if(typeof(element) == "string")
element = document.getElementById(element);
if(element == null)
return;
if(lowerBound != null && upperBound != null)
{
var temp = lowerBound.Min(upperBound);
upperBound = lowerBound.Max(upperBound);
lowerBound = temp;
}
var cursorStartPos = null;
var elementStartPos = null;
var dragging = false;
var listening = false;
var disposed = false;
function dragStart(eventObj)
{
if(dragging || !listening || disposed) return;
dragging = true;
if(startCallback != null)
startCallback(eventObj, element);
cursorStartPos = absoluteCursorPostion(eventObj);
elementStartPos = new Position(parseInt(element.style.left),
parseInt(element.style.top));
elementStartPos = elementStartPos.Check();
hookEvent(document, "mousemove", dragGo);
hookEvent(document, "mouseup", dragStopHook);
return cancelEvent(eventObj);
}
function dragGo(eventObj)
{
if(!dragging || disposed) return;
var newPos = absoluteCursorPostion(eventObj);
newPos = newPos.Add(elementStartPos)
newPos = newPos.Subtract(cursorStartPos);
newPos = newPos.Bound(lowerBound, upperBound)
newPos.Apply(element);
if(moveCallback != null)
moveCallback(newPos, element);
return cancelEvent(eventObj);
}
function dragStopHook(eventObj)
{
dragStop();
return cancelEvent(eventObj);
}
function dragStop()
{
if(!dragging || disposed) return;
unhookEvent(document, "mousemove", dragGo);
unhookEvent(document, "mouseup", dragStopHook);
cursorStartPos = null;
elementStartPos = null;
if(endCallback != null)
endCallback(element);
dragging = false;
}
this.Dispose = function()
{
if(disposed) return;
this.StopListening(true);
element = null;
attachElement = null
lowerBound = null;
upperBound = null;
startCallback = null;
moveCallback = null
endCallback = null;
disposed = true;
}
this.StartListening = function()
{
if(listening || disposed) return;
listening = true;
hookEvent(attachElement, "mousedown", dragStart);
}
this.StopListening = function(stopCurrentDragging)
{
if(!listening || disposed) return;
unhookEvent(attachElement, "mousedown", dragStart);
listening = false;
if(stopCurrentDragging && dragging)
dragStop();
}
this.IsDragging = function(){ return dragging; }
this.IsListening = function() { return listening; }
this.IsDisposed = function() { return disposed; }
if(typeof(attachElement) == "string")
attachElement = document.getElementById(attachElement);
if(attachElement == null)
attachElement = element;
if(!attachLater)
this.StartListening();
}
lowerBound, upperBound, startCallback,
moveCallback, endCallback, attachLater)
{
if(typeof(element) == "string")
element = document.getElementById(element);
if(element == null)
return;
if(lowerBound != null && upperBound != null)
{
var temp = lowerBound.Min(upperBound);
upperBound = lowerBound.Max(upperBound);
lowerBound = temp;
}
var cursorStartPos = null;
var elementStartPos = null;
var dragging = false;
var listening = false;
var disposed = false;
function dragStart(eventObj)
{
if(dragging || !listening || disposed) return;
dragging = true;
if(startCallback != null)
startCallback(eventObj, element);
cursorStartPos = absoluteCursorPostion(eventObj);
elementStartPos = new Position(parseInt(element.style.left),
parseInt(element.style.top));
elementStartPos = elementStartPos.Check();
hookEvent(document, "mousemove", dragGo);
hookEvent(document, "mouseup", dragStopHook);
return cancelEvent(eventObj);
}
function dragGo(eventObj)
{
if(!dragging || disposed) return;
var newPos = absoluteCursorPostion(eventObj);
newPos = newPos.Add(elementStartPos)
newPos = newPos.Subtract(cursorStartPos);
newPos = newPos.Bound(lowerBound, upperBound)
newPos.Apply(element);
if(moveCallback != null)
moveCallback(newPos, element);
return cancelEvent(eventObj);
}
function dragStopHook(eventObj)
{
dragStop();
return cancelEvent(eventObj);
}
function dragStop()
{
if(!dragging || disposed) return;
unhookEvent(document, "mousemove", dragGo);
unhookEvent(document, "mouseup", dragStopHook);
cursorStartPos = null;
elementStartPos = null;
if(endCallback != null)
endCallback(element);
dragging = false;
}
this.Dispose = function()
{
if(disposed) return;
this.StopListening(true);
element = null;
attachElement = null
lowerBound = null;
upperBound = null;
startCallback = null;
moveCallback = null
endCallback = null;
disposed = true;
}
this.StartListening = function()
{
if(listening || disposed) return;
listening = true;
hookEvent(attachElement, "mousedown", dragStart);
}
this.StopListening = function(stopCurrentDragging)
{
if(!listening || disposed) return;
unhookEvent(attachElement, "mousedown", dragStart);
listening = false;
if(stopCurrentDragging && dragging)
dragStop();
}
this.IsDragging = function(){ return dragging; }
this.IsListening = function() { return listening; }
this.IsDisposed = function() { return disposed; }
if(typeof(attachElement) == "string")
attachElement = document.getElementById(attachElement);
if(attachElement == null)
attachElement = element;
if(!attachLater)
this.StartListening();
}
This code is from the Javascript - Draggable Elements tutorial. Again, since there is a very detailed discussion of the draggable code in that tutorial, I'm just going to gloss right over it here.
So back to those drag object initializations - what are the upper and lower bounds of the arrows and the circle? They are position objects (and if you don't know what our position object is, go read the draggable elements tutorial again). And here is what they are defined as:
var arrowsLowBounds = new Position(0, -4);
var arrowsUpBounds = new Position(0, 251);
var circleLowBounds = new Position(-5, -5);
var circleUpBounds = new Position(250, 250);
var arrowsUpBounds = new Position(0, 251);
var circleLowBounds = new Position(-5, -5);
var circleUpBounds = new Position(250, 250);
The fact that both the lower and the upper X value for the arrows is the same number means that it can't be dragged left or right, but only up or down. The reason the other numbers aren't quite what you might expect is that these positions represent the upper left corner of the circle/arrow elements. This means that they are offset by the half the width and half the height so that the center of the elements can reach the edge of its bounding area.
So those are the bounds - now what about the callbacks? Well, first we have the start callbacks -
circleDown and arrowsDown. Lets see what they do:function arrowsDown(e, arrows)
{
var pos = getMousePos(e);
if(getEventTarget(e) == arrows)
pos.Y += parseInt(arrows.style.top);
pos = correctOffset(pos, arrowsOffset, true);
pos = pos.Bound(arrowsLowBounds, arrowsUpBounds);
pos.Apply(arrows);
arrowsMoved(pos);
}
function circleDown(e, circle)
{
var pos = getMousePos(e);
if(getEventTarget(e) == circle)
{
pos.X += parseInt(circle.style.left);
pos.Y += parseInt(circle.style.top);
}
pos = correctOffset(pos, circleOffset, true);
pos = pos.Bound(circleLowBounds, circleUpBounds);
pos.Apply(circle);
circleMoved(pos);
}
{
var pos = getMousePos(e);
if(getEventTarget(e) == arrows)
pos.Y += parseInt(arrows.style.top);
pos = correctOffset(pos, arrowsOffset, true);
pos = pos.Bound(arrowsLowBounds, arrowsUpBounds);
pos.Apply(arrows);
arrowsMoved(pos);
}
function circleDown(e, circle)
{
var pos = getMousePos(e);
if(getEventTarget(e) == circle)
{
pos.X += parseInt(circle.style.left);
pos.Y += parseInt(circle.style.top);
}
pos = correctOffset(pos, circleOffset, true);
pos = pos.Bound(circleLowBounds, circleUpBounds);
pos.Apply(circle);
circleMoved(pos);
}
These two functions may be small but they accomplish a lot. Lets go over the
arrowsDown one step by step. First we get the current mouse position from the event object. This is done with the following function:function getMousePos(eventObj)
{
eventObj = eventObj ? eventObj : window.event;
var pos;
if(isNaN(eventObj.layerX))
pos = new Position(eventObj.offsetX, eventObj.offsetY);
else
pos = new Position(eventObj.layerX, eventObj.layerY);
return correctOffset(pos, pointerOffset, true);
}
{
eventObj = eventObj ? eventObj : window.event;
var pos;
if(isNaN(eventObj.layerX))
pos = new Position(eventObj.offsetX, eventObj.offsetY);
else
pos = new Position(eventObj.layerX, eventObj.layerY);
return correctOffset(pos, pointerOffset, true);
}
Here we get the correct event object (see Javascript - Working With Events for an explanation of why), and then we get the current mouse position, relative to the element that triggered the event. Of course, Internet Explorer and Firefox do this differently, so we have the two different ways of getting the number here. Once we have that position we call the function
correctOffset, and get a new position back (which we then return). This correctOffset function is rather simplistic, but serves an important purpose:function correctOffset(pos, offset, neg)
{
if(neg)
return pos.Subtract(offset);
return pos.Add(offset);
}
{
if(neg)
return pos.Subtract(offset);
return pos.Add(offset);
}
All it does is take a position and an offset (both position objects), and either applies or unapplies that offset (according to the third argument). We use it in the
getMousePos function with the pointerOffset:var pointerOffset = new Position(0, navigator.userAgent.indexOf("Firefox")>= 0 ? 1 : 0);
var circleOffset = new Position(5, 5);
var arrowsOffset = new Position(0, 4);
var circleOffset = new Position(5, 5);
var arrowsOffset = new Position(0, 4);
For whatever horrible reason, where Firefox reports the tip of the pointer to be is not actually the tip of the pointer - it is 1 pixel off in the X direction. So, we need this pointer offset value. The other two offsets listed here are much more acceptable - they are the values to get from the upper left pixel of the circle/arrow to the center point value that we need (we will be using these two offsets a bunch later).
Ok, so back to the
arrowsDown code. We now have the current mouse position relative to the element that triggered the event. The next thing we do is check what element triggered the event - and if it is the arrows themselves, we add the current position of the arrows (in the Y direction) to the Y mouse position. This way we now have the mouse position relative to the hue bar. The reason we don't do anything with the X position here is that the X position of the arrows never changes. Now we call correctOffset again, but this time it is to adjust for the height of the arrows image (i.e., we adjust it using the arrowsOffset). This is so that the current mouse position ends up at the center of the arrows image, instead of at the top. Now that we have what will become the new position for the arrows, we bound it to make sure it falls within the correct range (using the lower and upper arrow bounds). Then we apply the new position to the arrows element. Finally, we call the function arrowsMoved with the new position. What all this code essentially accomplishes is to move the arrows to the point at which you clicked on the hue bar. The code for circleDown accomplishes the exact same task, but for the circle on the gradient image.Now on to the functions
arrowsMoved and circleMoved. They are called at the end of their respective 'Down' functions, and they are also called after every movement during a drag. So lets see what they do:function arrowsMoved(pos, element)
{
pos = correctOffset(pos, arrowsOffset, false);
currentColor.SetHSV((256 - pos.Y)*359.99/255,
currentColor.Saturation(), currentColor.Value());
colorChanged("arrows");
}
function circleMoved(pos, element)
{
pos = correctOffset(pos, circleOffset, false);
currentColor.SetHSV(currentColor.Hue(), 1-pos.Y/255.0,
pos.X/255.0);
colorChanged("circle");
}
{
pos = correctOffset(pos, arrowsOffset, false);
currentColor.SetHSV((256 - pos.Y)*359.99/255,
currentColor.Saturation(), currentColor.Value());
colorChanged("arrows");
}
function circleMoved(pos, element)
{
pos = correctOffset(pos, circleOffset, false);
currentColor.SetHSV(currentColor.Hue(), 1-pos.Y/255.0,
pos.X/255.0);
colorChanged("circle");
}
Again, the two functions look extremely similar. They start by offsetting the position (the position passed in is the one for the new upper-left position of the element, so we are bringing it back to the center of the their respective elements). Next we set the new current color, based on the position. For the arrows, the Y position transforms into the hue - first we flip it (since in coordinate space, 0 is at the top, but in the space of the hue bar, 0 is at the bottom), and then we scale it from the range 0-255 (the range of the hue bar image) to the range 0-359.99 (the actual range of hue that we deal with). For the circle, the Y position becomes the saturation (where again, it is flipped) and we scale it from the range 0-255 to the range 0-1. The X position becomes the new value, and here we just scale it from 0-255 to 0-1. Finally, we call a function called
colorChanged:function colorChanged(source)
{
document.getElementById("hexBox").value =
currentColor.HexString();
document.getElementById("redBox").value =
currentColor.Red();
document.getElementById("greenBox").value =
currentColor.Green();
document.getElementById("blueBox").value =
currentColor.Blue();
document.getElementById("hueBox").value =
Math.round(currentColor.Hue());
var str = (currentColor.Saturation()*100).toString();
if(str.length> 4)
str = str.substr(0,4);
document.getElementById("saturationBox").value = str;
str = (currentColor.Value()*100).toString();
if(str.length> 4)
str = str.substr(0,4);
document.getElementById("valueBox").value = str;
if(source == "arrows" || source == "box")
document.getElementById("gradientBox").style.backgroundColor=
Colors.ColorFromHSV(currentColor.Hue(), 1, 1).HexString();
if(source == "box")
{
var el = document.getElementById("arrows");
el.style.top = (256 - currentColor.Hue()*255/359.99 -
arrowsOffset.Y) + 'px';
var pos = new Position(currentColor.Value()*255,
(1-currentColor.Saturation())*255);
pos = correctOffset(pos, circleOffset, true);
pos.Apply("circle");
endMovement();
}
document.getElementById("quickColor").style.backgroundColor =
currentColor.HexString();
}
{
document.getElementById("hexBox").value =
currentColor.HexString();
document.getElementById("redBox").value =
currentColor.Red();
document.getElementById("greenBox").value =
currentColor.Green();
document.getElementById("blueBox").value =
currentColor.Blue();
document.getElementById("hueBox").value =
Math.round(currentColor.Hue());
var str = (currentColor.Saturation()*100).toString();
if(str.length> 4)
str = str.substr(0,4);
document.getElementById("saturationBox").value = str;
str = (currentColor.Value()*100).toString();
if(str.length> 4)
str = str.substr(0,4);
document.getElementById("valueBox").value = str;
if(source == "arrows" || source == "box")
document.getElementById("gradientBox").style.backgroundColor=
Colors.ColorFromHSV(currentColor.Hue(), 1, 1).HexString();
if(source == "box")
{
var el = document.getElementById("arrows");
el.style.top = (256 - currentColor.Hue()*255/359.99 -
arrowsOffset.Y) + 'px';
var pos = new Position(currentColor.Value()*255,
(1-currentColor.Saturation())*255);
pos = correctOffset(pos, circleOffset, true);
pos.Apply("circle");
endMovement();
}
document.getElementById("quickColor").style.backgroundColor =
currentColor.HexString();
}
This function is where we synchronize all the various elements of the color picker. The one argument,
source is a string representing where the new values have come from. This string has three possible values - "arrows", "circle", and "box" - where "arrows" means the position of the arrows has changes, "circle" means the position of the circle has changed, and "box" means that one of the input boxes has changed.So first in this function, we update the various text boxes - because these always need to be updated, no matter what changed. The saturation and value text boxes get multiplied up to be a percentage and then rounded to look nicer. If the source was the arrows or the box, we update the background color of the gradient div - whose color we get by taking the current hue and giving it a value and saturation of 1. If the source was a text box, we update the position of the arrows and the circle, from the current color. Essentially, we do the reverse of the calculation that we used to get from position to color. We also call a function
endMovement in this case, which I will explain in a moment. Finally, we update the current color of the quickColor div to be the currently selected color.So now we have the function
endMovement, which is called after a drag operation completes on the arrows or circle, and is also called whenever a text box causes something to change. It is pretty simple, all it does is set the staticColor div to the new color:function endMovement()
{
document.getElementById("staticColor").style.backgroundColor =
currentColor.HexString();
}
{
document.getElementById("staticColor").style.backgroundColor =
currentColor.HexString();
}
We are actually almost done now! The final call in initialization was to call
colorChanged('box');, which you understand now is to get all the color picker components synced with the initial color choice. There are only a couple more functions - and they are all extremely simple. They are the onchange functions for each of the text boxes:function hexBoxChanged(e)
{
currentColor.SetHexString(
document.getElementById("hexBox").value);
colorChanged("box");
}
function redBoxChanged(e)
{
currentColor.SetRGB(
parseInt(document.getElementById("redBox").value),
currentColor.Green(), currentColor.Blue());
colorChanged("box");
}
function greenBoxChanged(e)
{
currentColor.SetRGB(currentColor.Red(),
parseInt(document.getElementById("greenBox").value),
currentColor.Blue());
colorChanged("box");
}
function blueBoxChanged(e)
{
currentColor.SetRGB(currentColor.Red(),
currentColor.Green(),
parseInt(document.getElementById("blueBox").value));
colorChanged("box");
}
function hueBoxChanged(e)
{
currentColor.SetHSV(
parseFloat(document.getElementById("hueBox").value),
currentColor.Saturation(), currentColor.Value());
colorChanged("box");
}
function saturationBoxChanged(e)
{
currentColor.SetHSV(currentColor.Hue(),
parseFloat(
document.getElementById("saturationBox").value
)/100.0, currentColor.Value());
colorChanged("box");
}
function valueBoxChanged(e)
{
currentColor.SetHSV(currentColor.Hue(),
currentColor.Saturation(), parseFloat(
document.getElementById("valueBox").value
)/100.0);
colorChanged("box");
}
{
currentColor.SetHexString(
document.getElementById("hexBox").value);
colorChanged("box");
}
function redBoxChanged(e)
{
currentColor.SetRGB(
parseInt(document.getElementById("redBox").value),
currentColor.Green(), currentColor.Blue());
colorChanged("box");
}
function greenBoxChanged(e)
{
currentColor.SetRGB(currentColor.Red(),
parseInt(document.getElementById("greenBox").value),
currentColor.Blue());
colorChanged("box");
}
function blueBoxChanged(e)
{
currentColor.SetRGB(currentColor.Red(),
currentColor.Green(),
parseInt(document.getElementById("blueBox").value));
colorChanged("box");
}
function hueBoxChanged(e)
{
currentColor.SetHSV(
parseFloat(document.getElementById("hueBox").value),
currentColor.Saturation(), currentColor.Value());
colorChanged("box");
}
function saturationBoxChanged(e)
{
currentColor.SetHSV(currentColor.Hue(),
parseFloat(
document.getElementById("saturationBox").value
)/100.0, currentColor.Value());
colorChanged("box");
}
function valueBoxChanged(e)
{
currentColor.SetHSV(currentColor.Hue(),
currentColor.Saturation(), parseFloat(
document.getElementById("valueBox").value
)/100.0);
colorChanged("box");
}
Each of those functions just takes the new value from its repective text box, pushes that new value into the current color object, and then calls
colorChanged("box") in order to update everyone else.And that covers it for how to make a color picker using javascript! Hope you enjoyed it, and if you would like the raw javascript code to play around with, feel free to download it here. The html and couple lines of initialization javascript are not in that file, but you can copy all that from the boxes above or the source of this page. As always, if you have any questions, please leave them in the comments.
Posted in Javascript, All Tutorials by The Tallest |

August 24th, 2007 at 9:41 am
could you please give links to the .png and .gif files that are in your code?
August 24th, 2007 at 9:59 pm
Sure! Here are all the links:
Gradient, Hue Bar, Arrows, Circle
December 9th, 2007 at 5:10 pm
Is it possible, via javascript, to rotate the hue of an entire image, dynamicly? the ideal would be to provide a color pallete or a slider and according to the values(of the slider) that the user provides, the hue/saturation/brightness of the entire image, to change accordingly. if you know how to do so a tutorial would be just about great!
January 28th, 2008 at 3:01 pm
I play with this code but the drag object did not work. Do I miss something?
February 9th, 2008 at 7:34 am
Here’s an enhanced version of this color picker:
http://walidator.info/?s=colorpicker_en
May 28th, 2008 at 12:18 am
I second oasis’ motion. The drag object is not working… Have I done something wrong?
May 28th, 2008 at 7:19 am
Perhaps you forgot to include the chunk on initialization javascript at the end. Here is what the full html code should look like:
<head>
<script src=“javascript.js” type=“text/javascript”></script>
</head>
<body>
<div style=“position:relative;height:286px;
width:531px;border:1px solid black;”>
<div id=“gradientBox” style=“cursor:crosshair;
top:15px;position:absolute;left:15px;
width:256px;height:256px;”>
<img id=“gradientImg”
style=“display:block;width:256px;height:256px;”
src=“/Color_Picker/color_picker_gradient.png” />
<img id=“circle”
style=“position:absolute;height:11px;
width:11px;”
src=“/Color_Picker/color_picker_circle.gif” />
</div>
<div id=“hueBarDiv” style=“position:absolute;
left:310px;width:35px;
height:256px;top:15px;”>
<img style=“position:absolute;height:256px;
width:19px;left:8px;”
src=“/Color_Picker/color_picker_bar.png” />
<img id=“arrows” style=“position:absolute;
height:9px;width:35px;left:0px;”
src=“/Color_Picker/color_picker_arrows.gif” />
</div>
<div style=“position:absolute;left:370px;
width:145px;
height:256px;top:15px;”>
<div style=“position:absolute;
border: 1px solid black;
height:50px;width:145px;top:0px;left:0px;”>
<div id=“quickColor” style=“position:absolute;
height:50px;width:73px;top:0px;left:0px;”>
</div>
<div id=“staticColor” style=“position:absolute;
height:50px;width:72px;top:0px;left:73px;”>