Drawing knobs
How to render knob widgets for audio applications
This is an exploration on knobs UIs. It uses JavaScript to render knobs on your web browser and shows code, however it is intended to be a more general look at knobs. Therefore, some concerns specific to the web platform may be ignored.
Knobs are a common control on music and audio applications, such as virtual instruments, DAW (digital audio workstations) or audio effects plugins.
This is done in resemblance to real world musical objects, which have knobs and also because they are a very compact way of representing many different parameters’ state in a way that can be glanced at quickly.
Drawing knobs
Let there be a canvas:
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const size = 80;
const canvasSize = size * scaleFactor;
const center = canvasSize / 2;
const radius = (canvasSize - 20) / 2;
canvas.width = canvasSize;
canvas.height = canvasSize;
canvas.style = `width: ${size}px; height: ${size}px`;
const ctx = canvas.getContext("2d");
These simple knob controls above are just arcs.
function renderKnobArc({ color, value }) {
const revolution = 2 * Math.PI;
const totalSweepAngle = 0.75 * revolution;
const sweepAngle = value * totalSweepAngle;
const startAngle = 0.375 * revolution;
const endAngle = startAngle + sweepAngle;
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 10 * scaleFactor;
ctx.arc(center, center, radius, startAngle, endAngle);
ctx.stroke();
}
Reacting to input
We must react to mouse input. The most natural would be to react to the angle of the mouse relative to the knob:
For this, we’ll use atan2
:
const revolution = Math.PI * 2.0;
const startAngle = 0.375 * revolution;
const sweepAngle = 0.75 * revolution;
let angle =
Math.atan2(
mouse.y - center.y,
mouse.x - center.x,
// Offset the angle so 0 is at startAngle
) - startAngle;
// This fixes the angle so it goes from 0 to 2.pi radius
if (angle < 0) {
angle = 2 * Math.PI + angle;
}
// This truncates values under the knob so they either snap
// to start or end. In reality we should cancel the gesture
// so that there're no jumps.
if (angle > sweepAngle) {
angle = 0;
}
Above, we also rotate our reference points and clamp the results.
By “clamping”, I mean what to do when the drag/mouse is outside of the expected track. In the code above, the angle resets to zero. However that’s akward in practice and it’s best to ignore drags that are outside of the track.
You can see how clamping to 0 reacts to input here:
If we fix that and also add drag tracking and calculate a value that corresponds to the rotation:
We now have an usable knob:
Knobs that are sliders
One alternative UI for changing numeric parameters, would be to use sliders:
These have the advantage of being more generally available and intuitive for mouse users, but will take more space in one axis (horizontal / vertical).
And interesting interaction pattern is a “slider knob”. That is a knob which is interacted with as a slider:
In this case, the knob is simply a visual representation of the control. Clicking it reveals a secondary interaction layer, which contains a slider. The slider is positioned using the value and mouse position to show the user what their next gestures will cause.
This has the advantage that the knob may be really small but still usable:
The choice between horizontal and vertical backing sliders can be done on a case by case basis, but most of the time the vertical slider would make the most sense due to being analogue to mixer sliders.
However, I find this interaction is a bit akward. There’s a level of indirection between the visual representation and how to manipulate its value, which I think makes it harder to learn.
Expanding knobs
I’d like to explore expanding knobs GUI, where knobs would expand with interactions on another document.
That is it
Pretty short, thanks for reading.
You should find Swift, Rust (Skia), Flutter and JS implementations for knobs on https://github.com/yamadapc/augmented-audio/