How to Create a Color Picker Web App using JavaScript - Part 3

Nitish Kumar Singh

Feb 14, 2024

Hello developers! In this blog post, we will continue the color-picker web app project and understand how to write the rest of JavaScript logic code for our color-picker.

Until now, I have written two blog posts that cover how to design the color-picker UI in the first part and how to write JS code to handle color selection when the user drags the thumb on the color-picker's palette or sliders in the second part. If you have not read one of these posts, then please do so to better understand this blog post.

In this post, we will write the logic to position thumbs in the main palette or sliders when a color is given. This will occur when the color picker is opened and when the color component changes via input elements. All codes provided in this post may be from a class, so variables are accessed using the this keyword.

At this point, we only have color in the form of an RGB or hex code string (for now, I am focusing only on these two types of color formats). So first, we will validate the given color, extract or convert hex to RGB, initialize an array containing RGB values based on the given color, and if the color is invalid, then initialize the default red color. Below is the code to validate the color:

  validateColor = (color) => {
    if (color && typeof color === "string") {
      if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(color)) {
        const hexWithoutHash = color.slice(1);
        const hasShortFormat = hexWithoutHash.length === 3 || hexWithoutHash.length === 4;
        const fullHex = hasShortFormat ? hexWithoutHash.split("").map((char) => char + char).join("") : hexWithoutHash;
        if (fullHex.length === 6 || fullHex.length === 8) {
          this.color2Alpha = fullHex.length === 8 ? parseInt(fullHex.slice(6), 16) / 255 : 1;
          this.color2 = hexToRgb(fullHex.slice(0, 6));
       }
      } else if (/^rgba?\(.+\)$/.test(color)) {
        const match = color.match(/rgba?\(([^)]+)\)/)[1].split(",").map((trimmed) => trimmed.trim());
        const alphaStr = match.length === 4 ? match[3] : "1";
        this.color2Alpha = alphaStr.includes("%") ? parseFloat(alphaStr) / 100 : parseFloat(alphaStr);
        this.color2 = match.slice(0, 3).map(Number);
     }
    }
 };

Where color2Alpha and color2 are alpha and given color (final selected color) in array form. And this is the color at the thumb of the main palette. So according to this color, we need to calculate the thumb position of the main palette, color (say color X) at the top-right corner of the main palette, which is the color at the thumb of the color-slider, and finally the thumb position in the color-slider using color X.

To write code for calculating position and color, we need to observe what happens when thumbs have been dragging, and that's what I mentioned at the end of the second blog post. When you observe, notice that when the thumb of the main palette is at the top-right corner and moving the thumb of the color-slider, then any two color components of the selected color are 0 and 255.

It means two color components of color X are already we have, and those are 0 and 255, and we just need to calculate one component. But which two components are 0 and 255 out of these three components?

If you have carefully observed color changes by moving the thumb of the main palette, then you noticed that if color X has component values like [minValue (0), maxValue (255), midValue (say k)], then color components of the color at the thumb of the main palette also have the same pattern like [minValue (say a), maxValue (say b), midValue (say c)].

Therefore, we can easily find two components of color X by checking min and max components of the given color. For example, if the given color is [155, 71, 178], then color X will be [k, 0, 255], where k needs to be calculated.

You may remember from the previous post that to calculate the ratio (percentage or position), we only need to equate one value (coordinate) on a line. Because the position of the main palette thumb is two-dimensional, we equate two values and get x and y coordinates in percentage.

I have done these calculations and made expressions to get x and y coordinates in a long form with width and height dependence, but when I asked ChatGPT to shorten it, ChatGPT gave expressions in a very short form without width and height dependence.

So below is the function to do all the work:

  positionToSelectedColor = () => {
    const minIndex = this.color2.indexOf(Math.min(...this.color2));
    const maxIndex = this.color2.indexOf(Math.max(...this.color2));
    const mediumIndex = [0, 1, 2].find((index) => index !== minIndex && index !== maxIndex);
    
    const minValue = this.color2[minIndex];
    const maxValue = this.color2[maxIndex];
    const mediumValue = this.color2[mediumIndex];

    if (minValue === mediumValue && mediumValue === maxValue) {
      this.color = [255,0,0];
      this.position2 = [0 ,((255 - maxValue) * 100) / 255];
      this.position = 0;
    }else{
      const normalizedValue = (mediumValue - minValue) / (maxValue - minValue);
  
      const color = [0, 0, 0];
      color[minIndex] = 0;
      color[maxIndex] = 255;
      color[mediumIndex] = Math.round(255 * normalizedValue);
      let position = positionInSlider(color);
  
      this.color = color;
      this.position2 = [((maxValue - minValue) / maxValue) * 100,((255 - maxValue) * 100) / 255];
      this.position = position;
    }
  };

Where position and color are the position of the colors-slider's thumb and color at this position, and position2 and color2 are the position of the main palette's thumb and color at this position, and positionInSlider is a function to calculate the position of the thumb in the colors-slider, which is below:

const positionInSlider = (targetColor) => {
  for (let i = 0; i < colorStops.length - 1; i++) {
    const first = colorStops[i].color,
      second = colorStops[i + 1].color;
    let lie = isColorBetween(first, second, targetColor);
    if (lie) {
      for (let j = 0; j < targetColor.length; j++) {
        if (first[j] !== second[j]) {
          let p = (targetColor[j] - first[j]) / (second[j] - first[j]);
          return (
            colorStops[i].position +
            (colorStops[i + 1].position - colorStops[i].position) * p
          );
        }
      }
    }
  }
  return 0;
};

const isColorBetween = (colorA, colorC, colorB) => {
  return (
    colorB[0] >= Math.min(colorA[0], colorC[0]) &&
    colorB[0] <= Math.max(colorA[0], colorC[0]) &&
    colorB[1] >= Math.min(colorA[1], colorC[1]) &&
    colorB[1] <= Math.max(colorA[1], colorC[1]) &&
    colorB[2] >= Math.min(colorA[2], colorC[2]) &&
    colorB[2] <= Math.max(colorA[2], colorC[2])
  );
};

I think if you have read the previous blogs, you will automatically understand the above code or you can try to understand it yourself.

So in creating a color-picker, the main tasks are writing logic to calculate color when the thumb is dragging and calculate thumb position when the color is given. I have explained these things, but there are many more tasks to be done, and you can explore them by visiting the GitHub Repository and NPM package.

I hope you learn and understand how to create a color-picker by reading these blog posts, improve your logic writing, and find these posts helpful and informative. Happy Coding!

Published on Feb 14, 2024
Comments (undefined)

Read More