Before the company implemented a project can be configured in the rotary draw, mainly using the drawing canvas, to write this article is to through a point, you might be interested in to get familiar with some of the canvas API, I also do some review, articles, of course, not as complicated as in project implementation, is just a simple version of the, Try to be easy to understand and show you the path that interests you (actually lazy ~). 😁
One, achieve a rotary draw
Before we implement it, we need to have a general idea. Think back to the elements of the lottery wheel that we have seen online or in real life:
- A big turntable
- There are intervals on the wheel, indicating different prizes
- In the middle of the wheel is a button, the button has a pointer, to point to the drawn prize
Let’s think about the process of the lottery: when we click the button in the middle, the wheel starts to spin, and when we get the prize, the rotation speed starts to slow down, slowly stop, and finally point to the area where the prize is located, so we can achieve this step
- Draws all static elements
- Add a spin animation to the turntable
- The pointer points to the area where we specify the prize
1. Draw all static elements
1.1. Construction of development environment
Install the create-React-app globally to quickly generate the React development environment. This is not coupled to the turntable we are developing, but for debugging purposes, it is possible to use vue.
// Install globally
npm install create-react-app -g
// Create the development environment with lottery as the directory name
create-react-app lottery
Copy the code
After the installation is complete, change the directory structure as shown in the following figure
The turntable.jsx contents are as follows:
export default class Turntable {}Copy the code
The contents of app.js are as follows:
import React, { Component } from 'react'
import Turntable from './turntable/turntable'
class App extends Component {
constructor(props) {
super(props)
}
render() {
return <div>Lottery turntable</div>}}export default App
Copy the code
When this is done, open the command line tool in the current directory and type NPM start to start the project
1.2. Draw the big turntable
Modify app.js as follows:
import React, { Component } from 'react'
import Turntable from './turntable/turntable'
class App extends Component {
constructor(props) {
super(props)
// React to get the DOM element
this.canvas = React.createRef()
}
componentDidMount() {
// The canvas element is stored in the this.canvas current property
const canvas = this.canvas.current
// Get the context of the canvas. The context contains various apis to manipulate the Canvas
const context = canvas.getContext('2d')
// Set the canvas width and height
canvas.width = 300
canvas.height = 300
// Create the TurnTable object and pass in the Canvas element and context
const turntable = new Turntable({canvas: canvas, context: context})
turntable.render()
}
render() {
return <canvas
ref={this.canvas}
style={{
width: '300px',
height: '300px',
position: 'absolute',
top: 0.left: 0.right: 0.bottom: 0.margin: 'auto'
}}>
</canvas>}}export default App
Copy the code
Change the contents of turnTable.jsx:
export default class Turntable {
constructor(options) {
// Fetch and save the canvas,context
this.canvas = options.canvas
this.context = options.context
}
drawPanel() {
const context = this.context
// Save the current state of the canvas, using the restore call, to ensure that the current
// The draw is not affected by the previous draw
context.save()
// create a new path to return the brush to its default position (0,0)
// Ensure that the current draw does not affect the previous draw
context.beginPath()
// Set the color to fill the turntable. Fill is to fill, not to draw.
context.fillStyle = '#FD6961'
// Draw a circle with six parameters: the x coordinate of the center of the circle, and the x coordinate of the center of the circle
// The y coordinate, the radius of the circle, the Angle to start drawing, the Angle to end drawing, and the direction of drawing
// (false indicates clockwise)
context.arc(150.150.150.0.Math.PI * 2.false)
// Fill the circle with the color we set. ClosePath is not used here because
// For closePath not valid for fill.
context.fill()
// Restore the canvas to the state it was in the last time we saved ()
context.restore()
}
render() {
this.drawPanel()
}
}
Copy the code
After saving, the result in the browser is as follows:
1.2. Draw prize blocks
Add and modify the following contents to the Turntable class in the turntable.jsx file (to avoid duplication of contents resulting in too much space, all invariant parts will not be shown):
// add
drawPrizeBlock() {
const context = this.context
// The first prize color block is drawn when the beginning radian and the end radian, as we are here
// Temporarily fixed to 6 prizes, so base 6
let startRadian = 0, RadianGap = Math.PI * 2 / 6, endRadian = startRadian + RadianGap
for (let i = 0; i < 6; i++) {
context.save()
context.beginPath()
// To distinguish the different colors, we use randomly generated colors as the fill colors of the color blocks
context.fillStyle = The '#'+Math.floor(Math.random()*16777215).toString(16)
// Use the moveTo method to set the initial position of the circle at the dot
// The arc will be closed with a dot. Below is a comparison between moveTo and no moveTo
context.moveTo(150.150)
// When drawing an arc, moveTo is automatically used to move the brush to the beginning of the arc, half
// We set the diameter slightly smaller than the turntable
context.arc(150.150.140, startRadian, endRadian, false)
// After each prize is drawn, the radian of the next prize is increased
startRadian += RadianGap
endRadian += RadianGap
context.fill()
context.restore()
}
}
// modify
render() {
this.drawPanel()
this.drawPrizeBlock()
}
Copy the code
After saving, our turntable will look like the following:
The difference between using context.moveto (150, 150) and not using:
Use context.moveto (150, 150):
Not using context.moveto (150, 150):
The prize block is drawn, we need to add the name of each prize, suppose we have three prizes, the other three are set to not win
The contents of the turntable. JSX file are changed as follows:
constructor(options) {
this.canvas = options.canvas
this.context = options.context
// Add initializes the configuration of the prize
this.awards = [
{ level: 'Grand Prize'.name: 'My autograph'.color: '#576c0a' },
{ level: 'Not winning'.name: 'Not winning'.color: '#ad4411' },
{ level: 'First Prize'.name: 'Maserati Super Classic Limited Edition'.color: '#43ed04' },
{ level: 'Not winning'.name: 'Not winning'.color: '#d5ed1d' },
{ level: 'Second prize'.name: 'Latiao package'.color: '#32acc6' },
{ level: 'Not winning'.name: 'Not winning'.color: '#e06510']}},// add
// Think about it. Just like our first prize, the text is very long, which is beyond our prize block, and canvas
// It's not smart enough to give you a wrapping mechanism, so we have to do it manually
/** ** @param {*} context ~ * @param {*} text * @param {*} maxLineWidth */
// The whole idea is to add text that satisfies the width we defined as value to the array separately
// The last item in the returned array is each row after processing.
getLineTextList(context, text, maxLineWidth) {
let wordList = text.split(' '), tempLine = ' ', lineList = []
for (let i = 0; i < wordList.length; i++) {
// The measureText method measures the width of the text as set
// the size of fontSize, so based on this, we set maxLineWidth to a multiple of the current fontSize
if (context.measureText(tempLine).width >= maxLineWidth) {
lineList.push(tempLine)
maxLineWidth -= context.measureText(text[0]).width
tempLine = ' '
}
tempLine += wordList[i]
}
lineList.push(tempLine)
return lineList
}
// modify
drawPrizeBlock() {
const context = this.context
const awards = this.awards
let startRadian = 0, RadianGap = Math.PI * 2 / 6, endRadian = startRadian + RadianGap
for (let i = 0; i < awards.length; i++) {
context.save()
context.beginPath()
context.fillStyle = awards[i].color
context.moveTo(150.150)
context.arc(150.150.140, startRadian, endRadian, false)
context.fill()
context.restore()
// Start drawing our text
context.save();
// Set the text color
context.fillStyle = '#FFFFFF';
// Set the text style
context.font = "14px Arial";
// Change the canvas origin position, simply translate to the point, then that point will become the coordinate (0, 0)
context.translate(
150 + Math.cos(startRadian + RadianGap / 2) * 140.150 + Math.sin(startRadian + RadianGap / 2) * 140
);
// The rotation Angle, which is a rotation relative to the origin.
context.rotate(startRadian + RadianGap / 2 + Math.PI / 2);
MaxLineWidth = 70; maxLineWidth = 70
// We can display a maximum of 5 words per line
this.getLineTextList(context, awards[i].name, 70).forEach((line, index) = > {
// The method for drawing text takes three parameters: the text to be drawn, the x coordinate to start drawing, and the y coordinate to start drawing
context.fillText(line, -context.measureText(line).width / 2, ++index * 25);
})
context.restore();
startRadian += RadianGap
endRadian += RadianGap
}
}
Copy the code
After saving, our turntable will look like this:
1.3. Draw buttons and arrows
The Turntable class is added and modified as follows:
// add
// Draw the button, and the start text on the button. There is no new point here
drawButton() {
const context = this.context
context.save()
context.beginPath()
context.fillStyle = '#FF0000'
context.arc(150.150.30.0.Math.PI * 2.false)
context.fill()
context.restore()
context.save()
context.beginPath()
context.fillStyle = '#FFF'
context.font = '20px Arial'
context.translate(150.150)
context.fillText('Start', -context.measureText('Start').width / 2.8)
context.restore()
}
// add
// Draw an arrow to point to our prize
drawArrow() {
const context = this.context
context.save()
context.beginPath()
context.fillStyle = '#FF0000'
context.moveTo(140.125)
context.lineTo(150.100)
context.lineTo(160.125)
context.closePath()
context.fill()
context.restore()
}
render() {
this.drawPanel()
this.drawPrizeBlock()
this.drawButton()
this.drawArrow()
}
Copy the code
After saving, the turntable is as shown in the figure below:
1.4. Click the button to make the turntable turn
The effect should be that when we click the button, the button and the pointer are still, while the turntable and the prize block on the turntable rotate simultaneously.
And how do you get the turntable spinning? Remember us is how to draw the turntable and prizes, we took the form of drawing arc, starting from the Angle of 0 position, if we will draw the start Angle increases a little, so the position of the rotary table is equivalent to the rotation a bit, so we started when we constantly change Angle, wheel looks like a rotation.
The contents of the Turntable class are modified as follows:
// modify
constructor(options) {
this.canvas = options.canvas
this.context = options.context
// Added this property to record our initial Angle
this.startRadian = 0
this.awards = [
{ level: 'Grand Prize'.name: 'My autograph'.color: '#576c0a' },
{ level: 'Not winning'.name: 'Not winning'.color: '#ad4411' },
{ level: 'First Prize'.name: 'Maserati Super Classic Limited Edition'.color: '#43ed04' },
{ level: 'Not winning'.name: 'Not winning'.color: '#d5ed1d' },
{ level: 'Second prize'.name: 'Latiao package'.color: '#32acc6' },
{ level: 'Not winning'.name: 'Not winning'.color: '#e06510']}},// modify
drawPanel() {
const context = this.context
const startRadian = this.startRadian
context.save()
context.beginPath()
context.fillStyle = '#FD6961'
// Draw the turntable according to the initial Angle we set
context.arc(150.150.150, startRadian, Math.PI * 2 + startRadian, false)
context.fill()
context.restore()
}
// modify
drawPrizeBlock() {
const context = this.context
const awards = this.awards
// Draw the prize block according to the original Angle
let startRadian = this.startRadian, RadianGap = Math.PI * 2 / 6, endRadian = startRadian + RadianGap
for (let i = 0; i < awards.length; i++) {
context.save()
context.beginPath()
context.fillStyle = awards[i].color
context.moveTo(150.150)
context.arc(150.150.140, startRadian, endRadian, false)
context.fill()
context.restore()
context.save()
context.fillStyle = '#FFF'
context.font = "14px Arial"
context.translate(
150 + Math.cos(startRadian + RadianGap / 2) * 140.150 + Math.sin(startRadian + RadianGap / 2) * 140
)
context.rotate(startRadian + RadianGap / 2 + Math.PI / 2)
this.getLineTextList(context, awards[i].name, 70).forEach((line, index) = > {
context.fillText(line, -context.measureText(line).width / 2, ++index * 25)
})
context.restore()
startRadian += RadianGap
endRadian += RadianGap
}
}
// add
// This method is used to convert canvas coordinate points in the window to canvas coordinate points in the window
windowToCanvas(canvas, e) {
// The getBoundingClientRect method returns the size of the HTML element and its position relative to the viewport
const canvasPostion = canvas.getBoundingClientRect(), x = e.clientX, y = e.clientY
return {
x: x - canvasPostion.left,
y: y - canvasPostion.top
}
};
// add
// This method will act as the actual initialization method
startRotate() {
const canvas = this.canvas
const context = this.context
// The getAttribute method retrieves the attribute value of the element. We retrieve the canvas style and store it in the canvasStyle variable
const canvasStyle = canvas.getAttribute('style');
// Here we draw the canvas element we initialized
this.render()
// Add a click event. After clicking the button, we start spinning the turntable
canvas.addEventListener('mousedown', e => {
let postion = this.windowToCanvas(canvas, e)
context.beginPath()
// Here we draw a circle without color in the button area, and then determine whether the point we click is in the circle, which is equivalent to deciding whether to click our button
context.arc(150.150.30.0.Math.PI * 2.false)
if (context.isPointInPath(postion.x, postion.y)) {
// After clicking the button, we will call this method to change our initial Angle startRadian
this.rotatePanel()
}
})
// Add the mouse movement event, just to set the mouse pointer style
canvas.addEventListener('mousemove', e => {
let postion = this.windowToCanvas(canvas, e)
context.beginPath()
context.arc(150.150.30.0.Math.PI * 2.false)
if (context.isPointInPath(postion.x, postion.y)) {
canvas.setAttribute('style'.`cursor: pointer;${canvasStyle}`)}else {
canvas.setAttribute('style', canvasStyle)
}
})
}
// add
// Key ways to handle rotations
rotatePanel() {
// Each call increases the initial Angle by 1 degree
this.startRadian += Math.PI / 180
// After the initial Angle changes, we need to redraw
this.render()
// The rotatePanel function is called to make the drawing of the turntable continuous, resulting in the visual effect of rotation
window.requestAnimationFrame(this.rotatePanel.bind(this));
}
// modify
render() {
this.drawPanel()
this.drawPrizeBlock()
this.drawButton()
this.drawArrow()
}
Copy the code
App.js content is modified as follows:
// modify
componentDidMount() {
const canvas = this.canvas.current
const context = canvas.getContext('2d')
canvas.width = 300
canvas.height = 300
const turntable = new Turntable({ canvas: canvas, context: context })
// Replace render with calling startRotate
turntable.startRotate()
}
Copy the code
After saving, the operation effect will be as follows:
As you can see, when we click the button, the wheel starts to rotate slowly.
1.5. Let the turntable slowly stay at the prizes we designated
The contents of the Turntable class are modified as follows:
// modify
constructor(options) {
this.canvas = options.canvas
this.context = options.context
this.startRadian = 0
// We added a click limit here in order to control the sweepstakes by not allowing any more sweepstakes
this.canBeClick = true
this.awards = [
{ level: 'Grand Prize'.name: 'My autograph'.color: '#576c0a' },
{ level: 'Not winning'.name: 'Not winning'.color: '#ad4411' },
{ level: 'First Prize'.name: 'Maserati Super Classic Limited Edition'.color: '#43ed04' },
{ level: 'Not winning'.name: 'Not winning'.color: '#d5ed1d' },
{ level: 'Second prize'.name: 'Latiao package'.color: '#32acc6' },
{ level: 'Not winning'.name: 'Not winning'.color: '#e06510']}},// modify
startRotate() {
const canvas = this.canvas
const context = this.context
const canvasStyle = canvas.getAttribute('style');
this.render()
canvas.addEventListener('mousedown', e => {
// No more draws will be allowed as long as the draw has not ended
if (!this.canBeClick) return
this.canBeClick = false
let loc = this.windowToCanvas(canvas, e)
context.beginPath()
context.arc(150.150.30.0.Math.PI * 2.false)
if (context.isPointInPath(loc.x, loc.y)) {
// Each time we click on the draw, we will reset the initialization Angle
this.startRadian = 0
// Distance is the angular distance we calculate to rotate the specified prize to the pointer. DistanceToStop is described below
const distance = this.distanceToStop()
this.rotatePanel(distance)
}
})
canvas.addEventListener('mousemove', e => {
let loc = this.windowToCanvas(canvas, e)
context.beginPath()
context.arc(150.150.30.0.Math.PI * 2.false)
if (context.isPointInPath(loc.x, loc.y)) {
canvas.setAttribute('style'.`cursor: pointer;${canvasStyle}`)}else {
canvas.setAttribute('style', canvasStyle)
}
})
}
// modify
rotatePanel(distance) {
// We use a simple easing function to calculate how much Angle we need to change each time we draw to achieve a slow gradient of the turntable from block to block
let changeRadian = (distance - this.startRadian) / 10
this.startRadian += changeRadian
// In the end, when the gap between our target distance and startRadian is less than 0.05, we will run out of prizes by default and can continue to draw the next one.
if (distance - this.startRadian <= 0.05) {
this.canBeClick = true;
return
}
this.render()
window.requestAnimationFrame(this.rotatePanel.bind(this, distance))
}
// add
distanceToStop() {
// middleDegrees is the distance between the middle Angle of the prize block and the initial startRadian. Distance is the distance between the current prize and the pointer position.
let middleDegrees = 0, distance = 0
// Map the middleDegrees of each prize
const awardsToDegreesList = this.awards.map((data, index) = > {
let awardRadian = (Math.PI * 2) / this.awards.length
return awardRadian * index + (awardRadian * (index + 1) - awardRadian * index) / 2
});
// Generate a random index value to indicate the prize we should win in this draw
const currentPrizeIndex = Math.floor(Math.random() * this.awards.length)
console.log('The current prize should be:'+this.awards[currentPrizeIndex].name)
middleDegrees = awardsToDegreesList[currentPrizeIndex];
// Since the pointer is vertical and corresponds to math.pi /2 in the coordinate system, we need to make a judgment to move the Angle
distance = Math.PI * 3 / 2 - middleDegrees
distance = distance > 0 ? distance : Math.PI * 2 + distance
// This extra value is added to make the turntable turn a few more times, making it look more like a lottery
return distance + Math.PI * 10;
}
Copy the code
After saving, we can start the lottery, as shown in the picture below:
More lucky, the first time in two Maserati 😀
Article write here almost end, in order to facilitate understanding (lazy), a lot of parameters in the article are written dead, we must not do so in the work, to be scolded ~~
This is just a very, very simple rotary table, there are still many many places can be optimized and extensions, such as add images for each prize and wheel piece of color can be configured, color and each prize draw pointer rotation, beautify the turntable (sample wheel color is randomly generated six color, feel also not ugly ha ~ ~), mobile terminal adapter, etc. It’s time to test you.
Finally, the two Amway Chinese manga series, Stars Change, are touching in detail, but too short, only three episodes. The other is “Fox Monster Little Matchmaking girl”, with Su Su’s voice acting so cute that bilibili saw it
The source address