I create a game using javascript. The code is placed at the bottom of the page. Visualize it as if the game is below a footer. The game starts when the user selects the start game button. The mechanics of the game are controlled by the arrow key. Arrow keys are used to move up, right, and left down. The problem is when I press the up or down key, the page scrolls too. How to stop this? I want the page not to scroll when the game is active. I am attaching the code of the game I create. It is a snake game.There's CSS too, but I'm not attaching it as I don't think its relevant
<div id="app" class="app">
<div class="start-screen">
<h2>🦊 </h2>
<div class="options">
<h3>Choose Difficulty</h3>
<p class="end-score"></p>
<button data-difficulty="100" class="active">Easy</button>
<button data-difficulty="75">Medium</button>
<button data-difficulty="50">Hard</button>
</div>
<button class="play-btn">Play</button>
</div>
<canvas id="board"></canvas>
<div class="score">0</div>
</div>
<script>
class SnakeGame {
constructor() {
this.$app = document.querySelector('#app');
this.$canvas = this.$app.querySelector('canvas');
this.ctx = this.$canvas.getContext('2d');
this.$startScreen = this.$app.querySelector('.start-screen');
this.$score = this.$app.querySelector('.score');
this.settings = {
canvas: {
width: 500,
height: 500,
background: 'white',
border: 'black'
},
snake: {
size: 20,
background: '#73854A',
border: '#000'
}
};
this.game = {
// "direction" (set in setUpGame())
// "nextDirection" (set in setUpGame())
// "score" (set in setUpGame())
speed: 100,
keyCodes: {
38: 'up',
40: 'down',
39: 'right',
37: 'left'
}
}
this.soundEffects = {
score: new Audio('./sounds/score.mp3'),
gameOver: new Audio('./sounds/game-over.mp3')
};
this.setUpGame();
this.init();
}
init() {
// Choose difficulty
// Rather than using "this.$startScreen.querySelectorAll('button')" and looping over the node list
// and attaching seperate event listeners on each item, it's more efficient to just listen in on the container and run a check at runtime
this.$startScreen.querySelector('.options').addEventListener('click', event => {
this.chooseDifficulty(event.target.dataset.difficulty);
});
// Play
this.$startScreen.querySelector('.play-btn').addEventListener('click', () => {
this.startGame();
});
}
chooseDifficulty(difficulty) {
if(difficulty) {
this.game.speed = difficulty;
this.$startScreen.querySelectorAll('.options button').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
}
}
setUpGame() {
// The snake starts off with 5 pieces
// Each piece is 30x30 pixels
// Each following piece must be n times as far from the first piece
const x = 300;
const y = 300;
this.snake = [
{ x: x, y: y },
{ x: x - this.settings.snake.size, y: y },
{ x: x - (this.settings.snake.size * 2), y: y },
{ x: x - (this.settings.snake.size * 3), y: y },
{ x: x - (this.settings.snake.size * 4), y: y }
];
this.food = {
active: false,
background: '#EC5E0B',
border: '#73AA24',
coordinates: {
x: 0,
y: 0
}
};
this.game.score = 0;
this.game.direction = 'right';
this.game.nextDirection = 'right';
}
startGame() {
// Stop the game over sound effect if a new game was restarted quickly before it could end
this.soundEffects.gameOver.pause();
this.soundEffects.gameOver.currentTime = 0;
// Reset a few things from the prior game
this.$app.classList.add('game-in-progress');
this.$app.classList.remove('game-over');
this.$score.innerText = 0;
this.generateSnake();
this.startGameInterval = setInterval(() => {
if(!this.detectCollision()) {
this.generateSnake();
} else {
this.endGame();
}
}, this.game.speed);
// Change direction
document.addEventListener('keydown', event => {
this.changeDirection(event.keyCode);
});
}
changeDirection(keyCode) {
const validKeyPress = Object.keys(this.game.keyCodes).includes(keyCode.toString()); // Only allow (up|down|left|right)
if(validKeyPress && this.validateDirectionChange(this.game.keyCodes[keyCode], this.game.direction)) {
this.game.nextDirection = this.game.keyCodes[keyCode];
}
}
// When already moving in one direction snake shouldn't be allowed to move in the opposite direction
validateDirectionChange(keyPress, currentDirection) {
return (keyPress === 'left' && currentDirection !== 'right') ||
(keyPress === 'right' && currentDirection !== 'left') ||
(keyPress === 'up' && currentDirection !== 'down') ||
(keyPress === 'down' && currentDirection !== 'up');
}
resetCanvas() {
// Full screen size
this.$canvas.width = this.settings.canvas.width;
this.$canvas.height = this.settings.canvas.height;
this.$canvas.style.border = `3px solid ${this.settings.canvas.border}`;
// Background
this.ctx.fillStyle = this.settings.canvas.background;
this.ctx.fillRect(0, 0, this.$canvas.width, this.$canvas.height);
}
generateSnake() {
let coordinate;
switch(this.game.direction) {
case 'right':
coordinate = {
x: this.snake[0].x + this.settings.snake.size,
y: this.snake[0].y
};
break;
case 'up':
coordinate = {
x: this.snake[0].x,
y: this.snake[0].y - this.settings.snake.size
};
break;
case 'left':
coordinate = {
x: this.snake[0].x - this.settings.snake.size,
y: this.snake[0].y
};
break;
case 'down':
coordinate = {
x: this.snake[0].x,
y: this.snake[0].y + this.settings.snake.size
};
}
// The snake moves by adding a piece to the beginning "this.snake.unshift(coordinate)" and removing the last piece "this.snake.pop()"
// Except when it eats the food in which case there is no need to remove a piece and the added piece will make it grow
this.snake.unshift(coordinate);
this.resetCanvas();
const ateFood = this.snake[0].x === this.food.coordinates.x && this.snake[0].y === this.food.coordinates.y;
if(ateFood) {
this.food.active = false;
this.game.score += 10;
this.$score.innerText = this.game.score;
this.soundEffects.score.play();
} else {
this.snake.pop();
}
this.generateFood();
this.drawSnake();
}
drawSnake() {
const size = this.settings.snake.size;
this.ctx.fillStyle = this.settings.snake.background;
this.ctx.strokestyle = this.settings.snake.border;
// Draw each piece
this.snake.forEach(coordinate => {
this.ctx.fillRect(coordinate.x, coordinate.y, size, size);
this.ctx.strokeRect(coordinate.x, coordinate.y, size, size);
});
this.game.direction = this.game.nextDirection;
}
generateFood() {
// If there is uneaten food on the canvas there's no need to regenerate it
if(this.food.active) {
this.drawFood(this.food.coordinates.x, this.food.coordinates.y);
return;
}
const gridSize = this.settings.snake.size;
const xMax = this.settings.canvas.width - gridSize;
const yMax = this.settings.canvas.height - gridSize;
const x = Math.round((Math.random() * xMax) / gridSize) * gridSize;
const y = Math.round((Math.random() * yMax) / gridSize) * gridSize;
// Make sure the generated coordinates do not conflict with the snake's present location
// If so recall this method recursively to try again
this.snake.forEach(coordinate => {
const foodSnakeConflict = coordinate.x == x && coordinate.y == y;
if(foodSnakeConflict) {
this.generateFood();
} else {
this.drawFood(x, y);
}
});
}
drawFood(x, y) {
const size = this.settings.snake.size;
this.ctx.fillStyle = this.food.background;
this.ctx.strokestyle = this.food.border;
this.ctx.fillRect(x, y, size, size);
this.ctx.strokeRect(x, y, size, size);
this.food.active = true;
this.food.coordinates.x = x;
this.food.coordinates.y = y;
}
detectCollision() {
// Self collison
// It's impossible for the first 3 pieces of the snake to self collide so the loop starts at 4
for(let i = 4; i < this.snake.length; i++) {
const selfCollison = this.snake[i].x === this.snake[0].x && this.snake[i].y === this.snake[0].y;
if(selfCollison) {
return true;
}
}
// Wall collison
const leftCollison = this.snake[0].x < 0;
const topCollison = this.snake[0].y < 0;
const rightCollison = this.snake[0].x > this.$canvas.width - this.settings.snake.size;
const bottomCollison = this.snake[0].y > this.$canvas.height - this.settings.snake.size;
return leftCollison || topCollison || rightCollison || bottomCollison;
}
endGame() {
this.soundEffects.gameOver.play();
clearInterval(this.startGameInterval);
this.$app.classList.remove('game-in-progress');
this.$app.classList.add('game-over');
this.$startScreen.querySelector('.options h3').innerText = 'Game Over';
this.$startScreen.querySelector('.options .end-score').innerText = `Score: ${this.game.score}`;
this.setUpGame();
}
}
const snakeGame = new SnakeGame();
</script>
Stop the page from moving up and down when the game is active
You just need to prevent the default behaviour of these keys:
// Change direction
document.addEventListener('keydown', event => {
event.preventDefault(); // add this line
this.changeDirection(event.keyCode);
});
Note that this isn't a very good idea to do it that way, as it will prevent the default behaviour of all keypresses anywhere on your page. Most notably, F5 will never refresh the browser, and if you have other elements like buttons, links, etc, the standard keypresses to activate those will stop working (turning these off is a particular problem if you want your site to be accessible to those with disabilities).
You'd be better off checking the keyCode first and only preventing default if it corresponds to one of the arrow keys - which isn't so easy to do with your code as it stands but I'd encourage you to rewrite it that way.
Even doing that presents a problem to those who are not able to use a mouse, as (depending on the position of your game interface on the screen and its size) they may still need to scroll down and will be prevented from doing this by this preventDefault behaviour. But well that's what you asked for, and I've given you the solution - but with this important caveat.
I am trying to make a situation wherein a context menu appears when you click on the mesh of a 3D object. Herein what I have currently:
But what is happening is that the context menu is appearing behind the 3D object and so it is not properly visible. I was wondering what could I do to make the context menu appear in front of the 3D object? The raycast part of my code is herein stated:
public raycast(e: MouseEvent): void {
let mesh;
const x = e.clientX - this.rect.left;
const y = e.clientY - this.rect.top;
let scenePointer = new th.Vector2();
scenePointer.x = (x / this.canvas.clientWidth) * 2 - 1;
scenePointer.y = (y / this.canvas.clientHeight) * - 2 + 1;
var intersect;
// Determine the component in-focus using raycasting
this.raycaster.setFromCamera(scenePointer, this.camera);
const intersects = this.raycaster.intersectObjects(this.scene.children);
var menu = document.getElementById("menu")
var rightclick;
if ((<any>e).which) rightclick = ((<any>e).which == 3);
else if ((<any>e).button) rightclick = ((<any>e).button == 2);
if (!rightclick) {
if (intersects.length > 0) {
this.asmStateServ.focusedComponent = this.assemblyFetchServ.guidToName.get(intersects[0].object.name);
mesh = intersects[0].object
if ((<th.MeshPhongMaterial>mesh.material).color.getHex() != 0x00ff00) {
this.asmStateServ.lastColor = (<th.MeshPhongMaterial>mesh.material).color.getHex();
(<th.MeshPhongMaterial>mesh.material).color = new th.Color(0x00ff00);
}
else {
(<th.MeshPhongMaterial>mesh.material).color = new th.Color(this.asmStateServ.lastColor);
}
}
else {
this.asmStateServ.focusedComponent = '';
}
}
else {
if (intersects.length) {
intersect = intersects[0].object;
menu.style.left = x + "px";
menu.style.top = y + "px";
menu.style.display = "";
}
else {
intersect = undefined;
}
}
}
Any kind of help by modifying my part of the code would be much appreciated. :)
I have an array of TubeBufferGeometrys that im making animate to look as if they're growing out in length. When the animation runs the first time, it works really smoothly at 60fps. But once i set the geometrys position attributes back to zero, the FPS drop to 30.
Ive isolated my animation to run and then rerun once it finished with only the below changing. Heres the basics of my code:
Animation control view
stop() {
this.play = false;
// Start it again
setTimeout(() => {
let i = this.tubeCount;
while (i--) {
this.tubes[i].lastSegmentSet = 0;
this.tubes[i].updateToPercent(0);
}
this.elapsedTime = 0;
this.startTime = Date.now();
this.play = true;
}, 2000)
}
update() {
requestAnimationFrame(this.animate);
// ..render stuff + composer that ive disabled without effect
if (this.play) {
let percent = (Date.now() - this.startTime) / ANIMATE_DURATION;
if (percent >= 1) {
this.stop();
}
let i = this.lineCount;
while (i--) {
this.tubes[i].updateToPercent(percent);
}
}
}
Tube class (The main animation code)
constructor() {
//..other stuff
this.lastSegmentSet = 0;
}
// I first build the paths, then store the position data to use later to animate to. Then i set all the position data to zero
storeVerticies() {
this.positions = this.tube.geometry.attributes.position.array.slice(0);
const length = this.tube.geometry.attributes.position.array.length;
this.tube.geometry.attributes.position.array = new Float32Array(length);
}
setSegment(segment) {
this.setSegmentTo(segment, segment);
}
setSegmentTo(segment, target) {
let position = this.tube.geometry.attributes.position.array;
let startPoint = segment * JOINT_DATA_LENGTH; //JOINT_DATA_LENGTH is the number of values in the buffer geometry to update a segment
let targetPoint = target * JOINT_DATA_LENGTH;
let n = JOINT_DATA_LENGTH;
while (n--) {
position[startPoint + n] = this.positions[targetPoint + n];
}
}
updateToPercent(percent) {
let endSegment = Math.floor(percent * this.segmentCount);
while (this.lastSegmentSet <= endSegment) {
this.setSegment(this.lastSegmentSet++);
}
let n = this.lastSegmentSet;
while (n <= this.segmentCount + 1) {
this.setSegmentTo(n++, this.lastSegmentSet);
}
this.tube.geometry.attributes.position.needsUpdate = true;
}
Will put bounty when possible
I'm using Three.js on an app, currently I'm having issues with the object picking. Some objects are not getting intersected by the rays, but only on certain camera rotations. I'm trying to debug the code and to draw the ray.
I'm using this methods in my code(canvas is a namespace for the Three objects):
C.getXY = function(e) {
var click = {};
click.x = ( e.clientX / window.innerWidth ) * 2 - 1;
click.y = - ( e.clientY / window.innerHeight ) * 2 + 1;
return click;
};
C.doPicking = function(e) {
var picked = false;
if (canvas.boundingBox !== null) {
canvas.scene.remove(canvas.boundingBox);
}
var projector = new THREE.Projector(), click = C.getXY(e);
var ray = projector.pickingRay(new THREE.Vector3(click.x, click.y, 0), canvas.camera);
ray.linePrecision = 0.00000000000000001;
ray.precision = 0.00000000000000001;
var intersects = ray.intersectObjects(canvas.scene.children);
if (intersects.length > 0) {
var i = 0;
var ids = [];
while (i < intersects.length) {
if (intersects[i].object.visible) {
//Object is picked
}
++i;
}
}
};
My question is...are other points to consider in the debuging process?
Ooops...forgot to add mesh.geometry.computeFaceNormals(); before adding the meshes to the scene. Now the picking is working normally.
I have a project I am trying to get an animated <canvas> working with Paper JS. What I am curious about is if there is anything built into PaperJS that allows the ability to detect interactivity between items (i.e. if a item is X distance from any other item on the layer). Here is what I have so far:
HTML
<canvas id="myCanvas" resize></canvas>
CSS
html, body{margin:0; padding: 0;}
#myCanvas{width: 100%; height: 100%;}
JS
$(function(){
var canvas = $('#myCanvas')[0];
paper.setup(canvas);
var viewSize = paper.view.size;
var itemCount = 20;
var theBall = new paper.Path.Rectangle({
point : [0,0],
size : 10,
fillColor : '#00a950',
});
var theBallSymbol = new paper.Symbol(theBall);
// Create and place symbol on view
for (var i = 1; i <= itemCount; i++) {
var center = paper.Point.random().multiply(viewSize);
var placedSymbol = theBallSymbol.place(center);
placedSymbol.scale(i / itemCount);
placedSymbol.data = {
origin : center,
direction : (Math.round(Math.random())) ? 'right' : 'left',
}
placedSymbol.onFrame = function(e){
var pathWidth = this.bounds.width * 20;
var center = this.data.origin;
var moveValue = this.bounds.width / 20;
if(this.data.direction == 'right'){
if(this.position.x < center.x + pathWidth){
this.position.x += moveValue;
} else{
this.position.x -= moveValue;
this.data.direction = 'left';
}
} else {
if(this.position.x > center.x - pathWidth){
this.position.x -= moveValue;
} else {
this.position.x += moveValue;
this.data.direction = 'right';
}
}
}
}
paper.view.onFrame = function (e){
// For entire view
for (var i = 0; i < itemCount; i++) {
var item = paper.project.activeLayer.children[i];
// I imagine I would need to do something here
// I tried a hitTest already, but I'm not sure
// that will give me the information I would need
}
}
});
JSFiddle
That part so far is working well. What I am curious about how I can do the following:
Whenever any given item (the squares) come within a distance of X between each other, create a line (path) between them
The idea is very similar to this page: http://panasonic.jp/shaver/lamdash/dna/
Any ideas would be greatly appreciated. Thanks!
Paper.js does not keep track of the inter-point distance between an item's center and all other items. The only way to gather that information is to manually loop through them.
In your case, I think it would be easiest to:
Create an array of lines
Only keep lines that might become shorter than the threshold value
Loop through the lines array on each onFrame() and adjust the opacity.
By only choosing lines that will come within a threshold value, you can avoid creating unnecessary paths that would slow the framerate. Without this, you'd be checking ~5 times as many items.
Here's a quick example:
$(function(){
var canvas = $('#myCanvas')[0];
paper.setup(canvas);
var viewSize = paper.view.size;
var itemCount = 60;
//setup arrays to change line segments
var ballArray = [];
var lineArray = [];
//threshold distance for lines
var threshold = Math.sqrt(paper.view.size.width*paper.view.size.height)/5;
var theBall = new paper.Path.Rectangle({
point : [0,0],
size : 10,
fillColor : '#00a950',
});
var theBallSymbol = new paper.Symbol(theBall);
// Create and place symbol on view
for (var i = 1; i <= itemCount; i++) {
var center = paper.Point.random().multiply(viewSize);
var placedSymbol = theBallSymbol.place(center);
placedSymbol.scale(i / itemCount);
placedSymbol.data = {
origin : center,
direction : (Math.round(Math.random())) ? 'right' : 'left',
}
// Keep each placedSymbol in an array
ballArray.push( placedSymbol );
placedSymbol.onFrame = function(e){
var pathWidth = this.bounds.width * 20;
var center = this.data.origin;
var moveValue = this.bounds.width / 20;
if(this.data.direction == 'right'){
if(this.position.x < center.x + pathWidth){
this.position.x += moveValue;
} else{
this.position.x -= moveValue;
this.data.direction = 'left';
}
} else {
if(this.position.x > center.x - pathWidth){
this.position.x -= moveValue;
} else {
this.position.x += moveValue;
this.data.direction = 'right';
}
}
}
}
// Run through every possible line
// Only keep lines whose length might become less than threshold
for (var i = 0; i < itemCount; i++) {
for (j = i + 1, point1 = ballArray[i].data.origin; j < itemCount; j++) {
if ( Math.abs(point1.y - ballArray[j].bounds.center.y) < threshold && Math.abs(point1.x - ballArray[j].data.origin.x) < 4 * threshold) {
var line = new paper.Path.Line( point1, ballArray[j].bounds.center ) ;
line.strokeColor = 'black';
line.strokeWidth = .5;
//note the index of the line's segments
line.point1 = i;
line.point2 = j;
if (line.length > 1.4 * threshold && ballArray[j].data.direction == ballArray[i].data.direction) {
line.remove();
}
else {
lineArray.push(line);
}
}
}
}
paper.view.onFrame = function (e){
// Update the segments of each line
// Change each line's opacity with respect to distance
for (var i = 0, l = lineArray.length; i < l; i++) {
var line = lineArray[i];
line.segments[0].point = ballArray[line.point1].bounds.center;
line.segments[1].point = ballArray[line.point2].bounds.center;
if(line.length < threshold) {
line.opacity = (threshold - line.length) / threshold;
}
else line.opacity = 0;
}
}
});