function knob({
value,
min,
max,
step,
sensitivity,
size,
title = "",
color = "steelblue",
automated = false
} = {}) {
const { sin, cos, PI } = Math;
min = +min || 0;
max = +max || 100;
step = +step || 1;
sensitivity = +sensitivity || 10;
size = +size || 100;
const alpha = (30 * PI) / 180;
const beta = (330 * PI) / 180;
const r = 0.6;
const p1 = [cos(alpha) * r, sin(alpha) * r];
const p2 = [cos(beta) * r, sin(beta) * r];
const interpolateValue = interpolate(min, max);
const interpolateAngle = interpolate(alpha, beta);
const clampValue = clamp(min, max);
const roundValue = round(step);
const stepPrecision = precision(step);
const parseValue = (v) => parseFloat(roundValue(clampValue(v)).toFixed(stepPrecision));
value = parseValue(isNaN(value) ? (min + max) / 2 : value);
let t, phi, start, delta, _value = value;
function mousedown(evt) {
evt.preventDefault();
start = [evt.clientX, evt.clientY];
ui.style.cursor = "grabbing";
}
function mousemove(evt) {
if (!evt.buttons) start = null;
if (!start) return;
delta = start[1] - evt.clientY;
start[1] -= delta;
const offset = (delta * sensitivity) / width;
_value = parseValue(interpolateValue(t + offset));
update();
}
function mouseup(evt) {
start = null;
ui.style.cursor = "grab";
}
function touchstart(evt) {
evt.preventDefault();
}
function doubleclick(evt) {
_value = value;
update();
}
function update(raiseEvent = true) {
t = interpolateValue.invert(_value);
phi = interpolateAngle(t);
pointer.transform.baseVal
.getItem(0)
.setRotate((phi * 180) / PI + 180, 0, 0);
arc.setAttribute("d", `M ${cos(phi) * r} ${sin(phi) * r} A ${r} ${r} 0 ${+(phi >= PI + alpha)} 0 ${p1}`);
val.textContent = _value;
if (raiseEvent)
ui.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}
const ui = svg`
<svg
width="${size}"
height="${size}"
viewBox="-1 -1 2 2"
>
<g font-family="sans-serif" text-anchor="middle" pointer-events="none" fill="#333333">
<text class="title" y="-0.8" font-size="0.3">${title}</text>
<text class="value" y="0.9" font-size="0.3" font-weight="bold">${value}</text>
</g>
<g stroke-linecap="round" stroke-width="0.12" fill="none">
<g class="arc" transform="scale(-1 -1) rotate(-90)">
<path class="bottom" d="M ${p2} A ${r} ${r} 0 1 0 ${p1}" stroke="lightgrey" />
<path class="top" stroke="${color || "steelblue"}" />
</g>
<line class="pointer" y2="${-r}" transform="rotate(0)" stroke="#333333" />
</g>
${automated ? `<circle cx=${cos(PI / 2) * 0.5} cy=${sin(PI / 2) * 0.5} r="0.05" fill="red" />` : ""}
</svg>
`;
const pointer = ui.querySelector(".pointer");
const arc = ui.querySelector(".arc .top");
const val = ui.querySelector(".value");
update();
if (!automated) {
ui.style.cursor = "grab";
ui.addEventListener("pointerdown", mousedown);
ui.addEventListener("dblclick", doubleclick);
ui.addEventListener("touchstart", touchstart);
document.addEventListener("pointermove", mousemove);
document.addEventListener("pointerup", mouseup);
invalidation.then(() => {
ui.removeEventListener("pointerdown", mousedown);
ui.removeEventListener("dblclick", doubleclick);
ui.removeEventListener("touchstart", touchstart);
document.removeEventListener("pointermove", mousemove);
document.removeEventListener("pointerup", mouseup);
});
}
function setValue(v) {
_value = parseValue(v);
update(false);
}
ui.setValue = setValue;
Object.defineProperty(ui, "value", {
get: () => _value,
set: setValue
});
return ui;
}