Web Based Programming - CMP4011A
Dr. David Greenwood
canvas
elementrequestAnimationFrame
Eloquent JavaScript has a chapter on the canvas element.
<canvas>
A canvas is a single DOM element that contains a image.
<canvas width="150" height="150"></canvas>
<canvas width="150" height="150">
display this text if the browser
does not support HTML5 canvas
</canvas>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
<title>HTML Canvas</title>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
The Canvas API provides a means for drawing graphics using JavaScript and the <canvas>
DOM element.
We can use the canvas for:
The <canvas>
element creates a fixed-size drawing surface that exposes a rendering context.
We will use the 2d
rendering context.
There is also a 3D rendering context: WebGL
This has many powerful features, including access to the graphics hardware, and openGL like shaders.
We will not cover the 3D context in this lecture.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
You create a context with the getContext method on the <canvas>
DOM element.
Access the Canvas API via the ctx
object.
You should inspect the context object in the console.
console.log(ctx)
console.log(ctx)
You will see current values for all the attributes, and if you expand the CanvasRenderingContext2D
field you will see the many methods available.
The rendering context has a coordinate system which, by default, places the origin at the top left corner of the canvas.
Each unit of length is 1 pixel.
Canvas supports two primitive shapes: rectangles and paths.
A shape can be filled, meaning its area is given a certain colour or pattern, or it can be stroked, which means a line is drawn along its edge.
There are three functions that draw rectangles on the canvas:
fillRect(x, y, width, height)
strokeRect(x, y, width, height)
clearRect(x, y, width, height)
fillRect(x, y, width, height)
strokeRect(x, y, width, height)
clearRect(x, y, width, height)
The parameters are the same for all three functions:
x, y
define the top left cornerThe colour of the fill, thickness of the stroke, and so on, are not determined by an argument to the drawing method, but by properties of the context object.
ctx.fillStyle = "red";
fillStyle
defines the fill appearance.ctx.strokeStyle = "blue";
ctx.lineWidth = 5;
strokeStyle
specifies the colour of a stroked line.lineWidth
property.lineWidth
may be any positive number.const x = y = 75
const w = h = 250
ctx.fillStyle = "red"
ctx.strokeStyle = "blue"
ctx.fillRect(x, y, w, h)
ctx.strokeRect(x, y, w, h)
A path is a sequence of points, connected by segments of lines that can be of different shapes, of different width and of different colour.
It is possible to build any complex shape using a combination of the path tools.
Paths are not values that can be stored and passed around.
lineTo
starts at the path’s current position.moveTo
.When filling a path:
If the path is not already closed, a line is added from its end to its start.
The shape enclosed by the now completed path is filled.
ctx.fillStyle = "red"
ctx.beginPath()
ctx.moveTo(75, 200)
ctx.lineTo(300, 375)
ctx.lineTo(300, 25)
ctx.fill()
A path may also contain curved lines.
Complex curves and shapes can be drawn using Bezier and quadratic curves. We wont cover these functions for now.
quadraticCurveTo()
bezierCurveTo()
But you should know that they are available.
To draw circle segments we use the arc functions.
arc(x, y, radius, startAngle, endAngle, counterclockwise)
arcTo(x1, y1, x2, y2, radius)
ctx.fillStyle = "red"
ctx.arc(200, 200, 150, 0, Math.PI * 2)
ctx.fill()
The canvas rendering context provides two methods to render text:
fillText(text, x, y [, maxWidth])
strokeText(text, x, y [, maxWidth])
const text = "Hello World!"
const x = 15, y = 200
ctx.fillStyle = "red"
ctx.strokeStyle = "blue"
ctx.font = '72px serif'
ctx.fillText(text, x, y)
ctx.strokeText(text, x, y)
Images for computer graphics are usually in one of two categories:
So far we have been working with vector graphics - where we have specified shapes with lines and curves.
Bitmap graphics don’t specify shapes but work with pixel data.
Pixel data defines values on a regular 2D grid.
The drawImage()
method allows us to draw pixel data onto a canvas.
This pixel data can originate from an element or from another canvas.
let img = document.createElement("img")
img.src = "img.png"
However, if we just call drawImage()
, it is unlikely to display the image as we expect.
let img = document.createElement("img")
img.src = "img.png"
ctx.drawImage(img, 0, 0)
Why is this?
It is essential to ensure the image resource is loaded before drawing.
const canvas = document.getElementById("canvas")
const ctx = canvas.getContext("2d")
let img = document.createElement("img")
img.src = "img.png"
img.addEventListener("load", () => {
ctx.drawImage(img, 0, 0)
});
In addition to the previous example, the drawImage()
method can take two further arguments:
drawImage(image, dx, dy, dWidth, dHeight)
The drawImage()
method also has a nine argument version which lets us specify the source rectangle:
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
s*
define the source rectangle.d*
define the destination rectangle.Sprites are two-dimensional images included in a larger scene.
Storing all the image frames in a single file is often preferred for compression efficiency.
The ability to select a source rectangle allows us to render a section of the entire sheet.
41,0,40,29
200,100,120,87
consider this source code:
const sw = 40, sh = 29
const dw = 120, dh = 87
sprite.addEventListener('load', () => {
ctx.drawImage(sprite, 0, 0, sw, sh, 50, 100, dw, dh)
ctx.drawImage(sprite, 41, 0, sw, sh, 200, 100, dw, dh)
})
what does this code do?
We have cut out two regions of the sprite sheet and placed them on the canvas.
Here is the Idea…
Draw one image, then draw another image in the same place.
requestAnimationFrame()
requestAnimationFrame()
A callback is a function passed as an argument to another function.
You will write the callback function:
function myCallBack(timestamp) {
console.log(timestamp)
}
requestAnimationFrame(myCallBack)
You will notice that we get only one value printed to console.
requestAnimationFrame()
again to get the next value.We do this using recursion
function myCallBack(timestamp) {
console.log(timestamp)
requestAnimationFrame(myCallBack)
}
myCallBack()
requestAnimationFrame()
into our callbackWe make a call to our function, to start the recursion.
function myCallBack(timestamp) {
console.log(timestamp)
requestAnimationFrame(myCallBack)
}
myCallBack()
How often does the callback function get called?
1396.32
1412.986
1429.652
1446.318
1462.984
1479.65
1496.316
Often we want to do something after a period of time has passed.
let prevTime = 0
function myCallBack(timestamp) {
if (timestamp - prevTime > 500) {
prevTime = timestamp
console.log(timestamp)
}
requestAnimationFrame(myCallBack)
}
myCallBack()
Now we get this sort of output:
514.689
1031.335
1547.981
2064.627
2581.273
3097.919
3598.031
Instead of logging to console, we could draw our image on the canvas.
Store some global variables.
let prevTime = 0
let frame = 0
Write a draw function.
function draw(frame, x, y) {
let sx = 41
if (frame === 0) sx = 0
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(sprite, sx, 0, 40, 29, x, y, 120, 87)
}
Finally, we call our animate function.
function animate(timestamp) {
if (timestamp - prevTime > 500) {
prevTime = timestamp
frame = (frame + 1) % 2
}
draw(frame, 100, 50)
requestAnimationFrame(animate)
}
animate()
We now have our sprite’s frames drawn alternately.
Formally, an event is a message sent from the browser to a JavaScript function, for example:
Informally, we can describe events in our animation that require some sort of response, such as collision detection in a game.
const KEYS = {}
document.addEventListener("keydown", (event) => {
KEYS[event.code] = event.type === "keydown"
})
document.addEventListener("keyup", (event) => {
KEYS[event.code] = event.type === "keydown"
})
To check if a key is pressed, we can read the KEYS
object.
if (KEYS['ArrowLeft']) x -= 1
if (KEYS['ArrowRight']) x += 1
Often we want to know if two objects are touching, or overlapping.
What is an Axis Aligned Bounding Box (AABB)?
An AABB is the smallest rectangle that encloses an object and is aligned with the axes of the coordinate system.
compare two objects with x, y, width and height properties
function AABB(a, b) {
if (a.x > b.x + b.w) return false
if (a.x + a.w < b.x) return false
if (a.y > b.y + b.h) return false
if (a.y + a.h < b.y) return false
return true
}
You could consider a variation of this method to check if an object is within the bounds of the canvas.
Radial collision detection uses Pythagoras’ theorem to determine if two objects are touching.
If the squared sum of the radii is greater than the squared distance between the centres, then the objects are colliding.
compare two objects with x, y and radius properties
function radial(a, b) {
let radii = a.radius + b.radius
let dx = a.x - b.x
let dy = a.y - b.y
return radii * radii > dx * dx + dy * dy
}
Once we have detected a collision, we can respond…
canvas
elementrequestAnimationFrame
See you in the labs!