使用 javascript 根据旋转角度在 3D cylinder/wheel 上查找线段

Find a segment on a 3D cylinder/wheel based on a rotational angle using javascript

我有一个 3D 轮子,我正在使用 javascript requestAnimationFrame() 函数制作动画。

车轮看起来像:

有 4 个主要变量需要考虑:

  1. items轮子上的段数
  2. spinSpeed 旋转速度修改器。将每帧的角度 increase/decrease 乘以该值。
  3. spinDuration减速停止前的全速旋转动画时长
  4. spinDirection 轮子应该旋转的方向。接受 updown.

现在我想使用车轮停止的角度从 DOM 获取线段(红线相交的地方)。轮段的弧起点和终点角度存储在数据属性中。例如:

<div class="wheel__inner">
    <div class="wheel_segment" ... data-start-angle="0" data-end-angle="12.85">Item 1</div>
    <div class="wheel_segment" ... data-start-angle="12.85" data-end-angle="25.71">Item 2</div>
    <div class="wheel_segment" ... data-start-angle="25.71" data-end-angle="38.58">Item 3</div>
    ...
</div>

我通过在每个刻度上存储修改后的角度来跟踪当前的车轮旋转。例如:

let wheelAngle = 0;

window.requestAnimationFrame( function tick() {

    if ( spinDirection === 'up' ) {
        wheelAngle += speedModifier;
    } else {
        wheelAngle -= speedModifier;
    }

   window.requestAnimationFrame( tick );
} );

当动画停止时,我尝试通过使用开始和结束角度标准化旋转和过滤片段来获取片段。

我将旋转归一化,因为它可以高于 360° 和低于 ,我使用以下函数执行此操作:

function normaliseAngle( angle ) {
    angle = Math.abs( angle ) % 360;
    angle = 360 - angle; // Invert
    return angle;
}

并使用 jQuery 过滤元素,如下所示:

const $found = $wheel.find( '.wheel__segment' ).filter( function() {
    const startAngle = parseFloat( $( this ).data( 'start-angle' ) );
    const endAngle = parseFloat( $( this ).data( 'end-angle' ) );
    return angle >= startAngle && angle < endAngle;
} );

然而,尽管我尽了最大的努力,我还是无法让它工作。请在此处查看我的 JSFiddle:https://jsfiddle.net/thelevicole/ps04fnxm/2/

( function( $ ) {

    // Settings
    const items = 28; // Segments on wheel
    const spinSpeed = randNumber( 1, 10 ); // Spin speed multiplier
    const spinDuration = randNumber( 2, 5 ); // In seconds
    const spinDirection = randNumber( 0, 1 ) ? 'up' : 'down'; // Animate up  or down
    
    // Vars
    const $wheel = $( '.wheel .wheel__inner' );
    const diameter = $wheel.height();
    const radius = diameter / 2;
    const angle = 360 / items;
    const circumference = Math.PI * diameter;
    const height = circumference / items;
    
    // Trackers
    let wheelAngle = 0;
    const wheelStarted = new Date();
    
    // Add segments to the wheel
    for ( let i = 0; i < items; i++ ) {
        var startAngle = angle * i;
        var endAngle = angle * ( i + 1 );
        var transform = `rotateX(${ startAngle }deg) translateZ(${ radius }px)`;

        var $segment = $( '<div>', {
            class: 'wheel__segment',
            html: `<span>Item ${ i }</span>` 
        } ).css( {
            'transform': transform,
            'height': height,
        } );
        
        // Add start and end angles for this segment
        $segment.attr( 'data-start-angle', startAngle );
        $segment.attr( 'data-end-angle', endAngle );
        
        $segment.appendTo( $wheel );
    }
    
    
    /**
     * Print debug info to DOM
     *
     * @param {object}
     */
    function logInfo( data ) {
        const $log = $( 'textarea#log' );
        let logString = '';
        
        logString += '-----' + "\n";
        for ( var key in data ) {
            logString += `${ key }: ${ data[ key ] }` + "\n";
        }
        logString += "\n";
        
        // Prepend log to last value
        logString += $log.val();
        
        // Update field value
        $log.val( logString );
    }
    
    /**
     * Get random number between min & max (inclusive)
     *
     * @param {number} min
     * @param {number} max
     * @returns {number}
     */
    function randNumber( min, max ) {
        min = Math.ceil( min );
        max = Math.floor( max );
        return Math.floor( Math.random() * ( max - min + 1 ) ) + min;
    }
    
    /**
     * Limit angles to 0 - 360
     *
     * @param {number}
     * @returns {number}
     */
    function normaliseAngle( angle ) {
        angle = Math.abs( angle ) % 360;
        angle = 360 - angle;
        return angle;
    }
    
    /**
     * Get the wheel segment at a specific angle
     *
     * @param {number} angle
     * @returns {jQuery}
     */
    function segmentAtAngle( angle ) {

        angle = normaliseAngle( angle );
    
        const $found = $wheel.find( '.wheel__segment' ).filter( function() {
            const startAngle = parseFloat( $( this ).data( 'start-angle' ) );
            const endAngle = parseFloat( $( this ).data( 'end-angle' ) );
            return angle >= startAngle && angle < endAngle;
        } );
        
        return $found;
    }
    
    /**
     * @var {integer} Unique ID of requestAnimationFrame callback
     */
    var animationId = window.requestAnimationFrame( function tick() {
    
        // Time passed since wheel started spinning (in seconds)
        const timePassed = ( new Date() - wheelStarted ) / 1000;
        
        // Speed modifier value (can't be zero)
        let speedModifier = parseInt( spinSpeed ) || 1;
        
        // Decelerate animation if we're over the animation duration
        if ( timePassed > spinDuration ) {

            const decelTicks = ( spinDuration - 1 ) * 60;
            const deceleration = Math.exp( Math.log( 0.0001 / speedModifier ) / decelTicks );
            const decelRate = ( 1 - ( ( timePassed - spinDuration ) / 10 ) ) * deceleration;

            speedModifier = speedModifier * decelRate;

            // Stop animation from going in reverse
            if ( speedModifier < 0 ) {
                speedModifier = 0;
            }
        }
        
        // Print debug info
        logInfo( {
            timePassed: timePassed,
            speedModifier: speedModifier,
            wheelAngle: wheelAngle,
            normalisedAngle: normaliseAngle( wheelAngle )
        } );
        
        // Wheel not moving, animation must have finished
        if ( speedModifier <= 0 ) {
            window.cancelAnimationFrame( animationId );

            const $stopped = segmentAtAngle( wheelAngle );
            alert( $stopped.text() );

            return;
        }
        
        // Increase wheel angle for animating upwards
        if ( spinDirection === 'up' ) {
            wheelAngle += speedModifier;
        }
        
        // Decrease wheel angle for animating downwards
        else {
            wheelAngle -= speedModifier;
        }
        
        // CSS transform value
        const transform = `rotateX(${wheelAngle}deg) scale3d(0.875, 0.875, 0.875)`;

        $wheel.css( {
            '-webkit-transform': transform,
            '-moz-transform': transform,
            '-ms-transform': transform,
            '-o-transform': transform,
            'transform': transform,
            'transform-origin': `50% calc(50% + ${height/2}px)`,
            'margin-top': `-${height}px`
        } );
    
        // New tick
        animationId = window.requestAnimationFrame( tick );
    } );
    
} )( jQuery );
*, *:before, *:after {
  box-sizing: border-box;
}

