JS中的纺纱轮,如何在随机选择的切片中心自动停止旋转 [英] Spinning wheel in JS, how to automatically stop the spinning at the center of a randomly chosen slice

查看:42
本文介绍了JS中的纺纱轮,如何在随机选择的切片中心自动停止旋转的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

此问题的参考和延续:使用CSS和jQuery使图像旋转

In reference and a continuation of this question: Making an image spin with css and jQuery

我有兴趣进一步采用这种方法.车轮旋转七部分,当车轮停止旋转时,将显示相关块中的正确值.我有一个不同的需求,我想实现.单击时,我的七个零件的车轮会停在随机区域,但应始终停在该区域的中心.我将如何实现此功能?现在,我的轮子随机停下,有时停在轮子上没有任何内容的边缘或部分区域.我认为,要从零点开始第一站,我需要做一些数学运算,例如((360/7)/2),它得出第一个程序段的中心.我应该如何将数学运算纳入脚本中,以始终停在其目标代码块的中心?

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">&nbsp;</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>

推荐答案

这里是一种解决方案,用于处理类似弹性的贴紧到停止切片中心的情况.

Here is a solution to handle elastic-like snapping to the center of the stopping slice.

它利用我所谓的吸引子,根据目标(最接近的切片的中心)来改变旋转速度.不涉及CSS过渡,仅涉及普通javascript.

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.

您与吸引子越近,将您拉向吸引子的力越强.它被计算为到吸引子的距离的保护倒数.当距离接近0时(当然是恰好在0时),保护意义可以防止出现非常大的值

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.

编辑:

  • 在代码段中添加了控件,以进行实验并了解物理设置(将滑块的最小值/最大值设置为合理的"边界)
  • 修复了一个错误,该错误是多次单击中心按钮会同时产生多个动画处理程序(多次单击而无需等待滚轮停止),
  • 整个过程现在都处于自调用函数中,以避免用其var污染全局范围(已从html中删除 onclick ="roulette_spin(this)" ,并添加了js中的document.getElementsByClassName('roulette_center')[0] .addEventListener('click',roulette_spin); 将当前私有的(作用域)处理程序(roulette_spin)绑定到轮心)
  • 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, added document.getElementsByClassName('roulette_center')[0].addEventListener('click', roulette_spin); in the js to bind the now private (scoped) handler (roulette_spin) to the wheel center)

编辑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.

如果轮子旋转并单击了切片,则速度将发生变化,以便轮子应非常靠近目标切片停止,将吸引子压到所单击的切片上将确保轮子实际上会在此停止片.@mathspeople这里:提议的公式远非准确,我很高兴听到有关修改的停顿距离公式以计算适当的速度校正的信息.但是,嘿...它确实有效...

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...

如果轮子没有旋转,则单击一个切片会将轮子动画化为单击的切片.这是一个很好的功能,具有高摩擦(低惯性),因为车轮会快速旋转直到正确定位并放慢速度.它起着旋转菜单的作用,这是OP首先需要的.

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.

(我建议以整页模式查看代码段.注意:就目前而言,不支持在旋转期间多次单击切片,并且将失败,一旦单击切片,则需要等待滚轮停止修复很容易,但是即使很有趣,我也花了很多时间在这个答案上,限制/宵禁有其好处:D)

(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">&nbsp;</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>

这篇关于JS中的纺纱轮,如何在随机选择的切片中心自动停止旋转的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