I’d like a clock-like object that points to the cursor position from the centre of the clock and reports/sets the position of the ‘hand’ in terms of its percentage around the circle.
I’ve gotten as far as the following, which has these problems:
- calculating the angle of the ‘hand’ is relative to the whole svg, not the circle. The circle might be in any position within the svg viewbox, so calculating the angle would be relative to the centre of that circle.
- the ‘0’ percentage is at the 3 o’clock position rather than 12 o’clock.
// Get SVG and clock hand elements
const svg = document.getElementById('clock-svg');
const clockHand = document.getElementById('clock-hand');
const pc = document.getElementById('pc');
const handLength = 40; // clock hand length, matches the r of the circle
const circleX = 100;
const circleY = 100;
// listen to percentage spin control input event
pc.addEventListener('input', function(event) {
changePercentage(event.target.value);
});
// Add mousemove event listener
svg.addEventListener('mousemove', function(event) {
// Calculate mouse position relative to the SVG
const svgRect = svg.getBoundingClientRect(); // TODO make it relative to the position of the circle inside the SVG
const mouseX = event.clientX - svgRect.left; // TODO do I need to convert the DOM event coordinates to SVG coordinates?
const mouseY = event.clientY - svgRect.top;
// Calculate angle of the mouse position relative to the center of the clock
let angle = Math.atan2(mouseY - 100, mouseX - 100); // TODO what happens when the document is scrolled? clientX and clientY are relative to the viewport, not the document?
updateHand(angle);
// Calculate percentage of the circle completed
let percentage = (angle / (2 * Math.PI)) * 100;
// angle is negative when mouse is on the left side of the clock, lets adjust for that
if (percentage < 0) percentage += 100;
// set the percentage input control
document.getElementById('pc').value = parseInt(percentage,10);
});
function changePercentage(percentage) {
// Calculate angle based on percentage
const angle = (percentage / 100) * 2 * Math.PI;
updateHand(angle);
}
// Update clock hand position for the angle
function updateHand(angle) {
const handX = circleX + handLength * Math.cos(angle);
const handY = circleY + handLength * Math.sin(angle);
clockHand.setAttribute('x2', handX);
clockHand.setAttribute('y2', handY);
}
<input type="number" id="pc" min="0" max="100" value="0">
<br>
<svg id="clock-svg" width="250" height="300" viewBox="50 50 250 150" style="border:1px solid black">
<rect x="0" y="0" width="100%" height="100%" fill="lightgrey" />
<circle cx="100" cy="100" r="40" fill="none" stroke="black" stroke-width="2" />
<line id="clock-hand" x1="100" y1="100" x2="120" y2="120" stroke="black" stroke-width="2" />
</svg>
My rusty math has only gotten me this far, i need to remove a half radian somewhere yes? How do I make the angle relative to the centre of the circle not the size of the svg object?
2
Answers
The assumption 12 o’clock should be 0°/0% is incorrect and only based on the convention this is the "starting point" on the dial of a watch.
In fact 3 o’ clock describes a flat line so 0 degree, whereas 12 o’ clock describes an angle of -90/270°.
You need to add a 90 degree offset to set 12 o’clock as 0/100%.
I also recommend to translate cursor screen coordinates to svg user units.
This way you dont’t have to bother about scroll offsets or scaled placement of your svg.
So we need these helpers:
<line>
x2
andy2
attribute valuesYou can skip a calcullation or two by using a
<marker>
on the line (clock-hand). The line is not showing (stroke = none) and the marker is "styled as" the line.Thanks to @herrstrietzel for providing the function for calculating the angle.