.app {
  display: flex;
  flex-direction: row;
  padding: 15px;
}

textarea#log {
  width: 300px;
}

.wheel {
  perspective: 1000px;
  border: 1px solid #333;
  margin: 0 25px;
  flex-grow: 1;
}
.wheel:after {
  content: '';
  display: block;
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 2px;
  background-color: red;
  transform: translateY(-50%);
}
.wheel .wheel__inner {
  position: relative;
  width: 200px;
  height: 350px;
  margin: 0 auto;
  transform-style: preserve-3d;
}
.wheel .wheel__inner .wheel__segment {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 40px;
  position: absolute;
  top: 50%;
  background-color: #ccc;
}
.wheel .wheel__inner .wheel__segment:nth-child(even) {
  background-color: #ddd;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="app">
    <textarea id="log"></textarea>
    <div class="wheel">
        <div class="wheel__inner">
        </div>
    </div>
</div>

有两个问题。

下图为wheelAngle = 0时的状态:

在您的代码中,项目 0 有 startAngle = 0endAngle = some positive value。这与您所看到的不符。实际上,项目 0 应该以 0 为中心。所以你需要将你的区域偏移项目角度宽度的一半:

var rotateAngle = angle * i;        
var transform = `rotateX(${ rotateAngle }deg) translateZ(${ radius }px)`;
var startAngle = rotateAngle - angle / 2
var endAngle = rotateAngle + angle / 2;

第二个问题是你的归一化函数。您采用绝对值,因此会丢失任何方向信息。这是该函数的更好版本:

function normaliseAngle( angle ) {
    angle = -angle;
    return angle - 360 * Math.floor(angle / 360);
}

主要问题是 start/end 角度。我更新了如下逻辑:

$segment.attr('data-start-angle', -startAngle + angle / 2);
$segment.attr('data-end-angle', -endAngle + angle / 2);

还有

function normaliseAngle(angle) {
    angle = angle % 360;
    if (angle > 0)
      angle = angle - 360;
    return angle;
  }

负向旋转会显示从第一个元素开始的元素(而不是正向旋转)。您还需要考虑 angle / 2 的偏移量,因为 startAngle 会将您置于元素的中间。那么你应该在逻辑上将你的角度归一化为负值。

完整代码

(function($) {

  // Settings
  const items = 28; // Segments on wheel
  const spinSpeed = randNumber(1, 10); // Spin speed multiplier
  const spinDuration = randNumber(2, 5); // In seconds
  const spinDirection = randNumber(0, 1) ? 'up' : 'down'; // Animate up  or down

  // Vars
  const $wheel = $('.wheel .wheel__inner');
  const diameter = $wheel.height();
  const radius = diameter / 2;
  const angle = 360 / items;
  const circumference = Math.PI * diameter;
  const height = circumference / items;

  // Trackers
  let wheelAngle = 0;
  const wheelStarted = new Date();

  // Add segments to the wheel
  for (let i = 0; i < items; i++) {
    var startAngle = angle * i;
    var endAngle = angle * (i + 1);
    var transform = `rotateX(${ startAngle }deg) translateZ(${ radius }px)`;

    var $segment = $('<div>', {
      class: 'wheel__segment',
      html: `<span>Item ${ i }</span>`
    }).css({
      'transform': transform,
      'height': height,
    });

    // Add start and end angles for this segment
    $segment.attr('data-start-angle', -startAngle + angle / 2);
    $segment.attr('data-end-angle', -endAngle + angle / 2);

    $segment.appendTo($wheel);
  }


  /**
   * Print debug info to DOM
   *
   * @param {object}
   */
  function logInfo(data) {
    const $log = $('textarea#log');
    let logString = '';

    logString += '-----' + "\n";
    for (var key in data) {
      logString += `${ key }: ${ data[ key ] }` + "\n";
    }
    logString += "\n";

    // Prepend log to last value
    logString += $log.val();

    // Update field value
    $log.val(logString);
  }

  /**
   * Get random number between min & max (inclusive)
   *
   * @param {number} min
   * @param {number} max
   * @returns {number}
   */
  function randNumber(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }

  /**
   * Limit angles to 0 - 360
   *
   * @param {number}
   * @returns {number}
   */
  function normaliseAngle(angle) {
    angle = angle % 360;
    if (angle > 0)
      angle = angle - 360;
    return angle;
  }

  /**
   * Get the wheel segment at a specific angle
   *
   * @param {number} angle
   * @returns {jQuery}
   */
  function segmentAtAngle(angle) {

    angle = normaliseAngle(angle);

    const $found = $wheel.find('.wheel__segment').filter(function() {
      const startAngle = parseFloat($(this).data('start-angle'));
      const endAngle = parseFloat($(this).data('end-angle'));
      return angle >= endAngle && angle < startAngle;
    });

    return $found;
  }

  /**
   * @var {integer} Unique ID of requestAnimationFrame callback
   */
  var animationId = window.requestAnimationFrame(function tick() {

    // Time passed since wheel started spinning (in seconds)
    const timePassed = (new Date() - wheelStarted) / 1000;

    // Speed modifier value (can't be zero)
    let speedModifier = parseInt(spinSpeed) || 1;

    // Decelerate animation if we're over the animation duration
    if (timePassed > spinDuration) {

      const decelTicks = (spinDuration - 1) * 60;
      const deceleration = Math.exp(Math.log(0.0001 / speedModifier) / decelTicks);
      const decelRate = (1 - ((timePassed - spinDuration) / 10)) * deceleration;

      speedModifier = speedModifier * decelRate;

      // Stop animation from going in reverse
      if (speedModifier < 0) {
        speedModifier = 0;
      }
    }

    // Print debug info
    logInfo({
      timePassed: timePassed,
      speedModifier: speedModifier,
      wheelAngle: wheelAngle,
      normalisedAngle: normaliseAngle(wheelAngle)
    });

    // Wheel not moving, animation must have finished
    if (speedModifier <= 0) {
      window.cancelAnimationFrame(animationId);

      const $stopped = segmentAtAngle(wheelAngle);
      alert($stopped.text());

      return;
    }

    // Increase wheel angle for animating upwards
    if (spinDirection === 'up') {
      wheelAngle += speedModifier;
    }

    // Decrease wheel angle for animating downwards
    else {
      wheelAngle -= speedModifier;
    }

    // CSS transform value
    const transform = `rotateX(${wheelAngle}deg) scale3d(0.875, 0.875, 0.875)`;

    $wheel.css({
      '-webkit-transform': transform,
      '-moz-transform': transform,
      '-ms-transform': transform,
      '-o-transform': transform,
      'transform': transform,
      'transform-origin': `50% calc(50% + ${height/2}px)`,
      'margin-top': `-${height}px`
    });

    // New tick
    animationId = window.requestAnimationFrame(tick);
  });

})(jQuery);
*,
*:before,
*:after {
  box-sizing: border-box;
}

.app {
  display: flex;
  flex-direction: row;
  padding: 15px;
}

textarea#log {
  width: 300px;
}

.wheel {
  perspective: 1000px;
  border: 1px solid #333;
  margin: 0 25px;
  flex-grow: 1;
}

.wheel:after {
  content: '';
  display: block;
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 2px;
  background-color: red;
  transform: translateY(-50%);
}

.wheel .wheel__inner {
  position: relative;
  width: 200px;
  height: 350px;
  margin: 0 auto;
  transform-style: preserve-3d;
}

.wheel .wheel__inner .wheel__segment {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 40px;
  position: absolute;
  top: 50%;
  background-color: #ccc;
}

.wheel .wheel__inner .wheel__segment:nth-child(even) {
  background-color: #ddd;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="app">
  <textarea id="log"></textarea>
  <div class="wheel">
    <div class="wheel__inner">
    </div>
  </div>
</div>