html, body {
  font-family: sans-serif;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

canvas {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  margin: auto;
}
class Ball {
  constructor(x, y, radius) {
    this.x = x;
    this.y = y;
    this.shadowY = y;
    this.velocity = createVector(radius * 0.21, random(-radius * 0.084, radius * 0.084));
    this.radius = radius;
  }

  update(ballArc) {
    this.x += this.velocity.x;
    this.shadowY += this.velocity.y;
    this.y = this.shadowY - ballArc;
  }

  render() {
    push();
    noStroke();
    fill(54, 54, 54, 100);
    ellipse(this.x, this.shadowY, this.radius);
    stroke(54);
    strokeWeight(1);
    fill(255, 255, 0);
    ellipse(this.x, this.y, this.radius);
    pop();
  }
}

class Paddle {
  constructor(x, y, slope, bottomBound, topBound, images, isPlayer, width) {
    this.x = x;
    this.y = y;
    this.bottomBound = bottomBound;
    this.topBound = topBound;
    this.slope = -slope;
    this.images = images;
    this.counter = 0;
    this.animationSpeed = 0.1;
    this.width = width;
    this.height = width * 2;
    this.moveDirection = "none";
    this.isPlayer = isPlayer;
    this.playerHasMoved = false;
  }

  move() {
    const speed = this.height * 0.05;
    if (this.moveDirection === 'up' && this.y > this.topBound) {
      this.y -= speed
      this.x += (speed/this.slope)
      this.counter += this.animationSpeed;
    } else if (this.moveDirection === 'down' && this.y < this.bottomBound) {
      this.y += speed
      this.x -= (speed/this.slope)
      this.counter += this.animationSpeed;
    }
  }

  autoMove(ballY) {
    const diff = this.y - ballY;
    if (diff < 0 && diff < -this.height * 0.5) { // move down
      this.switchMoveDirection('down');
    } else if (diff > 0 && diff > this.height * 0.5) { // move up
      this.switchMoveDirection('up');
    } else {
      this.switchMoveDirection('none');
    }
    this.move();
  }

  switchMoveDirection(type) {
    switch (type) {
      case 'up':
        this.moveDirection = 'up';
        this.playerHasMoved = true;
        break
      case 'down':
        this.moveDirection = 'down';
        this.playerHasMoved = true;
        break
      default:
        this.moveDirection = 'none'
    }
  }

  render() {
    const y1 = this.y - this.height;
    const x1 = this.x + (this.height/this.slope);

    const x2 = x1 + this.width;
    const y2 = y1;

    const x3 = this.x + this.width;
    const y3 = this.y;

    this.imageIdx = floor(this.counter) % this.images.length;

    if (this.images.length > 0) {
      noStroke();
      fill(54, 54, 54, 25);
      ellipse(this.x, this.y + this.height, this.height, this.width * 1.5);
      imageMode(CENTER);
      image(this.images[this.imageIdx], this.x, this.y, this.width * 2, this.height * 2);
      if (this.isPlayer && !this.playerHasMoved) {
        textSize(14);
        fill(54);
        text("use the arrow keys to move!", this.x - padding, this.y - (this.height * 1.5));
      }
    } else {
      noStroke();
      fill(54);
      quad(this.x, this.y, x1, y1, x2, y2, x3, y3);
    }
  }
}

let width, height;
let courtTopLeftX, courtTopLeftY, courtBottomLeftX, courtBottomLeftY,
    courtBottomRightX, courtBottomRightY, courtTopRightX, courtTopRightY;
let netTopLeftX, netTopLeftY, netBottomLeftX, netBottomLeftY,
    netBottomRightX, netBottomRightY, netTopRightX, netTopRightY;
let netWidth, shadowSize, padding, quadOffset;
let paddleOne, paddleTwo;
let ball;
let leftPlayerScore, rightPlayerScore;
let tennisManRedOne, tennisManRedTwo, tennisManBlueOne, tennisManBlueTwo;
let courtHeight, courtWidth, maxBallArc, oneFourthPoint, threeFourthsPoint;

function preload() {
  tennisManRedOne = loadImage('https://res.cloudinary.com/dkw0kkkgd/image/upload/v1550623424/tennisManRedOne_zaszr5.png');
  tennisManRedTwo = loadImage('https://res.cloudinary.com/dkw0kkkgd/image/upload/v1550623420/tennisManRedTwo_mz3skr.png');
  tennisManBlueOne = loadImage('https://res.cloudinary.com/dkw0kkkgd/image/upload/v1550623406/tennisManBlueOne_jo7ppq.png');
  tennisManBlueTwo = loadImage('https://res.cloudinary.com/dkw0kkkgd/image/upload/v1550623416/tennisManBlueTwo_ac6ppw.png');
}

function setup() {
  setupCanvas();
  leftPlayerScore = 0; rightPlayerScore = 0; // setup scoreboard
  setupCourtCoordinates();
  setupNetCoordinates();
  setupPaddles();
  ball = new Ball(width * 0.5, height * 0.5, width * 0.021); // setup ball
}

function setupCanvas() {
  height = min(window.innerHeight, window.innerWidth / 2);
  // keep court dimensions nice
  width = min(window.innerWidth, height * 2);
  createCanvas(width, height);
}

function draw() {
  paddleOne.move(); // move player according to keyboard inputs
  paddleTwo.autoMove(ball.y); // computer player moves on its own relative to the ball's position
  updateBall();
  isColliding(); // check if ball is colliding with players
  didHitWall(); // check if ball is colliding with walls
  didScore(); // check if a player has scored
  background(255); // draw background
  drawCourt();
  drawCourtLines();
  paddleOne.render(); // draw player
  paddleTwo.render(); // draw computer player
  drawNet();
  ball.render(); // draw ball
  drawScore();
}

function setupCourtCoordinates() {
  padding = height * 0.125;
  quadOffset = width * 0.125;

  // top left coordinates of court
  courtTopLeftX = padding + quadOffset;
  courtTopLeftY = padding;

  // bottom left coordinates of court
  courtBottomLeftX = padding;
  courtBottomLeftY = height - padding;

  // bottom right coordinates of court
  courtBottomRightX = width - padding - quadOffset;
  courtBottomRightY = height - padding;

  // top right coordinates of court
  courtTopRightX = width - padding;
  courtTopRightY = padding;
}

function setupNetCoordinates() {
  // net settings
  netWidth = height * 0.125;
  shadowSize = width * 0.021;

  // top left of net
  netTopLeftX = (width * 0.5) + quadOffset * 0.5;
  netTopLeftY = padding - netWidth;

  // bottom left of net
  netBottomLeftX = width * 0.5 - quadOffset * 0.5;
  netBottomLeftY = height - padding - netWidth;

  // bottom right of net
  netBottomRightX = width * 0.5 - quadOffset * 0.5;
  netBottomRightY = height - padding;

  // top right of net
  netTopRightX = width * 0.5 + quadOffset * 0.5;
  netTopRightY = padding;
}

function setupPaddles() {
  const bottomBound = courtBottomLeftY;
  const topBound = courtTopRightY;
  const yBounds = [bottomBound, topBound];

  const position = [courtBottomLeftX + 30, courtBottomLeftY - 60];
  const slope = (courtBottomLeftY - courtTopLeftY)/(courtBottomLeftX - courtTopLeftX);
  const blueImages = [tennisManBlueOne, tennisManBlueTwo];
  paddleOne = new Paddle(...position, slope, ...yBounds, blueImages, isPlayer = true, width * 0.03);

  const twoPosition = [courtTopRightX - 70, courtTopRightY + 60];
  const twoSlope = (courtTopRightY - courtBottomRightY)/(courtTopRightX - courtBottomRightX);
  const redImages = [tennisManRedOne, tennisManRedTwo];
  paddleTwo = new Paddle(...twoPosition, twoSlope, ...yBounds, redImages, isPlayer = false, width * 0.03);
}

function keyReleased() {
  if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) {
    paddleOne.switchMoveDirection('none');
  } else if (keyCode === 87 || keyCode === 83) {
    paddleOne.switchMoveDirection('none');
  }
}

function keyPressed() {
  if (keyCode === UP_ARROW) {
    paddleOne.switchMoveDirection('up');
  } else if (keyCode === DOWN_ARROW) {
    paddleOne.switchMoveDirection('down');
  }
}

function isColliding() {
  const bottomOfBall = ball.y + ball.radius,
        topOfBall = ball.y - ball.radius,
        topOfPaddleOne = paddleOne.y - paddleOne.height,
        bottomOfPaddleOne = paddleOne.y + paddleOne.height,
        topOfPaddleTwo = paddleTwo.y - paddleTwo.height,
        bottomOfPaddleTwo = paddleTwo.y + paddleTwo.height;

  if (ball.x >= paddleOne.x - paddleOne.width && ball.x <= paddleOne.x + paddleOne.width) {
    if (topOfBall <= bottomOfPaddleOne && bottomOfBall >= topOfPaddleOne) {
      playerCollisionEvent();
    }
  } else if (ball.x >= paddleTwo.x - paddleTwo.width && ball.x <= paddleTwo.x + paddleTwo.width) {
    if (topOfBall <= bottomOfPaddleTwo && bottomOfBall >= topOfPaddleTwo) {
      computerCollisionEvent();
    }
  }
}

function playerCollisionEvent() {
  ball.velocity.x *= -1.03;
  ball.x = paddleOne.x + paddleOne.width + 5;
  let velocityMag;
  let xVel = ball.velocity.x;

  const maxTopSlope = (courtTopRightY - (paddleOne.y - paddleOne.height))/(courtTopRightX - (paddleOne.x + paddleOne.width)); // range from -0.38 to 0
  const maxBottomSlope = (courtBottomRightY - (paddleOne.y + paddleOne.height))/(courtBottomRightX - (paddleOne.x + paddleOne.width)); // range from 0 to 0.54

  if (ball.y < paddleOne.y) {
    velocityMag = -random(4);
    ball.velocity.y = map(velocityMag, -4, 0, xVel * maxTopSlope, 0);
  } else {
    velocityMag = random(4);
    ball.velocity.y = map(velocityMag, 0, 4, 0, xVel * maxBottomSlope);
  }
}

function computerCollisionEvent() {
  ball.velocity.x *= -1.03;
  ball.x = paddleTwo.x - paddleTwo.width - 4;
  let velocityMag;
  let xVel = ball.velocity.x;

  const maxTopSlope = (courtTopLeftY - (paddleTwo.y - paddleTwo.height))/(courtTopLeftX - (paddleTwo.x - paddleTwo.width)); // 0 to 0.56
  const maxBottomSlope = (courtBottomLeftY - (paddleTwo.y + paddleTwo.height))/(courtBottomLeftX - (paddleTwo.x - paddleTwo.width)); // 0 to -0.39

  if (ball.y < paddleTwo.y) {
    velocityMag = -random(4);
    ball.velocity.y = map(velocityMag, -4, 0, xVel * maxTopSlope, 0);
  } else {
    velocityMag = random(4);
    ball.velocity.y = map(velocityMag, 0, 4, 0, xVel * maxBottomSlope);
  }
}

function resetBall() {
  ball.x = width * 0.5;
  ball.y = height * 0.5;
  ball.shadowY = height * 0.5;
  ball.velocity.x = (ball.velocity.x < 0) ? ball.radius * 0.21 : -(ball.radius * 0.21);
  ball.velocity.y = random(-ball.radius * 0.084, ball.radius * 0.084);
}

function didHitWall() {
  const upperBound = courtTopLeftY;
  const lowerBound = courtBottomLeftY;

  if (ball.y <= upperBound) { ball.velocity.y *= -1 }
  if (ball.y >= lowerBound) { ball.velocity.y *= -1 }
}

function didScore() {
  const leftBound = courtBottomLeftX;
  const rightBound = courtTopRightX;

  if (ball.x <= leftBound) { rightPlayerScore++; resetBall(); }
  if (ball.x >= rightBound) { leftPlayerScore++; resetBall(); }
}

function updateBall() {
  let ballArc;
  maxBallArc = height * 0.0625;
  courtWidth = width - (2 * padding) - quadOffset;
  oneFourthPoint = courtBottomLeftX + quadOffset * 0.5 + courtWidth * 0.25;
  threeFourthsPoint = courtTopRightX - quadOffset * 0.5 - courtWidth * 0.25;

  const oneRacketX = paddleOne.x + paddleOne.width;
  const twoRacketX = paddleTwo.x - paddleTwo.width;

  if (ball.velocity.x > 0) { // ball is moving right
    if (ball.x <= width * 0.5) {
      ballArc = map(ball.x, oneRacketX, width * 0.5, maxBallArc * 0.5, maxBallArc);
    } else if (ball.x > width * 0.5 && ball.x <= threeFourthsPoint) {
      ballArc = map(ball.x, width * 0.5, threeFourthsPoint, maxBallArc, 0);
    } else {
      ballArc = map(ball.x, threeFourthsPoint, twoRacketX, 0, maxBallArc * 0.5);
    }
  } else { // ball is moving left
    if (ball.x >= width * 0.5) {
      ballArc = map(ball.x, twoRacketX, width * 0.5, maxBallArc * 0.5, maxBallArc);
    } else if (ball.x < width * 0.5 && ball.x >= oneFourthPoint) {
      ballArc = map(ball.x, width * 0.5, oneFourthPoint, maxBallArc, 0);
    } else {
      ballArc = map(ball.x, oneFourthPoint, oneRacketX, 0, maxBallArc * 0.5);
    }
  }
  ball.update(ballArc);
}

function drawCourt() {
  noStroke();
  fill(54);
  const shift = 10;
  quad(courtTopLeftX + shift, courtTopLeftY + shift, courtBottomLeftX + shift, courtBottomLeftY + shift, courtBottomRightX + shift, courtBottomRightY + shift, courtTopRightX + shift, courtTopRightY + shift);

  fill(161, 238, 168);
  quad(courtTopLeftX, courtTopLeftY, courtBottomLeftX, courtBottomLeftY, courtBottomRightX, courtBottomRightY, courtTopRightX, courtTopRightY);
}

