Creating a VSCode Style Color Picker with Plain JavaScript

Estimated read time 34 min read

In this guide, we’ll walk you through creating a VSCode-style color picker using HTML, CSS, and plain JavaScript. Our goal is to build a sophisticated and interactive color picker, similar to the one found in Visual Studio Code (VSCode). The colorobj.js script will be the core of this project, enabling users to select colors dynamically through a color wheel, color sliders, and opacity controls. We’ll break down the script’s functionality in detail, provide clear instructions for setting up and styling the color picker, and demonstrate how to integrate and test it in your web project.

1. Set Up the HTML Structure

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>VSCode Style Color Picker</title>
	<link rel="stylesheet" href="styles.css">
</head>
<body>
	<button onclick="ColorPicker.toggleDialog()">Open Color Picker</button>
	<div id="colorDialog" class="colorDialog">
		<div class="color-picker-container">
			<button class="change-palette-btn" onclick="ColorPicker.toggleColorMode()">Change Palette</button>
			<div class="color-wheel-container">
				<canvas id="colorWheel" class="color-wheel" width="200" height="200"></canvas>
				<div id="colorSelector" class="color-selector"></div>
			</div>
			<div class="color-slider">
				<input type="range" min="0" max="360" value="0" id="colorSlider">
			</div>
			<div class="opacity-slider">
				<input type="range" min="0" max="1" step="0.01" value="1" id="opacitySlider">
			</div>
			<input type="text" id="hexColorInput" value="" readonly>
		</div>
	</div>
	<script type="text/javascript" src="colorobj.js"></script>  
</body>
</html>

Detailed Explanation of Color Picker Components

colorDialog

The colorDialog element serves as the main container for the color picker interface. Initially, this container is hidden from view and is typically shown only when the user interacts with a triggering element, such as a button or input field. The purpose of the colorDialog is to house all the interactive elements of the color picker, including the color wheel, sliders, and input fields. By keeping this container hidden initially, we ensure that the color picker does not clutter the interface until it is needed, providing a cleaner and more user-friendly experience.

colorWheel

The colorWheel is a canvas element that is crucial for the visual aspect of the color picker. It is where the color wheel itself is drawn, allowing users to select colors by interacting with this graphical representation. The color wheel typically displays a spectrum of colors arranged in a circular pattern, which users can click or drag to choose a color. The canvas element is manipulated via JavaScript to render the wheel, handle user input, and update the selected color dynamically based on the user’s interaction with the wheel.

colorSelector

The colorSelector is a visual indicator placed on the color wheel to show the currently selected color. This component often takes the form of a small, movable dot or marker on the wheel. As the user interacts with the color wheel, the position of the colorSelector changes to reflect the chosen color. This immediate visual feedback helps users precisely identify and select their desired color. By dynamically positioning the colorSelector, users can easily see which color they are currently selecting and make adjustments as needed.

opacitySlider

The opacitySlider is a user interface component that allows users to adjust the opacity of the selected color. Positioned typically as a horizontal slider, this element provides a range from fully opaque to fully transparent. By moving the slider, users can fine-tune the transparency level of the color, which is particularly useful in design applications where color opacity plays a significant role. The opacitySlider works in conjunction with the color wheel and other sliders to provide a comprehensive color selection tool.

colorSlider

The colorSlider is another slider element, but it is used to adjust the hue of the selected color. Unlike the opacitySlider, which controls transparency, the colorSlider affects the color’s hue along a linear gradient. This slider is essential for refining the color choice by varying its hue while maintaining its saturation and lightness. Users can interact with the colorSlider to shift the color spectrum and achieve the exact hue they want, enhancing the flexibility and precision of the color picker.

hexColorInput

The hexColorInput is an input field that displays the selected color in hexadecimal format. This text input allows users to view and, if desired, manually enter a specific color code. Hexadecimal color codes are a standard way of representing colors in web design and development, making this input field a convenient way for users to get the exact color value they need. The hexColorInput is automatically updated as users interact with the color wheel and sliders, ensuring that the displayed color code always reflects the current selection.

change-palette-btn

The change-palette-btn is a button that enables users to switch between different color palettes or modes. This feature is useful for providing users with multiple sets of color options or themes. For example, the button might toggle between a light and dark mode or switch to different predefined color schemes. By incorporating this button, the color picker becomes more versatile and adaptable to various design needs, allowing users to quickly change their color palette without having to reconfigure the color picker from scratch.

2. Style the Color Picker

To ensure that the color picker looks good and functions properly, you need to style it with CSS. Here’s a sample styles.css file:

body{
  background-color: #e6e5d8ff;
}

/* Styles for the color picker container */
.color-picker-container {
	display: flex;
	flex-direction: column;
	align-items: center;
}

/* Styles for the color wheel */
.color-wheel-container {
	position: relative;
}
.color-wheel {
	width: 200px;
	height: 200px;
	overflow: hidden;
	position: relative;
	border-radius: 5%;
}

/* Styles for the color selector */
.color-selector {
	width: 20px;
	height: 20px;
	border-radius: 50%;
	border: 2px solid rgba(255, 255, 255, 0.8);
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	pointer-events: none;
}

/* Styles for the opacity slider */
.opacity-slider {
	width: 200px;
	margin-top: 20px;
}

