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