使用 javascript 根据旋转角度在 3D cylinder/wheel 上查找线段
Find a segment on a 3D cylinder/wheel based on a rotational angle using javascript
我有一个 3D 轮子,我正在使用 javascript requestAnimationFrame()
函数制作动画。
车轮看起来像:
有 4 个主要变量需要考虑:
items
轮子上的段数
spinSpeed
旋转速度修改器。将每帧的角度 increase/decrease 乘以该值。
spinDuration
减速停止前的全速旋转动画时长
spinDirection
轮子应该旋转的方向。接受 up
或 down
.
现在我想使用车轮停止的角度从 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°
和低于 0°
,我使用以下函数执行此操作:
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 = 0
和 endAngle = 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>
我有一个 3D 轮子,我正在使用 javascript requestAnimationFrame()
函数制作动画。
车轮看起来像:
有 4 个主要变量需要考虑:
items
轮子上的段数spinSpeed
旋转速度修改器。将每帧的角度 increase/decrease 乘以该值。spinDuration
减速停止前的全速旋转动画时长spinDirection
轮子应该旋转的方向。接受up
或down
.
现在我想使用车轮停止的角度从 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°
和低于 0°
,我使用以下函数执行此操作:
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 = 0
和 endAngle = 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>