.opacity-slider input[type="range"] {
	width: 100%;
	-webkit-appearance: none;
	appearance: none;
	height: 5px;
	border-radius: 5px;
	background: #d3d3d3;
	outline: none;
	opacity: 0.7;
	-webkit-transition: .2s;
	transition: opacity .2s;
}

.opacity-slider input[type="range"]::-webkit-slider-thumb {
	-webkit-appearance: none;
	appearance: none;
	width: 15px;
	height: 15px;
	border-radius: 50%;
	background: #4CAF50;
	cursor: pointer;
}

.opacity-slider input[type="range"]::-moz-range-thumb {
	width: 15px;
	height: 15px;
	border-radius: 50%;
	background: #4CAF50;
	cursor: pointer;
}

/* Styles for the dialog */
.colorDialog {
	position: absolute;
	background: white;
	padding: 20px;
	padding-top:10px;
	border: 2px solid #ccc;
	border-radius: 5px;
	box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
	z-index: 9999;
	display: none; /* Hide the dialog by default */
}

.color-slider {
	width: 200px;
	margin-top: 20px;
}

.color-slider input[type="range"] {
	width: 100%;
	-webkit-appearance: none;
	appearance: none;
	height: 5px;
	border-radius: 5px;
	background: linear-gradient(to right, red, orange, yellow, green, cyan, blue, violet);
	outline: none;
	opacity: 0.7;
	-webkit-transition: .2s;
	transition: opacity .2s;
}

.color-slider input[type="range"]::-webkit-slider-thumb {
	-webkit-appearance: none;
	appearance: none;
	width: 15px;
	height: 15px;
	border-radius: 50%;
	background: white;
	cursor: pointer;
}		

/* CSS for the palette button */
.change-palette-btn {
  background-color: #eee;
  border: none;
  color: #222;
  padding: 5px 20px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 14px;
  margin: 4px 2px;
  cursor: pointer;
  border-radius: 8px;
}

.change-palette-btn:hover {
	background-color: #ccc;
}

Explanation of Color Picker Styling

Dialog Styling: Sets the color picker’s position, border, background color, padding, and shadow to give it a distinct appearance.

Selector Styling: Adds a border and cursor style to the color selector for better visibility and interactivity.

Slider Styling: Ensures the sliders for opacity and color hue are full-width with proper margins for alignment and functionality.

Hex Input Styling: Controls the width and margin of the hexadecimal color input field for ease of use.

Button Styling: Styles the button for changing palettes, including hover effects, to make it visually appealing and responsive.

3. Detailed Breakdown of colorobj.js

Now let’s delve into the functionality of the colorobj.js script, step by step.

Object Initialization

const ColorPicker = {
    colorDialog: null,
    colorWheelCanvas: null,
    colorSelector: null,
    opacitySlider: null,
    colorSlider: null,
    ctx: null,
    changePaletteBtn: null,
    hexColorInput: null,
    canvasWidth: 200,
    canvasHeight: 200,
    isConicMode: true,
    lastColorPosition: { x: 0, y: 0 },

    initialize: function() {
        this.colorDialog = document.getElementById('colorDialog');
        this.colorWheelCanvas = document.getElementById('colorWheel');
        this.colorSelector = document.getElementById('colorSelector');
        this.opacitySlider = document.getElementById('opacitySlider');
        this.colorSlider = document.getElementById('colorSlider');
        this.ctx = (this.colorWheelCanvas) ? this.colorWheelCanvas.getContext('2d') : null;
        this.changePaletteBtn = this.colorDialog.querySelector('.change-palette-btn');
        this.hexColorInput = document.getElementById('hexColorInput');

        if (!this.ctx) {
            console.error('Canvas context is not supported');
            return;
        }

        this.canvasWidth = this.colorWheelCanvas.width;
        this.canvasHeight = this.colorWheelCanvas.height;

        this.setupEventListeners();
        this.drawColorWheel();

        let rgbArray = this.getDefaultBodyColorRGB();
        this.setColorSelectorToColor(rgbArray);
    },
};

The ColorPicker Object is the core component that encapsulates all functionalities and properties required for the color picker. It brings together the various elements and operations needed for the color picker to work effectively.

Properties Initialization within this object involves setting up references to various UI elements and configuring internal states. This initialization ensures that all parts of the color picker are properly set up and ready for interaction.

The initialize Method performs several crucial tasks. First, it selects the necessary DOM elements, such as the canvas and sliders, to ensure that the color picker is correctly integrated into the page. It then checks if the canvas context is supported by the browser, as this context is necessary for drawing the color wheel.

Following this, the method sets up event listeners to manage user interactions, such as clicks and drags. These listeners are essential for allowing users to interact with the color picker. The method then proceeds to draw the initial color wheel on the canvas, providing a visual spectrum from which users can select colors.

Lastly, the method sets the initial position of the color selector based on the background color of the body. This ensures that the color picker starts with a color that reflects the current theme of the page, providing a seamless user experience from the moment the color picker is displayed.

Setup Event Listeners

setupEventListeners: function() {
    this.colorWheelCanvas.addEventListener('mousedown', this.handleColorWheelMouseDown.bind(this));
    window.addEventListener('mouseup', this.handleWindowMouseUp.bind(this));
    this.opacitySlider.addEventListener('input', this.updateColor.bind(this));
    this.colorSlider.addEventListener('input', this.handleColorSliderChange.bind(this));
},

The function above attaches event listeners to manage user interactions with the color picker. It handles four key events:

  • Mouse Down on Color Wheel: This event initiates the color selection process when the user clicks on the color wheel.
  • Mouse Up on Window: This event concludes the color selection process when the user releases the mouse button, ensuring that the color picker stops updating as the mouse is no longer pressed.
  • Opacity Slider Change: When the opacity slider is adjusted, this event updates the color to reflect the new opacity level, allowing users to modify how transparent or opaque the selected color is.
  • Color Slider Change: Adjusting the color slider triggers this event, which redraws the color wheel to reflect any changes in hue, providing real-time feedback as the user modifies the color settings.

Draw the Color Wheel

drawColorWheel: function() {
	// Check if canvas context is supported
	if (!this.ctx) {
		console.error('Canvas context is not supported');
		return;
	}

	// Create ImageData
	let imageData;
	try {
		imageData = this.ctx.createImageData(this.canvasWidth, this.canvasHeight);
	} catch (error) {
		console.error('Failed to create ImageData:', error);
		return;
	}

	const center_x = this.canvasWidth / 2;
	const center_y = this.canvasHeight / 2;
	const max_radius = Math.sqrt(center_x ** 2 + center_y ** 2);

	// Define the gradient function
	function linearGradient(distance) {
		return 60 + (1 - distance) * 30; // Lightness decreases linearly from 90% to 60%
	}

	for (let y = 0; y < this.canvasHeight; y++) {
		for (let x = 0; x < this.canvasWidth; x++) {
			// Calculate distance from center
			const distance_from_center = Math.sqrt((x - center_x) ** 2 + (y - center_y) ** 2);

			// Normalize distance
			const normalized_distance = distance_from_center / max_radius / 0.3;

			// Convert coordinates to angle
			const angle = Math.atan2(y - center_y, x - center_x) + Math.PI;

			// Map angle to hue
			let hue = ((angle / (2 * Math.PI)) * 360 + 360) % 360;

			// Adjust saturation based on distance
			const saturation = 20 + normalized_distance * 20; // Saturation between 20% to 40%

			// Choose lightness function (linear gradient)
			let lightness = linearGradient(normalized_distance); // Linear Gradient

			// Convert HSL to RGB
			const rgb = this.hslToRgb(hue, saturation, lightness);

			// Set pixel color in the image data
			const index = (y * this.canvasWidth + x) * 4;
			imageData.data[index] = rgb[0]; // Red channel
			imageData.data[index + 1] = rgb[1]; // Green channel
			imageData.data[index + 2] = rgb[2]; // Blue channel
			imageData.data[index + 3] = 255; // Alpha channel (fully opaque)
		}
	}

	// Put the gradient data onto the canvas
	this.ctx.putImageData(imageData, 0, 0);
},

To effectively draw and update the color wheel on the canvas, several key steps are undertaken:

  • Image Data Creation: The first step involves creating a new ImageData object. This object will store the pixel data necessary for rendering the color wheel. ImageData provides a way to manipulate the image data at the pixel level, making it ideal for custom graphics like a color wheel.
  • Color Wheel Drawing:
    • Center and Radius Calculations: To draw the color wheel accurately, we need to determine its center and radius. The center is the point from which all color calculations are made, and the radius defines the extent of the wheel. These calculations ensure that the wheel is centered properly on the canvas and that it fills the intended space.
    • Linear Gradient Function: This function is crucial for creating a smooth transition of colors. It calculates a gradient based on the distance of each pixel from the center of the wheel. Pixels closer to the center will have different lightness compared to those further away, creating a gradient effect that is visually pleasing and functional for color selection.
    • Pixel Color Calculation: The heart of the color wheel’s functionality lies in this step. For each pixel, we calculate its color based on its distance from the center and its angular position. This involves converting these distances and angles into color values and updating the ImageData array accordingly. This array holds the color data for every pixel, ensuring that the wheel displays the correct colors when rendered.
    • Update Canvas: After calculating the color data for each pixel, this data is drawn onto the canvas. The ImageData object, now filled with the calculated pixel colors, is rendered onto the canvas. This step completes the process, resulting in a color wheel that users can interact with to select colors

Handling User Interaction

handleColorWheelMouseDown: function(e) {
	this.updateColor(e);
	this.updateColorHandler = this.updateColor.bind(this);
	window.addEventListener('mousemove', this.updateColorHandler);
},

handleWindowMouseUp: function() {
	window.removeEventListener('mousemove', this.updateColorHandler);
},


