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 { 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<
HTMLInputElement | HTMLSelectElement,
{ id: number; inverse?: number; scale?: number }
@@ -9,7 +88,12 @@ export const setting: Action<
{ id, inverse, scale },
) {
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 =
type === "number" || type === "range" || node instanceof HTMLSelectElement;
const min = node.hasAttribute("min")
@@ -30,6 +114,13 @@ export const setting: Action<
? scale * value
: value
).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 {
node.checked = value !== 0;
}
@@ -58,6 +149,22 @@ export const setting: Action<
? value / scale
: 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 {
value = node.checked ? 1 : 0;
}

View File

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