body {
  padding: 0;
  margin: 0;
  background: #1a1a1c;
  user-select: none;
  -webkit-user-select: none;
}

canvas {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  margin: auto;
}
let player, ball, blocks, scoreDisplay, livesLostDisplay, levelDisplay, optionsMenu, scoreboard, paused, muted, gameStarted

function setup() {
    const padding = 70
    let h = windowHeight - padding
    let w = h * 1.356
  
    if (w > windowWidth) {
      w = windowWidth - padding
      h = w * 0.7375
    } else if (w < 320) {
      w = 500
      h = w * 0.7375
    }
    createCanvas(w, h);
    
    player = new Player()
    ball = new Ball()
    blocks = []
    scoreDisplay = new NumberDisplay(3 * width / 8, height / 100, height / 18)
    livesLostDisplay = new NumberDisplay(5 * width / 8, height / 100, height / 18)
    levelDisplay = new NumberDisplay(7 * width / 8, height / 100, height / 18)
    scoreboard = new ScoreBoard()
    optionsMenu = new OptionsMenu(0, height / 100, height / 16)
    paused = true
    gameStarted = false
    muted = false

    setupBlocks()
    
    setTimeout(() => {
        ball.loading = false
        playSound('high')
    }, 1000);
}

function playSound(type) {
    if (muted) return

    const volume = 0.1
    const duration = 0.1
    var synth = new p5.MonoSynth();
    if (type == 'low') {
      synth.play('A4', volume, 0, duration);
    } else if (type == 'mid') {
      synth.play('A5', volume, 0, duration);
    } else {
      synth.play('A6', volume, 0, duration);
    }
    // Get rid of the MonoSynth and free up its resources / memory.
    setTimeout(() => {
        synth.dispose()
    }, 1000);
}

function mapValue(val, minOne, maxOne, minTwo, maxTwo) {
    const oneTotal = maxOne - minOne;
    if (val < minOne) {
      return minOne
    } else if (val > maxOne) {
      return maxOne
    } else {
      const valPercentOfTotal = val / oneTotal;
      return minTwo + (valPercentOfTotal * (maxTwo - minTwo))
    }
  }

function getRandomAngle(min, max) {
    return Math.random() * (max - min) + min;
}

class Player {
    constructor() {
      this.width = width / 8
      this.height = height / 42
  
      this.position = {
        x: (width / 2) - (this.width / 2),
        y: height - (this.height * 2)
      }
  
      this.velocity = {
        x: 0,
        y: 0
      }
    }
  
    draw() {
      noStroke()
      fill('#FF5733') // vermillion (light red) color
      rect(this.position.x, this.position.y, this.width, this.height)
    }
  
    update(ballHasHitTop) {
      if (ballHasHitTop) {
        this.width = width / 11
      } else {
        this.width = width / 8
      }
      this.position.x += this.velocity.x
  
      if (player.position.x <= width / 16) {
        player.position.x = width / 16
      }
      if (player.position.x + player.width >= width - (width / 16)) {
        player.position.x = width - (width / 16) - player.width
      }
  
    }
}
  
class Ball {
    constructor() {
      this.width = width / 81
      this.height = width / 81
      this.livesLost = 0
      this.speededUp = false
      this.hasHitTop = false
      this.loading = true
      this.loadRadius = width / 20
  
      this.position = {
        x: width / 2 - this.width / 2,
        y: height / 2 - this.height / 2
      }
  
      this.angle = getRandomAngle(3 * Math.PI/8, 5 * Math.PI/8)
  
      this.speed = width / 116
  
      this.velocity = {
        x: this.speed * Math.cos(this.angle),
        y: this.speed * Math.sin(this.angle)
      }
    }
  
    speedUp() {
      this.speededUp = true;
  
      let xSign = 1;
      let ySign = 1;
  
      if (this.velocity.x > 0 && Math.cos(this.angle) < 0) {
        xSign = -1;
      } else if (this.velocity.x < 0 && Math.cos(this.angle) > 0) {
        xSign = -1;
      }
      if (this.velocity.y > 0 && Math.sin(this.angle) < 0) {
        ySign = -1;
      } else if (this.velocity.y < 0 && Math.sin(this.angle) > 0) {
        ySign = -1;
      }
  
      this.speed = width / 81;
      this.velocity = {
        x: xSign * this.speed * Math.cos(this.angle),
        y: ySign * this.speed * Math.sin(this.angle)
      }
    }
  
    resetBall() {
      this.speededUp = false;
      this.speed = width / 116;
      this.hasHitTop = false;
  
      this.velocity = {
        x: this.speed * Math.cos(this.angle),
        y: this.speed * Math.sin(this.angle)
      }
    }
  
    draw() {
      if (this.loading) {
        if (this.loadRadius >= 0) {
          this.loadRadius -= (width / 20) / 60
        }
        noFill()
        stroke(255, 255, 255, `${((width / 20) - this.loadRadius)/(width / 20)})`);
        rect(width / 2 - this.loadRadius / 2, height / 2 - this.loadRadius / 2, this.loadRadius, this.loadRadius)
      } else {
        noStroke()
        if (!this.speededUp) {
          fill('#FF5733') // vermillion (light red) color
        } else {
          let rand = Math.random()
          if (rand < 0.5) {
            fill('#11faa4')
          } else {
            fill('#ffffff')
          }
        }
        rect(this.position.x, this.position.y, this.width, this.height)
      }
    }
  
    updateAngle(angle) {
      this.angle = angle;
  
      this.velocity = {
        x: this.speed * Math.cos(this.angle),
        y: this.speed * Math.sin(this.angle)
      }
    }
  
    update() {
      if (!this.loading) {
        this.position.x += this.velocity.x
        this.position.y += this.velocity.y
  
        if (this.position.y <= 2 * (width / 16)) {
          this.hasHitTop = true;
          this.position.y = 2 * (width / 16)
          this.velocity.y *= -1
          playSound('mid')
          if (!this.speededUp) {
            this.speedUp();
          }
        }
  
        if (this.position.x <= width / 16) {
          playSound('high')
          this.position.x = width / 16
          this.velocity.x *= -1
        }
  
        if (this.position.x + this.width >= width - (width / 16)) {
          playSound('high')
          this.position.x = (width - (width / 16)) - this.width
          this.velocity.x *= -1
        }
  
        if (this.position.y >= height) {
          this.loading = true;
          this.loadRadius = width / 20;
          this.livesLost += 1;

          setTimeout(() => {
            this.position = {
              x: width / 2 - this.width / 2,
              y: height / 2 - this.height / 2
            }
  
            this.angle = getRandomAngle(3 * Math.PI/8, 5 * Math.PI/8)
  
            this.resetBall();
            playSound('high')
            this.loading = false;
          }, 1000);
        }
      }
    }
}
  
class Block {
    constructor(w, x, row) {
      this.width = w
      this.height = height / 25
      this.row = row
      this.collidedWith = false
  
      this.position = {
        x: x,
        y: (2 * (width / 16)) + (this.row * height / 25)
      }
    }
  
    checkBlockCollision(ball) {
      const ballX = ball.position.x
      const ballY = ball.position.y
      const blockX = this.position.x
      const blockY = this.position.y
  
      if (ballY + ball.height >= blockY && ballY <= blockY + this.height) {
        if (ballX + ball.width >= blockX && ballX <= blockX + this.width) {
          this.collidedWith = true
          return true
        }
      }
      return false
    }
  
    draw() {
      switch(this.row) {
        case 1:
          fill(219, 43, 66) // red
          stroke(219, 43, 66) // red
          break;
        case 2:
          fill(212, 99, 45) // orange 1
          stroke(212, 99, 45) // orange 1
          break;
        case 3:
          fill(188, 123, 20) // orange 2
          stroke(188, 123, 20) // orange 2
          break;
        case 4:
          fill(165, 165, 0) // yellow
          stroke(165, 165, 0) // yellow
          break;
        case 5:
          fill(0, 169, 65) // green
          stroke(0, 169, 65) // green
          break;
        case 6:
          fill(73, 42, 216) // blue
          stroke(73, 42, 216) // blue
          break;
        default:
          fill('#FFFFFF')
          stroke('#FFFFFF')
      }
      rect(this.position.x, this.position.y, this.width, this.height)
    }
}

class OptionsMenu {
  constructor(x, y, sideLength) {
    this.position = {
      x: x,
      y: y
    }
    this.sideLength = sideLength
  }

  drawPausePlayButton() {
    fill('#FFF')
    stroke('#FFF')
    rect(this.position.x, this.position.y, this.sideLength, this.sideLength)
    fill('#333')
    stroke('#333')
    if (paused) {
      triangle(this.position.x + this.sideLength / 4, this.position.y + this.sideLength / 4, this.position.x + this.sideLength / 4, this.position.y + 3 * this.sideLength / 4, this.position.x + 3 * this.sideLength / 4, this.position.y + this.sideLength / 2)
    } else {
      rect(this.position.x + (this.sideLength / 4), this.position.y + (this.sideLength / 4), this.sideLength / 8, (this.sideLength) / 2)
      rect(this.position.x + (5 * this.sideLength / 8), this.position.y + (this.sideLength / 4), this.sideLength / 8, (this.sideLength) / 2)
    }
    // textSize(12)
    // fill('#FFF')
    // stroke('#FFF')
    // let pText = paused ? "play (p)" : 'pause (p)'
    // text(pText, this.position.x, this.position.y + (3 * this.sideLength)/2)
  }

  pointWithinPausePlay(mousePositionX, mousePositionY) {
    const pausePlayLeftX = this.position.x
    const pausePlayRightX = pausePlayLeftX + this.sideLength
    const pausePlayTopY = this.position.y
    const pausePlayBottomY = pausePlayTopY + this.sideLength
    if (mousePositionX >= pausePlayLeftX && 
        mousePositionX <= pausePlayRightX &&
        mousePositionY >= pausePlayTopY && 
        mousePositionY <= pausePlayBottomY) {
          return true
        }
  }

  drawMuteButton() {
    fill('#FFF')
    stroke('#FFF')
    rect(this.position.x + this.sideLength * 2, this.position.y, this.sideLength, this.sideLength)
    // let mText = muted ? "unmute (m)" : 'mute (m)'
    // text(mText, this.position.x + this.sideLength * 2, this.position.y + (3 * this.sideLength)/2)
    fill('#333')
    stroke('#333')
    rect(this.position.x + 9 * this.sideLength / 4, this.position.y + 3 * this.sideLength / 8, this.sideLength / 4, this.sideLength / 4)
    triangle(this.position.x + 9 * this.sideLength / 4, this.position.y + this.sideLength / 2, this.position.x + 10 * this.sideLength / 4, this.position.y + this.sideLength / 4, this.position.x + 10 * this.sideLength / 4, this.position.y + 3 * this.sideLength / 4)
    noFill();
    arc(this.position.x + 2.575 * this.sideLength, this.position.y + this.sideLength / 2, this.sideLength / 4, this.sideLength / 4, -HALF_PI, HALF_PI)
    arc(this.position.x + 2.575 * this.sideLength, this.position.y + this.sideLength / 2, this.sideLength / 2, this.sideLength / 2, -HALF_PI, HALF_PI)
    if (muted) {
      strokeWeight(3)
      line(this.position.x + 9 * this.sideLength / 4, this.position.y + this.sideLength / 4, this.position.x + 11 * this.sideLength / 4, this.position.y + 3 * this.sideLength / 4)
    }
  }

  pointWithinMute(mousePositionX, mousePositionY) {
    const muteLeftX = this.position.x + this.sideLength * 2
    const muteRightX = muteLeftX + this.sideLength
    const muteTopY = this.position.y
    const muteBottomY = this.position.y + this.sideLength
    if (mousePositionX >= muteLeftX && 
        mousePositionX <= muteRightX &&
        mousePositionY >= muteTopY && 
        mousePositionY <= muteBottomY) {
          return true
        }
  }
}
  
class NumberDisplay {
    constructor(x, y, height) {
      this.position = {
        x: x,
        y: y
      }
      this.height = height
      this.width = height / 2.5
      this.lineWidth = this.width / 3
    }
  
    zeroPad(num, places) {
      return String(num).padStart(places, '0')
    }
  
    drawNumber(number, numInSequence) {
      fill('#FFF')
      stroke('#FFF')
      strokeWeight(1)
      const numInt = parseInt(number)
      const x = this.position.x + (numInSequence * (2.75 * this.width))
  
      // top square
      if ([0, 2, 3, 5, 7, 8, 9].includes(numInt)) {
        // top line
        rect(x, this.position.y, this.width + this.lineWidth, this.lineWidth)
      }
  
      if ([2, 3, 4, 5, 6, 8, 9].includes(numInt)) {
        // bottom line
        rect(x, this.position.y + this.width, this.width, this.lineWidth)
      }
      if ([0, 4, 5, 6, 8, 9].includes(numInt)) {
        // left line
        rect(x, this.position.y, this.lineWidth, this.width)
      }
      if ([0, 1, 2, 3, 4, 7, 8, 9].includes(numInt)) {
        // right line
        rect(x + this.width, this.position.y, this.lineWidth, this.width + this.lineWidth)
      }
  
      // bottom rectangle
      if ([0, 2, 3, 5, 6, 8].includes(numInt)) {
        // bottom line
        rect(x, this.position.y + this.height, this.width + this.lineWidth, this.lineWidth)
      }
      if ([0, 2, 6, 8].includes(numInt)) {
        // left line
        rect(x, this.position.y + this.width, this.lineWidth, this.width * 1.5)
      }
      if ([0, 1, 3, 4, 5, 6, 7, 8, 9].includes(numInt)) {
        // right line
        rect(x + this.width, this.position.y + this.width, this.lineWidth, this.width * 1.5 + this.lineWidth)
      }
    }
  
    draw(numberInt, numDigits) {
      const numberString = this.zeroPad(numberInt, numDigits)
      const numberStringArr = numberString.split("");
      const that = this;
      for (let i = 0; i < numberStringArr.length; i++) {
        let num = numberStringArr[i]
        that.drawNumber(num, i)
      }
    }
}
  
class ScoreBoard {
    constructor() {
      this.score = 0
      this.level = 1
    }
  
    zeroPad(num, places) {
      return String(num).padStart(places, '0')
    }
  
    reset() {
      this.score = 0
      this.level = 1
    }
  
    updateScore(inc) {
      this.score += inc
    }
  
    updateLevel() {
      this.level += 1
    }
}

function setupBlocks() {
    const rowBlocks = 14;
    const rowBlocksPlusWalls = rowBlocks + 2;
    for (let row = 1; row < 7; row++) {
      for (let col = 0; col < rowBlocks; col++) {
        const w = (width / rowBlocksPlusWalls)
        const block = new Block(w, (col + 1) * w, row)
        blocks.push(block)
      }
    }
}

function checkPaddleCollision() {
    const ballX = ball.position.x
    const ballY = ball.position.y
    const playerX = player.position.x
    const playerY = player.position.y
  
    if (ballY + ball.height >= playerY && ballY <= playerY + player.height) {
      if (ballX + ball.width >= playerX && ballX <= playerX + player.width) {
        playSound('mid')
        return true
      }
    }
    return false
}
  
function updateScore(blockRow) {
    if (blockRow === 4 || blockRow === 3) {
      scoreboard.updateScore(4)
    } else if (blockRow === 2 || blockRow === 1) {
      scoreboard.updateScore(7)
    } else {
      scoreboard.updateScore(1)
    }
}
  
function checkBlocksCollision(ball) {
    let blockCollision = false
    for (let b = 0; b < blocks.length; b++) {
      if (!blocks[b].collidedWith) {
        let didCollide = blocks[b].checkBlockCollision(ball)
        if (didCollide) {
          playSound('low');
          blockCollision = true
          updateScore(blocks[b].row)
          if (blocks[b].row < 4 && !ball.speededUp) {
            ball.speedUp();
          }
          break;
        }
      }
    }
    return blockCollision
}

function draw() {
    // scoreboard area
    noStroke()
    fill('#1a1a1c') // very dark gray color
    rect(0, 0, width, width / 16);

    fill('#1a1a1c') // very dark gray color
    rect(0, width / 16, width, height - width / 16)

    // left wall
    fill('#8d8d8d') // dark gray color
    rect(0, width / 16, width / 16, height - (width / 16))

    fill('#00a280') // weird teal block on the bottom left in the atari game
    rect(0, height - height / 25, width / 16, height / 25)

    // right wall
    fill('#8d8d8d') // dark gray color
    rect(width - width / 16, width / 16, width / 16, height - (width / 16))

    fill('#d92f3f') // weird red block on the bottom right in the atari game
    rect(width - width / 16, height - (height / 25), width / 16, height / 25)

    // ceiling
    fill('#8d8d8d') // dark gray color
    rect(0, width/16, width, width / 16);

    player.draw()
    ball.draw()

    if (!paused) {
      player.update(ball.hasHitTop)
      ball.update()
    }

    blocks.forEach((b) => {
        if (!b.collidedWith) {
            b.draw()
        }
    })

    if (blocks.length > 0 && blocks.every(b => b.collidedWith === true)) {
        if (ball.position.y > height / 2) {
            scoreboard.updateLevel();
            blocks.forEach((b) => {
                b.collidedWith = false
            })
        }
    }
    
    optionsMenu.drawPausePlayButton()
    const mouseWithinMute = optionsMenu.pointWithinMute(mouseX, mouseY)
    const mouseWithinPausePlay = optionsMenu.pointWithinPausePlay(mouseX, mouseY)
    if (mouseWithinMute || mouseWithinPausePlay) {
      cursor(HAND)
    } else {
      cursor(ARROW)
    }
    optionsMenu.drawMuteButton()
    scoreDisplay.draw(scoreboard.score, 3)
    livesLostDisplay.draw(ball.livesLost, 1)
    levelDisplay.draw(scoreboard.level, 1)
  
    if (!gameStarted) {
      fill('#FFF')
      textSize(20)
      textAlign(CENTER);
      text("Click to start", width / 2, 5 * height / 8)
    }


    ballDidCollideWithPaddle = checkPaddleCollision()
    ballDidCollideWithABlock = checkBlocksCollision(ball)

    if (ballDidCollideWithPaddle) {
      ball.position.y = player.position.y - ball.height
      ball.velocity.y *= -1

      const diff = ball.position.x - player.position.x
      const radianValOne = 210 * (Math.PI/180);
      const radianValTwo = 330 * (Math.PI/180);
      const angle = mapValue(diff, 0, player.width, radianValOne, radianValTwo)
      ball.updateAngle(angle);
    }

    if (ballDidCollideWithABlock) {
      ball.velocity.y *= -1
    }

    if (keys.left.pressed && player.position.x > width / 16) {
      player.velocity.x = -Math.abs(ball.speed)
    } else if (keys.right.pressed && player.position.x + player.width < width - (width / 16)) {
      player.velocity.x = Math.abs(ball.speed)
    } else {
      player.velocity.x = 0
    }
}

const keys = {
    left: {
        pressed: false
    },
    right: {
        pressed: false
    }
}

function keyPressed() { // arrows and 'wasd'
    if (keyCode === LEFT_ARROW || keyCode === 65) { // 65 is the 'a' key
      keys.left.pressed = true
    } else if (keyCode === RIGHT_ARROW || keyCode === 68) { // 68 is the 'd' key
      keys.right.pressed = true
    }
    if (keyCode === 80) {
      paused = !paused
    }
    if (keyCode === 77) {
      muted = !muted
    }
    return false
}

function keyReleased() {
    if (keyCode === LEFT_ARROW || keyCode === 65) {
        keys.left.pressed = false
    } else if (keyCode === RIGHT_ARROW || keyCode === 68) {
        keys.right.pressed = false
    }
    return false;
}

function touchStarted() {
  const mouseWithinMute = optionsMenu.pointWithinMute(mouseX, mouseY)
  const mouseWithinPausePlay = optionsMenu.pointWithinPausePlay(mouseX, mouseY)
  if (!(mouseWithinMute || mouseWithinPausePlay) && gameStarted) {
    if (mouseX > player.position.x) {
      keys.right.pressed = true
    } else if (mouseX < player.position.x) {
      keys.left.pressed = true
    }
  }
}

function mousePressed() {
  const mouseWithinMute = optionsMenu.pointWithinMute(mouseX, mouseY)
  const mouseWithinPausePlay = optionsMenu.pointWithinPausePlay(mouseX, mouseY)
  if (!(mouseWithinMute || mouseWithinPausePlay) && gameStarted) {
    if (mouseX > player.position.x) {
      keys.right.pressed = true
    } else if (mouseX < player.position.x) {
      keys.left.pressed = true
    }
  }
}

function touchEnded() {
  const mouseWithinMute = optionsMenu.pointWithinMute(mouseX, mouseY)
  const mouseWithinPausePlay = optionsMenu.pointWithinPausePlay(mouseX, mouseY)
  if (mouseWithinMute) {
    muted = !muted
  }
  if (mouseWithinPausePlay) {
    paused = !paused
  }
  keys.left.pressed = false
  keys.right.pressed = false
  if (!gameStarted) {
    gameStarted = true
    paused = false
  }
}

function mouseReleased() {
  const mouseWithinMute = optionsMenu.pointWithinMute(mouseX, mouseY)
  const mouseWithinPausePlay = optionsMenu.pointWithinPausePlay(mouseX, mouseY)
  if (mouseWithinMute) {
    muted = !muted
  }
  if (mouseWithinPausePlay) {
    paused = !paused
  }
  keys.left.pressed = false
  keys.right.pressed = false
  if (!gameStarted) {
    gameStarted = true
    paused = false
  }
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/addons/p5.sound.min.js