title: Make a draggable, rotating wheel snap to evenly distributed positions
body: <p>I'm rendering a wheel in a WebGL canvas using mrdoob's THREE.js.</p>
  
  <p><img src="https://i.stack.imgur.com/n4q2Y.png" alt="wheel"></p>
  
  <p>I want the wheel to </p>
  
  <ol>
  <li>Spin around it's center</li>
  <li>Be draggable by mouse or touch interaction</li>
  <li>Slow down by applying fake friction</li>
  <li>Snap to the center of a wedge whenever the rotation speed reaches a certain threshold.</li>
  </ol>
  
  <p>You may think of the behaviour of the wheel as that of a lottery wheel.</p>
  
  <p>So far I have achieved points 1-3. This is my code:</p>
  
  <pre><code>'use strict';
  
  var WIDTH = 1080,
      HEIGHT = 1080;
  
  var VIEW_ANGLE = 45,
      ASPECT = WIDTH / HEIGHT,
      NEAR = 0.1,
      FAR = 10000;
  
  var camera = new THREE.PerspectiveCamera(
      VIEW_ANGLE,
      ASPECT,
      NEAR,
      FAR);
  
  var scene = new THREE.Scene();
  
  scene.add(camera);
  
  camera.position.z = 300;
  
  // Create renderer
  
  var container = document.querySelector('#test');
  
  var renderer = new THREE.WebGLRenderer();
  renderer.setSize(WIDTH, HEIGHT);
  renderer.setClearColor(0x000000, 0);
  
  container.appendChild(renderer.domElement);
  
  // Create objects
  
  var wheelMaterial = new THREE.MeshBasicMaterial({
      map: THREE.ImageUtils.loadTexture('wheel.png'),
      depthWrite: false,
      alphaTest: 0.5
  });
  
  wheelMaterial.overdraw = true;
  
  var wheel = new THREE.Mesh(
      new THREE.PlaneGeometry(240, 240),
      wheelMaterial);
  
  scene.add(wheel);
  
  
  // Mouse interaction
  
  var isDragging = false;
  var lastMouseCoords = null;
  var mouseCoords = null;
  
  container.addEventListener('mousedown', onDragStart, false);
  container.addEventListener('touchstart', onDragStart, false);
  
  container.addEventListener('mouseup', onDragEnd, false);
  container.addEventListener('mouseout', onDragEnd, false);
  container.addEventListener('touchend', onDragEnd, false);
  
  container.addEventListener('mousemove', onMouseMove, false);
  container.addEventListener('touchmove', onMouseMove, false);
  
  
  function onDragStart(e) {
      isDragging = true;
      console.log('Dragging', e);
      mouseCoords = pageCoordsToCanvasCoords(e);
      rotationHistory = [];
  }
  
  
  function onDragEnd(e) {
      isDragging = false;
      lastMouseCoords = null;
      mouseCoords = null;
      console.log('Drag end');
  }
  
  function onMouseMove(e) {
      e.preventDefault();
      mouseCoords = pageCoordsToCanvasCoords(e);
  }
  
  // Utility functions
  
  function pageCoordsToCanvasCoords(e) {
      var canvasX;
      var canvasY;
  
      if ('touches' in e &amp;&amp; e.touches.length &gt; 0) {
          canvasX = e.touches[0].pageX;
          canvasY = e.touches[0].pageY;
      } else {
          canvasX = e.pageX
          canvasY = e.pageY
      }
  
      canvasX -= e.target.offsetLeft;
      canvasY -= e.target.offsetTop;
  
      console.log(canvasX, canvasY);
  
      return {
          x: canvasX,
          y: canvasY
      };
  }
  
  function mouseCoordsToRotation(x, y) {
      var origoX = WIDTH / 2.0,
          origoY = HEIGHT / 2.0;
  
      x = x - origoX;
      y = y - origoY;
  
      var atan = Math.atan2(x, y);
      return atan;
  }
  
  function getMeanVelocity(history) {
      if (history.length &lt;= 1) {
          return 0;
      }
  
      var movements = [];
      var startTime = history[0].time;
      var totalTimeElapsed = 0;
  
      // Start from the second item in deltaRadians
      for (var i = 1; i &lt; history.length; i++) {
          var item = history[i];
  
          var movement = item.deltaRad;
          movements.push(item.deltaRad);
  
          var movementTimeDelta = item.time - startTime - totalTimeElapsed;
  
          if (movementTimeDelta &lt; 0) {
              console.error('movementTimeDelta for history entry #' +
                  i + ' has travelled back in time somehow.');
          }
  
          totalTimeElapsed += movementTimeDelta;
      }
  
      var sum = movements.reduce(function (a, b) {
          return a + b;
      });
  
      return sum / totalTimeElapsed;
  }
  
  function applyFakeFriction(velocity, time) {
      /*
      var currentRotation = wheel.rotation.z;
  
      var nearestBorder = 0;
      var nearestBorderDistance = 100;
  
      for (var i = 0; i &lt; PARTITIONS; i++) {
          var partition =  PARTITION_ARC * i - PARTITION_ARC * PARTITIONS / 2;
          var distance = currentRotation - partition;
  
          if (distance &lt; 0) {
              distance /= -1;
          }
  
          if (distance &lt; nearestBorderDistance) {
              console.log('distance is less than nearestBorderDistance')
              nearestBorder = partition;
              nearestBorderDistance = distance;
              if (nearestBorderDistance &lt; 0) {
                  nearestBorderDistance /= -1;
              }
          }
      }
  
  
      console.log('nearestBorderDistance: ', nearestBorderDistance);
      */
  
      for (var i = 0; i &lt; time; i++) {
          velocity -= WHEEL_FRICTION; // * (nearestBorderDistance * BORDER_FRICTION);
      }
      return velocity;
  }
  
  var rotation = 1;
  
  function snap() {
      isSnapping = true;
  
      /* Disabled, this the issue I'm asking about in the post
      var update = function () {
          cube.position.rotation = current.rotation;
      }
      var current = {
          rotation: rotation
      };
  
      TWEEN.removeAll();
  
      var easing = TWEEN.Easing['Elastic']['EaseInOut'];
  
      var tweenHead = neww TWEEN.Tween(current)
          .to({rotation: rotation})
          .easing(easing)
          .onUpdate(update);
  
      tweenHead.start();
      */ 
  }
  
  var rotationHistory = []
  var ROTATION_HISTORY_MAX_LENGTH = 5;
  
  
  var WHEEL_FRICTION = 0.000001;
  var BORDER_FRICTION = 2;
  
  var PARTITIONS = 12;
  var PARTITION_ARC = 1 * Math.PI / (PARTITIONS / 2); // The width of each section
  
  var wheelVelocity = 0.1;
  var wheelSlowDownVelocity = 0;
  var lastWheelRotationTime;
  
  var isSnapping = false;
  
  // Render
  
  function  tick() {
      // Rotate wheel
      var currentTime = (new Date).getTime();
  
      if (lastMouseCoords &amp;&amp; isDragging) {
          // Reset the velocity for the slowdown
          wheelSlowDownVelocity = 0;
  
          // Get the delta rotation since last mouse coordinates
          var deltaRadians = mouseCoordsToRotation(mouseCoords.x, mouseCoords.y)
              - mouseCoordsToRotation(lastMouseCoords.x, lastMouseCoords.y);
  
          // Set the wheel rotation
          wheel.rotation.z += deltaRadians;
  
          // Save the rotation in the history and remove any excessive elements
          rotationHistory.push({
              time: currentTime,
              deltaRad: deltaRadians
          });
  
          while (rotationHistory.length &gt; ROTATION_HISTORY_MAX_LENGTH) {
              rotationHistory.shift();
          }
      }
  
      if (isDragging) {
          lastMouseCoords = mouseCoords;
      }
  
      // Continue rotation of the released wheel
      if (!isDragging &amp;&amp; !lastMouseCoords &amp;&amp; lastWheelRotationTime) {
          var delta = currentTime - lastWheelRotationTime;
  
          if (wheelSlowDownVelocity == 0) {
              var meanVelocityOverTime = getMeanVelocity(rotationHistory);
  
              wheelSlowDownVelocity = meanVelocityOverTime;
          } else {
              var currentIsNegative = wheelSlowDownVelocity &lt; 0 ? true : false;
  
              var currentVelocity = wheelSlowDownVelocity;
  
              if (currentIsNegative) {
                  currentVelocity /= -1;
              }
  
              console.log('Current velocity: ', currentVelocity);
  
              console.log('delta: ', delta);
  
              var newVelocity = applyFakeFriction(currentVelocity,
                                                  delta);
  
              console.log('New velocity: ', newVelocity);
  
              if (newVelocity &lt; 0) {
                  wheelSlowDownVelocity = 0;
                  rotationHistory = [];
              } else {
                  if (currentIsNegative) {
                      // Revert to old polarity
                      newVelocity /= -1;
                  }
                  wheelSlowDownVelocity = newVelocity;
              }
          }
  
          wheel.rotation.z += wheelSlowDownVelocity * delta;
      }
  
      while (wheel.rotation.z &gt; 2 * Math.PI) {
          console.log('Correcting rotation: ', wheel.rotation.z);
          wheel.rotation.z -= 2 * Math.PI;
      }
  
      while (wheel.rotation.z &lt; - (2 * Math.PI)) {
          console.log('Correcting rotation: ', wheel.rotation.z);
          wheel.rotation.z += 2 * Math.PI;
      }
  
  
      // Update the history record
      lastWheelRotationTime = currentTime;
  
      // Render scene and attach render callback to next animation frame.
      renderer.render(scene, camera);
      window.requestAnimationFrame(tick);
  }
  
  tick();
  </code></pre>
  
  <p>I have the complete code, minus wheel.png over at <a href="https://gist.github.com/joar/5747498" rel="nofollow noreferrer">https://gist.github.com/joar/5747498</a>.</p>
  
  <p>I have been searching for examples of this behaviour but this far I haven't found any.</p>
  
  <hr>
  
  <p>Note to editors. Please do not change the tags of this post. <a href="https://github.com/sole/tween.js/" rel="nofollow noreferrer">tween.js</a> <code>!=</code> <a href="http://www.createjs.com/#!/TweenJS" rel="nofollow noreferrer">TweenJS</a>.</p>