Javascript - Object-oriented Canvas Game With Requestanimationframe
Solution 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
andlowerBoxHitTest
. - 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(newOption(buttonType, buttonTypeIndex[buttonType]));
});
btnTypeSelectElem.options.selectedIndex = 1; // set to page source language's codeconst initialButtonType = buttonRanges[Object.keys(buttonRanges)[btnTypeSelectElem.options.selectedIndex]];
classGame {
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 containerthis.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 selectionthis.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;
} elseif (this.sideButtons.lowerHoverBoxHitTest(this.hoverX, this.hoverY)) {
game.overDist = game.hoverScrollZoneSize - (game.footer.top - this.hoverY);
this.overBox = this.overTypes.raise;
} else {
game.overDist = 0this.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;
classBoard {
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 = newBoard();
}
initiateSideButtons() {
const game = this;
classButtonBar {
constructor(text) {
this.text = text;
this.left = 0;
this.top = 0;
this.width = 1;
this.height = 1;
this.selected = false;
}
hitTest(x, y) {
returnthis.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);
}
}
classSideButtons {
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 herefor (let buttonIndex = 0; buttonIndex < 10; buttonIndex++) {
this.buttons.push(newButtonBar(`Button ${game.buttonRange[buttonIndex]}`));
}
}
}
this.sideButtons = newSideButtons();
// note: fix an out-of-index bug herefor (let buttonIndex = 0; buttonIndex < 10; buttonIndex++) {
this.sideButtons.buttons.push(newButtonBar(`Button ${game.buttonRange[buttonIndex]}`));
}
}
initiateTitle() {
classTitle {
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 = newTitle('Test', this.width, this.height / 10);
}
initiateFooter() {
classFooter {
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 = newFooter();
}
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 behaviorthis.hoverScrollZoneSize = this.height * (100 / 500);
this.render();
}
}
const game = newGame('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
});
functionmainLoop() {
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><htmllang="en"><body><divid='game'></div><divclass="styled-select"><selectid="languageSelection"></select></div><scripttype='text/javascript'src='game.js'></script></body></html>
Solution 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] = newOption(buttonType, buttonTypeIndex[buttonType]);
}, buttonRanges);
buttonTypeSelection.options.selectedIndex = 1; // set to page source language's code
initialButtonType=buttonRanges[Object.keys(buttonRanges)[buttonTypeSelection.options.selectedIndex]];
functionGame (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 selectionthis.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;
varBoard = 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 = newBoard();
};
Game.prototype.initialSideButtons = function(){
var game = this;
varButtonBar =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);
};
varSideButtons = 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 = newSideButtons();
for (var buttonNumber=1; buttonNumber<=10; buttonNumber++) {
this.sideButtons.buttons.push(newButtonBar('Button '+buttonNumber));
}
};
Game.prototype.initialTitle = function(){
varTitle = 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 = newTitle('Test',this.width,this.height / 10);
}
Game.prototype.initialFooter = function(){
varFooter = 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 = newFooter();
};
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 = newGame('game',window.innerWidth -50,window.innerWidth * 2/3);
window.addEventListener('resize', function(){
game.resize(window.innerWidth -50,window.innerWidth * 2/3);
});
<!doctype html><htmllang="en"><body><divid='game'></div><divclass="styled-select"><selectid="languageSelection"></select></div><scripttype='text/javascript'src='scaleStack.js'></script></body></html>
Post a Comment for "Javascript - Object-oriented Canvas Game With Requestanimationframe"