【问题标题】:Find a segment on a 3D cylinder/wheel based on a rotational angle using javascript使用javascript根据旋转角度在3D圆柱体/车轮上查找段
【发布时间】:2020-12-27 21:00:54
【问题描述】:

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

轮子的样子:

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

  1. items 轮子上的段数。
  2. spinSpeed 旋转速度修改器。将每帧的角度增加/减少乘以该值。
  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>

【问题讨论】:

    标签: javascript jquery css geometry css-transforms


    【解决方案1】:

    有两个问题。

    下图显示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);
    }
    

    【讨论】:

    • 感谢您的回答,并为延误道歉!你的答案是正确的,我存储的数据属性从 0 开始不正确,而不是负半角。你的 normalize 功能是最节省我的。再次感谢。
    【解决方案2】:

    主要问题是开始/结束角度。我更新了如下逻辑:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-07-29
      • 2018-03-07
      • 2017-02-01
      • 1970-01-01
      • 2021-05-31
      • 2019-05-20
      • 1970-01-01
      相关资源
      最近更新 更多