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,
    // 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);
<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;
}

/* display the header and canvas in a column, horizontally centered in the viewport */
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%;
}