updateColor: function(e) {
	// Check if the event target is the opacity slider
	if (e.target === this.opacitySlider) {
		// Update the background color of the body with opacity using the last color position
		const pixel = this.ctx.getImageData(this.lastColorPosition.x, this.lastColorPosition.y, 1, 1).data;
		document.body.style.backgroundColor = `rgba(${pixel[0]}, ${pixel[1]}, ${pixel[2]}, ${this.opacitySlider.value})`;
		this.hexColorInput.value = this.rgbaToHex(pixel[0], pixel[1], pixel[2], this.opacitySlider.value);
		
		// Update the gradient CSS palette based on the selected hue
		const hue = this.colorSlider.value;
		const gradient = `linear-gradient(to right, hsl(${hue}, 100%, 50%), #ffffff)`;
		this.colorWheelCanvas.style.background = gradient;

		return; // Exit the function early if interacting with opacity slider
	}

	// Get the position of the mouse relative to the color wheel container
	const rect = this.colorWheelCanvas.getBoundingClientRect();
	const x = e.clientX - rect.left;
	const y = e.clientY - rect.top;

	// Ensure the mouse is within the color wheel boundaries
	if (x >= 0 && x < rect.width && y >= 0 && y < rect.height) {
		// Update the last color position
		this.lastColorPosition.x = x;
		this.lastColorPosition.y = y;

		// Get the pixel color at the mouse position
		const pixel = this.ctx.getImageData(x, y, 1, 1).data;

		// Convert RGB to HSL
		const hueSaturation = this.rgbToHsl(pixel[0], pixel[1], pixel[2]);

		// Update the position and color of the color selector
		this.colorSelector.style.left = `${x}px`;
		this.colorSelector.style.top = `${y}px`;

		// Update the hue and saturation of the color selector based on color slider value
		const hue = this.colorSlider.value;
		const saturation = hueSaturation[1];

		// Calculate the border color based on the background color
		const contrastColor = this.getContrastColor(pixel[0], pixel[1], pixel[2]);
		this.colorSelector.style.borderColor = `rgb(${contrastColor[0]}, ${contrastColor[1]}, ${contrastColor[2]})`;

		// Update the background color of the body with opacity
		document.body.style.backgroundColor = `rgba(${pixel[0]}, ${pixel[1]}, ${pixel[2]}, ${this.opacitySlider.value})`;
		this.hexColorInput.value = this.rgbaToHex(pixel[0], pixel[1], pixel[2], this.opacitySlider.value);
	}
},	

The above code manages user interactions in the color picker, particularly mouse events related to color selection and updates. The key functions include handling mouse events for color wheel interaction, updating the color, and managing the opacity slider. Here’s a detailed look at each part:

handleColorWheelMouseDown Method:

This method is triggered when a user clicks on the color wheel. It begins by calling the updateColor function to immediately update the color based on the mouse position. To ensure that the color selection updates continuously as the user drags the mouse, it binds the updateColor method to this.updateColorHandler and attaches it to the mousemove event on the window. This allows the color picker to react in real-time as the user moves the mouse across the color wheel.

handleWindowMouseUp Method:

When the user releases the mouse button, this method is invoked. It removes the previously added mousemove event listener. This stops the continuous updating of the color when the mouse is no longer being dragged. By detaching the event listener, it ensures that the color picker does not continue processing mouse movements once the user has finished selecting a color.

updateColor Method:

This method is central to updating the color picker’s state based on user interactions:

  • Interaction with Opacity Slider: If the user interacts with the opacity slider, the method first checks if the event target is the opacity slider. It updates the background color of the body to reflect the selected color with the current opacity. It also updates the hexadecimal color input field with the new color and opacity values. The gradient background of the color wheel canvas is updated based on the hue selected on the color slider.
  • Color Wheel Interaction: If the event target is not the opacity slider, the method proceeds to calculate the color based on the mouse position relative to the color wheel canvas. It ensures that the mouse coordinates are within the boundaries of the canvas. It updates the position of the color selector to follow the mouse and retrieves the color at that position. The color is converted from RGB to HSL to adjust the color selector’s appearance and ensure that the selector’s border color contrasts well with the selected color.
  • Background Color Update: The background color of the body is updated with the newly selected color and opacity. The hexadecimal color input field is also updated to reflect these changes.

With these interactions, the color picker ensures a seamless and intuitive user experience, enabling precise control over color selection and adjustment.

Parsing and Handling Color Values

parseDefaultColor: function(color) {
	const rgbMatch = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[01](?:\.\d+)?)?\)$/);

	if (rgbMatch && rgbMatch.length >= 4) {
		const r = parseInt(rgbMatch[1]);
		const g = parseInt(rgbMatch[2]);
		const b = parseInt(rgbMatch[3]);

		if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
			return [r, g, b]; // Return RGB values
		}
	}

	console.error('Invalid color:', color);
	return null;
},

parseColor: function (color) {
	// Check if the color is an array
	if (Array.isArray(color) && color.length === 3) {
		// Ensure all elements in the array are numbers
		if (color.every(value => typeof value === 'number' && !isNaN(value))) {
			// Ensure RGB values are within valid range (0-255)
			const validRGBValues = color.every(value => value >= 0 && value <= 255);
			if (validRGBValues) {
				return color.map(value => Math.round(value)); // Round and return RGB values
			}
		}
	} else if (typeof color === 'string') {
		// Try parsing the color string to obtain RGB values
		const match = color.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
	
		if (match && match.length === 4) {
			const r = parseInt(match[1]);
			const g = parseInt(match[2]);
			const b = parseInt(match[3]);
	
			if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
				return [r, g, b];
			}
		}
	}

	// Return null if unable to parse
	console.error('Invalid color:', color);
	return null;
},	

// Function to get the default body color in RGB format
getDefaultBodyColorRGB: function() {
	// Get the RGB or RGBA values of the default body color
	const defaultColor = getComputedStyle(document.body).backgroundColor;
	const rgbaValues = this.parseDefaultColor(defaultColor);


	// If the color is already in RGB format, return it
	if (rgbaValues) {
		return rgbaValues;
	}

	// If the color is in RGBA format, extract RGB values
	const rgbValues = this.parseColor(defaultColor.replace(/^rgba?\((\d+),\s*(\d+),\s*(\d+),.*?\)$/, 'rgb($1, $2, $3)'));
	
	return rgbValues;
},

