console
const {
Engine,
Render,
World,
Body,
Bodies,
Mouse,
MouseConstraint,
Constraint,
Events,
} = Matter;
const engine = Engine.create();
const render = Render.create({
element: document.querySelector('main'),
engine,
options: {
wireframes: false,
background: 'transparent',
},
});
const { world } = engine;
engine.world.gravity.y = 0;
const margin = 40;
const width = 600;
const height = 800;
const borderSize = margin;
const pocketSize = margin;
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;
render.canvas.width = width + margin * 2;
render.canvas.height = height + margin * 2;
const floor = Bodies.rectangle(width / 2, height / 2, width, height, {
render: {
fillStyle: 'hsl(150, 30%, 20%)',
},
isSensor: true,
});
const makeBorder = (x, y, w, h) =>
Bodies.rectangle(x, y, w, h, {
render: {
fillStyle: 'hsl(260, 2%, 10%)',
},
});
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);
const makePocket = (x, y, r, isSensor = true, label = '') =>
Bodies.circle(x, y, r, {
isStatic: true,
isSensor,
label,
render: {
fillStyle: 'hsl(260, 2%, 10%)',
},
});
const pockets = pocketsPosition.map(({ x, y }) => makePocket(x, y, pocketSize));
const pocketsCollision = pocketsPosition.map(
({ x, y }) => makePocket(x, y, pocketSize / 3, false, 'pocket')
);
const table = Body.create({
parts: [
floor,
borderTop,
borderRight,
borderBottom,
borderLeft,
...pockets,
...pocketsCollision,
],
isStatic: true,
});
Body.translate(table, {
x: margin,
y: margin,
});
World.add(world, table);
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,
},
});
const triangularNumber = n => {
const numbers = Array(n - 1)
.fill()
.map((number, index) => index + 1);
return numbers.reduce((acc, curr) => acc + curr, 0);
};
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;
};
const balls = makePattern(5, width / 2 + margin, height / 3 + margin, ballSize);
World.add(world, [...balls]);
const ballX = width / 2 + margin;
const ballY = (height * 4) / 5;
const ball = makeBall(
ballX,
ballY,
ballSize,
'hsl(0, 0%, 90%)',
'ball white',
0x0001
);
const constraint = Constraint.create({
pointA: {
x: ballX,
y: ballY,
},
bodyB: ball,
stiffness: 0.2,
});
World.add(world, [ball, constraint]);
const mouse = Mouse.create(render.canvas);
const mouseConstraint = MouseConstraint.create(engine, {
mouse,
});
mouseConstraint.collisionFilter.mask = 0x0001;
World.add(world, mouseConstraint);
const scoreboard = document.querySelector('#score');
let score = 0;
function handleScore(point) {
score += point;
scoreboard.textContent = score;
}
function handleCollision(event) {
const { pairs } = event;
pairs.forEach(pair => {
const { bodyA, bodyB } = pair;
if (bodyA.label.includes('ball') && bodyB.label === 'pocket') {
if (bodyA.label.includes('white')) {
handleScore(-1);
Body.setVelocity(bodyA, {
x: 0,
y: 0,
});
Body.setPosition(bodyA, {
x: ballX,
y: ballY,
});
} else {
handleScore(1);
World.remove(world, bodyA);
}
}
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);
const body = document.querySelector('body');
let isConstrained = true;
function removeConstraint() {
const timeoutID = setTimeout(() => {
isConstrained = false;
World.remove(world, constraint);
clearTimeout(timeoutID);
}, 25);
}
body.addEventListener('mouseup', removeConstraint);
body.addEventListener('mouseout', removeConstraint);
function handleCollisionActive() {
if (isConstrained) {
return false;
}
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);
<script src="https://unpkg.com/matter-js@0.14.2/build/matter.min.js"></script>
<main>
<header>
<h1>
Play ball
</h1>
<h2>
Score:
<mark id="score">0</mark>
</h2>
</header>
</main>
@import url("https://fonts.googleapis.com/css?family=Quicksand:500,700&display=swap");
*,
*:before,
*:after {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body {
color: hsl(260, 2%, 5%);
background: hsl(55, 30%, 95%);
font-family: "Quicksand", sans-serif;
}
main {
max-width: 400px;
margin: 3rem auto;
display: flex;
flex-direction: column;
}
main header {
padding: 0 0.25rem;
display: flex;
align-items: center;
justify-content: space-between;
}
h1 {
font-size: 1.25rem;
font-weight: 700;
padding: 0.25rem 0.5rem;
position: relative;
}
h1:before,
h1:after {
position: absolute;
left: 100%;
transform: translate(-50%, 0);
}
h1:after {
bottom: 100%;
content: "8";
font-weight: inherit;
font-size: 12px;
text-align: center;
border-radius: 50%;
width: 30px;
height: 30px;
line-height: 30px;
color: #3d3c3f;
background: radial-gradient(circle at 50% 50%, hsl(25, 70%, 94%) 40%, transparent 42%), hsl(260, 2%, 25%);
transition: transform 0.5s ease-in-out;
transition-delay: 0s;
}
h1:hover:after {
transform: translate(-50%, -8px);
transition-delay: 0.45s;
transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.5);
}
h1:before {
content: "";
bottom: 0%;
height: 100%;
width: 5px;
background: linear-gradient(to bottom, hsl(25, 70%, 70%) 4px, transparent 4px), hsl(260, 2%, 25%);
transform: translate(-50%, 0);
transition: transform 0.5s ease-in-out;
transition-delay: 0.1s;
}
h1:hover:before {
transform: translate(-50%, -2px);
transition-delay: 0s;
transition-timing-function: cubic-bezier(0.6, -1.25, 0.735, 0.045);
}
h2 {
font-size: 1rem;
font-weight: 500;
}
h2 mark {
font-weight: 700;
background: hsl(25, 70%, 70%);
padding: 0.2rem 0.5rem;
}
canvas {
width: 100%;
}