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:
// 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?
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.
// 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;
let ptCenter = { x: circleX, y: circleY };
let angleOffset = 90
// listen to percentage spin control input event
pc.addEventListener('input', e=>{
let value= +e.currentTarget.value
let angle = 360/100 * value - angleOffset
updateHand(angle);
})
// Add mousemove event listener
svg.addEventListener("mousemove", function (event) {
let ptCursor = { x: event.clientX, y: event.clientY };
let pt = screenToSVG(svg, ptCursor);
let angle = getAngle(ptCenter, pt)
let percent = 100/360 * (angle) +25
percent = percent<100 ? percent : percent-100
pc.value = percent
updateHand(angle);
});
// Update clock hand position for the angle
function updateHand(angle) {
let ptHand = getPointOnCircle(handLength, circleX, circleY, angle)
clockHand.setAttribute("x2", ptHand.x);
clockHand.setAttribute("y2", ptHand.y);
}
// get angle between 2 points
function getAngle(p1, p2) {
let angle = (Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180) / Math.PI;
return angle > 0 ? angle : 360 + angle;
}
function getPointOnCircle(r, cx, cy, deg) {
let { cos, sin, PI } = Math;
let rad = (deg * PI) / 180;
return {
x: cx + r * cos(rad),
y: cy + r * sin(rad)
};
}
/** Based on @Paul LeBeau's answer
* https://stackoverflow.com/questions/48343436/how-to-convert-svg-element-coordinates-to-screen-coordinates#48354404
*/
function screenToSVG(svg, pt) {
let p = new DOMPoint(pt.x, pt.y);
return p.matrixTransform(svg.getScreenCTM().inverse());
}
/*
function SVGToScreen(svg, pt) {
let p = new DOMPoint(pt.x, pt.y);
p = p.matrixTransform(svg.getScreenCTM());
return p;
}
*/
svg{
width:2400px
}
#pc{
position:fixed;
top:50%;
left:50%;
z-index:9;
width:10em;
}
<input type="number" id="pc" min="0" max="100" value="0">
<br>
<svg id="clock-svg" 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="100" y2="60" stroke="black" stroke-width="2" />
</svg>
So we need these helpers:
<line>
x2
and y2
attribute values