JS: Making a Color Picker from Scratch, part 2

Try it here.

The final product

Last time I went over the basic layout in HTML and CSS. Now I’ll go over creating the gradients and user interaction.

Let’s start by creating a Picker class and assigning all the buttons to associate with. target is the canvas we’ll be using for the main gradient area.

class Picker {
constructor(target, width, height) {
this.target = target;
this.width = width;
this.height = height;
this.target.width = width;
this.target.height = height;
//Get context
this.context = this.target.getContext("2d");
//mouse
this.mouseState = "none";
//color selector circle
this.pickerCircle = { x: 10, y: 10, width: 6, height: 6 };
this.clicked = false;
//hue slider
this.hueRange = document.getElementById("hueslider");
//color
this.swatch = "swatch btn";
this.hue;
this.saturation;
this.lightness;
this.red;
this.green;
this.blue;
this.hexcode;
//*interface*//
this.rgbahsl = document.getElementById("rgbahsl");
this.rgba = document.getElementById("rgba");
this.r = document.getElementById("r");
this.g = document.getElementById("g");
this.b = document.getElementById("b");
this.hsl = document.getElementById("hsl");
this.h = document.getElementById("h");
this.s = document.getElementById("s");
this.l = document.getElementById("l");
this.hex = document.getElementById("hexcode");
//Colors
this.oldcolor = document.getElementById("oldcolor");
this.newcolor = document.getElementById("newcolor");
//OK/Cancel
this.confirmBtn = document.getElementById("confirm-btn");
this.cancelBtn = document.getElementById("cancel-btn");
}
}

The Hue slider is the easiest part. In the HSL color space, Hue is a value represented in degrees, so it can range from 0 to 359. This is primarily an HSL color picker, so the hue slider is a range slider with those values as its minimum and maximum. To draw the rainbow gradient, it looks like this:

drawHueGrad() {
//hue slider gradient
this.hueRange.style.background = "linear-gradient(90deg, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%)"
}

The only reason I’m drawing this in Javascript instead of just assigning the gradient background in CSS is to give me the option of changing the type of color picker later, which isn’t important for most use cases, so it’s up to you which you prefer.

The main area gradient however, is much easier to draw with JS by using a for loop.

drawHSLGrad() {
//draw hsl gradient
for (let row = 0; row < this.height; row++) {
let grad = this.context.createLinearGradient(0, 0, this.width, 0);
grad.addColorStop(0, 'hsl(' + this.hue + ', 0%, ' + ((row / this.height) * 100) + '%)');
grad.addColorStop(1, 'hsl(' + this.hue + ', 100%, ' + ((row / this.height) * 100) + '%)');
this.context.fillStyle = grad;
this.context.fillRect(0, row, this.width, 1);
}

Next draw the selector. I’m being stubborn about the pixel style and my code ends up being very verbose here so I’ll spare you, but you could shorten it by just drawing a circle:

calcSelector() {
this.pickerCircle.x = Math.round(this.saturation * this.width / 100) - 3;
this.pickerCircle.y = Math.round(this.lightness * this.height / 100) - 3;
}
drawHSLGrad() {
...
this.calcSelector(); //draw selector
this.context.beginPath();
...
}

To make the selector circle behave like any good color picker does, we can’t simply use event listeners on the canvas itself.

In the initial build function, add these event listeners:

this.target.addEventListener("mousedown", (e) => {
this.handleMouseDown(e);
});
window.addEventListener("mousemove", (e) => {
this.handleMouseMove(e);
});
window.addEventListener("mouseup", (e) => {
this.handleMouseUp(e);
});

One helper function to update the rest of the color picker according to coordinates we’ll get from these event listeners:

selectSL(x, y) {
this.saturation = Math.round(x / this.width * 100);
this.lightness = Math.round(y / this.height * 100);
this.drawHSLGrad();
//set newcolor
this.HSLToRGB();
this.RGBToHex();
this.updateColor();
}

Technically the canvas area is only for choosing saturation and lightness, and the hue is selected using the hue slider.

handleMouseDown(e) {
this.clicked = true;
this.selectSL(e.offsetX, e.offsetY);
}
handleMouseMove(e) {
if (this.clicked) {
let canvasXOffset = this.target.getBoundingClientRect().left - document.getElementsByTagName("html")[0].getBoundingClientRect().left;
let canvasYOffset = this.target.getBoundingClientRect().top - document.getElementsByTagName("html")[0].getBoundingClientRect().top;
let canvasX = e.pageX - canvasXOffset;
let canvasY = e.pageY - canvasYOffset;
//constrain coordinates
if (canvasX > this.width) { canvasX = this.width }
if (canvasX < 0) { canvasX = 0 }
if (canvasY > this.height) { canvasY = this.height }
if (canvasY < 0) { canvasY = 0 }
this.selectSL(canvasX, canvasY);
}
}
handleMouseUp(e) {
this.clicked = false;
}

Mousedown is simple enough. We just use the event offset coordinates. Things get more complicated on mousemove. To keep interacting with the canvas while outside of it, we set a variable to tell if the user is holding the mouse button, this.clicked. So if that variable is true, we can calculate the canvas offset coordinates from the window by calculating how far the canvas element is from the boundaries of the page, then subtracting that value from the page coordinates. Then we can constrain those values so that the selector circle doesn’t go beyond the bounds of the canvas when the mouse is outside the canvas. Instead, it will slide around the border, following the mouse.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store