Trying to turn canvas code into pure Javascript for React

Asked

Viewed 49 times

1

I’m making a piano mini-game similar to Guitar Hero, where blocks fall from the sky and you have to press the note (in my case, the piano note/key) equivalent at the right time, they fall in specific time to simulate the music.

Falling blocks are being programmed in Canvas.

I made a pure Javascript version and is working as I want, but I’m not able to transfer to React.

There will be a JSON object, which is the music score, important to talk about it now, is that it contains the frame number (frameNo) in which the corresponding note block should appear (at each canvas update, a frame is counted):

const cheatSheet = [
      {
         "note": {
             "left hand" : "F4",
             "right hand":"G4"
         },
         "frameNo": 25
      }
]

At first I have the following example musical score, in case you want to test the code:

const cheatSheet = [{"note":{"left hand":"F4","right hand":"G4"},"frameNo":25},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":50},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":75},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":100},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":125},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":150},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":175},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":200},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":225},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":250},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":275},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":300},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":325},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":350},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":375},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":400},{"note":{"left hand":"D4","right hand":"A4"},"frameNo":425},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":450},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":475},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":500},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":525},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":575},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":600},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":625},{"note":{"left hand":"B4","right hand":"D4"},"frameNo":650},{"note":{"left hand":"E4","right hand":"A4"},"frameNo":675},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":700},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":725},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":750},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":775},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":800},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":825},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":850},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":875},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":900},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":925},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":950},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":975},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1000},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1025},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1050},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1075},{"note":{"left hand":"D4","right hand":"A4"},"frameNo":1100},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1125},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":1150},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":1200},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":1300}];

My code in pure Javascript is like this:

var cheatSheet = [{"note":{"left hand":"F4","right hand":"G4"},"frameNo":25},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":50},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":75},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":100},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":125},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":150},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":175},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":200},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":225},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":250},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":275},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":300},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":325},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":350},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":375},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":400},{"note":{"left hand":"D4","right hand":"A4"},"frameNo":425},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":450},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":475},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":500},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":525},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":575},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":600},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":625},{"note":{"left hand":"B4","right hand":"D4"},"frameNo":650},{"note":{"left hand":"E4","right hand":"A4"},"frameNo":675},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":700},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":725},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":750},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":775},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":800},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":825},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":850},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":875},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":900},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":925},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":950},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":975},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1000},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1025},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1050},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1075},{"note":{"left hand":"D4","right hand":"A4"},"frameNo":1100},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1125},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":1150},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":1200},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":1300}];

var gameArea = {
    canvas: document.createElement("canvas"),
    start : function() {
        this.context = this.canvas.getContext("2d");
        this.frameNo = 0;
        this.noteNo = 0;
        document.body.insertBefore(this.canvas, document.body.childNodes[0]);
        this.interval = setInterval(updateGameArea, 20);
    },
    clear : function() {
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
    },
    stop  : function() {
        clearInterval(this.interval);
    }    
}
var blocks = [];

function startGame() {
    gameArea.start();
}

function Block() {
    
    this.width = 10;
    this.height = 10;

    this.x = 0;
    this.y = 0;

    this.update = () => {
        let ctx = gameArea.context;
        ctx.fillStyle = 'green';
        ctx.fillRect(this.x, this.y, this.width, this.height);
    }
}

function updateGameArea() {
    gameArea.clear();
    gameArea.frameNo += 1;

    let currNote = gameArea.noteNo;

    if (currNote < cheatSheet.length) {
        let noteFrame = cheatSheet[currNote].frameNo;
        let currFrame = gameArea.frameNo;

        if (noteFrame === currFrame) {
            blocks.push(new Block());
            gameArea.noteNo += 1;
        }
    }

    for (let i = 0; i < blocks.length; i++) {
        blocks[i].y += 1;
        blocks[i].update();
    }

}
canvas {
  border:1px solid #d3d3d3;
  background-color: #f1f1f1;
}
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body onload="startGame()">
</body>
</html>

If you take and run this code in some environment (or right here), this is how it should work.

However, when it comes to React, Canvas simply doesn’t rotate (it goes blank).

const cheatSheet = [{"note":{"left hand":"F4","right hand":"G4"},"frameNo":25},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":50},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":75},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":100},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":125},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":150},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":175},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":200},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":225},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":250},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":275},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":300},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":325},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":350},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":375},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":400},{"note":{"left hand":"D4","right hand":"A4"},"frameNo":425},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":450},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":475},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":500},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":525},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":575},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":600},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":625},{"note":{"left hand":"B4","right hand":"D4"},"frameNo":650},{"note":{"left hand":"E4","right hand":"A4"},"frameNo":675},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":700},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":725},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":750},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":775},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":800},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":825},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":850},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":875},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":900},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":925},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":950},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":975},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1000},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1025},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1050},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1075},{"note":{"left hand":"D4","right hand":"A4"},"frameNo":1100},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1125},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":1150},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":1200},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":1300}];

  function Block(gameArea) {

      this.width = 10;
      this.height = 10;

      this.x = 0;
      this.y = 0;

      this.update = () => {
          let ctx = gameArea.context;
          ctx.fillStyle = 'green';
          ctx.fillRect(this.x, this.y, this.width, this.height);
      }
  }

  class CanvasComponent extends React.Component {
      constructor() {
          super();
          this.gameArea = {
              canvas: React.createRef(),
              start : function() {
                  this.context = this.canvas.current.getContext("2d");
                  this.frameNo = 0;
                  this.noteNo = 0; 
                  this.blocks = []; 
                  this.interval = setInterval(this.updateGameArea, 20);
              },
              clear : function() {
                  this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
              },
              stop  : function() {
                  clearInterval(this.interval);
              }    
          }
      }

      updateGameArea() {

          const cheatSheet = this.props.cheatSheet;
          const gameArea = this.gameArea;

          gameArea.clear();
          gameArea.frameNo += 1;

          let currNote = gameArea.noteNo;
          let blocks = gameArea.blocks;

          if (currNote < cheatSheet.length) {
              let noteFrame = cheatSheet[currNote].frameNo;
              let currFrame = gameArea.frameNo;

              if (noteFrame === currFrame) {
                  blocks.push(new Block(gameArea));
                  gameArea.noteNo += 1;
              }
          }

          for (let i = 0; i < blocks.length; i++) {
              blocks[i].y += 1;
              blocks[i].update();
          }

      }

      componentDidMount() {
          const { gameArea } = this;
          gameArea.start();
      }

      render() {
          return <canvas ref={this.gameArea.canvas} width={300} height={300} />;
      }
  }

  ReactDOM.render(<CanvasComponent cheatSheet={cheatSheet} />, document.getElementById('root'));
canvas {
    border:1px solid #d3d3d3;
    background-color: #f1f1f1;
}
<!DOCTYPE html>
<html>
<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
</head>
<body>
  <div id="root"></div>
</body>
</html>

What I tried to...

Unfortunately, I haven’t tried much, because I don’t even know what to hypothesize. But I tried to simulate in another environment a simple canvas in React, and noticed that putting the setInterval in the place where it was not running, so I put it in the componentDidMount and solved the situation. This is only the first mistake, even after this change the code does not work. I tried searching the internet for examples of canvas in React to see if I didn’t make some conceptual or syntax error of something. But it’s really hard to find examples of this in React classes, not Hooks.

1 answer

1

I’ll start by saying you have a lot of misconceptions, but let’s start at the very least so you can get the code running:

this.interval = setInterval(this.updateGameArea, 20);

That one this of updateGameArea refers to the object this.gameArea and not the component, so the reference to the function will give undefined not running anything on the timer.

Even building the right reference will need to do bind do this to ensure it gets right.

In the clear:

this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);

The use of ref understands that the native field is accessed through the property current, something that was missing in the this.canvas.width and this.canvas.height, which therefore result in undefined.

Correcting these points already works:

