feat: color picker for hsv settings

This commit is contained in:
2025-04-23 15:56:58 +02:00
parent 24fc861ef4
commit bc06e8ee80
2 changed files with 151 additions and 38 deletions

View File

@@ -1,6 +1,85 @@
import type { Action } from "svelte/action"; import type { Action } from "svelte/action";
import { changes, ChangeType, settings } from "$lib/undo-redo"; import { changes, ChangeType, settings } from "$lib/undo-redo";
/**
* https://gist.github.com/mjackson/5311256
*/
function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
const v = max;
const d = max - min;
const 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 [Math.floor(h * 0xffff), Math.floor(s * 0xff), Math.floor(v * 0xff)];
}
/**
* https://gist.github.com/mjackson/5311256
*/
function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
h /= 0xffff;
s /= 0xff;
v /= 0xff;
let r = 0;
let g = 0;
let b = 0;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
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.floor(r * 0xff), Math.floor(g * 0xff), Math.floor(b * 0xff)];
}
export const setting: Action< export const setting: Action<
HTMLInputElement | HTMLSelectElement, HTMLInputElement | HTMLSelectElement,
{ id: number; inverse?: number; scale?: number } { id: number; inverse?: number; scale?: number }
@@ -9,7 +88,12 @@ export const setting: Action<
{ id, inverse, scale }, { id, inverse, scale },
) { ) {
node.setAttribute("disabled", ""); node.setAttribute("disabled", "");
const type = node.getAttribute("type") as "number" | "checkbox" | "range"; const type = node.getAttribute("type") as
| "number"
| "checkbox"
| "range"
| "color";
const isColor = type === "color";
const isNumeric = const isNumeric =
type === "number" || type === "range" || node instanceof HTMLSelectElement; type === "number" || type === "range" || node instanceof HTMLSelectElement;
const min = node.hasAttribute("min") const min = node.hasAttribute("min")
@@ -30,6 +114,13 @@ export const setting: Action<
? scale * value ? scale * value
: value : value
).toString(); ).toString();
} else if (isColor) {
const rgb = hsvToRgb(
settings[id]!.value,
settings[id + 1]!.value,
settings[id + 2]!.value,
);
node.value = `#${rgb.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
} else { } else {
node.checked = value !== 0; node.checked = value !== 0;
} }
@@ -58,6 +149,22 @@ export const setting: Action<
? value / scale ? value / scale
: value, : value,
); );
} else if (isColor) {
const r = parseInt(node.value.slice(1, 3), 16);
const g = parseInt(node.value.slice(3, 5), 16);
const b = parseInt(node.value.slice(5, 7), 16);
const hsv = rgbToHsv(r, g, b);
changes.update((changes) => {
changes.push(
hsv.map((value, i) => ({
type: ChangeType.Setting,
id: id + i,
setting: value,
})),
);
return changes;
});
return;
} else { } else {
value = node.checked ? 1 : 0; value = node.checked ? 1 : 0;
} }

View File

@@ -117,43 +117,49 @@
{/if} {/if}
{#each category.items as item} {#each category.items as item}
{#if item.name !== "enable"} {#if item.name !== "enable"}
<label {#if item.unit === "H"}
>{#if item.enum} <label
<select use:setting={{ id: item.id }}> ><input type="color" use:setting={{ id: item.id }} /> Color</label
{#each item.enum as name, value} >
<option {value}>{titlecase(name)}</option> {:else if item.unit !== "S" && item.unit !== "B"}
{/each} <label
</select> >{#if item.enum}
{:else if item.range[0] === 0 && item.range[1] === 1} <select use:setting={{ id: item.id }}>
<input type="checkbox" use:setting={{ id: item.id }} /> {#each item.enum as name, value}
{:else} <option {value}>{titlecase(name)}</option>
<span class="unit" {/each}
><input </select>
type="number" {:else if item.range[0] === 0 && item.range[1] === 1}
min={settingValue(item.range[0], item)} <input type="checkbox" use:setting={{ id: item.id }} />
max={settingValue(item.range[1], item)} {:else}
step={item.inverse !== undefined || <span class="unit"
item.scale !== undefined || ><input
item.step === undefined type="number"
? undefined min={settingValue(item.range[0], item)}
: settingValue(item.step, item)} max={settingValue(item.range[1], item)}
use:setting={{ step={item.inverse !== undefined ||
id: item.id, item.scale !== undefined ||
inverse: item.inverse, item.step === undefined
scale: item.scale, ? undefined
}} : settingValue(item.step, item)}
/>{item.unit}</span use:setting={{
> id: item.id,
{/if} inverse: item.inverse,
{#if item.description} scale: item.scale,
<span }}
>{titlecase(item.name)} />{item.unit}</span
<p>{item.description}</p></span >
> {/if}
{:else} {#if item.description}
{titlecase(item.name)} <span
{/if} >{titlecase(item.name)}
</label> <p>{item.description}</p></span
>
{:else}
{titlecase(item.name)}
{/if}
</label>
{/if}
{/if} {/if}
{/each} {/each}
</fieldset> </fieldset>