This code provides methods to parse and handle color values in various formats. These functions are responsible for managing and converting color data within the color picker.

parseDefaultColor Function:

This function is designed to parse a color string in either RGB or RGBA format. It uses a regular expression to match the color string and extract the red, green, and blue values. If the color string is valid, it returns these values as an array of integers. If the input color string is invalid, it logs an error message and returns null.

parseColor Function:

The parseColor function handles both arrays and string representations of color values. It first checks if the input is an array of three numbers, ensuring all values fall within the valid RGB range (0-255). If the input is a string, it tries to match and extract RGB values from it using a regular expression. If the string format is valid, it parses and returns the RGB values. If the color cannot be parsed, the function logs an error and returns null. The function supports multiple color formats, providing flexibility in color data handling.

getDefaultBodyColorRGB Function:

This function retrieves the default background color of the document body and converts it to an RGB format. It first obtains the computed style for the body’s background color and parses it using the parseDefaultColor method. If the color is not already in RGB format, it converts any RGBA color to RGB format.

Setting the Color Selector to the Closest Color

setColorSelectorToColor: function(rgbArray) {	
	// Get the RGB values of the default body color
	const defaultRGB = rgbArray;
	// Calculate the canvas gradient once
	const canvasImageData = this.ctx.getImageData(0, 0, this.canvasWidth, this.canvasHeight);

	// Initialize variables to store closest values
	let closestPixel = { x: 0, y: 0 };
	let minDistance = Number.MAX_VALUE;
	let closestHue = 0;

	// Iterate through the canvas gradient to find the closest color to the default body color
	for (let i = 0; i < canvasImageData.data.length; i += 4) {
		// Get the RGB values of the current pixel
		const pixelRGB = [
			canvasImageData.data[i],   // Red
			canvasImageData.data[i + 1], // Green
			canvasImageData.data[i + 2]  // Blue
		];

		// Calculate the Euclidean distance between the current pixel color and the default body color
		const distance = Math.sqrt(
			Math.pow(defaultRGB[0] - pixelRGB[0], 2) +
			Math.pow(defaultRGB[1] - pixelRGB[1], 2) +
			Math.pow(defaultRGB[2] - pixelRGB[2], 2)
		);

		// Update closest pixel if this pixel is closer to the default color
		if (distance < minDistance) {
			minDistance = distance;
			closestPixel.x = (i / 4) % this.canvasWidth; // Convert 1D index to 2D coordinates
			closestPixel.y = Math.floor((i / 4) / this.canvasWidth);

			// Calculate the hue of the closest color
			const hsv = this.rgbToHsv(pixelRGB[0], pixelRGB[1], pixelRGB[2]);
			closestHue = hsv[0];
		}
	}

	// Set the position of the color selector to the position of the closest matching pixel
	this.colorSelector.style.left = `${closestPixel.x}px`;
	this.colorSelector.style.top = `${closestPixel.y}px`;
	
	// Update lastColorPosition to reflect the closest color position
	this.lastColorPosition.x = closestPixel.x;
	this.lastColorPosition.y = closestPixel.y;	

	// Update the color under the color selector
	this.updateColor({ clientX: closestPixel.x, clientY: closestPixel.y });

	// Set the color slider to the nearest hue value
	this.colorSlider.value = Math.round(closestHue);
	
},

The setColorSelectorToColor function is used to position the color selector on the canvas at the location that corresponds to the color closest to the provided RGB values. This function is integral for ensuring that the color picker accurately reflects the color adjustments made by the user.

The function begins by initializing the defaultRGB variable with the provided RGB array. It then retrieves the pixel data from the color wheel canvas and initializes variables to track the closest color and its position.

To find the closest color, the function iterates over each pixel’s RGB values in the canvas’s gradient. It calculates the Euclidean distance between each pixel’s color and the provided defaultRGB values. This distance measure helps determine how close the current pixel’s color is to the target color. The pixel with the smallest distance is considered the closest match.

Once the closest color is identified, the function updates the position of the color selector to this pixel’s location. It also updates the lastColorPosition to reflect this position, ensuring consistency in color selection. The color under the selector is refreshed using the updateColor method, which updates the displayed color values.

Finally, the function adjusts the color slider to match the hue of the closest color found. This ensures that both the visual position of the color selector and the slider value accurately represent the selected color.

Helper Functions for Color Manipulation

The color picker script includes several helper functions for converting and manipulating colors. These functions enable the conversion between different color formats and facilitate color interpolation.

