SOURCE

console 命令行工具 X clear

                    
>
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%;
}