Javascript - Object-Oriented Canvas Game with requ

2019-08-02 04:06发布

问题:

I am trying to integrate the code in this answer (run snippet in question's answer to see an example) with the rest of the script below to allow the user to scroll down the sideButtons selection menu by hovering in the bottom or top areas of the selection menu. However, I'm not sure how to write the requestAnimationFrame function for it to work with the rest of the object structure or where to place it.

Attached to the sideButtons' mouseMove eventListener function are two hitTest's: 'lowerHoverBoxHitTest(x, y)' and 'upperHoverBoxHitTest(x, y)'. These detect if the top or bottom half of the selection menu is being hovered over. If so, hoverAmount should be incremented accordingly such that sideButtons is pushed up or down depending on which hoverBox is selected. However, this part - which must (I think) occur within the requestAnimationFrame function - is not working in the code above.

If it is still not clear how the animation should work please see the attached link above. It should be clear that it is not currently working properly... Any help will be much appreciated.

var buttonTypeSelection = document.getElementById('languageSelection');

var initialButtonType;
var buttonRanges = {'1-10': [1,2,3,4,5,6,7,8,9,10],
                    'One to Ten': ['One','Two','Three','Four','Five',
                                   'Six','Seven','Eight','Nine','Ten'],
                    '0000-1010': ['0001','0010','0011','0100','0101',
                                  '0110','0111','1000','1001','1010']};
var buttonTypeIndex = {'1-10': 1, 'One to Ten': 2, '0000-1010': 3};
Object.keys(buttonRanges).forEach(function(buttonType) {
  buttonTypeSelection.options[buttonTypeSelection.options.length] = new Option(buttonType, buttonTypeIndex[buttonType]);
}, buttonRanges);

buttonTypeSelection.options.selectedIndex = 1; // set to page source language's code
initialButtonType=buttonRanges[Object.keys(buttonRanges)[buttonTypeSelection.options.selectedIndex]];

function Game (elementID,width,height){
	this.elementID = elementID;
	this.element   = document.getElementById(elementID);
	this.width = width;
	this.height = height;

	this.palette = {
		color1:'#fff',
		color2:'#000',
		color3:'#9F3A9B',
		color4:'#a84ea5',
		color5:'#b56ab2',
		color6:'#bf7dbd',
		color7:'#d5a8d2'
	};

	this.element.style.width = width + 'px';
	this.element.style.height= height + 'px';
	this.element.style.border='solid thin ' + this.palette.color2;
	this.element.style.display= 'block';
	//this.element.style.margin='1em auto';
	this.element.style.background=this.palette.color3;

  this.buttonType=buttonRanges[buttonTypeSelection.options[buttonTypeSelection.selectedIndex].text];

  this.hoverAmount = 0;
  this.overTypes = {none:0, lower:1, raise:2}
  this.overBox = 0;
  this.overDist = 0;

	this.initialGame();
}

Game.prototype.initialGame = function(){
	this.canvas  = document.createElement("canvas");
	this.canvas.width  =  this.width;
	this.canvas.height =  this.height;
	this.element.appendChild(this.canvas);

    this.initialSideButtons();
	this.initialTitle();
	this.initialBoard();
	this.initialFooter();

  // initial selection
  this.sideButtons.select(this.sideButtons.buttons[0]);

	this.resize(this.width,this.height);
	this.render();
	this.attachEvents();
}

Game.prototype.attachEvents = function(){
	var element = this.element;

	var getX = function(evt){return evt.offsetX || evt.layerX || (evt.clientX - element.offsetLeft);};
	var getY = function(evt){return evt.offsetY || evt.layerY || (evt.clientY - element.offsetTop);};

	var game = this;
	this.element.addEventListener('mousemove',function(evt){
		game.hover(getX(evt),getY(evt));
    if (game.sideButtons.lowerHoverBoxHitTest(game.hoverX, game.hoverY)) {
			game.overBox=game.overTypes.raise;
		} else if (game.sideButtons.upperHoverBoxHitTest(game.hoverX, game.hoverY)) {
			game.overBox=game.overTypes.lower;
		} else {
			game.overBox=game.overTypes.none;
		}
		game.render();
	});

	this.element.addEventListener('click',function(evt){
		game.sideButtons.click();
		game.render();
	});
}

Game.prototype.onSelect = function(button){
	this.selected = button;
};

Game.prototype.hover=function(x,y){
	this.hoverX = x;
	this.hoverY = y;
};

Game.prototype.initialBoard = function(){
	var game = this;
	var Board = function(){
		this.left   = 0;
		this.top    = 0;
		this.width  = 0;
		this.height = 0;
	};

	Board.prototype.render = function(ctx){
		if(game.selected){

			var shapeWidth = this.width/3;

			ctx.fillStyle = game.palette.color1;
			ctx.strokeStyle = game.palette.color1;
			var fontSize =  14;
			ctx.font = 'bold '+ fontSize +'px Noto Sans';
			ctx.textAlign='center';
			ctx.lineWidth=8;
			ctx.lineJoin = 'round';
			ctx.strokeRect(this.left + this.width/2 - (shapeWidth/2),this.height/2-(shapeWidth/2) + this.top,shapeWidth,shapeWidth);
			ctx.fillText(game.selected.text,this.left + this.width/2,this.height/2 + this.top );
		}
	};

	this.board =  new Board();
};

Game.prototype.initialSideButtons = function(){
	var game = this;
	var ButtonBar =function(text){
		this.text = text;
		this.left = 0;
		this.top  = 0;
		this.width = 1;
		this.height= 1;
		this.selected=false;
	};

	ButtonBar.prototype.hitTest=function(x,y){
		return 	(this.left < x) && (x < (this.left + this.width)) &&
				(this.top <y) && (y < (this.top + this.height));
	};

	ButtonBar.prototype.getColor=function(){
		var hovered = this.hitTest(game.hoverX,game.hoverY);

		if(this.selected){
			if(hovered)
			{
				return game.palette.color7;
			}
			return game.palette.color6;
		}

		if(hovered){
			return game.palette.color5;
		}
		return game.palette.color4;
	};

	ButtonBar.prototype.render = function(ctx){
		var fontSize = 14;
		ctx.fillStyle = this.getColor();
		ctx.fillRect(this.left, this.top, this.width, this.height);
		ctx.fillStyle = game.palette.color1;
		ctx.textAlign = 'left';
		ctx.font ='bold '+ fontSize +'px Noto Sans';
		ctx.fillText(this.text,this.left + 10,this.top+ this.height/2);
	};

	var SideButtons = function(){
		this.buttons = [];
		this.width = 1;
		this.height= 1;
		this.left=1;
		this.top=1;
	};

  SideButtons.prototype.lowerHoverBoxHitTest = function(x, y) {
    game.overDist = y - (game.title.height + game.footer.top) - game.hoverScrollSize;
    return (x >= this.width) && (x <= game.width) &&
    (y >= ((game.title.height + game.footer.top) - game.hoverScrollSize)) && (y <= (game.title.height + game.footer.top));
  }

  SideButtons.prototype.upperHoverBoxHitTest = function(x, y) {
    game.overDist = game.hoverScrollSize - y;
    return (x>=this.width) && (x <= game.width) &&
    (y >= game.title.height) && (y <= (game.title.height+game.hoverScrollSize));
  }

	SideButtons.prototype.render = function(ctx){
		if(!this.buttons.length){
			return;
		}

		var height = (this.height / this.buttons.length)/0.45;
		for(var i=0;i<this.buttons.length;i++){
			var btn = this.buttons[i];
			btn.left = this.left;
			btn.top = i * height + this.top;
			btn.width = this.width;
			btn.height = height;
			this.buttons[i].render(ctx);
		}
	};

	SideButtons.prototype.click = function() {
    var current = null;
		for(var i=0;i<this.buttons.length;i++){
			var btn = this.buttons[i];
      if(btn.hitTest(game.hoverX,game.hoverY)) {
				this.select(btn);
        break;
			}
		}
	};

  SideButtons.prototype.select = function(btn) {
    for(var i=0; i<this.buttons.length; i++) {
      this.buttons[i].selected = false;
    }
    btn.selected=true;
    game.onSelect(btn);
  };

  SideButtons.prototype.refreshShapes = function() {
    this.buttons = [];
    for (var buttonIndex=1; buttonIndex<=10; buttonIndex++) {
      this.buttons.push(new ButtonBar('Button ' + game.buttonType[buttonIndex]));
    }
  }

	this.sideButtons = new SideButtons();

  for (var buttonIndex=1; buttonIndex<=10; buttonIndex++) {
    this.sideButtons.buttons.push(new ButtonBar('Button ' + game.buttonType[buttonIndex]));
  }
};

Game.prototype.initialTitle = function(){
	var Title = function(value,width,height){
		this.value=value;
		this.width = width;
		this.height= height;
	};

	var game = this;
	Title.prototype.render=function(ctx){
		var k = 2;
		var fontSize =  this.height / k;
		ctx.fillStyle=game.palette.color1;
		ctx.fillRect(0,0,this.width,this.height);
		ctx.font='bold '+ fontSize +'px Noto Sans'; // check
		ctx.fillStyle=game.palette.color3;
		ctx.textAlign='center';
		ctx.fillText(this.value,this.width/2,this.height - fontSize/2);

	};

	this.title = new Title('Test',this.width,this.height / 10);
}

Game.prototype.initialFooter = function(){
	var Footer = function(){
		this.width = 1;
		this.height= 1;
		this.left=0;
		this.top=0;
	}
	var game = this;
	Footer.prototype.render = function(ctx){
		ctx.fillStyle =  game.palette.color5;
		ctx.fillRect(this.left,this.top,this.width,this.height);
	};

	this.footer = new Footer();
};

Game.prototype.resetCanvas = function() {
	this.canvas.width  =  this.width;
	this.canvas.height =  this.height;
};

Game.prototype.render = function () {
   var that = this;
   that._render();
}

Game.prototype._render = function() {
	this.resetCanvas();

	var context = this.canvas.getContext('2d');

    this.sideButtons.render(context);
	this.title.render(context);
	this.board.render(context);
	this.footer.render(context);

};

Game.prototype.resize =  function (width,height){
	this.width = width;
	this.height= height;

	this.element.style.width = width + 'px';
	this.element.style.height= height+ 'px';

	this.title.height = this.height / 14;
	this.title.width   = this.width;

	this.footer.height = this.title.height;
	this.footer.width  = this.width;
	this.footer.top = this.height - this.footer.height;
	this.footer.left = 0;

	this.board.top   = this.title.height;
	this.board.left  = 0;
	this.board.width = this.width / 2;
	this.board.height= this.height - this.title.height - this.footer.height;

	this.sideButtons.left= this.board.width;
	this.sideButtons.top = this.board.top + this.hoverAmount;
	this.sideButtons.width = this.width - this.board.width;
	this.sideButtons.height = this.board.height;

	this.maxSpeed = this.height*(5/500);
	this.shapeSize = this.height*(30/500);
	this.hoverScrollSize = this.height*(100/500);

	this.render();
};


var game = new Game('game',window.innerWidth -50,window.innerWidth * 2/3);

window.addEventListener('resize', function(){
	game.resize(window.innerWidth -50,window.innerWidth * 2/3);
});

buttonTypeSelection.addEventListener('change', function() {
  game.buttonType=buttonRanges[buttonTypeSelection.options[buttonTypeSelection.selectedIndex].text];
  var selectedIndex = game.sideButtons.buttons.indexOf(game.selected);
  game.sideButtons.refreshShapes();
  game.selected = game.sideButtons.buttons[selectedIndex];
  game.render();
});

requestAnimationFrame(() => {
  game.resize(window.innerWidth - 50, window.innerWidth * 2/3);
  requestAnimationFrame(mainLoop); // start main loop
});

function mainLoop() {
  if (game.overBox !== game.overTypes.none) {
    game.hoverAmount += game.overDist/game.hoverScrollSize * (game.overBox === game.overTypes.lower ? game.maxSpeed : -game.maxSpeed);
    var bottom = (game.height - (game.title.height + game.footer.height) + (game.sideButtons.buttons.length * game.shapeSize));

    // game.hoverAmount = (game.hoverAmount > 0) ? 0 : (game.hoverAmount < bottom) ? bottom : game.hoverAmount;
    game.resize(window.innerWidth - 50, window.innerWidth * 2/3);
  }
  requestAnimationFrame(mainLoop);
}
<!doctype html>
<html lang="en">
<body>
	<div id='game'></div>
	<div class="styled-select">
		<select id="languageSelection"></select>
	</div>
  <script type='text/javascript' src='scaleStack.js'></script>
</body>
</html>

回答1:

Why it's not working?

It's not about requestionAnimationFrame, but about your logic to calculate the scrolling(the scroll offset, the hitness for the hit zone).

  • You must check your logic for calculation of the upperBoxHitTest and lowerBoxHitTest.
  • Of course, your calculation inside mainloop is full of problem.
  • And you must be aware of out-of-index iteration inside your code.

Hint for your code style

You can't just copy a code snippet, do some simple replacements and hope it works properly. You should figure out how it works, its internal logic and you won't be afraid of push it forward with more complex implementation.

So I suggest you check your code again, and try to find out what wrong with your code. Until you make some progress or you actually can't work it out, then you may check out how my code works. If the later situation, you may have to read more books about logical thinking, problem analysis and methodology of programming.

Good Luck!

Altered code

code snippets iframe area of stackoverflow is really small, you should check out here instead https://jsbin.com/bucisupugu/edit?js,output.

const btnTypeSelectElem = document.getElementById('languageSelection');

const buttonRanges = {
    '1-10': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
    'One to Ten': ['One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten'],
    '0000-1010': ['0001', '0010', '0011', '0100', '0101', '0110', '0111', '1000', '1001', '1010']
};
const buttonTypeIndex = {
    '1-10': 1,
    'One to Ten': 2,
    '0000-1010': 3
};

Object.keys(buttonRanges)
    .forEach(function (buttonType) {
        btnTypeSelectElem.add(new Option(buttonType, buttonTypeIndex[buttonType]));
    });

btnTypeSelectElem.options.selectedIndex = 1; // set to page source language's code
const initialButtonType = buttonRanges[Object.keys(buttonRanges)[btnTypeSelectElem.options.selectedIndex]];

class Game {
    constructor(elementID, width, height) {
        this.elementID = elementID;
        this.element = document.getElementById(elementID);
        this.width = width;
        this.height = height;

        this.palette = {
            color1: '#fff',
            color2: '#000',
            color3: '#9F3A9B',
            color4: '#a84ea5',
            color5: '#b56ab2',
            color6: '#bf7dbd',
            color7: '#d5a8d2'
        };

        this.element.style.width = `${width}px`;
        this.element.style.height = `${height}px`;
        this.element.style.border = `solid thin ${this.palette.color2}`;
        this.element.style.display = 'block';
        //this.element.style.margin='1em auto';
        this.element.style.background = this.palette.color3;

        this.buttonRange = buttonRanges[btnTypeSelectElem.options[btnTypeSelectElem.selectedIndex].text];

        this.scrollTop = 0;
        this.overTypes = {
            none: 0,
            lower: 1,
            raise: 2
        };
        this.overBox = 0;

        // overDist have different meanings for upper box and lower box
        // for upper: y offset to the top of hover scroll zone
        // for lower: y offset to the bottom of hover scroll zone
        // and in fact it's actually for sidebuttons container, coz the sidebuttons is
        // the simulated scroll container
        this.overDist = 0;

        this.initiateGame();
    }

    initiateGame() {
        this.canvas = document.createElement("canvas");
        this.canvas.width = this.width;
        this.canvas.height = this.height;
        this.element.appendChild(this.canvas);

        this.initiateSideButtons();
        this.initiateTitle();
        this.initiateBoard();
        this.initiateFooter();

        // initial selection
        this.sideButtons.select(this.sideButtons.buttons[0]);

        this.resize(this.width, this.height);
        this.render();
        this.attachEvents();
    }

    attachEvents() {
        const element = this.element;

        const getX = function (evt) {
            return evt.offsetX || evt.layerX || evt.clientX - element.offsetLeft;
        };
        const getY = function (evt) {
            return evt.offsetY || evt.layerY || evt.clientY - element.offsetTop;
        };

        this.element.addEventListener('mousemove', (evt) => {
            this.hover(getX(evt), getY(evt));
            if (this.sideButtons.upperHoverBoxHitTest(this.hoverX, this.hoverY)) {
                game.overDist = game.hoverScrollZoneSize - (this.hoverY - game.title.height);
                this.overBox = this.overTypes.lower;
            } else if (this.sideButtons.lowerHoverBoxHitTest(this.hoverX, this.hoverY)) {
                game.overDist = game.hoverScrollZoneSize - (game.footer.top - this.hoverY);
                this.overBox = this.overTypes.raise;
            } else {
                game.overDist = 0
                this.overBox = this.overTypes.none;
            }
            this.render();
        });

        this.element.addEventListener('click', (evt) => {
            this.sideButtons.click();
            this.render();
        });
    }

    onSelect(button) {
        this.selected = button;
    }

    hover(x, y) {
        this.hoverX = x;
        this.hoverY = y;
    }

    initiateBoard() {
        const game = this;

        class Board {
            constructor() {
                this.left = 0;
                this.top = 0;
                this.width = 0;
                this.height = 0;
            }

            render(ctx) {
                if (game.selected) {

                    const shapeWidth = this.width / 3;

                    ctx.fillStyle = game.palette.color1;
                    ctx.strokeStyle = game.palette.color1;
                    const fontSize = 14;
                    ctx.font = `bold ${fontSize}px Noto Sans`;
                    ctx.textAlign = 'center';
                    ctx.lineWidth = 8;
                    ctx.lineJoin = 'round';
                    ctx.strokeRect(this.left + this.width / 2 - shapeWidth / 2, this.height / 2 - shapeWidth / 2 + this.top, shapeWidth, shapeWidth);
                    ctx.fillText(game.selected.text, this.left + this.width / 2, this.height / 2 + this.top);
                }
            }
        }

        this.board = new Board();
    }

    initiateSideButtons() {
        const game = this;

        class ButtonBar {
            constructor(text) {
                this.text = text;
                this.left = 0;
                this.top = 0;
                this.width = 1;
                this.height = 1;
                this.selected = false;
            }

            hitTest(x, y) {
                return this.left < x &&
                    x < this.left + this.width &&
                    this.top < y &&
                    y < this.top + this.height;
            }

            getColor() {
                const hovered = this.hitTest(game.hoverX, game.hoverY);

                if (this.selected) {
                    if (hovered) {
                        return game.palette.color7;
                    }
                    return game.palette.color6;
                }

                if (hovered) {
                    return game.palette.color5;
                }
                return game.palette.color4;
            }

            render(ctx) {
                const fontSize = 14;
                ctx.fillStyle = this.getColor();
                ctx.fillRect(this.left, this.top, this.width, this.height);
                ctx.fillStyle = game.palette.color1;
                ctx.textAlign = 'left';
                ctx.font = `bold ${fontSize}px Noto Sans`;
                ctx.fillText(this.text, this.left + 10, this.top + this.height / 2);
            }
        }

        class SideButtons {
            constructor() {
                this.buttons = [];
                this.width = 1;
                this.height = 1;
                this.left = 1;
                this.top = 1;
            }

            upperHoverBoxHitTest(x, y) {
                return x >= this.left &&
                    x <= this.left + this.width &&
                    y >= game.title.height &&
                    y <= game.title.height + game.hoverScrollZoneSize;
            }

            lowerHoverBoxHitTest(x, y) {
                return x >= this.left &&
                    x <= this.left + this.width &&
                    y >= game.footer.top - game.hoverScrollZoneSize &&
                    y <= game.footer.top;
            }

            render(ctx) {
                if (!this.buttons.length) {
                    return;
                }

                const height = this.height / this.buttons.length / 0.45;
                for (let i = 0; i < this.buttons.length; i++) {
                    const btn = this.buttons[i];
                    btn.left = this.left;
                    btn.top = i * height + this.top;
                    btn.width = this.width;
                    btn.height = height;
                    this.buttons[i].render(ctx);
                }
            }

            click() {
                const current = null;
                for (let i = 0; i < this.buttons.length; i++) {
                    const btn = this.buttons[i];
                    if (btn.hitTest(game.hoverX, game.hoverY)) {
                        this.select(btn);
                        break;
                    }
                }
            }

            select(btn) {
                for (let i = 0; i < this.buttons.length; i++) {
                    this.buttons[i].selected = false;
                }
                btn.selected = true;
                game.onSelect(btn);
            }

            refreshShapes() {
                this.buttons = [];
                // note: fix an out-of-index bug here
                for (let buttonIndex = 0; buttonIndex < 10; buttonIndex++) {
                    this.buttons.push(new ButtonBar(`Button ${game.buttonRange[buttonIndex]}`));
                }
            }
        }

        this.sideButtons = new SideButtons();

        // note: fix an out-of-index bug here
        for (let buttonIndex = 0; buttonIndex < 10; buttonIndex++) {
            this.sideButtons.buttons.push(new ButtonBar(`Button ${game.buttonRange[buttonIndex]}`));
        }
    }

    initiateTitle() {
        class Title {
            constructor(value, width, height) {
                this.value = value;
                this.width = width;
                this.height = height;
            }

            render(ctx) {
                const k = 2;
                const fontSize = this.height / k;
                ctx.fillStyle = game.palette.color1;
                ctx.fillRect(0, 0, this.width, this.height);
                ctx.font = `bold ${fontSize}px Noto Sans`; // check
                ctx.fillStyle = game.palette.color3;
                ctx.textAlign = 'center';
                ctx.fillText(this.value, this.width / 2, this.height - fontSize / 2);
            }
        }

        const game = this;

        this.title = new Title('Test', this.width, this.height / 10);
    }

    initiateFooter() {
        class Footer {
            constructor() {
                this.width = 1;
                this.height = 1;
                this.left = 0;
                this.top = 0;
            }

            render(ctx) {
                ctx.fillStyle = game.palette.color5;
                ctx.fillRect(this.left, this.top, this.width, this.height);
            }
        }

        const game = this;

        this.footer = new Footer();
    }

    resetCanvas() {
        this.canvas.width = this.width;
        this.canvas.height = this.height;
    }

    render() {
        const that = this;
        that._render();
    }

    _render() {
        this.resetCanvas();

        const context = this.canvas.getContext('2d');

        this.sideButtons.render(context);
        this.title.render(context);
        this.board.render(context);
        this.footer.render(context);
    }

    resize(width, height) {
        this.width = width;
        this.height = height;

        this.element.style.width = `${width}px`;
        this.element.style.height = `${height}px`;

        this.title.height = this.height / 14;
        this.title.width = this.width;

        this.footer.height = this.title.height;
        this.footer.width = this.width;
        this.footer.top = this.height - this.footer.height;
        this.footer.left = 0;

        this.board.top = this.title.height;
        this.board.left = 0;
        this.board.width = this.width / 2;
        this.board.height = this.height - this.title.height - this.footer.height;

        this.sideButtons.left = this.board.width;
        this.sideButtons.top = this.board.top + this.scrollTop;
        this.sideButtons.width = this.width - this.board.width;
        this.sideButtons.height = this.board.height;

        this.maxSpeed = this.height * (5 / 500);
        this.shapeSize = this.height * (30 / 500);
        // hover scroll zone is that area when mouse hovers on it will trigger scrolling behavior
        this.hoverScrollZoneSize = this.height * (100 / 500);

        this.render();
    }
}

const game = new Game('game', window.innerWidth - 50, window.innerWidth * 2 / 3);

window.addEventListener('resize', function () {
    game.resize(window.innerWidth - 50, window.innerWidth * 2 / 3);
});

btnTypeSelectElem.addEventListener('change', function () {
    game.buttonRange = buttonRanges[btnTypeSelectElem.options[btnTypeSelectElem.selectedIndex].text];
    const selectedIndex = game.sideButtons.buttons.indexOf(game.selected);
    game.sideButtons.refreshShapes();
    game.selected = game.sideButtons.buttons[selectedIndex];
    game.render();
});

requestAnimationFrame(() => {
    game.resize(window.innerWidth - 50, window.innerWidth * 2 / 3);
    requestAnimationFrame(mainLoop); // start main loop
});

function mainLoop() {
    if (game.overBox !== game.overTypes.none) {
        game.scrollTop += game.overDist / game.hoverScrollZoneSize * (game.overBox === game.overTypes.lower ? game.maxSpeed : -game.maxSpeed);
        const bottom = -game.sideButtons.height;

        game.scrollTop = (game.scrollTop > 0) ? 0 : (game.scrollTop < bottom) ? bottom : game.scrollTop;
        game.resize(window.innerWidth - 50, window.innerWidth * 2 / 3);
    }
    requestAnimationFrame(mainLoop);
}
<!doctype html>
<html lang="en">

<body>
    <div id='game'></div>
    <div class="styled-select">
        <select id="languageSelection"></select>
    </div>
    <script type='text/javascript' src='game.js'></script>
</body>

</html>



回答2:

move Game.render method's body into Game._render private method and call the _render method inside of the render method with requestAnimationFrame.

var buttonTypeSelection = document.getElementById('languageSelection');

var initialButtonType;
var buttonRanges = {'1-10': [1,2,3,4,5,6,7,8,9,10],
                    'One to Ten': ['One','Two','Three','Four','Five',
                                   'Six','Seven','Eight','Nine','Ten'],
                    '0000-1010': ['0001','0010','0011','0100','0101',
                                  '0110','0111','1000','1001','1010']};
var buttonTypeIndex = {'1-10': 1, 'One to Ten': 2, '0000-1010': 3};
Object.keys(buttonRanges).forEach(function(buttonType) {
  buttonTypeSelection.options[buttonTypeSelection.options.length] = new Option(buttonType, buttonTypeIndex[buttonType]);
}, buttonRanges);

buttonTypeSelection.options.selectedIndex = 1; // set to page source language's code
initialButtonType=buttonRanges[Object.keys(buttonRanges)[buttonTypeSelection.options.selectedIndex]];

function Game (elementID,width,height){
	this.elementID = elementID;
	this.element   = document.getElementById(elementID);
	this.width = width;
	this.height = height;

	this.palette = {
		color1:'#fff',
		color2:'#000',
		color3:'#9F3A9B',
		color4:'#a84ea5',
		color5:'#b56ab2',
		color6:'#bf7dbd',
		color7:'#d5a8d2'
	};

	this.element.style.width = width + 'px';
	this.element.style.height= height + 'px';
	this.element.style.border='solid thin ' + this.palette.color2;
	this.element.style.display= 'block';
	//this.element.style.margin='1em auto';
	this.element.style.background=this.palette.color3;


	this.initialGame();
}

Game.prototype.initialGame = function(){
	this.canvas  = document.createElement("canvas");
	this.canvas.width  =  this.width;
	this.canvas.height =  this.height;
	this.element.appendChild(this.canvas);

	this.initialTitle();
	this.initialSideButtons();
	this.initialBoard();
	this.initialFooter();

    // initial selection
    this.sideButtons.select(this.sideButtons.buttons[0]);

	this.resize(this.width,this.height);
	this.render();
	this.attachEvents();
}

Game.prototype.attachEvents = function(){
	var element = this.element;

	var getX = function(evt){return evt.offsetX || evt.layerX || (evt.clientX - element.offsetLeft);};
	var getY = function(evt){return evt.offsetY || evt.layerY || (evt.clientY - element.offsetTop);};

	var game = this;
	this.element.addEventListener('mousemove',function(evt){
		game.hover(getX(evt),getY(evt));
		game.render();
	});

	this.element.addEventListener('click',function(evt){
		game.sideButtons.click();
		game.render();
	});
}

Game.prototype.onSelect = function(button){
	this.selected = button;
};

Game.prototype.hover=function(x,y){
	this.hoverX = x;
	this.hoverY = y;
};

Game.prototype.initialBoard = function(){
	var game = this;
	var Board = function(){
		this.left = 0;
		this.top  = 0;
		this.width =0;
		this.height=0;
	};

	Board.prototype.render = function(ctx){
		if(game.selected){

			var shapeWidth = this.width/3;

			ctx.fillStyle = game.palette.color1;
			ctx.strokeStyle = game.palette.color1;
			var fontSize =  14;
			ctx.font = 'bold '+ fontSize +'px Noto Sans';
			ctx.textAlign='center';
			ctx.lineWidth=8;
			ctx.lineJoin = 'round';
			ctx.strokeRect(this.left + this.width/2 - (shapeWidth/2),this.height/2-(shapeWidth/2) + this.top,shapeWidth,shapeWidth);
			ctx.fillText(game.selected.text,this.left + this.width/2,this.height/2 + this.top );
		}
	};

	this.board =  new Board();
};

Game.prototype.initialSideButtons = function(){
	var game = this;
	var ButtonBar =function(text){
		this.text = text;
		this.left = 0;
		this.top  = 0;
		this.width = 1;
		this.height= 1;
		this.selected=false;
	};

	ButtonBar.prototype.hitTest=function(x,y){
		return 	(this.left < x) && (x < (this.left + this.width)) &&
				(this.top <y) && (y < (this.top + this.height));
	};

	ButtonBar.prototype.getColor=function(){
		var hovered = this.hitTest(game.hoverX,game.hoverY);

		if(this.selected){
			if(hovered)
			{
				return game.palette.color7;
			}
			return game.palette.color6;
		}

		if(hovered){
			return game.palette.color5;
		}
		return game.palette.color4;
	};

	ButtonBar.prototype.render = function(ctx){
		var fontSize = 14;
		ctx.fillStyle = this.getColor();
		ctx.fillRect(this.left,this.top,this.width,this.height);
		ctx.fillStyle = game.palette.color1;
		ctx.textAlign = 'left';
		ctx.font ='bold '+ fontSize +'px Noto Sans';
		ctx.fillText(this.text,this.left + 10,this.top+ this.height/2);
	};

	var SideButtons = function(){
		this.buttons = [];
		this.width = 1;
		this.height= 1;
		this.left=1;
		this.top=1;
	};

	SideButtons.prototype.render = function(ctx){
		if(!this.buttons.length){
			return;
		}

		var height = (this.height / this.buttons.length)/0.45;
		for(var i=0;i<this.buttons.length;i++){
			var btn = this.buttons[i];
			btn.left = this.left;
			btn.top = i * height + this.top;
			btn.width = this.width;
			btn.height = height;
			this.buttons[i].render(ctx);
		}
	};

	SideButtons.prototype.click = function(){
            var current = null;
		for(var i=0;i<this.buttons.length;i++){
			var btn = this.buttons[i];
                    if(  btn.hitTest(game.hoverX,game.hoverY))
                     {
				this.select(btn);
                            break;
			 }
		}
	};

    SideButtons.prototype.select = function(btn)
    {
       for(var i=0;i<this.buttons.length;i++)
       {
          this.buttons[i].selected = false;
       }
       btn.selected=true;
       game.onSelect(btn);
    };

	this.sideButtons = new SideButtons();

  for (var buttonNumber=1; buttonNumber<=10; buttonNumber++) {
    this.sideButtons.buttons.push(new ButtonBar('Button '+buttonNumber));
  }

};

Game.prototype.initialTitle = function(){
	var Title = function(value,width,height){
		this.value=value;
		this.width = width;
		this.height= height;
	};

	var game = this;
	Title.prototype.render=function(ctx){
		var k = 2;
		var fontSize =  this.height / k;
		ctx.fillStyle=game.palette.color1;
		ctx.fillRect(0,0,this.width,this.height);
		ctx.font='bold '+ fontSize +'px Noto Sans'; // check
		ctx.fillStyle=game.palette.color3;
		ctx.textAlign='center';
		ctx.fillText(this.value,this.width/2,this.height - fontSize/2);

	};

	this.title = new Title('Test',this.width,this.height / 10);
}

Game.prototype.initialFooter = function(){
	var Footer = function(){
		this.width = 1;
		this.height= 1;
		this.left=0;
		this.top=0;
	}
	var game = this;
	Footer.prototype.render = function(ctx){
		ctx.fillStyle =  game.palette.color5;
		ctx.fillRect(this.left,this.top,this.width,this.height);
	};

	this.footer = new Footer();
};

Game.prototype.resetCanvas = function(){
	this.canvas.width  =  this.width;
	this.canvas.height =  this.height;
};

Game.prototype.render = function (){
   var that = this;
   requestAnimationFrame(function(){that._render();});
}

Game.prototype._render = function(){
	this.resetCanvas();

	var context = this.canvas.getContext('2d');

	this.title.render(context);
	this.sideButtons.render(context);
	this.board.render(context);
	this.footer.render(context);

};

Game.prototype.resize =  function (width,height){
	this.width = width;
	this.height= height;

	this.element.style.width = width + 'px';
	this.element.style.height= height+ 'px';

	this.title.height = this.height / 14;
	this.title.width   = this.width;

	this.footer.height = this.title.height;
	this.footer.width  = this.width;
	this.footer.top = this.height - this.footer.height;
	this.footer.left = 0;

	this.board.top   = this.title.height;
	this.board.left  = 0;
	this.board.width = this.width / 2;
	this.board.height= this.height - this.title.height - this.footer.height;

	this.sideButtons.left= this.board.width;
	this.sideButtons.top = this.board.top;
	this.sideButtons.width = this.width - this.board.width;
	this.sideButtons.height = this.board.height;

	this.render();
};


var game = new Game('game',window.innerWidth -50,window.innerWidth * 2/3);

window.addEventListener('resize', function(){
	game.resize(window.innerWidth -50,window.innerWidth * 2/3);
});
<!doctype html>
<html lang="en">
<body>
	<div id='game'></div>
	<div class="styled-select">
		<select id="languageSelection"></select>
	</div>
  <script type='text/javascript' src='scaleStack.js'></script>
</body>
</html>