// Helper functions
hexToRgb: function(hex) {
	// Remove the hash character from the beginning (if any)
	hex = hex.replace(/^#/, '');
	// Check if the hex color code is in the short form (e.g., #222)
	if (hex.length === 3) {
		// Expand the short form to the full form (e.g., #222 to #222222)
		hex = hex.split('').map(function(char) {
			return char + char;
		}).join('');
	}
	// Convert hex color to RGB format
	const bigint = parseInt(hex, 16);
	const r = (bigint >> 16) & 255;
	const g = (bigint >> 8) & 255;
	const b = bigint & 255;
	return [r, g, b];
},

rgbToHsl: function(r, g, b) {
	r /= 255, g /= 255, b /= 255;
	const max = Math.max(r, g, b), min = Math.min(r, g, b);
	let h, s, l = (max + min) / 2;

	if (max === min) {
		h = s = 0;
	} else {
		const d = max - min;
		s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
		switch (max) {
			case r: h = (g - b) / d + (g < b ? 6 : 0); break;
			case g: h = (b - r) / d + 2; break;
			case b: h = (r - g) / d + 4; break;
		}
		h /= 6;
	}

	return [h * 360, s * 100, l * 100];
},

// Function to convert HSL to RGB
hslToRgb: function(h, s, l) {
	h /= 360;
	s /= 100;
	l /= 100;
	let r, g, b;
	if (s === 0) {
		r = g = b = l;
	} else {
		const hue2rgb = (p, q, t) => {
			if (t < 0) t += 1;
			if (t > 1) t -= 1;
			if (t < 1 / 6) return p + (q - p) * 6 * t;
			if (t < 1 / 2) return q;
			if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
			return p;
		};
		const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
		const p = 2 * l - q;
		r = hue2rgb(p, q, h + 1 / 3);
		g = hue2rgb(p, q, h);
		b = hue2rgb(p, q, h - 1 / 3);
	}
	return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
},

rgbaToHex: function(r, g, b, a) {
	// Ensure the values are within the valid range (0-255 for RGB, 0-1 for Alpha)
	if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255 || a < 0 || a > 1) {
		throw new Error('Invalid color values');
	}

	// Convert RGB values to hex and pad with zeroes if necessary
	let red = r.toString(16).padStart(2, '0');
	let green = g.toString(16).padStart(2, '0');
	let blue = b.toString(16).padStart(2, '0');

	// Convert alpha value to hex (0-255) and pad with zeroes
	let alpha = Math.round(a * 255).toString(16).padStart(2, '0');

	// Return the combined hex color value
	return `#${red}${green}${blue}${alpha}`;
},

// Function to convert RGB to HSV
rgbToHsv: function (r, g, b) {
	r /= 255, g /= 255, b /= 255;
	const max = Math.max(r, g, b), min = Math.min(r, g, b);
	let h, s, v = max;

	const d = max - min;
	s = max === 0 ? 0 : d / max;

	if (max === min) {
		h = 0; // achromatic
	} else {
		switch (max) {
			case r: h = (g - b) / d + (g < b ? 6 : 0); break;
			case g: h = (b - r) / d + 2; break;
			case b: h = (r - g) / d + 4; break;
		}
		h /= 6;
	}

	return [h * 360, s * 100, v * 100];
},	

interpolateColors: function(color1, color2, position) {
	// Parse colors to RGB values
	const rgb1 = this.parseColor(color1);
	const rgb2 = this.parseColor(color2);

	// Check if both colors are valid
	if (!rgb1 || !rgb2) {
		return null;
	}

	// Interpolate between RGB values
	const r = Math.round(rgb1[0] * (1 - position) + rgb2[0] * position);
	const g = Math.round(rgb1[1] * (1 - position) + rgb2[1] * position);
	const b = Math.round(rgb1[2] * (1 - position) + rgb2[2] * position);

	// Return interpolated color as RGB string
	return `rgb(${r}, ${g}, ${b})`;
},

getColorFromGradient: function(percentage) {
	// Define RGB values corresponding to each stop in the gradient
	const rgbValues = [
		[255, 0, 0],     // red
		[255, 165, 0],   // orange
		[255, 255, 0],   // yellow
		[0, 128, 0],     // green
		[0, 255, 255],   // cyan
		[0, 0, 255],     // blue
		[238, 130, 238]  // violet
	];

	// Ensure the percentage is within bounds
	if (percentage <= 0) {
		return rgbValues[0]; // Return the first color if percentage is 0 or less
	} else if (percentage >= 100) {
		return rgbValues[rgbValues.length - 1]; // Return the last color if percentage is 100 or more
	}

	// Calculate the index of the color array based on the percentage
	const index = Math.floor((percentage / 100) * (rgbValues.length - 1));

	// Calculate the position between the two adjacent colors
	const position = (percentage / 100) * (rgbValues.length - 1) - index;

	// Interpolate between the two adjacent RGB values
	return this.interpolateColors(rgbValues[index], rgbValues[index + 1], position);
},

getColorFromSliderValue: function(sliderValue) {
	// Calculate the position of the slider within the gradient
	const percentage = sliderValue / 255 * 100;

	// Get the color based on the gradient
	const color = this.getColorFromGradient(percentage);

	// Parse the color to obtain RGB values
	const rgbValues = this.parseColor(color);

	// Check if RGB values are valid
	if (!rgbValues) {
		console.error('Failed to parse color:', color);
		return null;
	}

	return rgbValues;
},

// Function to get the color under the color selector position
getColorUnderSelector: function() {
	// Get the position of the color selector relative to the canvas
	const canvasRect = this.colorWheelCanvas.getBoundingClientRect();
	const selectorRect = this.colorSelector.getBoundingClientRect();
	const selectorX = selectorRect.left - canvasRect.left + selectorRect.width / 2;
	const selectorY = selectorRect.top - canvasRect.top + selectorRect.height / 2;

	// Get the pixel color at the selector position from the canvas
	const pixel = this.ctx.getImageData(selectorX, selectorY, 1, 1).data;

	// Convert pixel color to RGB string
	const color = `rgb(${pixel[0]}, ${pixel[1]}, ${pixel[2]})`;

	return color;
},

