So as the title suggests I am rotating an element, nothing special. To do so I am using the PointerEvents. I do calculate an offset and update an initial value for the degree to make sure that the element does not jump on consecutive user actions, e.g. when you let loose of the element and start to rotate over again.
What I am struggling with is to add a few constraints. For example I would want to make sure that:
- If
N
is pointing to north, allow only counter clockwise rotation. - Only if
N
is not pointing to north allow rotation in both directions untilN
is pointing to north. - Overall allow only one full turn.
The problem is not so much to set up the conditions for those constraints but rather that they do not seem to be working consistently while the pointermove
event is being triggered.
For example for the first rule I did try to do something like this:
if (currentDegree + initialDegree - offsetDegree <= 0) {
box.style.transform = `rotate(${
currentDegree + initialDegree - offsetDegree
}deg)`;
}
This does somewhat work but if I only move the pointer fast enough I can easily overcome the restriction. Sometimes the element even stops before N
is pointing to north, so I am wondering if that is even the right approach to the problem.
I am posting a fiddle with no constraints so you can get an idea on what I am trying to do.
I would appreciate any advice, but suggesting a library which would help me to implement those rules is not what I need. Thanks in advance.
const box = document.querySelector('.box');
const { x, y, width } = box.getBoundingClientRect();
let boxCenter = width / 2;
let boxXPos = x;
let boxYPos = y;
let initialDegree = 0;
let offsetDegree = 0;
let currentDegree = 0;
function calcDegree(x, y) {
const degree = Math.floor(Math.atan2(y, x) * (180 / Math.PI) + 90);
return degree < 0 ? 360 + degree : degree;
}
function onPointerMove(event) {
const x = event.clientX - boxXPos - boxCenter;
const y = event.clientY - boxYPos - boxCenter;
currentDegree = calcDegree(x, y);
box.style.transform = `rotate(${
currentDegree + initialDegree - offsetDegree
}deg)`;
}
box.addEventListener('pointerdown', (event) => {
box.setPointerCapture(event.pointerId);
const x = event.clientX - boxXPos - boxCenter;
const y = event.clientY - boxYPos - boxCenter;
offsetDegree = calcDegree(x, y);
box.addEventListener('pointermove', onPointerMove);
box.addEventListener(
'pointerup',
() => {
initialDegree += currentDegree - offsetDegree;
box.removeEventListener('pointermove', onPointerMove);
},
{ once: true }
);
});
window.addEventListener('resize', () => {
const { x, y } = box.getBoundingClientRect();
boxXPos = x;
boxYPos = y;
});
body {
margin: 0;
height: 100vh;
}
#app {
display: grid;
place-items: center;
height: inherit;
}
.box {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
align-items: center;
justify-items: center;
width: 200px;
height: 200px;
background-color: #bbd6b8;
border: 1px solid #94af9f;
cursor: move;
}
.direction {
width: 50px;
height: 50px;
background-color: #333;
border-radius: 50%;
color: white;
text-align: center;
line-height: 50px;
user-select: none;
}
.north {
grid-column: 2;
grid-row: 1;
}
.east {
grid-column: 3;
grid-row: 2;
}
.south {
grid-column: 2;
grid-row: 3;
}
.west {
grid-column: 1;
grid-row: 2;
}
<div id="app">
<div class="box">
<div class="direction north">N</div>
<div class="direction east">E</div>
<div class="direction south">S</div>
<div class="direction west">W</div>
</div>
</div>
2
Answers
Your solution depends on the pointer location wrt box center. The closer you are to the center, the curvature (dS/dTheta) of your pointer trail will increase, thus you cross quadrants (0-90,90-180,180-270,270-360) very fast and the box will spin erratic. You can certainly fix your code to put proper restraints on these values.
I prefer going stepwise in these kind of tasks. That makes it a bit easier and pixel independent. I also throttled the
mouseover
to 60fps. Replace the mouse event with the pointer ones if you wish.In the example below you can adjust
dRotMax
,dRotMin
androtStep
insideresize
event if you wanted to.If you want to change how axis control the rotation based on angle quadrants, then convert
rotStep
into a function. The main advantage is we get rid ofgetBoundingClientRect
andatan2
all together.You can limit the angle of "North" to -360 and 0 degrees (clamping). This is negative because you want to allow a counter clockwise move from the initial position, which is a moving in negative direction. If it were clockwise, the range would have to be [0, 360].
To know which way to update the current angle, you could check the difference that the last pointer move made and interpret a difference of less than 180 degrees as a clockwise move, and anything else a counter clockwise move. This will determine how the current angle should be updated (by increasing it or decreasing it). This means that angle could exceed the range we want to stay within, i.e. [-360, 0]. If that gets exceeded, you would need to re-calibrate the global variable(s) that are involved.
To help myself understand the meaning of the global variables involved, I named one
pointerToNorthDegree
, which is the angle between the location of "North" and the Pointer. This angle is to remain constant when the users drags the pointer (because North will move accordingly). Only when there is an overrun, this angle needs to be updated without moving North.There was one doubt I had: if the user makes a full counter clockwise turn, putting North again at the top, releases the pointer, and then wants to repeat that operation again, would they still only be allowed to turn counter clock wise, or would they now need to "unwind" and actually move in clockwise direction. The difference is one line of code, which I have marked with a comment.
I added some other comments as well in the updated code below: