console
// 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,
// 将画布的背景设置为完全透明,以支持使用矩形和圆形元素构建的设计
background: 'transparent',
},
});
// world
const {
world
} = engine;
// 去除重力使球仅受到碰撞
engine.world.gravity.y = 0;
// 项目中使用的全局变量
// 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;
// 描述袋口位置的数组
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;
// 背景填充
const floor = Bodies.rectangle(width / 2, height / 2, width, height, {
render: {
fillStyle: 'hsl(150, 80%, 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
// 指定不同的标签以区分球的行为
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.1,
});
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 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;
// 如果在球和口袋之间检测到碰撞,则循环通过对阵列并更新得分
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);
<main>
<header>
<h1>
Play ball
</h1>
<h2>
Score:
<mark id="score">0</mark>
</h2>
</header>
</main>
<script src='https://unpkg.com/matter-js@0.14.2/build/matter.min.js'></script>
@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;
}
/* display the headings in a row */
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;
}
/* with pseudo elements draw a pool-inspired graphic with a #8 ball
when the heading is hovered on apply a silly transition moving the stick and ball upwards
*/
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 and delay applied as the :hover state is removed
specify different values as the hover state occurs
*/
transition: transform 0.5s ease-in-out;
transition-delay: 0s;
}
/* translate the ball as the stick reaches its new vertical position */
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%;
}