0

I'm trying to implement my own color wheel picker, and I'm using this color wheel, as the base. I (finally) successfully added RGB sliders. So now when you change the color wheels color, the RGB sliders change dynamically, and when you change the RGB sliders, the color wheels color updates too.

The problem is, when I slide a slider from the RGB sliders, the other sliders tend to move a little bit too. For example, if I slide the green value, the red and blue values change a little bit.

I'm not exactly sure what's wrong. How can I get the other sliders to not move when I move one slider? (Obviously if I don't set the sliders value in redraw(), the sliders won't change when I move 1 slider, but I'm trying to find the core issue.)

JSFiddle

var b = document.body;
var colorWheelDiv = document.getElementById('colorWheelDiv');
var colorWheel = document.createElement('canvas');
var colorWheelOverlay = document.createElement('div');

var a = colorWheel.getContext('2d');
var label = document.getElementById('label');
var input = document.getElementById('input');

var redInput = document.getElementById('red');
var greenInput = document.getElementById('green');
var blueInput = document.getElementById('blue');
var alphaInput = document.getElementById('alpha');
var rgbInput = document.getElementsByClassName('rgbInput');
document.body.clientWidth; // fix bug in webkit: http://qfox.nl/weblog/218

// Jquery Elements

var $redSlider = $('#red');
var $greenSlider = $('#green');
var $blueSlider = $('#blue');
var $alphaSlider = $('#alpha');


(function() {

  // Declare constants and variables to help with minification
  // Some of these are inlined (with comments to the side with the actual equation)
  var doc = document;
  doc.colorWheel = doc.createElement;
  b.a = b.appendChild;


  // Add the colorWheel and the colorWheelOverlay
  colorWheelDiv.appendChild(colorWheelOverlay);
  colorWheelDiv.appendChild(colorWheel);
  colorWheelOverlay.id = 'colorWheelOverlay';
  colorWheel.id = 'colorWheel';


  var width = colorWheel.width = colorWheel.height = colorWheelDiv.clientHeight,
    imageData = a.createImageData(width, width),
    pixels = imageData.data,
    oneHundred = input.value = input.max = 100,
    circleOffset = 10,
    diameter = width - circleOffset * 2,
    radius = diameter / 2,
    radiusPlusOffset = radius + circleOffset,
    radiusSquared = radius * radius,
    two55 = 255,
    currentY = oneHundred,
    currentX = -currentY,
    center = radius / 2,
    wheelPixel = circleOffset * 4 * width + circleOffset * 4;

  // Math helpers
  var math = Math,
    PI = math.PI,
    PI2 = PI * 2,
    sqrt = math.sqrt,
    atan2 = math.atan2;

  // Load color wheel data into memory.
  for (y = input.min = 0; y < width; y++) {
    for (x = 0; x < width; x++) {
      var rx = x - radius,
        ry = y - radius,
        d = rx * rx + ry * ry,
        rgb = colorWheel_hsvToRgb(
          (atan2(ry, rx) + PI) / PI2, // Hue
          sqrt(d) / radius, // Saturation
          1 // Value
        );

      // Print current color, but hide if outside the area of the circle
      pixels[wheelPixel++] = rgb[0];
      pixels[wheelPixel++] = rgb[1];
      pixels[wheelPixel++] = rgb[2];
      pixels[wheelPixel++] = d > radiusSquared ? 0 : two55;
    }
  }

  // Bind Event Handlers
  input.oninput = redraw;
  colorWheel.onmousedown = doc.onmouseup = function(e) {
    // Unbind mousemove if this is a mouseup event, or bind mousemove if this a mousedown event
    doc.onmousemove = /p/.test(e.type) ? 0 : (redraw(e), redraw);
  }

  $(".rgbInput").not($alphaSlider).slider({
    range: "max",
    min: 0,
    max: 255,
    value: 0,
    slide: function(event, ui) {
      redrawRGB();
    }
  });


  function redrawRGB() {
    var red = $('#red').slider('value');
    var green = $('#green').slider('value');
    var blue = $('#blue').slider('value');

    var hsv = colorWheel_rgbToHsv(red, green, blue);

    var newD = math.round(math.pow(radius * hsv.s, 2));
    var newTheta = (hsv.h * PI2) - PI;

    currentX = math.round(math.sqrt(newD) * math.cos(newTheta));
    currentY = math.round(math.sqrt(newD) * math.sin(newTheta));
    input.value = math.round(hsv.v * 100);

    redraw(0);
  }

  // Handle manual calls + mousemove event handler + input change event handler all in one place.
  function redraw(e) {

    // Only process an actual change if it is triggered by the mousemove or mousedown event.
    // Otherwise e.pageX will be undefined, which will cause the result to be NaN, so it will fallback to the current value
    currentX = e.pageX - colorWheelDiv.offsetLeft - colorWheel.offsetLeft - radiusPlusOffset || currentX;
    currentY = e.pageY - colorWheelDiv.offsetTop - colorWheel.offsetTop - radiusPlusOffset || currentY;

    // Scope these locally so the compiler will minify the names.  Will manually remove the 'var' keyword in the minified version.
    var theta = atan2(currentY, currentX),
      d = currentX * currentX + currentY * currentY;

    // If the x/y is not in the circle, find angle between center and mouse point:
    //   Draw a line at that angle from center with the distance of radius
    //   Use that point on the circumference as the draggable location
    if (d > radiusSquared) {
      currentX = radius * math.cos(theta);
      currentY = radius * math.sin(theta);
      theta = atan2(currentY, currentX);
      d = currentX * currentX + currentY * currentY;
    }

    var vValue = parseInt(input.value, 10);
    var rgb = colorWheel_hsvToRgb(
      (theta + PI) / PI2, // Current hue (how many degrees along the circle)
      sqrt(d) / radius, // Current saturation (how close to the middle)
      vValue / oneHundred // Current value (input type="range" slider value)
    )

    label.textContent = b.style.background = rgb[3];
    colorWheelOverlay.style.opacity = ((vValue + 100 - 15) - (vValue * 2)) / oneHundred;


    // Set slider Position \\

    $redSlider.slider("value", math.round(rgb[0]));
    $greenSlider.slider("value", math.round(rgb[1]));
    $blueSlider.slider("value", math.round(rgb[2]));



    // Reset to color wheel and draw a spot on the current location. 
    a.putImageData(imageData, 0, 0);

    // Draw the current spot.
    // I have tried a rectangle, circle, and heart shape.
    /*
    // Rectangle:
    a.fillStyle = '#000';
    a.fillRect(currentX+radiusPlusOffset,currentY+radiusPlusOffset, 6, 6);
    */
    // Circle:
    a.beginPath();
    a.strokeStyle = '#000';
    a.arc(~~currentX + radiusPlusOffset, ~~currentY + radiusPlusOffset, 4, 0, PI2);
    a.stroke();

    // Heart:
    /* a.font = "1em arial";
     a.fillText("♥", currentX+radiusPlusOffset-4,currentY+radiusPlusOffset+4);*/

  }

  // Created a shorter version of the HSV to RGB conversion function in TinyColor
  // https://github.com/bgrins/TinyColor/blob/master/tinycolor.js
  function colorWheel_hsvToRgb(h, s, v) {
    h *= 6;
    var i = ~~h,
      f = h - i,
      p = v * (1 - s),
      q = v * (1 - f * s),
      t = v * (1 - (1 - f) * s),
      mod = i % 6,
      r = [v, q, p, p, t, v][mod] * two55,
      g = [t, v, v, q, p, p][mod] * two55,
      b = [p, p, t, v, v, q][mod] * two55;

    return [r, g, b, "rgb(" + math.round(r) + "," + math.round(g) + "," + math.round(b) + ")"];
  }

  function colorWheel_rgbToHsv(r, g, b) {

    r = r / two55;
    g = g / two55;
    b = b / two55;

    var max = math.max(r, g, b),
      min = math.min(r, g, b);
    var h, s, v = max;

    var 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: h,
      s: s,
      v: v
    };
  }

  // Kick everything off
  redraw(0);

})();
#colorWheelDiv {
  width: 400px;
  height: 400px;
  position: relative;
}
#colorWheelOverlay {
  background-color: black;
  position: absolute;
  pointer-events: none;
}
#colorWheelDiv,
#colorWheelOverlay,
#colorWheel {
  border-radius: 50%;
}
#colorWheelOverlay,
#colorWheel {
  width: 100%;
  height: 100%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>


R:
<div id="red" class="rgbInput"></div>
<br />G:
<div id="green" class="rgbInput"></div>
<br />B:
<div id="blue" class="rgbInput"></div>
<br />A:
<div id="alpha" class="rgbInput"></div>
<br />

<div id=colorWheelDiv></div>
<p id='label' style="font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 2em; line-height: normal; font-family: courier;">rgb(239,183,131)</p>
<input id="input" max="100" type="range" min="0">

Update

I tried that answer, and it didn't help. I still get the same results. The title may be the same, but I don't think the actual question is the same

Horay
  • 1,388
  • 2
  • 19
  • 36
  • 3
    Possible Duplicate of [Javascript convert HSB/HSV color to RGB accurately](http://stackoverflow.com/questions/17242144/javascript-convert-hsb-hsv-color-to-rgb-accurately) – Tushar Dec 18 '15 at 06:48
  • I tried that answer, and it didn't help. I still get the same results. The title may be the same, but I don't think the actual question is the same – Horay Dec 18 '15 at 07:16
  • @Horay Then please update your title to better reflect your question, which is a UI bug rather than a colour or css problem (i.e. please remove the tags too.) When user moves sliders, you should compose colour from the sliders and update the other parts, instead of having the result feed back to the sliders. – Sheepy Dec 18 '15 at 07:53
  • @Sheepy Can you please provide an updated JSFiddle? – Horay Dec 18 '15 at 07:55
  • @Horay (1) You can put code using the code snippet in Stack Overflow, generally preferred over jsfiddle. (2) No, I am just reviewing the close queue, sorry. – Sheepy Dec 18 '15 at 07:58

1 Answers1

1

You are converting the RGB values to HSV and then converting them back to RGB to set the other slider values. The conversion process has finite precision, so that's why you see the wiggling of the other sliders. The solution would be to set the RGB slider values directly from the original RGB values, not from the computed HSV values.

UPDATE

You can accomplish this as follows:

// Set slider Position \\
if(e){
$redSlider.slider( "value", math.round(rgb[0]));
$greenSlider.slider( "value", math.round(rgb[1]));
$blueSlider.slider( "value", math.round(rgb[2]));
}
raduation
  • 792
  • 5
  • 11
  • Am I not accessing the same value, whether in hsv or rgb? – Horay Dec 18 '15 at 07:15
  • It's not exactly the same, due to limited precision in converting back and forth. That's the source of the little wiggles in the sliders, imperfect precision and rounding. – raduation Dec 18 '15 at 07:16
  • Is there a way to perfect it? – Horay Dec 18 '15 at 07:17
  • No, it is the nature of the math involved that roundings have to be done. So there will be some imprecision. Follow the suggestion I gave in my post and it will work perfectly. – raduation Dec 18 '15 at 07:18
  • The sliders visual wiggliness won't be seen, but will the colors be accurate? – Horay Dec 18 '15 at 07:20
  • Will the following work? In the function of `redraw` add an if statement `if (e)`, then in there call another function adjusting the sliders. – Horay Dec 18 '15 at 07:38
  • I would just use the RGB values directly. In other words, recalculate the other 2 sliders based on the value of the slider you just moved. – raduation Dec 18 '15 at 07:44
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/98314/discussion-between-raduation-and-horay). – raduation Dec 18 '15 at 07:58