WIP: implement Edit Colors dialog

Fixes https://github.com/1j01/jspaint/issues/114 and makes opening the color dialog work with Eye Gaze Mode dwell clicks and Speech Recognition as well (which can't trigger a "user gesture" as far as the browser's security model is concerned).
main
Isaiah Odhner 2020-05-30 09:35:46 -04:00
parent 4ef30d7eb0
commit d62d244777
9 changed files with 400 additions and 96 deletions

View File

@ -142,7 +142,7 @@ context('tool tests', () => {
// it('brush tool', () => {
// cy.get(".tool[title='Brush']").click();
// // gesture([{x: 50, y: 50}, {x: 100, y: 100}]);
// cy.get(":nth-child(21) > input").rightclick();
// cy.get(".swatch:nth-child(21)").rightclick();
// cy.window().then({timeout: 8000}, async (win)=> {
// for (let secondary=0; secondary<=1; secondary++) {
// for (let b=0; b<12; b++) {
@ -195,7 +195,7 @@ context('tool tests', () => {
it(`${toolName.toLowerCase()} tool`, () => {
cy.get(`.tool[title='${toolName}']`).click();
// gesture([{x: 50, y: 50}, {x: 100, y: 100}]);
cy.get(":nth-child(22) > input").rightclick();
cy.get(".swatch:nth-child(22)").rightclick();
cy.window().then({timeout: 60000}, async (win)=> {
for (let row=0; row<4; row++) {
const secondary = !!(row % 2);

View File

@ -16,11 +16,6 @@
</ol>
<p><b>Notes</b></p>
<ul>
<!-- @TODO: maybe emulate the color picker dialog and make this note unnecessary: -->
<li>
These instructions are very operating system specific and you may get a completely different color picker.
On some systems the color picker may be utterly useless, and give you less color options than the default palette.
</li>
<li>You can also double click on a color in the color box to edit.</li>
</ul>
</body>

View File

@ -1,21 +1,23 @@
/** Used by the Colors Box and by the Edit Colors dialog */
function $Swatch(color){
const $b = $(E("div")).addClass("swatch");
const $swatch = $(E("div")).addClass("swatch");
const swatch_canvas = make_canvas();
$(swatch_canvas).css({pointerEvents: "none"}).appendTo($b);
$(swatch_canvas).css({pointerEvents: "none"}).appendTo($swatch);
$b.update = (set_color_to = color) => {
$swatch.update = (set_color_to = color) => {
color = set_color_to;
if(color instanceof CanvasPattern){
$b.addClass("pattern");
$swatch.addClass("pattern");
$swatch[0].dataset.color = "";
}else{
$b.removeClass("pattern");
$swatch.removeClass("pattern");
$swatch[0].dataset.color = color;
}
requestAnimationFrame(() => {
swatch_canvas.width = $b.innerWidth();
swatch_canvas.height = $b.innerHeight();
swatch_canvas.width = $swatch.innerWidth();
swatch_canvas.height = $swatch.innerHeight();
// I don't think disable_image_smoothing() is needed here
if(color){
@ -25,11 +27,11 @@ function $Swatch(color){
});
};
$G.on("theme-load", () => {
$b.update();
$swatch.update();
});
$b.update();
$swatch.update();
return $b;
return $swatch;
}
function $ColorBox(vertical){
@ -57,73 +59,45 @@ function $ColorBox(vertical){
$G.triggerHandler("option-changed");
});
// the one color editted by "Edit Colors..."
let $last_fg_color_button;
function set_color(col){
if(ctrl){
colors.ternary = col;
}else if(button === 0){
colors.foreground = col;
}else if(button === 2){
colors.background = col;
}
$G.trigger("option-changed");
}
function color_to_hex(col){
if(!col.match){ // i.e. CanvasPattern
return "#000000";
}
const rgb_match = col.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
const rgb = rgb_match ? rgb_match.slice(1) : get_rgba_from_color(col).slice(0, 3);
function hex(x){
return (`0${parseInt(x).toString(16)}`).slice(-2);
}
return rgb ? (`#${hex(rgb[0])}${hex(rgb[1])}${hex(rgb[2])}`) : col;
}
const make_color_button = (color) => {
const $b = $Swatch(color).addClass("color-button");
$b.appendTo($palette);
const $i = $(E("input")).attr({type: "color"});
$i.appendTo($b);
$i.on("change", () => {
color = $i.val();
$b.update(color);
set_color(color);
$b.data("set_color", (new_color)=> {
$b.update(new_color);
});
$i.css("opacity", 0);
$i.prop("enabled", false);
$i.val(color_to_hex(color));
const double_click_period_ms = 400;
let within_double_click_period = false;
let double_click_button = null;
let double_click_tid;
// @TODO: handle left+right click at same time
// can do this with mousedown instead of pointerdown, but may need to improve eye gaze mode click simulation
$b.on("pointerdown", e => {
// @TODO: how should the ternary color, and selection cropping, work on macOS?
// @TODO: allow metaKey for ternary color, and selection cropping, on macOS?
ctrl = e.ctrlKey;
button = e.button;
if(button === 0){
$last_fg_color_button = $b;
$c.data("$last_fg_color_button", $b);
}
set_color(color);
$i.val(color_to_hex(color));
if(e.button === button && $i.prop("enabled")){
$i.trigger("click", "synthetic");
}
$i.prop("enabled", true);
setTimeout(() => {
$i.prop("enabled", false);
}, 400);
});
$i.on("click", (e, synthetic) => {
if(!synthetic){
e.preventDefault();
const color_selection_slot = ctrl ? "ternary" : button === 0 ? "foreground" : button === 2 ? "background" : null;
if (color_selection_slot) {
if (within_double_click_period && button === double_click_button) {
show_edit_colors_window($b, color_selection_slot);
} else {
colors[color_selection_slot] = color;
$G.trigger("option-changed");
}
clearTimeout(double_click_tid);
double_click_tid = setTimeout(() => {
within_double_click_period = false;
double_click_button = null;
}, double_click_period_ms);
within_double_click_period = true;
double_click_button = button;
}
});
};
@ -150,11 +124,8 @@ function $ColorBox(vertical){
}
// the "last foreground color button" starts out as the first in the palette
$last_fg_color_button = $palette.find(".color-button");
$c.data("$last_fg_color_button", $palette.find(".color-button:first-child"));
};
build_palette();
$(window).on("theme-change", build_palette);
let $c;
if (vertical) {
$c = $Component("Colors", "tall", $cb);
@ -164,16 +135,8 @@ function $ColorBox(vertical){
$c.appendTo($bottom);
}
$c.edit_last_color = () => {
// Edit the last color cell that's been selected as the foreground color.
create_and_trigger_input({type: "color"}, input => {
// window.console && console.log(input, input.value);
// @FIXME
$last_fg_color_button.trigger({type: "pointerdown", ctrlKey: false, button: 0});
$last_fg_color_button.find("input").val(input.value).triggerHandler("change");
})
.show().css({width: 0, height: 0, padding: 0, border: 0, position: "absolute", pointerEvents: "none", overflow: "hidden"});
};
build_palette();
$(window).on("theme-change", build_palette);
$c.rebuild_palette = build_palette;

View File

@ -26,6 +26,30 @@ let palette = default_palette;
let polychrome_palette = palette;
let monochrome_palette = make_monochrome_palette();
// https://github.com/kouzhudong/win2k/blob/ce6323f76d5cd7d136b74427dad8f94ee4c389d2/trunk/private/shell/win16/comdlg/color.c#L38-L43
// These are a fallback in case colors are not recieved from some driver.
// const default_basic_colors = [
// "#8080FF", "#80FFFF", "#80FF80", "#80FF00", "#FFFF80", "#FF8000", "#C080FF", "#FF80FF",
// "#0000FF", "#00FFFF", "#00FF80", "#40FF00", "#FFFF00", "#C08000", "#C08080", "#FF00FF",
// "#404080", "#4080FF", "#00FF00", "#808000", "#804000", "#FF8080", "#400080", "#8000FF",
// "#000080", "#0080FF", "#008000", "#408000", "#FF0000", "#A00000", "#800080", "#FF0080",
// "#000040", "#004080", "#004000", "#404000", "#800000", "#400000", "#400040", "#800040",
// "#000000", "#008080", "#408080", "#808080", "#808040", "#C0C0C0", "#400040", "#FFFFFF",
// ];
// Grabbed with Color Cop from the screen with Windows 98 SE running in VMWare
const basic_colors = [
"#FF8080", "#FFFF80", "#80FF80", "#00FF80", "#80FFFF", "#0080FF", "#FF80C0", "#FF80FF",
"#FF0000", "#FFFF00", "#80FF00", "#00FF40", "#00FFFF", "#0080C0", "#8080C0", "#FF00FF",
"#804040", "#FF8040", "#00FF00", "#008080", "#004080", "#8080FF", "#800040", "#FF0080",
"#800000", "#FF8000", "#008000", "#008040", "#0000FF", "#0000A0", "#800080", "#8000FF",
"#400000", "#804000", "#004000", "#004040", "#000080", "#000040", "#400040", "#400080",
"#000000", "#808000", "#808040", "#808080", "#408080", "#C0C0C0", "#400040", "#FFFFFF",
];
let custom_colors = [
"#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF",
"#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF",
];
// declared like this for Cypress tests
window.default_brush_shape = "circle";
window.default_brush_size = 4;

View File

@ -332,6 +332,197 @@ function show_custom_zoom_window() {
$w.center();
}
let $edit_colors_window;
// @TODO: add custom colors to the list
// @TODO: initially select the first color cell that matches the swatch to edit, if any
// @TODO: initialize custom colors list index to matched cell, if matched
// @TODO: persist custom colors list
// @TODO: more keyboard navigation
// @TODO: OK with Enter, after selecting a focused color if applicable
// @TODO: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Grid_Role
// or https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role
// @TODO: question mark button in titlebar that lets you click on parts of UI to ask about them; also context menu "What's this?"
// $(()=> { show_edit_colors_window(); $(".expando-button").click(); $edit_colors_window.center(); }); // for development
function show_edit_colors_window($swatch_to_edit, color_selection_slot_to_edit) {
// console.log($swatch_to_edit, $colorbox.data("$last_fg_color_button"));
$swatch_to_edit = $swatch_to_edit || $colorbox.data("$last_fg_color_button");
color_selection_slot_to_edit = color_selection_slot_to_edit || "foreground";
if ($edit_colors_window) {
$edit_colors_window.close();
}
const $w = new $FormToolWindow("Edit Colors");
$w.addClass("edit-colors-window");
$edit_colors_window = $w;
let hue_degrees = 0;
let sat_percent = 50;
let lum_percent = 50;
const make_color_grid = (colors, name)=> {
const $color_grid = $(`<div class="color-grid">`).attr({name});
for (const color of colors) {
const $swatch = $Swatch(color);
$swatch.appendTo($color_grid).addClass("inset-deep");
$swatch.attr("tabindex", 0);
}
const num_colors_per_row = 8;
const navigate = (relative_index)=> {
const $focused = $color_grid.find(".swatch:focus");
if (!$focused.length) { return; }
const $swatches = $color_grid.find(".swatch");
const from_index = $swatches.toArray().indexOf($focused[0]);
if (relative_index === -1 && (from_index % num_colors_per_row) === 0) { return; }
if (relative_index === +1 && (from_index % num_colors_per_row) === num_colors_per_row - 1) { return; }
const to_index = from_index + relative_index;
const $to_focus = $($swatches.toArray()[to_index]);
// console.log({from_index, to_index, $focused, $to_focus});
if (!$to_focus.length) { return; }
$to_focus.focus();
};
const select = ($swatch)=> {
$color_grid.find(".swatch").removeClass("selected");
$swatch.addClass("selected");
const [r, g, b] = get_rgba_from_color($swatch[0].dataset.color);
const [h, s, l] = rgb_to_hsl(r, g, b);
hue_degrees = h * 360;
sat_percent = s * 100;
lum_percent = l * 100;
draw();
};
$color_grid.on("keydown", (event)=> {
// console.log(event.code);
if (event.code === "ArrowRight") { navigate(+1); }
if (event.code === "ArrowLeft") { navigate(-1); }
if (event.code === "ArrowDown") { navigate(+num_colors_per_row); }
if (event.code === "ArrowUp") { navigate(-num_colors_per_row); }
if (event.code === "Home") { $color_grid.find(".swatch:first-child").focus(); }
if (event.code === "End") { $color_grid.find(".swatch:last-child").focus(); }
if (event.code === "Space" || event.code === "Enter") {
select($color_grid.find(".swatch:focus"));
}
});
$color_grid.on("pointerdown", (event)=> {
const $swatch = $(event.target).closest(".swatch");
if ($swatch.length) {
select($swatch);
}
});
$color_grid.on("dragstart", (event)=> {
event.preventDefault();
});
return $color_grid;
};
const $left_right_split = $(`<div class="left-right-split">`).appendTo($w.$main);
const $left = $(`<div class="left-side">`).appendTo($left_right_split);
const $right = $(`<div class="right-side">`).appendTo($left_right_split).hide();
$left.append(`<label for="basic-colors">Basic colors:</label>`);
make_color_grid(basic_colors, "basic-colors").appendTo($left);
$left.append(`<label for="custom-colors">Custom colors:</label>`);
make_color_grid(custom_colors, "custom-colors").appendTo($left);
const $expando_button = $(`<button class="expando-button">`)
.text("Define Custom Colors >>")
.appendTo($left)
.on("click", ()=> {
$right.show();
$expando_button.attr("disabled", "disabled");
})
const rainbow_canvas = make_canvas(175, 187);
const luminosity_canvas = make_canvas(10, 187);
const result_canvas = make_canvas(58, 40);
const lum_arrow_canvas = make_canvas(5, 9);
const draw = ()=> {
for (let y = 0; y < rainbow_canvas.height; y += 6) {
for (let x = -1; x < rainbow_canvas.width; x += 3) {
rainbow_canvas.ctx.fillStyle = `hsl(${x/rainbow_canvas.width*360}deg, ${(1-y/rainbow_canvas.height)*100}%, 50%)`;
rainbow_canvas.ctx.fillRect(x, y, 3, 6);
}
}
const x = ~~(hue_degrees/360*rainbow_canvas.width);
const y = ~~((1-sat_percent/100)*rainbow_canvas.height);
rainbow_canvas.ctx.fillStyle = "black";
rainbow_canvas.ctx.fillRect(x-1, y-9, 3, 5);
rainbow_canvas.ctx.fillRect(x-1, y+5, 3, 5);
rainbow_canvas.ctx.fillRect(x-9, y-1, 5, 3);
rainbow_canvas.ctx.fillRect(x+5, y-1, 5, 3);
for (let y = -2; y < luminosity_canvas.height; y += 6) {
luminosity_canvas.ctx.fillStyle = `hsl(${hue_degrees}deg, ${sat_percent}%, ${(1-y/luminosity_canvas.height)*100}%)`;
luminosity_canvas.ctx.fillRect(0, y, luminosity_canvas.width, 6);
}
lum_arrow_canvas.ctx.fillStyle = "black";
for (let x = 0; x < lum_arrow_canvas.width; x++) {
lum_arrow_canvas.ctx.fillRect(x, lum_arrow_canvas.width-x-1, 1, 1+x*2);
}
lum_arrow_canvas.style.position = "absolute";
lum_arrow_canvas.style.right = "7px";
lum_arrow_canvas.style.top = `${3 + ~~((1-lum_percent/100)*luminosity_canvas.height)}px`;
result_canvas.ctx.fillStyle = `hsl(${hue_degrees}deg, ${sat_percent}%, ${lum_percent}%)`;
result_canvas.ctx.fillRect(0, 0, result_canvas.width, result_canvas.height);
};
draw();
$(rainbow_canvas).addClass("rainbow-canvas inset-shallow");
$(luminosity_canvas).addClass("luminosity-canvas inset-shallow");
$(result_canvas).addClass("result-color-canvas inset-shallow");
const select_hue_sat = (event)=> {
hue_degrees = Math.min(1, Math.max(0, event.offsetX/rainbow_canvas.width))*360;
sat_percent = Math.min(1, Math.max(0, (1 - event.offsetY/rainbow_canvas.height)))*100;
draw();
event.preventDefault();
};
$(rainbow_canvas).on("pointerdown", (event)=> {
select_hue_sat(event);
$(rainbow_canvas).on("pointermove", select_hue_sat);
rainbow_canvas.setPointerCapture(event.pointerId);
});
$G.on("pointerup pointercancel", (event)=> {
$(rainbow_canvas).off("pointermove", select_hue_sat);
// rainbow_canvas.releasePointerCapture(event.pointerId);
});
const select_lum = (event)=> {
lum_percent = Math.min(1, Math.max(0, (1 - event.offsetY/luminosity_canvas.height)))*100;
draw();
event.preventDefault();
};
$(luminosity_canvas).on("pointerdown", (event)=> {
select_lum(event);
$(luminosity_canvas).on("pointermove", select_lum);
luminosity_canvas.setPointerCapture(event.pointerId);
});
$G.on("pointerup pointercancel", (event)=> {
$(luminosity_canvas).off("pointermove", select_lum);
// luminosity_canvas.releasePointerCapture(event.pointerId);
});
$right.append(rainbow_canvas, luminosity_canvas, result_canvas, lum_arrow_canvas);
$w.$Button("OK", () => {
const color = `hsl(${hue_degrees}deg, ${sat_percent}%, ${lum_percent}%)`;
// console.log($swatch_to_edit, $swatch_to_edit.data("set_color"));
$swatch_to_edit.data("set_color")(color);
colors[color_selection_slot_to_edit] = color;
$G.triggerHandler("option-changed");
$w.close();
})[0].focus();
$w.$Button("Cancel", () => {
$w.close();
});
$left.append($w.$buttons);
$w.center();
}
function toggle_grid() {
show_grid = !show_grid;
// $G.trigger("option-changed");

View File

@ -221,3 +221,38 @@ function get_icon_for_tools(tools) {
})
return icon_canvas;
}
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSL representation
*/
function rgb_to_hsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var 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, s, l];
}

View File

@ -612,7 +612,7 @@ window.menus = {
"edit last color", "create new color", "choose new color", "create a new color", "pick a new color",
],
action: ()=> {
$colorbox.edit_last_color();
show_edit_colors_window();
},
description: "Creates a new color.",
},

View File

@ -390,16 +390,99 @@ html, body, .jspaint {
height: 15px;
border: 0;
}
.color-button input {
margin: 0;
padding: 0;
.edit-colors-window .color-grid {
width: 222px;
display: grid;
grid-template-columns: repeat(8, 16px);
grid-gap: 5px 9px;
user-select: none;
}
.edit-colors-window .swatch {
width: 16px;
height: 13px;
display: flex;
}
.edit-colors-window .window-content {
font-family: Tahoma, sans-serif;
font-size: 12px;
}
.edit-colors-window .swatch {
outline: none; /* we'll provide a new focus indicator below */
}
.edit-colors-window .swatch.selected {
outline: 1px solid black;
outline-offset: 0px;
}
.edit-colors-window .swatch:focus::after {
content: "";
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
right: 0;
bottom: 0;
outline: 1px dotted black;
outline-offset: 5px;
}
.edit-colors-window .window-content .left-right-split {
display: flex;
flex-flow: row;
}
.edit-colors-window .window-content .left-side {
display: flex;
flex-flow: column;
width: 217px;
height: 298px;
}
.edit-colors-window .window-content .right-side {
flex-flow: column;
width: 218px;
position: relative;
padding-top: 7px;
padding-left: 10px;
}
.edit-colors-window .window-content .button-group {
display: flex;
flex-flow: row;
}
.edit-colors-window .window-content .button-group button {
min-width: 66px;
margin: 3px;
}
.edit-colors-window .window-content .expando-button,
.edit-colors-window .window-content .button-group button:first-of-type {
margin-left: 5px;
}
.edit-colors-window .window-content button {
height: 23px;
box-sizing: border-box;
padding: 0;
margin-left: 3px;
}
.edit-colors-window .window-content .expando-button {
margin-top: 13px;
width: 210px;
}
.edit-colors-window .left-side label {
display: block;
margin-top: 7px;
margin-bottom: 5px;
margin-left: 5px;
}
.edit-colors-window .left-side label:nth-of-type(2) {
margin-top: 18px;
margin-bottom: 7px;
}
.edit-colors-window .color-grid {
margin-left: 8px;
}
.edit-colors-window .luminosity-canvas {
margin-left: 16px;
}
.edit-colors-window .result-color-canvas {
margin-top: 3px;
}
.canvas-area {
flex: 1;

View File

@ -209,14 +209,19 @@ body {
height: 18px;
}
.color-button, /* this outer one is only for detection for the hover halo in eye gaze mode */
.edit-colors-window .swatch, /* this outer one is only for detection for the hover halo in eye gaze mode */
.color-button canvas,
.color-selection canvas,
.edit-colors-window .swatch canvas,
.color-button:after,
.color-selection:after {
.color-selection:after,
.edit-colors-window .swatch:after {
border-radius: 3px;
position: relative;
}
.color-button:after,
.color-selection:after {
.color-button::after,
.color-selection::after,
.edit-colors-window .swatch::after {
content: '';
position: absolute;
left: 0;
@ -229,6 +234,14 @@ body {
margin-left: 1px;
}
.edit-colors-window .swatch {
width: 20px;
height: 17px;
}
.edit-colors-window .swatch:focus::after {
outline-offset: 3px;
}
/* @TODO: padding/margin on the top at least when in the sidebar */
.tools {
width: 50px;