Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the purpose of Canvas.Context Save and Restore in this example?

This page shows some animations in HTML5 canvas. If you look at the source of the scroller, there's a statement to save the context after clearing the rectangle and restoring it after the animation. If I substitute the restore statement with another ctx.clearRect(0, 0, can.width, can.height statement, nothing works. I thought the restore is restoring the cleared rectangle but it seems its restoring more info. What's that extra info that's needed for the next frame?

I am not looking for HTML5 textbook definitions of Save and Restore but I want to understand why they are needed in this specific example.

UPDATE

It's frustrating to get an answer where I specifically already mentioned in the question I don't want to get the definitions of save() and restore(). I already know Save() saves the state of the context and Restor()e restores it. My question is very specific. Why is restore() used in the manner in the example when all the Save did is saved an empty canvas. Why is restoring an empty canvas not the same as clearing it?

like image 629
Tony_Henrich Avatar asked May 06 '13 23:05

Tony_Henrich


Video Answer


2 Answers

Canvas state isn't what's drawn on it. It's a stack of properties which define the current state of the tools which are used to draw the next thing.

Canvas is an immediate-mode bitmap. Like MS Paint. Once it's there, it's there, so there's no point "saving" the current image data, because that would be like saving the whole JPEG, every time you make a change, every frame...

...no, the state you save is the state which will dictate what coordinate-orientation, dimension-scale, colour, etc, you use to draw the NEXT thing (and all things thereafter, until you change those values by hand).

var canvas = document.createElement("canvas"),
    easel  = canvas.getContext("2d");

easel.fillStyle = "rgb(80, 80, 120)";
easel.strokeStyle = "rgb(120, 120, 200)";

easel.fillRect(x, y, width, height);
easel.strokeRect(x, y, width, height);

easel.save();  // stores ALL current status properties in the stack

easel.rotate(degrees * Math.PI / 180); // radians
easel.scale(scale_X, scale_Y); // any new coordinates/dimensions will now be multiplied by these
easel.translate(new_X, new_Y); // new origin coordinates, based on rotated orientation, multiplied by the scale-factor

easel.fillStyle = "gold";
easel.fillRect(x, y, width, height); // completely new rectangle
// origin is different, and the rotation is different, because you're in a new coordinate space

easel.clearRect(0, 0, width, height); // not even guaranteed to clear the actual canvas, anymore
easel.strokeRect(width/2, height/2, width, height); // still in the new coordinate space, still with the new colour


easel.restore(); // reassign all of the previous status properties
easel.clearRect(0, 0, width, height);

Assuming that you were only one state-change deep on the stack, that last line, now that your canvas' previous state was restored, should have successfully cleared itself (subpixel shenanigans notwithstanding).

So as you can see, it has very, VERY little to do with erasing the canvas.
In fact, it has nothing to do with erasing it, at all.

It has to do with wanting to draw something, and doing the basic outlining and sweeping colours/styles, and then manually writing in the colours for the smaller details on top, and then manually writing all of the styles back the way they were before, to go back to sweeping strokes for the next object, and on and on...

Instead, save general states that will be reused, create a new state for smaller details, and return to the general state, without having to hard-code it, every time, or write setter functions to set frequently-used values on the canvas over and over (resetting scale/rotation/affine-transforms/colours/fonts/line-widths/baseline-alignment/etc).

In your exact example, then, if you're paying attention, you'll see that the only thing that's changing is the value of step.

They've set the state of a bunch of values for the canvas (colour/font/etc).
And then they save. Well, what did they save?
You're not looking deep enough. They actually saved the default translation (ie: origin=0,0 in original world-space).
But you didn't see them define it?
That's because it's defined as default.

They then increase the step 1 pixel (actually, they do this first, but it doesn't matter after the first loop -- stay with me here).
Then they set a new origin point for 0,0 (ie: from now on, when they type 0,0 that new origin will point to a completely different place on the canvas).

That origin point is equal to x being the exact middle of the canvas, and y being equal to the current step (ie: pixel 1 or pixel 2, etc... and why the difference between starting at 0 and starting at 1 really doesn't matter).

Then what do they do?
They restore.

Well, what have they restored?
...well, what have they changed?

They're restoring the point of origin to 0,0
Why?

Well, what would happen if they didn't?
If the canvas is 500px x 200px, and it starts at 0,0 in our current screen space... ...that's great...
Then they translate it to width/2, 1
Okay, so now when they ask to draw text at 0,0 they'll actually be drawing at 250, 1

Wonderful. But what happens next time?

Now they're translating by width/2, 2
You think, well, that's fine... ...the draw call for 0,0 is going to happen at 250, 2, because they've set it to clear numbers: canvas.width/2, 2

Nope. Because current 0,0 is actually 250,1 according to our screen. And one translation is relative to its previous translation...

...so now you're telling the canvas to start at it's current-coordinates' 0,0 and go left 250, and down 2.
According to the screen (which is like a window, looking at the map, and not the map, itself) we're now 500px to the right, and 3 pixels down from where we started... And only one frame has gone by.

So they restore the map's coordinates to be the same origin as the screen's coordinates (and the rotation to be the same, and the scale, and the skew, etc...), before setting the new one.

And as you might guess, by looking at it, now, you can see that the text should actually move top to bottom. Not right to left, like the page says...

Why do this?
Why go to the trouble of changing the coordinate-system of the drawing-context, when the draw commands give you an x and y right there in the function?

If you want to draw a picture on the canvas, and you know how high and wide it is, and where you'd like the top-left corner to be, why can't you just do this:

easel.drawImage(myImg, x, y, myImg.width, myImg.height);

Well, you can.
You can totally do that. There's nothing stopping you.

In fact, if you wanted to make it zoom around the screen, you could just update the x and y on a timer, and call it a day.

But what about if you were drawing a game character? What if the character had a hat, and had gloved hands, and big boots, and all of those things were drawn separate from the character?

So first you'd say "well, he's standing at x and y in the world, so x plus where his hand is in relation to his body would be x + body.x - hand.x...or was that plus..."

...and now you have draw calls for all of his parts that are all looking like a notebook full of Grade 5 math homework.

Instead, you can say: "He's here. Set my coordinates so that 0,0 is right in the middle of my guy". Now your draw calls are as simple as "My right hand is 6 pixels to the right of the body, my left hand is 3 pixels to the left".

And when you're done drawing your character, you can set your origin back to 0,0 and then the next character can be drawn. Or, if you want to attempt it, you can then translate from there to the origin of the next character, based on the delta from one to the other (this will save you a function call per translation). And then, if you only saved state once the whole time (the original state), at the end, you can return to 0,0 by calling .restore.

like image 67
Norguard Avatar answered Oct 09 '22 09:10

Norguard


The context save() saves stuff like transformation color among other stuff. Then you can change the context and restore it to have the same as when you saved it. It works like a stack so you can push multiple canvas states onto the stack and recover them. http://html5.litten.com/understanding-save-and-restore-for-the-canvas-context/

like image 1
aaronman Avatar answered Oct 09 '22 09:10

aaronman