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

后端 未结 2 563
自闭症患者
自闭症患者 2021-02-07 04:46

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 i

相关标签:
2条回答
  • 2021-02-07 05:21

    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.

    0 讨论(0)
  • 2021-02-07 05:28

    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/

    0 讨论(0)
提交回复
热议问题