-
-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How dynamically trigger move of selected point in color-picker:sv element - n points up, left, down or right? #71
Comments
Good idea. Currently the method for manually move the pointer of X and Y axis is not exposed, but may be useful in the future. I won’t be integrating this feature into the core, but will probably provide it as another tweak. I assume that you will not use the mouse at all. What do you expect when you press the Tab key? What to do with the hue and alpha slider? |
Well, I was thinking to use the mouse to click on a starting point (starting color). "What to do with the hue and alpha slider?" |
So, I just took out the const picker = new CP(document.querySelector('input'));
function P2RGB(a) {
let h = +a[0],
s = +a[1],
v = +a[2],
r,
g,
b,
i,
f,
p,
q,
t;
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
i = i || 0;
q = q || 0;
t = t || 0;
switch (i % 6) {
case 0:
r = v, g = t, b = p;
break;
case 1:
r = q, g = v, b = p;
break;
case 2:
r = p, g = v, b = t;
break;
case 3:
r = p, g = q, b = v;
break;
case 4:
r = t, g = p, b = v;
break;
case 5:
r = v, g = p, b = q;
break;
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function RGB2HSV(a) {
let r = +a[0] / 255,
g = +a[1] / 255,
b = +a[2] / 255,
max = Math.max(r, g, b),
min = Math.min(r, g, b),
h,
s,
v = max,
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, s, v];
}
picker.on('change', function(r, g, b, a) {
this.source.value = this.color(r, g, b, a);
});
const step = .05; // Adjust the color step here!
picker.source.addEventListener('keydown', function(e) {
let key = e.key,
color = picker.get();
let a = color.pop(); // Detach alpha value from the color list
let [h, s, v] = RGB2HSV(color);
if ('ArrowDown' === key) {
v -= step;
v = v < 0 ? .01 : v; // Cannot use `0` because it will be normalized to red :(
picker.set(...[...P2RGB([h, s, v]), a]);
e.preventDefault();
} else if ('ArrowLeft' === key) {
s -= step;
s = s < 0 ? .01 : s; // Cannot use `0` because it will be normalized to red :(
picker.set(...[...P2RGB([h, s, v]), a]);
e.preventDefault();
} else if ('ArrowRight' === key) {
s += step;
s = s > 1 ? 1 : s;
picker.set(...[...P2RGB([h, s, v]), a]);
e.preventDefault();
} else if ('ArrowUp' === key) {
v += step;
v = v > 1 ? 1 : v;
picker.set(...[...P2RGB([h, s, v]), a]);
e.preventDefault();
}
}, false); No need to add special method to move the cursor by arrow keys. Changing the saturation and value is enough. |
That was quick!!! Now I can probably write the code for moving it diagonally myself. Much appreciated!!!! |
Is it possible to link the eventlistener keydown to both the i tag in the color-picker:sv and i tag in color-picker:h (instead of picker.source) ? |
That It was only for presentational purpose. |
By the way, here is the final result. It is still buggy because of the color value rounding that occurs. Maybe it will be more precise if the pointer can be moved directly without any rounding and normalization. The rounding process can then be done in the color value. People usually don’t notice that. But the pointer position seems to be easily noticed. /* Fake focus state */
.color-picker.h-is-selected .color-picker\:h div,
.color-picker.sv-is-selected .color-picker\:sv div,
.color-picker.a-is-selected .color-picker\:a div {
outline: 2px solid rgba(0, 0, 255, .5);
outline-offset: -2px;
} const picker = new CP(document.querySelector('input'));
function P2RGB(a) {
let h = +a[0],
s = +a[1],
v = +a[2],
r,
g,
b,
i,
f,
p,
q,
t;
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
i = i || 0;
q = q || 0;
t = t || 0;
switch (i % 6) {
case 0:
r = v, g = t, b = p;
break;
case 1:
r = q, g = v, b = p;
break;
case 2:
r = p, g = v, b = t;
break;
case 3:
r = p, g = q, b = v;
break;
case 4:
r = t, g = p, b = v;
break;
case 5:
r = v, g = p, b = q;
break;
}
return [r * 255, g * 255, b * 255];
}
function RGB2HSV(a) {
let r = +a[0] / 255,
g = +a[1] / 255,
b = +a[2] / 255,
max = Math.max(r, g, b),
min = Math.min(r, g, b),
h,
s,
v = max,
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, s, v];
}
picker.on('change', function(r, g, b, a) {
this.source.value = this.color(r, g, b, a);
});
const step = .05; // Adjust the color step here!
let tabIndex = 0; // 0: sv, 1: h, 2: a
// Reset tab index on focus
picker.source.addEventListener('focus', function() {
tabIndex = 0;
picker.self.classList.add('sv-is-selected');
picker.self.classList.remove('a-is-selected');
picker.self.classList.remove('h-is-selected');
});
picker.source.addEventListener('keydown', function(e) {
let key = e.key,
stop = false,
update = false;
if (picker.visible) {
// Press `Escape` to close the color picker panel
if ('Escape' === key) {
picker.exit();
picker.source.focus();
picker.source.select();
stop = true;
// Press `Tab` or `Shift+Tab` to cycle between color picker controls
} else if ('Tab' === key) {
if (e.shiftKey) {
tabIndex = tabIndex < 1 ? 2 : tabIndex - 1;
} else {
tabIndex = tabIndex > 1 ? 0 : tabIndex + 1;
}
// console.info(tabIndex);
picker.self.classList.remove('a-is-selected');
picker.self.classList.remove('h-is-selected');
picker.self.classList.remove('sv-is-selected');
picker.self.classList.add(['sv', 'h', 'a'][tabIndex] + '-is-selected');
stop = true;
}
if (stop) {
e.preventDefault();
return;
}
}
let [r, g, b, a] = picker.get();
let [h, s, v] = RGB2HSV([r, g, b]);
// Saturation/Value
if (0 === tabIndex) {
if ('ArrowDown' === key) {
v -= step;
v = v < 0 ? .01 : v; // Cannot use `0` because it will be normalized to red :(
stop = update = true;
} else if ('ArrowLeft' === key) {
s -= step;
s = s < 0 ? .01 : s; // Cannot use `0` because it will be normalized to red :(
stop = update = true;
} else if ('ArrowRight' === key) {
s += step;
s = s > 1 ? 1 : s;
stop = update = true;
} else if ('ArrowUp' === key) {
v += step;
v = v > 1 ? 1 : v;
stop = update = true;
}
// Hue
} else if (1 === tabIndex) {
if ('ArrowDown' === key) {
h -= step;
h = h < 0 ? 0 : h;
stop = update = true;
} else if ('ArrowUp' === key) {
h += step;
h = h > 1 ? 1 : h;
stop = update = true;
}
// Alpha
} else if (2 === tabIndex) {
if ('ArrowDown' === key) {
a -= .01;
a = a < 0 ? 0 : a;
stop = update = true;
} else if ('ArrowUp' === key) {
a += .01;
a = a > 1 ? 1 : a;
stop = update = true;
}
}
stop && e.preventDefault();
update && picker.enter().set(...[...P2RGB([h, s, v]), a]);
}, false); |
UPDATE: added a third and fourth issue: Kind of working - but found some issues that I hope can be fixed:
|
Done some more testing and found a couple of more issues. |
@bongobongo Try this one. It moves the pointer by pixels → https://taufik-nurrohman.js.org/color-picker/tweak/keyboard.html |
Just tested it.
and this was missing: Except for this - it really works very well |
Tested it again.
The issues can be seen here: |
That’s because of the color data rounding done by Actually, I can fix this using debounce function but I am afraid that this solution won’t last long. Can you confirm that the movement will be straight on the line when you keep pressing the arrow key? |
If I keep the down arrow pressed, it keeps on a straight line. When it reaches bottom of saturation - the marker is moved to bottom left. UPDATE: Now also tested in Firefox. |
Yes, because there is no point in moving the pointer to the left or right at the bottom of the saturation. It will always be black. |
"....there is no point in moving the pointer to the left or right at the bottom of the saturation. It will always be black..." Anyway... before this last "fix" - using arrow down, did not make marker change direction nor to pop to a bottom corner. It is counterinutitive - and not very functional - in my opinion. |
I’m sure the source that causes that eratic movement comes from Including the hue direction: both 0 and 100% distance from top have the same red color. Because hue is basically a circle and so 0° is equal to 360°. If we, for example put the hue slider to the top to get a red color and assuming that normally the default red placement is at the bottom, then if we refresh the color picker, it is likely the pointer will be placed at bottom because the color converter only understand about the input color. And input color does not have directions. |
I am afraid that I cannot fix the buggy pointer position at the bottom unless I use some hack to store the exact pointer position somewhere. But I can try to find a better “RGB to HSV” converter function. By the way, if you ever know other JavaScript color picker application that has keyboard navigation support, please let me know. Maybe I can implement some feature from there for this tweak. |
If you remove the |
"...I am afraid that I cannot fix the buggy pointer position at the bottom unless I use some hack to store the exact pointer position somewhere. .." |
Also want to mention that when I get the issue (without this last "fix" implemented, as of today), Regardless if that is possible or not: |
Ended up storing the pointer position before Not the cleanest solution, and sometimes there still small movement in the SV control when doing mixed keyboard and mouse interaction. But at least I can keep the pointer position now. 4c9f1fc#diff-6b752c354cc55775f0bc979cbd9fc9f2716158d743fddd3515dd15f7f8fa796dR230 You will still, however, get the color normalization fired when you hide the color picker element and then show it again. |
Did a quick test in Chrome.
UPDATE: I also notice that after the behaviour ref. #2 above, if I click somewhere in saturation area, and use arrow up - it moves first time much longer than it should. I tested this on your testpage. |
I wrote this some comments back, after you introduced an improved version: click inside hue. I looked a bit more on this version now, and for me - the most imporant thing is that it is "pixel perfect" in the saturation area - when you use the arrow keys. But after testing this feature, I realize that it's not very important to be able to use arrow keys to select the hue. If I want to select a specific hue, then I can allways enter a hex code in the input field and hit enter (after adding an onenter event to input field). So - perhaps - remove arrow navigation for hue and alpha for that version - and voila - there it is - a fully functional - and "pixel perfect" color picker with arrow navigation. |
Are you sure you are not testing on the cached version of the page? I suggest you to clone this repository and test it locally via
Peek.2021-10-11.06-44.mp4Peek.2021-10-11.06-57.mp4
Peek.2021-10-11.06-50.mp4Peek.2021-10-11.06-49.mp4Peek.2021-10-11.06-59.mp4 |
Here with exact click event. Peek.2021-10-11.07-18.mp4 |
Sigh! Found the issue after dragging. Peek.2021-10-11.07-26.mp4 |
As noted in my previous comment above. After testing this kind of navigation for a while now, I do not think there is a real need for using arrows to navigate in hue and opacity area. Using click and click-drag, is more than good enough for me. And if one need a specific hue, one could allways get that by entering a specific color in the input field + enter (with onenter event enabled), or e.g. click a predefined color. So if there is problems getting this to work - consistently - why not just drop those two features? The most important thing for me is that it's possible to repeat previous steps (using arrow navigation in saturation) - with excact color accuracy - when using the arrow keys in saturation area. |
@bongobongo If your goal is to make a specific palette composition, you can always hook the change event to the pre-defined color palletes tweak so that it will generate solid tint/shade color list which you can click. You will need some tint/shade function to convert the current color into array of tint/shade color pallete. Like this one. I can make an example for that. But this “keyboard accessible color picker” thing is a good idea in my opinion. |
"...tint/shade function to convert the current color into array of tint/shade color pallete..." |
HTML<input type="text"> CSS.color-pallete {
border: 1px solid #000;
border-top-width: 0;
display: flex;
}
.color-pallete .color {
cursor: pointer;
flex: 1;
height: 1.5em;
}
.color-pallete .color:first-child {
border-right: 1px solid #000;
min-width: 1.5em;
}
.color-pallete .color:last-child {
border-left: 1px solid #000;
min-width: 1.5em;
} JSfunction createColorPicker(source, onChange) {
if (source.hasColorPallete) {
return;
}
source.hasColorPallete = true;
function createColorBlends(hex1, hex2, amt) {
let rgb1 = hex1.match(/../g),
rgb2 = hex2.match(/../g),
v = i => parseInt(i, 16),
r = Math.round(v(rgb1[0]) + (v(rgb2[0]) - v(rgb1[0])) * amt).toString(16).padStart(2, '0'),
g = Math.round(v(rgb1[1]) + (v(rgb2[1]) - v(rgb1[1])) * amt).toString(16).padStart(2, '0'),
b = Math.round(v(rgb1[2]) + (v(rgb2[2]) - v(rgb1[2])) * amt).toString(16).padStart(2, '0'),
a = rgb1[3] || "";
return r + g + b + a;
}
function createColorPallete(picker, hex1, hex2, count) {
const pallete = document.createElement('span');
pallete.className = 'color-pallete';
for (let color, i = 0; i < count; ++i) {
color = document.createElement('span');
color.addEventListener('click', function(e) {
let value = this.title;
picker.source.value = value;
// picker.set.apply(picker, CP.HEX(value));
onChange.apply(picker, CP.HEX(value));
e.preventDefault();
e.stopPropagation();
}, false);
color.className = 'color';
color.style.backgroundColor = color.title = '#' + createColorBlends(hex1, hex2, i / (count - 1));
pallete.appendChild(color);
}
picker.self.appendChild(pallete);
return pallete.children;
}
const picker = new CP(source);
let color = picker.source.value.slice(1); // HEX color code without the `#` part
let mix1 = createColorPallete(picker, color, '808080', 12),
mix2 = createColorPallete(picker, color, 'ffffff', 12),
mix3 = createColorPallete(picker, color, '000000', 12);
function updateColorPallete(value) {
for (let i = 0, j = mix1.length; i < j; ++i) {
mix1[i].style.backgroundColor = mix1[i].title = '#' + createColorBlends(value.slice(1), '808080', i / (j - 1));
}
for (let i = 0, j = mix2.length; i < j; ++i) {
mix2[i].style.backgroundColor = mix2[i].title = '#' + createColorBlends(value.slice(1), 'ffffff', i / (j - 1));
}
for (let i = 0, j = mix3.length; i < j; ++i) {
mix3[i].style.backgroundColor = mix3[i].title = '#' + createColorBlends(value.slice(1), '000000', i / (j - 1));
}
}
picker.on('change', function(r, g, b, a) {
let value = this.color(r, g, b, a);
this.source.value = value;
onChange.apply(this, CP.HEX(value));
updateColorPallete(value);
});
picker.on('enter', function(r, g, b, a) {
updateColorPallete(this.color(r, g, b, a));
});
}
createColorPicker(document.querySelector('input'), function(r, g, b, a) {
document.documentElement.style.backgroundColor = this.color(r, g, b, a);
}); |
Great, but have the issues with arrow navigation been fixed? The two most important one now after short testing:
|
I will try to fix it later after #73. |
Found an issue when using arrows in saturation area. Ideally - for me, one step, should move the same distance - relative to size of saturation area. Example 2: |
The enableKeyboardControls(picker, 5); // Move cursor by `5px` To make it dynamic, you can measure it based on the current font size, but you can’t get it right easily. const step = parseInt(getComputedStyle(picker.self).getPropertyValue('font-size'), 10);
enableKeyboardControls(picker, step); |
I would like to make it possible, in custom javascript, to move the selected round point in color-picker:sv element - using e.g. the up, down, left and right arrows (and/or using the I, M, J and L keys) on the keyboard.
And I would like to make it so the selected point moves n points in a given direction on each press of the arrow -
where the value of n is in relation to how many points there are in total horisontally (and vertically).
How do i get number of points in the color-picker:sv element - horisontally and vertically?
How do I move the selected point n points up, right, left or down - and trigger the same events that are triggered when I left click to select a color?
The text was updated successfully, but these errors were encountered: