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);