// Colour maths for the page's Blender-style picker. A plain ES module so the
// browser loads it and brittle-bare tests it (test/color.test.js).
//
// All values are 0..1. A slot is { r, g, b, level }: the colour at full drive,
// normalised so its brightest channel is 1, and `level` the brightness. That
// is exactly HSV with V = level, which is why the picker's value bar and the
// row's brightness slider are the same control.
export function hsvToRgb (h, s, v) {
h = ((h % 1) + 1) % 1
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: return { r: v, g: t, b: p }
case 1: return { r: q, g: v, b: p }
case 2: return { r: p, g: v, b: t }
case 3: return { r: p, g: q, b: v }
case 4: return { r: t, g: p, b: v }
default: return { r: v, g: p, b: q }
}
}
export function rgbToHsv (r, g, b) {
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const d = max - min
let h = 0
if (d > 0) {
if (max === r) h = ((g - b) / d) % 6
else if (max === g) h = (b - r) / d + 2
else h = (r - g) / d + 4
h /= 6
if (h < 0) h += 1
}
return { h, s: max > 0 ? d / max : 0, v: max }
}
// Hue and saturation come from the slot's colour, V from its level -- so a
// slot dimmed to 0 still knows its colour. `fallbackHue` is for a colourless
// (grey or black) slot, where hue is undefined.
export function slotToHsv (slot, fallbackHue = 0) {
const { h, s } = rgbToHsv(slot.r, slot.g, slot.b)
return { h: s > 0 ? h : fallbackHue, s, v: slot.level }
}
export function hsvToSlot (h, s, v) {
const c = hsvToRgb(h, s, 1)
return { r: c.r, g: c.g, b: c.b, level: v }
}
const hex2 = (x) => Math.round(Math.max(0, Math.min(1, x)) * 255).toString(16).padStart(2, '0')
export function toHex (c) {
return '#' + hex2(c.r) + hex2(c.g) + hex2(c.b)
}
export function fromHex (str) {
const m = /^#?([0-9a-f]{6})$/i.exec(String(str).trim())
if (!m) return null
const n = parseInt(m[1], 16)
return { r: ((n >> 16) & 255) / 255, g: ((n >> 8) & 255) / 255, b: (n & 255) / 255 }
}