react-turntable-custom-colors
Version:
A HTML5 Turntable component for React
263 lines (246 loc) • 7.37 kB
JavaScript
/**
* @author Jinke.Li
* @version 1.2.7
* @description https://juejin.im/post/5992b6065188257dd3664dbc
*/
import React, { PureComponent } from 'react'
import cls from 'classnames'
import PropTypes from 'prop-types'
export function easeOut(t, b, c, d) {
if ((t /= d / 2) < 1) return (c / 2) * t * t + b
return (-c / 2) * (--t * (t - 2) - 1) + b
}
const prefix = 'react-turntable'
export default class ReactTurntable extends PureComponent {
state = {
isRotate: false,
startRotate: 0
}
constructor(props) {
super(props)
this.canvas = null
this.ctx = null
this.animateId = null
}
static defaultProps = {
width: 500,
height: 500,
speed: 1000, //旋转速度
duration: 5000, //旋转时间
prizes: [],
clickText: 'Click',
colors: ['#3f51b5', '#ff9800', '#e91e63', '#4caf50', '#009688', '#795548', '#9c27b0', '#f44336'],
fontStyle: {
color: '#fff',
size: '14px',
fontWeight: 'bold',
fontVertical: false,
fontFamily: 'Microsoft YaHei'
},
onStart: () => true,
hiddenButton: false
}
static propTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
prizes: PropTypes.array.isRequired,
clickText: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
colors: PropTypes.array,
speed: PropTypes.number,
duration: PropTypes.number,
onComplete: PropTypes.func,
onStart: PropTypes.func,
fontVertical: PropTypes.bool,
fontStyle: PropTypes.object,
hiddenButton: PropTypes.bool
}
render() {
const {
clickText,
style,
className,
width,
height,
hiddenButton
} = this.props
const styles = { ...style, width, height }
return (
<div
className={cls(prefix, `${prefix}-section`, className)}
style={styles}
>
<canvas
id="react-turntable-section-canvas"
ref={node => (this.canvas = node)}
/>
{!hiddenButton &&
(Object.is(typeof clickText, 'string') ? (
<div
className="react-turntable-section-btn"
onClick={this.onStartRotate}
>
{clickText}
</div>
) : (
<div onClick={this.onStartRotate}>{clickText}</div>
))}
</div>
)
}
noticePrize = () => {
const prize = this.getSelectedPrize()
this.props.onComplete && this.props.onComplete(prize)
}
rotateTurntable = () => {
this.rotateTime += 20
if (this.rotateTime >= this.rotateAllTime) {
this.setState({ isRotate: false })
this.noticePrize()
return
}
let _rotateChange =
(this.rotateChange -
easeOut(this.rotateTime, 0, this.rotateChange, this.rotateAllTime)) *
(Math.PI / 180)
this.startRotate += _rotateChange
this.drawTurntable()
this.animateId = requestAnimationFrame(this.rotateTurntable)
}
getSelectedPrize = () => {
let startAngle = (this.startRotate * 180) / Math.PI,
awardAngle = (this.awardRotate * 180) / Math.PI,
pointerAngle = 90,
overAngle = (startAngle + pointerAngle) % 360,
restAngle = 360 - overAngle,
index = Math.floor(restAngle / awardAngle)
return this.prizes[index]
}
drawTurntable() {
const ctx = this.ctx
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
const {
colors,
fontStyle: { fontVertical, fontWeight, fontFamily, size, color }
} = this.props
for (let [i, prize] of this.prizes.entries()) {
const _currentStartRotate = this.startRotate + this.awardRotate * i
const _currentEndRotate = _currentStartRotate + this.awardRotate
this.ctx.save()
ctx.fillStyle = colors[i]
ctx.beginPath()
ctx.arc(
this.centerX,
this.centerY,
this.R,
_currentStartRotate,
_currentEndRotate,
false
)
ctx.arc(
this.centerX,
this.centerY,
this.INSERT_R,
_currentEndRotate,
_currentStartRotate,
true
)
ctx.fill()
ctx.closePath()
ctx.restore()
ctx.save()
ctx.beginPath()
ctx.font = `${fontWeight} ${
/.*px$/.test(size) ? size : size + 'px'
} ${fontFamily}`
ctx.fillStyle = color
ctx.textBaseline = 'middle'
const currentX =
Math.cos(_currentStartRotate + this.awardRotate / 2) * this.TEXT_R
const currentY =
Math.sin(_currentStartRotate + this.awardRotate / 2) * this.TEXT_R
ctx.translate(this.centerX + currentX, this.centerY + currentY)
ctx.rotate(_currentStartRotate + this.awardRotate / 2 + Math.PI / 2)
const { width: fontWidth } = ctx.measureText(prize)
if (fontVertical === true) {
ctx.translate(0, Math.min(fontWidth, 25))
ctx.rotate((90 / 180) * Math.PI)
}
ctx.fillText(prize, -fontWidth / 2, 0)
ctx.closePath()
ctx.restore()
}
}
destroyContext() {
window.cancelAnimationFrame(this.animateId)
delete this.canvas
delete this.ctx
}
compatibilityFrame() {
window.requestAnimFrame = (() => {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
(callback => window.setTimeout(callback, 1000 / 60))
)
})()
window.cancelAnimationFrame =
window.cancelAnimationFrame || window.mozCancelAnimationFrame
}
componentWillMount() {
if (this.props.prizes.length < 2)
throw new Error(
'options prizes It needs to be an array , Not less than two'
)
}
componentWillUnmount() {
this.destroyContext()
}
onStartRotate = () => {
const { speed, duration, onStart } = this.props
if (this.state.isRotate) return
if (onStart && !onStart()) return
this.setState({ isRotate: true }, () => {
this.rotateTime = 0
this.rotateAllTime = Math.random() * 5 + duration
this.rotateChange = Math.random() * 10 + speed / 100
this.rotateTurntable()
})
}
stopRotate = () => {
this.setState({ isRotate: false })
window.cancelAnimationFrame(this.animateId)
this.noticePrize()
}
initTurntable = () => {
const { width, height, prizes } = this.props
this.prizes = prizes
this.startRotate = 0
this.rotateTime = 0
this.rotateAllTime = 0
this.rotateChange = 0
this.ctx = this.canvas.getContext('2d')
this.canvas.width = width
this.canvas.height = height
this.awardRotate = (Math.PI * 2) / prizes.length
this.centerX = this.canvas.width / 2
this.centerY = this.canvas.height / 2
this.R = this.canvas.width / 2 - 20
this.TEXT_R = this.R - 50
this.INSERT_R = 0
this.drawTurntable()
}
getTurntable = () => {
if (this.props.getTurntable) {
this.props.getTurntable({
start: this.onStartRotate,
stop: this.stopRotate
})
}
}
componentDidMount() {
this.compatibilityFrame()
this.initTurntable()
this.getTurntable()
}
}