A simple circular slider

Shaw
Hard Mode
Published in
6 min readMar 30, 2020

I do web programming for my day job. Recently I needed to create a circular
slider, and I find the result rather satisfying.

The essential idea is that there are a a few layers: a circle with a thin border and the control button, a circle with a thick border that represents the “fill”, and a layer of rectangular overlays that mask the thick border. The circle with the thick border is the bottom layer (furthest from the viewer’s eye), then the overlay layer, and finally the thin circle layer.

Building the slider

The first step was to create a circle with a button that can be clicked and dragged along the circumference of the circle. To draw the circle, I used a div with a border-radius set to 50%. The control button is the same, but smaller and with a background color. The concept for the control button is to make it follow the mouse, but restrict its movement to the circumference of the circle. This can be done with basic trigonometry.

The idea is to get the angle between the mouse and the horizontal axis that passes through the circle’s center. To do this, I first obtain the mouse
position and the circle center in window coordinates. Then I calculate the mouse position relative to the center of the circle, in order to construct a right triangle with the hypotenuse connecting the circle center and the mouse position. From this triangle, the tangent of the desired angle (theta) can be obtained from the mouse’s relative position:

tan(theta) = mouseRelY / mouseRelX
theta = arctan(mouseRelY / mouseRelX)

Now if you remember from your high school math class, when referring to the unit circle centered at the origin the tangent is positive in quadrants 1 and 3, but negative in quadrants 2 and 4. The effect is that our calculated angle will be skewed in all quadrants but 1. What we want is an angle that increases from 0 to 2π. So to account for the tangent skew, we can add π to our calculated angle if the angle occurs in quadrant 2 or 3, and add 2π if the angle occurs in quadrant 4. The sign (not sin) of the mouse relative x and y positions can be used to determine the quadrant that the angle occurs in.

Tangent sign in unit circle quadrants
// quadrant 2
if (mouseRelX < 0 && mouseRelY > 0) {
theta += Math.PI
// quadrant 3
} else if (mouseRelX < 0 && mouseRelY <= 0) {
theta += Math.PI
// quadrant 4
} else if (mouseRelX > 0 && mouseRelY <= 0) {
theta += 2 * Math.PI
}

Now that we have obtained the angle along the circle where the control should be, we need to place it along the circumference of the circle. Since we know the radius of the circle and the angle along the circle, the position can be calculated as follows:

controlX = R * cos(theta)
controlY = R * sin(theta)

To position the control, I set its css-attribute ‘position’ to absolute and the
circle’s position to ‘relative’. I also define a translation transform (-50%, -50%) on the control to position it relative to its center rather than the top-leftmost pixel. Then I continuously update its ‘top’ and ‘left’ attributes as follows:

left = circleRadius + controlX
top = circleRadius — controlY

The way I update is dead simple, there are event listeners for mousedown,
mouseup, and mousemove events that each call respective event handlers. The mousedown handler sets a controlHeld variable to true, the mouseup handler sets controlHeld to false, and the mousemove handler does all calculations and updates the control’s position if the control is currently being held.

The next step is to interpret the slider’s position as something useful. Here I will interpret it as a percentage value (0–100). For my use case, the designer wanted 0 to be at the top center of the circle and values to increase as the slider is pulled clockwise around the circle.

We can obtain such a percentage value by transforming the previously calculated angle. Recall that in the canonical representation of the unit circle, angles start at 0, with the point on the circle being at the center right, and angles increase counter-clockwise. Angles have values between 0 and 2π. So the transformation is calculated as:

newPercent = 100 — ((100 * theta) / (2 * PI)) + 25

We map the angle that goes from 0–2π to a value from 0–100, subtract it
from 100 to flip the direction, and add 25 to shift the starting point to the
top of the circle.

We have one problem left. Since we added 25, the values now range from 25–125. So to correct the values that occur in the first quadrant, we just subtract 100.

if (newPercent > 100) newPercent -= 100

Fill Visualization

We now have a circular slider that can be interpreted as a value between 0 and 100. The last step is to add some visualization of the “fill” percentage. By this I mean drawing the fill as an arc from 0 (the top center of the circle) to the control’s current position. The approach I took was to create a circle with a thick border that sits directly beneath the thin circle, and to mask the thick circle with rectangular overlays that rotate to reveal portions of the thick border as the control is dragged.

Practical considerations require two separate rectangles. A single rectangle can only reveal one half of the circle. Each rectangle is placed inside of a wrapper div with overflow set to hidden. The rectangles are rotated about the circle center such that when one rectangle is rotated and spills over into the opposite half of the circle, it is cut off by the wrapper element since its overflow is hidden. The gif below will be much easier to understand than a text description.

The rotation of each rectangle is calculated as follows:

overlayRotation1 = 2 * PI * newPercent / 100
overlayRotation2 = overlayRotation1 + PI

The rotations are restricted to their respective semicircles:

if (newPercent > 50) {
overlayRotation1 = PI
} else {
overlayRotation2 = 0
}

Within the mousemove handler, I apply each rotation to its respective rectangle. In the previous gif, the rectangles are green and orange so they are easier to understand. In practice these rectangles would have a white background (or whatever the page background is) to effectively be invisible.

If you need the background to be something other than a solid color it is possible to tweak the code to achieve this, but I will leave that as an exercise for the reader. Here is an example of the circular slider with a changing gradient background that was used in production for a pair of solar headphones.

Thanks for reading. Go outside now.

--

--

Shaw
Hard Mode

programming sorcery and black magic bit witchery