Skip to content Skip to sidebar Skip to footer

Javascript - Object-oriented Canvas Game With Requestanimationframe

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 sideBut

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 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(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"