getContrastColor: function(r, g, b) {
	const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
	return luminance > 0.5 ? [0, 0, 0] : [255, 255, 255];
},	

Here’s a detailed look at each helper function:

hexToRgb Function:

This function converts a hexadecimal color code to RGB values. It first removes any leading hash from the hex string. If the hex code is in short form (e.g., #222), it expands it to the full form (e.g., #222222). It then parses the hex string into a number and extracts the red, green, and blue components using bitwise operations. The function returns these RGB values as an array.

rgbToHsl Function:

This function converts RGB values to HSL (Hue, Saturation, Lightness) format. The RGB values are normalized to a range of 0 to 1. It calculates the maximum and minimum values among R, G, and B to determine lightness. Depending on whether the maximum and minimum values are the same, it computes saturation and hue values. The function returns the hue (in degrees), saturation, and lightness as percentages.

hslToRgb Function:

This function performs the inverse operation, converting HSL values back to RGB. It first normalizes the HSL values. Depending on the saturation level, it calculates RGB values using a helper function to compute intermediate color values. The function then converts these values to the 0-255 range and rounds them to the nearest integer before returning them as an array.

rgbaToHex Function:

The rgbaToHex function converts RGBA color values into a hexadecimal format. It first checks that all color values are within valid ranges. RGB values are converted to hexadecimal and padded to ensure they have two digits. The alpha value is scaled to the 0-255 range, converted to hexadecimal, and padded. The function combines these hex values to produce a final color string in the format #RRGGBBAA.

rgbToHsv Function:

This function converts RGB values to HSV (Hue, Saturation, Value). The RGB values are normalized, and the maximum and minimum values are used to determine value and saturation. If the values are not the same, it calculates the hue based on which color component is the maximum. The function returns the hue (in degrees), saturation, and value as percentages.

interpolateColors Function:

The interpolateColors function blends two colors based on a given position (a value between 0 and 1). It first parses the colors into RGB format. Then, it calculates the interpolated RGB values by computing a weighted average of the two colors based on the position. The function returns the resulting color as an RGB string.

getColorFromGradient Function:

The getColorFromGradient function determines the color from a gradient based on a given percentage. It starts by defining a series of RGB colors that represent key points in the gradient, ranging from red to violet. If the percentage is at the boundaries (0% or 100%), it returns the corresponding end colors. For percentages in between, it calculates the appropriate position between two adjacent colors in the gradient. Using interpolation, it finds the color that represents the specified percentage within the gradient.

getColorFromSliderValue Function:

This function maps a slider value to a color within a gradient. It converts the slider value (ranging from 0 to 255) to a percentage that reflects its position in the gradient. This percentage is then used to retrieve the corresponding color from the gradient using getColorFromGradient. The color is parsed to obtain its RGB values. If parsing fails, it logs an error. The function returns the RGB values of the color corresponding to the slider position.

getColorUnderSelector Function:

The getColorUnderSelector function retrieves the color from the canvas directly beneath the color selector. It calculates the position of the selector relative to the canvas and then uses this position to get the pixel color data from the canvas. The pixel data, which includes the red, green, and blue components, is converted to an RGB string representing the color at the selector’s location.

getContrastColor Function:

This function calculates the contrast color to ensure readability against a given background color. It uses luminance, a measure of the perceived brightness of the color, to determine whether a dark or light contrast color is needed. If the luminance is above a threshold, it returns black; otherwise, it returns white. This helps in maintaining text visibility or other UI elements against varying backgrounds.

Handling Color Slider Changes

// Function to handle color slider change
handleColorSliderChange: function(event) {
	this.isConicMode = false;
	const sliderValue = event.target.value;

	// Calculate the position of the slider within the gradient
	const percentage = sliderValue / 255 * 100;

	// Get the color based on the gradient
	const color = this.getColorFromGradient(percentage);

	// Parse the color to obtain RGB values
	const rgbValues = this.parseColor(color);

	// Check if RGB values are valid
	if (!rgbValues) {
		console.error('Failed to parse color:', color);
		return;
	}

	// Redraw the canvas gradient with the new RGB value
	this.redrawCanvasGradient(rgbValues[0], rgbValues[1], rgbValues[2]);

	// Get the color under the color selector position
	const selectorColor = this.getColorUnderSelector();	

	// Set the color of the color selector to the color under it
	//colorSelector.style.background = selectorColor;
	document.body.style.backgroundColor = selectorColor;
	this.hexColorInput.value = this.rgbaToHex(rgbValues[0], rgbValues[1], rgbValues[2], this.opacitySlider.value);
	
},

The handleColorSliderChange function is triggered when the user interacts with the color slider. It updates the color picker based on the new slider value and performs several key tasks to reflect the changes in the color selection. Here is a detailed explanation:

  1. Reset to Default Mode: The function starts by ensuring that the color picker is not in conic mode. This flag is reset to false, indicating that the picker should not use conic gradients but rather linear gradients based on the slider value.
  2. Calculate Percentage from Slider Value: The slider value, which ranges from 0 to 255, is converted to a percentage (0% to 100%). This percentage represents the slider’s position within the gradient spectrum.
  3. Retrieve Color from Gradient: Using the calculated percentage, the function determines the color from the gradient. This is done by interpolating between colors defined in the gradient based on the given percentage.
  4. Parse and Validate Color: The color obtained from the gradient is parsed to extract its RGB values. If parsing fails, an error message is logged, and the function returns early to prevent further processing.
  5. Update Canvas Gradient: The canvas gradient is redrawn to reflect the new color values. This method updates the gradient to visually match the color selected by the slider.
  6. Get and Set Color Under Selector: The function retrieves the color from the area directly beneath the color selector on the canvas. This color is then applied as the background color of the document body. Additionally, the hexadecimal representation of the RGB color (including opacity) is set in the color input field.

Redrawing the Gradient

redrawCanvasGradient: function(selectedColorRed, selectedColorGreen, selectedColorBlue) {
    const imageData = this.ctx.createImageData(this.canvasWidth, this.canvasHeight);

    for (let y = 0; y < this.canvasHeight; y++) {
        for (let x = 0; x < this.canvasWidth; x++) {
            // Calculate the position ratios for white, selected color, and black
            const whiteRatio = 1 - x / this.canvasWidth; // White from right to left
            const selectedColorRatio = x / this.canvasWidth; // Selected color from left to right
            const blackRatio = y / this.canvasHeight; // Black from top to bottom

            // Interpolate colors
            const r = Math.round(255 * (whiteRatio + selectedColorRatio * selectedColorRed / 255) * (1 - blackRatio));
            const g = Math.round(255 * (whiteRatio + selectedColorRatio * selectedColorGreen / 255) * (1 - blackRatio));
            const b = Math.round(255 * (whiteRatio + selectedColorRatio * selectedColorBlue / 255) * (1 - blackRatio));

            // Set pixel color in the image data
            const index = (y * this.canvasWidth + x) * 4;
            imageData.data[index] = r;
            imageData.data[index + 1] = g;
            imageData.data[index + 2] = b;
            imageData.data[index + 3] = 255; // Alpha value
        }
    }

    // Put the gradient data onto the canvas
    this.ctx.putImageData(imageData, 0, 0);
},

The redrawCanvasGradient function updates the canvas gradient with a selected color. Like drawColorWheel, it begins by creating an ImageData object to hold the pixel data. It then iterates over each pixel on the canvas, calculating its color based on ratios for white, the selected color, and black. The RGB values are interpolated according to these ratios and the pixel’s position. These values, along with full opacity, are then set for each pixel. Finally, the updated gradient is rendered onto the canvas using this.ctx.putImageData.

The redrawCanvasGradient function creates a linear gradient on the canvas that blends from white to a selected color and then to black, with color transitions depending on the pixel’s position. It updates the canvas with this gradient by calculating the RGB values for each pixel based on its position. In contrast, the drawColorWheel function generates a circular color wheel, transitioning through various hues around the center of the canvas. This function calculates the color for each pixel based on its angle and distance from the center, creating a radial gradient that displays a full spectrum of colors. Essentially, redrawCanvasGradient is used for linear color transitions, while drawColorWheel creates a circular gradient of hues.

Function to Toggle the Color Picker Dialog

// Function to open or close the color picker dialog
toggleDialog: function() {
	if (this.colorDialog.style.display === 'block') {
		this.colorDialog.style.display = 'none';
	} else {
		const buttonRect = this.changePaletteBtn.getBoundingClientRect();
		this.colorDialog.style.display = 'block';
		this.colorDialog.style.left = `${buttonRect.left}px`;
		this.colorDialog.style.top = `${buttonRect.bottom}px`;
	}
}

This is a simple function to toggle the color picker dialog’s visibility. It hides the dialog if it’s already visible, or shows it if it’s hidden. The dialog is positioned just below the button (changePaletteBtn), aligning its left edge with the button’s left edge.

4. Integrate and Test

To ensure your color picker is fully functional and integrated into your project, follow these steps:

  1. Include the JavaScript File: Ensure that your HTML file includes the colorobj.js script correctly.
  2. Add Initialization Script: Insert the following code into your HTML to initialize the color picker when the page loads:
<script>
window.addEventListener('DOMContentLoaded', (event) => {
    ColorPicker.initialize();
});
</script>
  1. Open the HTML File: Open your HTML file in a web browser.
  2. Test Functionality: Click the “Open Color Picker” button to display the color picker dialog. Interact with the color wheel and sliders, and observe the changes in the background color and hex color input field.
  3. Adjust Styles and Functionality: Customize the CSS and JavaScript to fit your design and feature requirements as needed.

By following these steps, you’ll have successfully integrated a fully functional color picker similar to the one in VSCode. We hope you enjoyed the process of building and customizing it. For a comprehensive look at the code, please stay tuned as the GitHub repository is being set up. If you have any feedback or suggestions, feel free to leave a comment.

Subscribe to our newsletter!

Dimitrios S. Sfyris https://aspectsoft.gr/en/

Dimitrios S. Sfyris is a leading expert in systems engineering and web
architectures. With years of experience in both academia and industry, he has published numerous articles and research papers. He is the founder of AspectSoft, a company that developed the innovative e-commerce platform AspectCart, designed to revolutionize the way businesses operate in the e-commerce landscape. He also created the Expo-Host platform for 3D interactive environments.

https://www.linkedin.com/in/dimitrios-s-sfyris/

You May Also Like

More From Author

+ There are no comments

Add yours