问题
In reference and a continuation of this question: Making an image spin with css and jQuery
I am interested in taking this approach further. A seven part Wheel spins and the correct values from a related block is depicted when the wheel stops its spin. I have a different need that I would like to achieve. My seven part wheel stop at a random area when clicked, but should always stop at the center of the section. How would I achieve this feature? Right now, my wheel stops randomly, sometimes at the edges or at a part of my sections of the wheel that does not have any content. In my opinion, to get the first stop from the zero point, I will need to do some math, such as ((360/7)/2), which yields the center of the first block. how should I incorporate the math in the script to always stop at the center of its targeted block?
Here is what I was able to build so far...
<html>
<style>
.roulette_center {
position: absolute;
top: 255px;
left: 258px;
cursor: pointer;
}
.roulette_center:hover {
transform: scale(1.01);
}
.roulette_wheel{
touch-action:auto;pointer-events:painted
}
</style>
<body>
<div style="z-index: 0; position: relative">
<span style="position: absolute; top: 0px; left: 360px; font-weight:700;font-size:24px;" id="score"> </span>
<img class="roulette_wheel" src="https://www.dropbox.com/s/6kp3fmtp72aj3vy/wellness-wheel.png?raw=1" />
<img class="roulette_center" src="https://www.dropbox.com/s/52x8iyb1nvkjm0w/wheelCenter.png?raw=1" onclick="roulette_spin(this)" onfocus="transform()">
<div style="position: absolute; top: 30px; left: 350px; background-color: red; width: 1px; height: 60px;"></div>
</div>
<script>
var force = 0;
var angle = 0;
var rota = 1;
var inertia = 0.985;
var minForce = 15;
var randForce = 15;
var rouletteElem = document.getElementsByClassName('roulette_wheel')[0];
var scoreElem = document.getElementById('score');
var values = [
"Spititual", "Emotional", "Intellectual", "Physical", "Social", "Environmental", "Financial"
].reverse();
function roulette_spin(btn) {
// set initial force randomly
force = Math.floor(Math.random() * randForce) + minForce;
requestAnimationFrame(doAnimation);
}
function doAnimation() {
// new angle is previous angle + force modulo 360 (so that it stays between 0 and 360)
angle = (angle + force) % 360;
// decay force according to inertia parameter
force *= inertia;
rouletteElem.style.transform = 'rotate(' + angle + 'deg)';
// stop animation if force is too low
if (force < 0.05) {
// score roughly estimated
scoreElem.innerHTML = values[Math.floor(((angle / 360) * values.length) - 0.5)];
return;
}
requestAnimationFrame(doAnimation);
}
</script>
</body>
</html>
回答1:
Here is a solution to handle elastic-like snapping to the center of the stopping slice.
It make use of what I'd call an attractor, to alter the rotation speed according to a target (center of the closest slice). No CSS transitions are involved, only vanilla javascript.
The closer your are to the attractor the stronger the force pulling you to it. It is computed as the guarded inverse of the distance to the attractor. Guarded meaning protected against very big values when distance is close to 0 (and of course when it is exactly 0)
I like this solution because it models what we need using simple artificial physics (wheel inertia and a kind of magnet under the wheel slices), and let the physics only settle things after an initial random push.
You can play with the physical parameters and the stopping condition but be carefull as numerical instabilities arise easily.
EDIT:
- added controls in the snippet to experiment with and understand the physical settings (min/max of the sliders set to "reasonable" boundaries)
- fixed a bug where clicking multiple times on the center button spawned multiple animation handlers at the same time (when clicking multiple times without waiting for the wheel to stop),
- the whole thing is also now in a self called function to avoid polluting the global scope with its vars (removed
onclick="roulette_spin(this)"
from the html, addeddocument.getElementsByClassName('roulette_center')[0].addEventListener('click', roulette_spin);
in the js to bind the now private (scoped) handler (roulette_spin) to the wheel center)
EDIT 2:
Added detection of clicked slice
If the wheel spins and no slice is clicked, it will stop according to the initial speed and inertia, snapping the end in the center of a (random) slice.
If the wheel spins and a slice is clicked, the speed will be altered so that the wheel should stop very close to the target slice, forcing the attractor to the clicked slice will ensure the wheel will actually stop on this slice. @mathspeople here: the proposed formula is far from accurate and I would be pleased to hear about a modified stoppping distance formula to calculate the proper speed correction. But hey... it just works...
If the wheel is not spinning, clicking on a slice will animate the wheel to the clicked slice. This is a nice feature with high friction (low inertia) as the wheel will quickly turn until positioned correctly and slows down nicely. It acts as a rotating menu which is what the OP may want in the first place.
(I recommend looking at the snippet in full page mode. Notice: as it stands, multiple click on slice during a spin is not supported and will fail, once a slice is clicked, you need to wait for the wheel to stop. The fix is easy but even if it was fun, I've spent way to much time on this answser, confinment/curfew has its perks :D)
(function() {
var values = [
"Financial", "Environmental", "Social", "Physical", "Intellectual", "Emotional", "Spititual"
];
var speed = 0;
// should init angle so that the cursor is at the center of the desired value,
// this is just to test the offset is right
var angle = getAngleFor("Intellectual");
// how much of the speed is kept at each update step (0 < i < 1, the bigger the longer the wheel spins)
var inertia = 0.988;
var minSpeed = 15;
// how much randomness in initial push speed
var randRange = 15;
// strongest absolute force that an attractor can exerce (is squashed to before scaling is applied)
var maxAttractionForce = 0.5;
// scale attraction force
var attractionForceFactor = 0.02;
var rouletteElem = document.getElementsByClassName('roulette_wheel')[0];
var scoreElem = document.getElementById('score');
// offset the display angle so that angle 0 is at the edge of the first slice
var offset = 26.42;
var currentAnimationFrame = null;
var clickedSliceIndex = -1;
var estimatedSliceIndex = -1;
var totalAngle = 0;
function addParamInput(name, value, min, max, step, handler) {
var id = name + '-input-id';
var wrapper = document.createElement('div');
wrapper.classList.add('single-input-wrapper');
var label = document.createElement('label');
label.setAttribute('for', id);
label.innerHTML = name;
var input = document.createElement('input');
input.setAttribute('type', 'range');
input.setAttribute('min', ''+min);
input.setAttribute('max', ''+max);
input.setAttribute('step', ''+step);
input.setAttribute('value', ''+value);
input.addEventListener('change', handler);
var meter = document.createElement('span');
meter.classList.add('input-meter');
meter.innerHTML = value;
wrapper.appendChild(label);
wrapper.appendChild(input);
wrapper.appendChild(meter);
document.getElementById('all-input-wrappers').appendChild(wrapper);
}
function updateInputMeter(input) {
input.parentElement.getElementsByClassName('input-meter')[0].innerHTML = input.value;
}
function roulette_spin(evt) {
evt.stopPropagation();
clickedSliceIndex = -1;
totalAngle = 0;
// erase score
scoreElem.innerHTML = '';
// set initial speed randomly
speed = Math.floor(Math.random() * randRange) + minSpeed;
// console.log('initial speed', speed);
// probably far from accurate but it kind of works, attractor
// completely ignored because when spinning fast it cost speed as much
// as it adds, and after it won't matter
// since we will be forcing the attractor to where we want to stop
var estimatedSpin = speed / (1 - inertia);
// console.log('estimated total spin approximation', estimatedSpin);
estimatedSliceIndex = getSliceIndex((angle + estimatedSpin) % 360);
// console.log('estimated stopping slice', values[estimatedSliceIndex]);
if (currentAnimationFrame) {
cancelAnimationFrame(currentAnimationFrame);
}
currentAnimationFrame = requestAnimationFrame(doAnimation);
}
function getSliceIndex(angle) {
return Math.floor(((angle / 360) * values.length));
}
function getCircularDist(a, b) {
// assuming both a and b are positive angles 0 <= angle <= 360
// find shortest oriented distance
// example: if a is 350 degrees and b is 10 degrees what's the actuals angular dist from a to b
// naive implementation would be b - a which gives - 340 => wrong, it only takes
// 20 degrees to rotate from 350 to 10 => b - (a - 360)
// conversely if a is 300 and b is 250 the shortest distance is
// the other way around => b - a = -50 degrees
var d1 = b - a;
var d2 = b - (a - 360);
return Math.abs(d1) >= Math.abs(d2) ? d2 : d1;
}
function setRouletteAngle(angle) {
rouletteElem.style.transform = 'rotate(' + (offset + angle) + 'deg)';
}
function getAngleFor(value) {
return getAngleForIndex(values.indexOf(value));
}
function getAngleForIndex(sliceIndex) {
return (sliceIndex + 0.5) * (360 / values.length);
}
function handleRouletteClick(evt) {
// coordinate of the center of the wheel
var centerX = rouletteElem.offsetWidth / 2;
var centerY = rouletteElem.offsetHeight / 2;
// console.log('centerX', centerX, 'centerY',centerY);
// roulette's parent bounding rect
var rect = evt.target.parentElement.getBoundingClientRect();
// coordinates of the click in roulette's parent coordinates
var clickX = evt.clientX - rect.x;
var clickY = evt.clientY - rect.y;
// resulting coordinates relative to the center of the wheel
var x = clickX - centerX;
var y = clickY - centerY;
// console.log('x', x, 'y', y);
var norm = Math.sqrt(x*x+y*y);
// -pi < rad < pi
var rad = Math.atan2(x, y);
var deg = (rad * 180) / Math.PI;
// 0 <= clickedAngle < 360 starting from the first slice
var clickedAngle = ((deg < 0 ? deg + 360 : deg) + angle + 180) % 360;
clickedSliceIndex = Math.floor((clickedAngle / 360) * values.length);
scoreElem.style.color = 'lightgrey';
scoreElem.innerHTML = 'clicked ' + values[clickedSliceIndex];
// outside of an animation started with the center button it will make the wheel spin to target
if (!currentAnimationFrame) {
// assume wheel is not mmoving so the estimated slice is the slice at the current angle
estimatedSliceIndex = getSliceIndex(angle);
currentAnimationFrame = requestAnimationFrame(doAnimation);
}
var estimatedAngle = getAngleForIndex(estimatedSliceIndex);
var targetFinalAngle = getAngleForIndex(clickedSliceIndex);
var spinError = getCircularDist(estimatedAngle, targetFinalAngle);
var speedCorrection = spinError * (1 - inertia);
speed += speedCorrection;
}
function doAnimation() {
// new angle is previous angle + speed modulo 360 (so that it stays between 0 and 360)
totalAngle += speed;
angle = (angle + speed) % 360;
// decay speed according to inertia parameter
speed = speed - (1 - inertia) * speed;
// add attractor force: inverse of circular oriented dist to the closest slice center * some factor
var sliceIndex = clickedSliceIndex !== -1 ? clickedSliceIndex : getSliceIndex(angle);
var target = getAngleForIndex(sliceIndex);
var orientedDist = getCircularDist(angle, target);
// protect against infinity (division by 0 or close to 0)
var inverseOrientedDistAmplitude = orientedDist === 0
? maxAttractionForce
: Math.min(1/Math.abs(orientedDist), maxAttractionForce);
var finalAttractionForce = Math.sign(orientedDist) * inverseOrientedDistAmplitude * attractionForceFactor;
speed = speed + finalAttractionForce;
setRouletteAngle(angle);
// stop animation if speed is low and close to attraction point
if (speed < 0.01 && Math.abs(orientedDist) < 0.05 ) {
// console.log('total spin', totalAngle);
scoreElem.style.color = 'black';
scoreElem.innerHTML = values[getSliceIndex(angle)];
currentAnimationFrame = null;
return;
}
currentAnimationFrame = requestAnimationFrame(doAnimation);
}
setRouletteAngle(angle);
document.getElementsByClassName('roulette_center')[0].addEventListener('click', roulette_spin);
rouletteElem.parentElement.addEventListener('click', handleRouletteClick);
addParamInput('minSpeed', 15, 0, 60, 0.5, function(evt) {
var rangeInput = evt.target;
minSpeed = parseFloat(rangeInput.value);
updateInputMeter(rangeInput)
});
addParamInput('inertia', 0.988, 0.900, 0.999, 0.001, function(evt) {
var rangeInput = evt.target;
inertia = parseFloat(rangeInput.value);
updateInputMeter(rangeInput)
});
addParamInput('attractionForceFactor', 0.02, 0, 0.1, 0.001, function(evt) {
var rangeInput = evt.target;
attractionForceFactor = parseFloat(rangeInput.value);
updateInputMeter(rangeInput)
});
addParamInput('maxAttractionForce', 0.5, 0, 1.5, 0.01, function(evt) {
var rangeInput = evt.target;
maxAttractionForce = parseFloat(rangeInput.value);
updateInputMeter(rangeInput)
});
})();
.roulette_center {
position: absolute;
top: 255px;
left: 258px;
cursor: pointer;
}
.roulette_center:hover {
transform: scale(1.01);
}
.roulette_wheel{
touch-action: auto;
pointer-events:painted
}
.single-input-wrapper {
display: flex;
align-items: center;
}
.single-input-wrapper > label {
width: 150px;
}
.single-input-wrapper > input {
width: 150px;
}
#score {
position: absolute;
top: 0px;
left: 360px;
font-weight: 700;
font-size: 24px;
}
#indicator {
position: absolute;
top: 30px;
left: 350px;
background-color: red;
width: 1px;
height: 60px;
}
#all-input-wrappers {
z-index: 10;
position: absolute;
top: 0px;
left: 0px;
opacity: 0.2;
background-color: white;
display: flex;
flex-direction: column;
transition: opacity 0.2s;
}
#all-input-wrappers:hover {
opacity: 0.8;
}
<html>
<head>
</head>
<body>
<div style="z-index: 0; position: relative">
<div id="all-input-wrappers"></div>
<span id="score"> </span>
<img
class="roulette_wheel"
src="https://www.dropbox.com/s/6kp3fmtp72aj3vy/wellness-wheel.png?raw=1"
/>
<img
class="roulette_center"
src="https://www.dropbox.com/s/52x8iyb1nvkjm0w/wheelCenter.png?raw=1"
onfocus="transform()"
>
<div id="indicator"></div>
</div>
</body>
</html>
回答2:
So I tried something,
I let css handle the rotate transition and set a floored random value as the angle of rotation. Now it stops at the center of the pin, I'll let you figure out where it has stopped and set the score.
var force = 0;
var angle = 0;
var rota = 1;
var inertia = 0.985;
var minForce = 15;
var randForce = 15;
var rouletteElem = document.getElementsByClassName('roulette_wheel')[0];
var scoreElem = document.getElementById('score');
var values = [
"Spititual", "Emotional", "Intellectual", "Physical", "Social", "Environmental", "Financial"
].reverse();
function roulette_spin(btn) {
var additionalRotation = Math.floor(Math.random() * values.length) * 360
angle += additionalRotation;
rouletteElem.style.transform = 'rotate(' + angle + 'deg)';
}
.roulette_center:hover {
transform: scale(1.01);
}
.roulette_wheel {
transition: ease-in-out 3s transform;
}
<div style="z-index: 0; position: relative">
<span style="position: absolute; top: 0px; left: 360px; font-weight:700;font-size:24px;" id="score"> </span>
<img class="roulette_wheel" src="https://www.dropbox.com/s/6kp3fmtp72aj3vy/wellness-wheel.png?raw=1" />
<img class="roulette_center" style="position:absolute;left:2px;cursor:pointer;" src="https://www.dropbox.com/s/52x8iyb1nvkjm0w/wheelCenter.png?raw=1" onclick="roulette_spin(this)" onfocus="transform()">
<div style="position: absolute; top: 30px; left: 350px; background-color: red; width: 1px; height: 60px;"></div>
</div>
回答3:
If you wanted to keep your angle changes inside your current function and still utilize how you're keeping score this is a way you can find the closest 1/7th to which your angle is at any given time (this will allow you to know how far away or close to a 1/7th at any moment). So long as your graphic is designed to start on the top 1/7th, in the middle of the first section at the top, then you can use this angle_difference
to adjust your angle variable however you wish. For instance you could at start slowing towards the nearest 1/7th at force < 1.0
by taking the angle_difference
and dividing it into so many steps and instead of modifying your new angle by (angle + force) % 360
you could modify it with these tiny steps to slowly inch forward or backward to the nearest 1/7th.
Here is some example code to see what I mean:
if (force < 1.0) {
closest_seventh = Math.round(7*(angle/360))*(1/7);
closest_seventh_angle = 360 * closest_seventh;
angle_difference = abs(angle - closest_seventh);
angle_step = angle_difference; / 10 #arbitrarily chosen number of steps
if (angle - closest_seventh < 0) {
angle = angle - angle_step;
} else {
angle = angle + angle_step;
}
force = force - (0.5 / 10);
if (force < 0.05) {
// score roughly estimated
scoreElem.innerHTML = values[Math.floor(((angle / 360) * values.length) - 0.5)];
return;
}
requestAnimationFrame(doAnimation);
You can adjust the arbitrary values as you need to make the animation look the way you wish, but this processes:
- Waits until the force is low but not low enough to score
- Upon force dropping below 1.0 finds the closest 1/7th
- Decides how far away it is
- Calculates the number of degrees per step the image still needs to rotate to get there
- Then alters the force and angle by the number of steps (chosen by you) to then rotate the image by the time force gets below 0.5 and a score is obtained.
There would need to be some other minor modifications in regards to when you change your force value when Force > 1.0 but I hope this offers an alternative mindset on how to get to that middle section of your wheel segments.
EDIT
I forgot one line of code where you take the closest 7th and multiply it by 360 to actually find the angle we need to get to to be in the middle of the pie piece, so I have added that to the code.
来源:https://stackoverflow.com/questions/65959865/spinning-wheel-in-js-how-to-automatically-stop-the-spinning-at-the-center-of-a