JavaScript
语言:
JaveScriptBabelCoffeeScript
确定
// 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);