function drawCourtLines() {
  stroke(255);

  courtHeight = height - (2 * padding);
  const topSingleLineX1 = padding + ((1 - 0.125) * quadOffset),
        topSingleLineY = padding + (0.125 * courtHeight),
        topSingleLineX2 = width - padding - (0.125 * quadOffset);

  // middle line
  line(oneFourthPoint, height* 0.5, threeFourthsPoint, height* 0.5);

  // center marker notch thingies
  const markerLength = width/64;
  const leftX = courtBottomLeftX + quadOffset * 0.5;
  const rightX = courtTopRightX - quadOffset * 0.5;
  line(leftX, height * 0.5, leftX + markerLength, height * 0.5);
  line(rightX - markerLength, height * 0.5, rightX, height * 0.5);

  line(topSingleLineX1, topSingleLineY, topSingleLineX2, topSingleLineY);

  const bottomSingleLineX1 = padding + (0.125 * quadOffset),
        bottomSingleLineY = padding + ((1 - 0.125) * courtHeight),
        bottomSingleLineX2 = width - padding - ((1 - 0.125) * quadOffset);

  line(bottomSingleLineX1, bottomSingleLineY, bottomSingleLineX2, bottomSingleLineY);

  // vertical lines in the middle of the court
  line(bottomSingleLineX1 + courtWidth * 0.25, bottomSingleLineY, topSingleLineX1 + courtWidth * 0.25, topSingleLineY);

  line(bottomSingleLineX2 - courtWidth * 0.25, bottomSingleLineY, topSingleLineX2 - courtWidth * 0.25, topSingleLineY);
}

function drawNet() {
  strokeWeight(1);
  // shadow
  noStroke();
  fill(54, 54, 54, 54);
  quad(netTopRightX, netTopRightY, netBottomRightX, netBottomRightY, netBottomRightX + shadowSize, netBottomRightY, netTopRightX + shadowSize, netTopRightY);

  stroke(54);
  // line across
  line(netBottomLeftX, netBottomRightY - netWidth * 0.75, netTopRightX, netTopRightY - netWidth * 0.75);
  line(netBottomLeftX, netBottomRightY - netWidth * 0.5, netTopRightX, netTopRightY - netWidth * 0.5);
  line(netBottomLeftX, netBottomRightY - netWidth * 0.25, netTopRightX, netTopRightY - netWidth * 0.25);

  line(netBottomRightX, netBottomRightY, netTopRightX, netTopRightY);

  // vertical net lines
  line(netBottomLeftX + quadOffset/10, (9*courtHeight)/10 + netWidth, netBottomLeftX + quadOffset/10, (9*courtHeight)/10);
  line(netBottomLeftX + (2*quadOffset)/10, (8*courtHeight)/10 + netWidth, netBottomLeftX + (2*quadOffset)/10, (8*courtHeight)/10);
  line(netBottomLeftX + (3*quadOffset)/10, (7*courtHeight)/10 + netWidth, netBottomLeftX + (3*quadOffset)/10, (7*courtHeight)/10);
  line(netBottomLeftX + (4*quadOffset)/10, (6*courtHeight)/10 + netWidth, netBottomLeftX + (4*quadOffset)/10, (6*courtHeight)/10);
  line(netBottomLeftX + (6*quadOffset)/10, (4*courtHeight)/10 + netWidth, netBottomLeftX + (6*quadOffset)/10, (4*courtHeight)/10);
  line(netBottomLeftX + (7*quadOffset)/10, (3*courtHeight)/10 + netWidth, netBottomLeftX + (7*quadOffset)/10, (3*courtHeight)/10);
  line(netBottomLeftX + (8*quadOffset)/10, (2*courtHeight)/10 + netWidth, netBottomLeftX + (8*quadOffset)/10, (2*courtHeight)/10);
  line(netBottomLeftX + (9*quadOffset)/10, courtHeight/10 + netWidth, netBottomLeftX + (9*quadOffset)/10, courtHeight/10);

  stroke(245);

  strokeWeight(4);
  line(netTopLeftX, netTopLeftY, netBottomLeftX, netBottomLeftY);
  // line through the middle of the net
  line(width * 0.5, height * 0.5, width * 0.5, height * 0.5 - netWidth)
  stroke(54);
  // left post
  line(netBottomRightX, netBottomRightY, netBottomLeftX, netBottomLeftY);

  // right post
  strokeWeight(3);
  line(netTopRightX, netTopRightY, netTopLeftX, netTopLeftY);
}

function drawScore() {
  textSize(height * 0.1);
  noStroke();
  fill(54);
  textAlign(RIGHT);
  text(leftPlayerScore, padding + quadOffset + (courtWidth * 0.25), padding - 10); // player score
  textAlign(LEFT);
  text(rightPlayerScore, width - padding - (courtWidth * 0.25), padding - 10); // computer score
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/p5.min.js