const cheatSheet = [{"note":{"left hand":"F4","right hand":"G4"},"frameNo":25},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":50},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":75},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":100},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":125},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":150},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":175},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":200},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":225},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":250},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":275},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":300},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":325},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":350},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":375},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":400},{"note":{"left hand":"D4","right hand":"A4"},"frameNo":425},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":450},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":475},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":500},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":525},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":575},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":600},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":625},{"note":{"left hand":"B4","right hand":"D4"},"frameNo":650},{"note":{"left hand":"E4","right hand":"A4"},"frameNo":675},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":700},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":725},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":750},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":775},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":800},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":825},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":850},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":875},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":900},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":925},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":950},{"note":{"left hand":"E4","right hand":"G4"},"frameNo":975},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1000},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1025},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1050},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1075},{"note":{"left hand":"D4","right hand":"A4"},"frameNo":1100},{"note":{"left hand":"D4","right hand":"B4"},"frameNo":1125},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":1150},{"note":{"left hand":"F4","right hand":"G4"},"frameNo":1200},{"note":{"left hand":"C4","right hand":"C5"},"frameNo":1300}];

  function Block(gameArea) {

    this.width = 10;
    this.height = 10;

    this.x = 0;
    this.y = 0;

    this.update = () => {
        let ctx = gameArea.context;
        ctx.fillStyle = 'green';
        ctx.fillRect(this.x, this.y, this.width, this.height);
    }
  }

  class CanvasComponent extends React.Component {
    constructor() {
        super();
        
        this.updateGameArea = this.updateGameArea.bind(this); //acrescentado
        const component = this; //acrescentado
        
        this.gameArea = {
            canvas: React.createRef(),
            start : function() {
                this.context = this.canvas.current.getContext("2d");
                this.frameNo = 0;
                this.noteNo = 0; 
                this.blocks = []; 
                this.interval = setInterval(component.updateGameArea, 20); //alterado
            },
            clear : function() {
                this.context.clearRect(0, 0, this.canvas.current.width, this.canvas.current.height); //alterado
            },
            stop  : function() {
                clearInterval(this.interval);
            }    
        }
    }

    updateGameArea() {

        const cheatSheet = this.props.cheatSheet;
        const gameArea = this.gameArea;

        gameArea.clear();
        gameArea.frameNo += 1;

        let currNote = gameArea.noteNo;
        let blocks = gameArea.blocks;

        if (currNote < cheatSheet.length) {
            let noteFrame = cheatSheet[currNote].frameNo;
            let currFrame = gameArea.frameNo;

            if (noteFrame === currFrame) {
                blocks.push(new Block(gameArea));
                gameArea.noteNo += 1;
            }
        }

        for (let i = 0; i < blocks.length; i++) {
            blocks[i].y += 1;
            blocks[i].update();
        }

    }

    componentDidMount() {
        const { gameArea } = this;
        gameArea.start();
    }

    render() {
        return <canvas ref={this.gameArea.canvas} width={300} height={300} />;
    }
}


  ReactDOM.render(<CanvasComponent cheatSheet={cheatSheet} />, document.getElementById('root'));
canvas {
    border:1px solid #d3d3d3;
    background-color: #f1f1f1;
}
<!DOCTYPE html>
<html>
<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
</head>
<body>
  <div id="root"></div>
</body>
</html>

I commented on all the lines I added and changed.

Transformation in the World React

I am not going to make the transformation that is too laborious, but needs to change the following concepts:

  • Status - you are not using status. At least these 3 values:

      this.frameNo = 0;
      this.noteNo = 0; 
      this.blocks = []; 
    

    They should be in the state of the component placed in this.state in the builder. Then I would update them at every interval with setState.

  • The creation of objects within objects that have no meaning since there are components precisely to subdivide logic. In that sense I should create a component Block to have the logic that each block will have. You can even create more components if you want but at least these 2 make sense. And the logic that you have in the object gameArea shall pass to a component, whether it is the CanvasComponent or another.

  • The rendering of the blocks in the canvas should be done in the ComponentDidUpdate as a consequence of state change. This is the point that is equal to such useEffect Talking Hook.

  • The timer should be cleaned on ComponentWillUnmount, ensuring that it has no resource leak and that it does not continue to run even after the component is destroyed. This is a point that it is normal to fail and that in real scenarios can bring serious problems.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.