Created
January 18, 2022 06:19
-
-
Save shohan4556/b7e1a9b65ab4a22f327f9abbe15a9275 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// necessary modules | |
const { | |
Engine, | |
Render, | |
World, | |
Body, | |
Bodies, | |
Mouse, | |
MouseConstraint, | |
Constraint, | |
Events, | |
} = Matter; | |
// engine | |
const engine = Engine.create(); | |
// renderer | |
const render = Render.create({ | |
element: document.querySelector('main'), | |
engine, | |
options: { | |
wireframes: false, | |
// set the background ozf the canvas to be fully transparent, in favor of the design built with rectangle and circle elements | |
background: 'transparent', | |
}, | |
}); | |
// world | |
const { world } = engine; | |
// remove gravity to have the balls subject to collision only | |
engine.world.gravity.y = 0; | |
// global variables used in the project | |
// the idea is to translate the table to show a smaller rectangle surrounded by four borders and six circles | |
const margin = 40; | |
const width = 600; | |
const height = 800; | |
const borderSize = margin; | |
const pocketSize = margin; | |
// array describing the position of the pockets | |
const pocketsPosition = [ | |
{ x: 0, y: 0 }, | |
{ x: width, y: 0 }, | |
{ x: width, y: height / 2 }, | |
{ x: width, y: height }, | |
{ x: 0, y: height }, | |
{ x: 0, y: height / 2 }, | |
]; | |
const ballSize = pocketSize * 0.5; | |
// specify a larger surface for the canvas, inspired by d3 margin convention | |
render.canvas.width = width + margin * 2; | |
render.canvas.height = height + margin * 2; | |
// rectangle for the floor, with a fill | |
const floor = Bodies.rectangle(width / 2, height / 2, width, height, { | |
render: { | |
fillStyle: 'hsl(150, 30%, 20%)', | |
}, | |
// isSensor means the ball with not bounce off of the rectangle as if it were a solid shape | |
isSensor: true, | |
}); | |
// utility function to create a rectangle for the borders | |
const makeBorder = (x, y, w, h) => | |
Bodies.rectangle(x, y, w, h, { | |
render: { | |
fillStyle: 'hsl(260, 2%, 10%)', | |
}, | |
}); | |
// rectangles for the border, surrounding the floor | |
const borderTop = makeBorder(width / 2, -borderSize / 2, width, borderSize); | |
const borderRight = makeBorder( | |
width + borderSize / 2, | |
height / 2, | |
borderSize, | |
height | |
); | |
const borderBottom = makeBorder( | |
width / 2, | |
height + borderSize / 2, | |
width, | |
borderSize | |
); | |
const borderLeft = makeBorder(-borderSize / 2, height / 2, borderSize, height); | |
// utility function creating a circle for the pockets | |
// isSensor is specified to later have smaller circles used to detect a collision | |
const makePocket = (x, y, r, isSensor = true, label = '') => | |
Bodies.circle(x, y, r, { | |
isStatic: true, | |
isSensor, | |
label, | |
render: { | |
fillStyle: 'hsl(260, 2%, 10%)', | |
}, | |
}); | |
// create two sets of circles, one purely aesthetical and one functional (smaller and used to detect a collision) | |
const pockets = pocketsPosition.map(({ x, y }) => makePocket(x, y, pocketSize)); | |
const pocketsCollision = pocketsPosition.map( | |
({ x, y }) => makePocket(x, y, pocketSize / 3, false, 'pocket') // the label is picked up following the collisionStart event | |
); | |
const table = Body.create({ | |
parts: [ | |
floor, | |
borderTop, | |
borderRight, | |
borderBottom, | |
borderLeft, | |
...pockets, | |
...pocketsCollision, | |
], | |
isStatic: true, | |
}); | |
// translate the table in the canvas | |
Body.translate(table, { | |
x: margin, | |
y: margin, | |
}); | |
World.add(world, table); | |
// utility function returning a circle for the ball(s) | |
// add a field for the category, to have the mouse cursor interact only with the starter white ball | |
const makeBall = (x, y, r, fillStyle, label = 'ball', category = 0x0002) => | |
Bodies.circle(x, y, r, { | |
restitution: 1, | |
friction: 0.3, | |
label, | |
collisionFilter: { | |
category, | |
}, | |
render: { | |
fillStyle, | |
}, | |
}); | |
// utility function returning the sum of all numbers from 1 up to the input value | |
// used to color the balls in the triangular pattern with different hues around the color wheel | |
const triangularNumber = n => { | |
const numbers = Array(n - 1) | |
.fill() | |
.map((number, index) => index + 1); | |
return numbers.reduce((acc, curr) => acc + curr, 0); | |
}; | |
/* utility function creating a pattern for the balls | |
accepting as input the number of rows, returning as many balls as to create the following pattern | |
x x x | |
x x | |
x | |
*/ | |
const makePattern = (rows, x, y, r) => { | |
let columns = 1; | |
const ballsPattern = []; | |
for (let i = 0; i < rows; i += 1) { | |
const ballsRow = Array(columns) | |
.fill() | |
.map((col, colIndex) => | |
makeBall( | |
x + ((colIndex - columns / 2) * r * 2 + r), | |
y - (columns - 1) * r * 2, | |
r, | |
`hsl(${(360 / triangularNumber(rows)) * | |
(columns + colIndex)}, 45%, 50%)` | |
) | |
); | |
ballsPattern.push(...ballsRow); | |
columns += 1; | |
} | |
return ballsPattern; | |
}; | |
// balls included in the top section of the billiard | |
const balls = makePattern(5, width / 2 + margin, height / 3 + margin, ballSize); | |
World.add(world, [...balls]); | |
// ball included in the bottom section | |
// specify a different label to differentiate the behavior of the ball | |
const ballX = width / 2 + margin; | |
const ballY = (height * 4) / 5; | |
const ball = makeBall( | |
ballX, | |
ballY, | |
ballSize, | |
'hsl(0, 0%, 90%)', | |
'ball white', | |
0x0001 | |
); | |
// constraint included for the ball | |
const constraint = Constraint.create({ | |
pointA: { | |
x: ballX, | |
y: ballY, | |
}, | |
bodyB: ball, | |
stiffness: 0.2, | |
}); | |
World.add(world, [ball, constraint]); | |
// add a mouse constraint | |
const mouse = Mouse.create(render.canvas); | |
const mouseConstraint = MouseConstraint.create(engine, { | |
mouse, | |
}); | |
// filter the objects covered by the mouse constraint, preventing the player from moving the colored variants willy-nilly | |
mouseConstraint.collisionFilter.mask = 0x0001; | |
World.add(world, mouseConstraint); | |
// increase the score displayed in the heading following a collision between a ball and one of the inner pockets | |
const scoreboard = document.querySelector('#score'); | |
let score = 0; | |
// function updating the score with the input value | |
function handleScore(point) { | |
score += point; | |
scoreboard.textContent = score; | |
} | |
// function following the collisionStart event | |
function handleCollision(event) { | |
const { pairs } = event; | |
// loop through the pairs array(s) and update the score if a collision is detected between a ball and a pocket | |
pairs.forEach(pair => { | |
const { bodyA, bodyB } = pair; | |
// String.includes allows to find if the label contains a certain string of text | |
if (bodyA.label.includes('ball') && bodyB.label === 'pocket') { | |
// if the ball is the white, starter ball, decrease the score and reset the position of the ball | |
if (bodyA.label.includes('white')) { | |
handleScore(-1); | |
// set the velocity to 0 to stop the otherwise moving ball | |
Body.setVelocity(bodyA, { | |
x: 0, | |
y: 0, | |
}); | |
Body.setPosition(bodyA, { | |
x: ballX, | |
y: ballY, | |
}); | |
} else { | |
// else remove the scoring ball and increase the score | |
handleScore(1); | |
World.remove(world, bodyA); | |
} | |
} | |
// repeat the logic for the opposite scenario | |
if (bodyB.label.includes('ball') && bodyA.label === 'pocket') { | |
if (bodyB.label.includes('white')) { | |
handleScore(-1); | |
Body.setVelocity(bodyB, { | |
x: 0, | |
y: 0, | |
}); | |
Body.setPosition(bodyB, { | |
x: width / 2 + margin, | |
y: (height * 4) / 5, | |
}); | |
} else { | |
handleScore(1); | |
World.remove(world, bodyB); | |
} | |
} | |
}); | |
} | |
Events.on(engine, 'collisionStart', handleCollision); | |
// body for the mouse events | |
const body = document.querySelector('body'); | |
// boolean used to toggle the constraint on the white ball | |
let isConstrained = true; | |
// following a mouseup event remove the constraint | |
function removeConstraint() { | |
// remove the constaint following a brief delay to have the ball move in the desired direction | |
const timeoutID = setTimeout(() => { | |
isConstrained = false; | |
World.remove(world, constraint); | |
clearTimeout(timeoutID); | |
}, 25); | |
} | |
body.addEventListener('mouseup', removeConstraint); | |
body.addEventListener('mouseout', removeConstraint); | |
// following the collisionActive event, and only if the constraint is not already present, locate the white ball and add the constraint on top of the ball | |
function handleCollisionActive() { | |
if (isConstrained) { | |
return false; | |
} | |
// if the white ball is slow enough reset the constraint on the ball | |
if (Math.abs(ball.velocity.x) <= 0.2 && Math.abs(ball.velocity.y) < 0.2) { | |
isConstrained = true; | |
const { x, y } = ball.position; | |
constraint.pointA.x = x; | |
constraint.pointA.y = y; | |
World.add(world, constraint); | |
} | |
} | |
Events.on(engine, 'collisionActive', handleCollisionActive); | |
Engine.run(engine); | |
Render.run(render); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment