Saturday, October 17, 2015

Moving objects in html5 canvas with Scala.js

First version, bars of constant size:

package example

import scala.scalajs.js
import scala.scalajs.js.annotation.JSExport
import org.scalajs.dom
import org.scalajs.dom.html
import scala.util.Random

case class Point(x: Int, y: Int) {
  def +(p: Point) = Point(x + p.x, y + p.y)

  def /(d: Int) = Point(x / d, y / d)
}

@JSExport
object ScalaJSExample {
  @JSExport
  def main(canvas: html.Canvas): Unit = {
    /*setup*/
    val renderer = canvas.getContext("2d")
      .asInstanceOf[dom.CanvasRenderingContext2D]

    canvas.width = canvas.parentElement.clientWidth
    canvas.height = 400

    renderer.font = "50px sans-serif"
    renderer.textAlign = "center"
    renderer.textBaseline = "middle"

    val obstacleGap = 200
    val holeSize = 50
    var frame = -50
    val obstacles = collection.mutable.Queue.empty[Int]

    def runLive(): Unit = {
      frame += 2
      println(frame)
      if(frame > 0 && frame % obstacleGap == 0) {
//        obstacles.enqueue(Random.nextInt(canvas.height - 2 * holeSize) + holeSize)
        obstacles.enqueue(canvas.height - 2 * holeSize)
      }
      if(obstacles.length > 7) {
        obstacles.dequeue()
        frame -= obstacleGap
      }
      renderer.fillStyle = "darkblue"
      for((holeY, i) <- obstacles.zipWithIndex) {
        val holeX = i * obstacleGap + canvas.width - frame
        renderer.fillRect(holeX, 0, 5, holeY - holeSize)
      }
    }

    def run(): Unit = {
      renderer.clearRect(0, 0, canvas.width, canvas.height)
      runLive()
    }
    dom.setInterval(run _, 20)
  }
}

Second version: Bars of random size, and

package example

import scala.scalajs.js
import scala.scalajs.js.annotation.JSExport
import org.scalajs.dom
import org.scalajs.dom.html
import scala.util.Random

case class Point(x: Int, y: Int) {
  def +(p: Point) = Point(x + p.x, y + p.y)

  def /(d: Int) = Point(x / d, y / d)
}

@JSExport
object ScalaJSExample {
  @JSExport
  def main(canvas: html.Canvas): Unit = {
    /*setup*/
    val renderer = canvas.getContext("2d")
      .asInstanceOf[dom.CanvasRenderingContext2D]

    canvas.width = canvas.parentElement.clientWidth
    canvas.height = 400

    renderer.font = "50px sans-serif"
    renderer.textAlign = "center"
    renderer.textBaseline = "middle"

    val obstacleGap = 200
    val holeSize = 50
    var frame = -50
    val obstacles = collection.mutable.Queue.empty[Int]

    def runLive(): Unit = {
      frame += 2
      println(frame)
      if(frame > 0 && frame % obstacleGap == 0) {
        obstacles.enqueue(Random.nextInt(canvas.height - 2 * holeSize) + holeSize)
      }
      if(obstacles.length > 7) {
        obstacles.dequeue()
        frame -= obstacleGap
      }
      renderer.fillStyle = "darkblue"
      for((holeY, i) <- obstacles.zipWithIndex) {
        val holeX = i * obstacleGap + canvas.width - frame
        renderer.fillRect(holeX, 0, 5, holeY - holeSize)
      }
    }

    def run(): Unit = {
      renderer.clearRect(0, 0, canvas.width, canvas.height)
      runLive()
    }
    dom.setInterval(run _, 20)
  }
}

Version 3: Obstacles refactored out

package example

import scala.scalajs.js
import scala.scalajs.js.annotation.JSExport
import org.scalajs.dom
import org.scalajs.dom.{CanvasRenderingContext2D, html}
import scala.util.Random

case class Point(x: Int, y: Int) {
  def +(p: Point) = Point(x + p.x, y + p.y)

  def /(d: Int) = Point(x / d, y / d)
}

case class Obstacle(gap: Int, holeSize: Int, renderer: CanvasRenderingContext2D, width: Int, height: Int) {
  val obstacles = collection.mutable.Queue.empty[Int]
  var frame = 0
  def render(): Unit = {
    frame += 1
    if(frame > 0 && frame % gap == 0) {
      obstacles.enqueue(Random.nextInt(height - 2 * holeSize) + holeSize)
    }
    if(obstacles.length > 7) {
      obstacles.dequeue()
      frame -= gap
    }
    renderer.fillStyle = "darkblue"
    for((holeY, i) <- obstacles.zipWithIndex) {
      val holeX = i * gap + width - frame
      renderer.fillRect(holeX, 0, 5, holeY - holeSize)
      renderer.fillRect(holeX, holeY, 5, height - holeY)
    }
  }
}

@JSExport
object ScalaJSExample {
  @JSExport
  def main(canvas: html.Canvas): Unit = {
    /*setup*/
    val renderer: CanvasRenderingContext2D = canvas.getContext("2d")
      .asInstanceOf[dom.CanvasRenderingContext2D]

    canvas.width = canvas.parentElement.clientWidth
    canvas.height = 400

    renderer.font = "50px sans-serif"
    renderer.textAlign = "center"
    renderer.textBaseline = "middle"

    val obstacle = new Obstacle(200, 10, renderer, canvas.parentElement.clientWidth, 400)

    def run(): Unit = {
      renderer.clearRect(0, 0, canvas.width, canvas.height)
      obstacle.render()
    }
    dom.setInterval(run _, 20)
  }
}

Version 4: Adding a player

package example

import scala.scalajs.js
import scala.scalajs.js.annotation.JSExport
import org.scalajs.dom
import org.scalajs.dom.{CanvasRenderingContext2D, html}
import scala.util.Random

case class Point(x: Int, y: Int) {
  def +(p: Point) = Point(x + p.x, y + p.y)

  def /(d: Int) = Point(x / d, y / d)
}


@JSExport
object ScalaJSExample {
  @JSExport
  def main(canvas: html.Canvas): Unit = {
    /*setup*/
    val renderer: CanvasRenderingContext2D = canvas.getContext("2d")
      .asInstanceOf[dom.CanvasRenderingContext2D]

    canvas.width = canvas.parentElement.clientWidth
    canvas.height = 400

    renderer.font = "50px sans-serif"
    renderer.textAlign = "center"
    renderer.textBaseline = "middle"

    case class Obstacle(gap: Int, holeSize: Int) {
      val obstacles = collection.mutable.Queue.empty[Int]
      var frame = 0
      def render(): Unit = {
        frame += 1
        if(frame > 0 && frame % gap == 0) {
          obstacles.enqueue(Random.nextInt(canvas.height - 2 * holeSize) + holeSize)
        }
        if(obstacles.length > 7) {
          obstacles.dequeue()
          frame -= gap
        }
        renderer.fillStyle = "darkblue"
        for((holeY, i) <- obstacles.zipWithIndex) {
          val holeX = i * gap + canvas.width - frame
          renderer.fillRect(holeX, 0, 5, holeY - holeSize)
          renderer.fillRect(holeX, holeY, 5, canvas.height - holeY)
        }
      }
    }

    case class Player(x: Int, y: Int, v: Int, gravity: Int) {
      def isDead = false
      def render(): Unit = {
        renderer.fillStyle = "darkgreen"
        renderer.fillRect(x - 5, y - 5, 10, 10)
      }
    }

    val obstacle = new Obstacle(200, 50)
    val player = new Player(canvas.width / 2, canvas.height / 2, 0, 1)

    def run(): Unit = {
      renderer.clearRect(0, 0, canvas.width, canvas.height)
      obstacle.render()
      player.render()
    }
    dom.setInterval(run _, 20)
  }
}

Version 5: Player responsive to click

package example

import scala.scalajs.js
import scala.scalajs.js.annotation.JSExport
import org.scalajs.dom
import org.scalajs.dom.{CanvasRenderingContext2D, html}
import scala.util.Random

case class Point(x: Int, y: Int) {
  def +(p: Point) = Point(x + p.x, y + p.y)

  def /(d: Int) = Point(x / d, y / d)
}


@JSExport
object ScalaJSExample {
  @JSExport
  def main(canvas: html.Canvas): Unit = {
    /*setup*/
    val renderer: CanvasRenderingContext2D = canvas.getContext("2d")
      .asInstanceOf[dom.CanvasRenderingContext2D]

    canvas.width = canvas.parentElement.clientWidth
    canvas.height = 400

    renderer.font = "50px sans-serif"
    renderer.textAlign = "center"
    renderer.textBaseline = "middle"

    case class Obstacle(gap: Int, holeSize: Int) {
      val obstacles = collection.mutable.Queue.empty[Int]
      var frame = 0
      def render(): Unit = {
        frame += 1
        if(frame > 0 && frame % gap == 0) {
          obstacles.enqueue(Random.nextInt(canvas.height - 2 * holeSize) + holeSize)
        }
        if(obstacles.length > 7) {
          obstacles.dequeue()
          frame -= gap
        }
        renderer.fillStyle = "darkblue"
        for((holeY, i) <- obstacles.zipWithIndex) {
          val holeX = i * gap + canvas.width - frame
          renderer.fillRect(holeX, 0, 5, holeY - holeSize)
          renderer.fillRect(holeX, holeY, 5, canvas.height - holeY)
        }
      }
    }

    class Player(var x: Double, var y: Double, var v: Double, val gravity: Double) {
      def isDead = false
      def render(): Unit = {
        renderer.fillStyle = "darkgreen"
        renderer.fillRect(x - 5, y - 5, 10, 10)
      }
    }

    val obstacle = new Obstacle(200, 50)
    val player = new Player(canvas.width / 2, canvas.height / 2, 0, 0.1)

    def run(): Unit = {
      renderer.clearRect(0, 0, canvas.width, canvas.height)
      obstacle.render()
      player.y += player.v
      player.v += player.gravity
      player.render()
    }
    dom.setInterval(run _, 20)
    canvas.onclick = (e: dom.MouseEvent) => {
      player.v -= 2
    }
  }
}

Version 6: Collision detection

Separating obstacles and player code is actually not a good idea, it makes the interaction logic very cumbersome…

package example

import scala.scalajs.js
import scala.scalajs.js.annotation.JSExport
import org.scalajs.dom
import org.scalajs.dom.{CanvasRenderingContext2D, html}
import scala.util.Random

case class Point(x: Int, y: Int) {
  def +(p: Point) = Point(x + p.x, y + p.y)

  def /(d: Int) = Point(x / d, y / d)
}


@JSExport
object ScalaJSExample {
  @JSExport
  def main(canvas: html.Canvas): Unit = {
    /*setup*/
    val renderer: CanvasRenderingContext2D = canvas.getContext("2d")
      .asInstanceOf[dom.CanvasRenderingContext2D]

    canvas.width = canvas.parentElement.clientWidth
    canvas.height = 400

    renderer.font = "50px sans-serif"
    renderer.textAlign = "center"
    renderer.textBaseline = "middle"

    case class Obstacle(gap: Int, holeSize: Int) {
      val obstacles = collection.mutable.Queue.empty[Int]
      var frame = 0
      def render(): Unit = {
        frame += 1
        if(frame > 0 && frame % gap == 0) {
          obstacles.enqueue(Random.nextInt(canvas.height - 2 * holeSize) + holeSize)
        }
        if(obstacles.length > 7) {
          obstacles.dequeue()
          frame -= gap
        }
        renderer.fillStyle = "darkblue"
        for((holeY, i) <- obstacles.zipWithIndex) {
          val holeX = i * gap + canvas.width - frame
          renderer.fillRect(holeX, 0, 5, holeY - holeSize)
          renderer.fillRect(holeX, holeY, 5, canvas.height - holeY)
        }
      }
    }

    class Player(var x: Double, var y: Double, var v: Double, val gravity: Double) {
      def render(): Unit = {
        renderer.fillStyle = "darkgreen"
        renderer.fillRect(x - 5, y - 5, 10, 10)
      }
    }

    val obstacle = new Obstacle(200, 50)
    val player = new Player(canvas.width / 2, canvas.height / 2, 0, 0.1)

    def isPlayerDead(player: Player): Boolean = {
      for((holeY, i) <- obstacle.obstacles.zipWithIndex) {
        val holeX = i * obstacle.gap + canvas.width - obstacle.frame
        if(math.abs(holeX - canvas.width / 2) < 5 &&  math.abs(holeY - player.y) > obstacle.holeSize) {
          return true
        }
      }
      if(player.y < 0 || player.y > canvas.height) {
        return true
      }
      false
    }

    def run(): Unit = {
      renderer.clearRect(0, 0, canvas.width, canvas.height)
      obstacle.render()
      if(! isPlayerDead(player)) {
        player.render()
      } else {
        renderer.clearRect(0, 0, canvas.width, canvas.height)
        renderer.fillStyle = "darkred"
        renderer.fillText("Game Over", canvas.width/2, canvas.height/2)
      }
      player.y += player.v
      player.v += player.gravity
    }
    dom.setInterval(run _, 20)
    canvas.onclick = (e: dom.MouseEvent) => {
      player.v -= 2
    }
  }
}

0 comments: