The Canvas was added to allow JavaScript to have direct access to the bitmap of the screen in a web page. It shorts out all the text, images, etc... and just lets your code draw on the screen. 2D graphics are the obvious application, but with a little help from WebGL, 3D graphics are also very possible. Apple, Inc came up with it in 2004 for their Safari browser and it became a standard in 2005. It is supported right from the start in Chrome and Safari, and in current versions of all other browsers.
To use it, in the HTML page, just add a canvas tag. You can specify a size which is in pixels on the screen, and probably want to give it an id so your code can find it.
<canvas height="400" width="600" id="screen">This browser doesn't support canvas.</canvas>
The text between the tags won't display if the browser understands a canvas. By itself, the canvas just sets aside some space on the page. To talk to it, you need to get it's "context".
const canvas = document.getElementById("screen") const ctx = canvas.getContext("2d")
A graphics context is a way of looking at a section of the screen. You can look at in different ways; with different points of view. Most of the time, we want the "2d" context, but we could get the "webgl" context for 3D work or "imageBitmap" (which is good for displaying camera images).
To better see the canvas, we can fill it, or part of it with some style, like a solid color.
ctx.fillStyle = "black"
let p1={x:10, y:200, w:20, h:75} //w is the width, h is height. ctx.fillRect(p1.x, p1.y, p1.w, p1.h)
Note that we are in the 4th quadrant: X is the distance from the left edge. Y is the distance down from the top.
And we can draw stuff on there as well.
let b={x:300, y:200, r:10} ctx.beginPath()
ctx.arc(b.x, b.y, b.r, 0, Math.PI*2, false)
//draw arc from 0 to Math.PI*2 (0-360 degrees), clockwize
ctx.closePath()
ctx.fill()
p1.msg = "1:0"
ctx.font = "50px fantasy"
ctx.fillText(p1.msg, p1.x, 40)
If we have a bunch of points, we can draw pretty much anything at any size
function drawPath(pts, x, y, size, fill_style, stroke_style) { ctx.beginPath() ctx.moveTo(pts[0].x*size + x, pts[0].y*size + y) for (let i = 1; i<pts.length; ++i) { ctx.lineTo(pts[i].x*size + x, pts[i].y*size + y) } ctx.closePath() ctx.fillStyle = fill_style ctx.fill() ctx.strokeStyle = stroke_style ctx.stroke() //outlines the shape } let points = [ {x:10,y:10}, {x:15,y:10}, {x:15,y:15}, {x:20,y:20}, {x:15,y:20}, {x:15,y:15}, {x:10,y:15} ] drawPath(points, 10, 50, 2, "rgb(255,0,0)", "rgb(0,0,255)")
Animation is nothing more than drawing them in once place, erasing them, and drawing them in a new place for people to look at for a little while. We know how to draw things, and we can change where we draw them, how do we erase things and how do we pause?
Erasing is pretty easy: Just draw a rectangle over the entire canvas.
ctx.fillStyle = "black"
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height)
Pausing for a bit is also easy. JavaScript has a great command:
let interval =
setInterval(function_def,
milliseconds)
Note that "function_def" here is not a call to a function, but a reference
to a functions code directly or via it's name. E.g. if we want a function
called "render" to run every 100ms then we say setInterval(render,
100) instead of saying setInterval(render(),
100) because "()" calls the function, and we need setInterval
to do the call, when the time has passed. We aren't calling the function,
we are telling setInterval what function to call.
So, all we need to do is create a function that will...
and then we call that function every 100 ms or so. That is 10 FPS (Frames per Second).
const fps = 10 // Frames Per Second (10 is enough to start) let ms = 1000/fps // Milliseconds between frames let p2={x:ctx.canvas.width-30, y:200, w:20, h:75, dy:5} function render() { //erase everything ctx.fillStyle = "black" ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height) //re-draw everything ctx.fillStyle = "white" ctx.fillRect(p2.x, p2.y, p2.w, p2.h) } function move() { p2.y += p2.dy if (p2.y < 0 | p2.y + p2.h > ctx.canvas.height) { p2.dy *= -1 } } function frame(){ move() render() } let interval = setInterval(frame, ms)
Of course, that isn't very object oriented. p2 should know how to move itself. And we should only have to call the move() method of each object in the move program. We can move the move code into the objects move method, and we can do that for each object. And we might as well move in the draw stuff as well:
let p2 = { x:ctx.canvas.width-30, y:200, w:20, h:75, dy:5, move: function() { this.y += this.dy if (this.y < 0 | this.y + this.h > ctx.canvas.height) { this.dy *= -1 } }, draw: function() { ctx.fillStyle = "white" ctx.fillRect(this.x, this.y, this.w, this.h) } }
And then in the move function:
function move() { p2.move() b.move() //etc... }
And in the render function:
function erase() { ctx.fillStyle = "black" ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height) } function render() { erase() //re-draw everything p2.move() b.move() //etc... }
This is nice for more complex animations with a bunch of different things moving in different ways. Each sort of thing can have it's own way of moving, it's own way of being drawn, and the code for it is just right there in the object definition. As long as we call every objects move method every frame, we are good to go. In fact... we could just make an array of all the objects, and then iterate the array.
let stuff = [p1, p2, b] function move() { for (let i = 0; i < stuff.length; ++i) { stuff[i].move() } }
And, of course, the same sort of thing can be used in render.
We can go faster to get a smoother animation, but depending on how many things we are drawing, and how big they are, we may run out of processor cycles between frames. We can reduce the frame rate to compensate, but a more common issue is that the computer will get busy doing something totally unrelated (checking for updates, etc...) and that will cause a momentary glitch during just one or 2 frames. We may ask the computer to call update every 100 ms, but it might actually call it 110 ms later, and then 90ms after that. This can make the movement we calculate be wrong and look jerky. One way to compensate for that is to record the time when we do a frame, then subtract that from the next time, and use this actual interval to decide how much to move stuff. But that's pretty complex and may not be needed. Modern browsers implement this with the requestAnimationFrame command^
This video series does a great job of explaining it all and showing
how to make some really nice effects with it. Be sure to watch all 4
episodes.
See also: