How to restore a canvas to status saved in another

2019-06-02 00:22发布

问题:

I want to save the status of a canvas in a function draw1(), and restore it in another function draw2(). So I wrote following code. But it doesn't work.

<canvas id="canvas" style="width:500px; height:500px;" height="500" width="500"></canvas>
ctx = document.querySelector('#canvas').getContext('2d');
function draw1(){
   ctx.save();
   ctx.fillRect(25,25,100,100);
}
function draw2(){
   ctx.restore();
}
draw1();
draw2();

I guess the reason is that the status is saved in the call stack. So after the function returns, the saving state is also cleared.

Is there other way to implement my need?

UPD: The background is that I want to implement a simple animation, most of which are static. I expect to use setInterval() to execute a drawing function draw(). In draw(), restore the canvas back first and draw the remaining dynamic part.

回答1:

If I understand right you only need to draw some static objects once and then draw the animated objects each frame.

First off all, you completely misunderstood the save and restore methods and Michael Geary showed you why. Also markE teaches you the toDataURL method for take snapshots of your canvas at any time and save into an image object. It's a powerful feature, but isn't what you really want for simple animations.

So, how do I create animations with static and dynamic objects?

How to create animations with static and dynamic objects

There's two main options:

  1. Using one single canvas and draw all objects (static and dynamic) each frame of your animation, which probably is not the best for you since most of your objects are static.
  2. Have a canvas for static objects and another canvas for dynamic objects. Using this technic you only need to draw the static objects once and forget it there (no need for "restore" your canvas) and we perform the animations (drawing dynamic objects each frame) in a separated canvas.

I think the best for you will be option 2. Ok, but how we setup those canvases?

Using multiple canvases as layers

Use CSS to set all the canvases to an absolute position of (0,0) inside our parent div tag.

Also use CSS to set the z-index of our canvases. The z-index property specifies the stack order of an element. Items with lower z-index values go behind items with higher z-index values.

Now that we properly define our canvases, let's play!

Demo

I made a jsFiddle to show you how to accomplish the desired animations.

Check the Fiddle

and the code used in that fiddle:

HTML:

<div id="canvasesdiv">
    <canvas id="static" width=400 height=400>This text is displayed if your browser does not support HTML5 Canvas</canvas>
    <canvas id="dynamic" width=400 height=400>This text is displayed if your browser does not support HTML5 Canvas</canvas>
</div>

CSS:

#canvasesdiv {
    position:relative;
    width:400px;
    height:300px;
}
#static {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 1;
}
#dynamic {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 2;
}

Javascript:

// static canvas
var static = document.getElementById("static");
var staticCtx = static.getContext("2d");

// dynamic canvas
var dynamic = document.getElementById("dynamic");
var dynamicCtx = dynamic.getContext("2d");

// animation status
var FPS = 30;
var INTERVAL = 1000 / FPS;

// our background
var myStaticObject = {
    x: 0,
    y: 0,
    width: static.width,
    height: static.height,
    draw: function () {
        staticCtx.fillStyle = "rgb(100, 100, 0)";
        staticCtx.fillRect(0, 0, static.width, static.height);
    }
};

// our bouncing rectangle
var myDynamicObject = {
    x: 30,
    y: 30,
    width: 50,
    height: 50,
    gravity: 0.98,
    elasticity: 0.90,
    friction: 0.1,
    velX: 10,
    velY: 0,
    bouncingY: false,
    bouncingX: false,
    draw: function () {   // example of dynamic animation code
        // clear the last draw of this object
        dynamicCtx.clearRect(this.x - 1, this.y - 1, this.width + 2, this.height + 2);            
        // compute gravity
        this.velY += this.gravity;
        // bounce Y
        if (!this.bouncingY && this.y >= dynamic.height - this.height) {
            this.bouncingY = true;
            this.y = dynamic.height - this.height;
            this.velY = -(this.velY * this.elasticity);
        } else {
            this.bouncingY = false;
        }
        // bounce X
        if (!this.bouncingX && (this.x >= dynamic.width - this.width) || this.x <= 0) {
            this.bouncingX = true;
            this.x = (this.x < 0 ? 0 : dynamic.width - this.width);
            this.velX = -(this.velX * this.elasticity);
        } else {
            this.bouncingX = false;
        }
        // compute new position
        this.x += this.velX;
        this.y += this.velY;            
        // render the object
        dynamicCtx.fillStyle = "rgb(150, 100, 170)";
        dynamicCtx.fillRect(this.x, this.y, this.width, this.height);
    }
};

function drawStatic() {
    myStaticObject.draw();
    // you can add more static objects and draw here
}

function drawDynamic() {        
    myDynamicObject.draw();
    // you can add more dynamic objects and draw here
}

function animate() {
    setInterval(function () {
        // only need to redraw dynamic objects
        drawDynamic();
    }, INTERVAL);
}

drawStatic(); // draw the static objects
animate(); // entry point for animated (dynamic) objects


回答2:

You can save and reload the pixels on the canvas by using canvas.toDataURL()

Here's the save:

dataURL=canvas.toDataURL();

Here's the reload:

var image=new Image();
image.onload=function(){
    ctx.drawImage(image,0,0);
}
image.src=dataURL;

If you need to save the context attributes (fillStyle, etc), you'll have to save those in an object and reload them into the context when you reload the pixels.

If you need to save the transforms then you will have to create a transform matrix (an array of 6 numerics) Then you will need to track each transform you do by manipulating your transform matrix. See this blog post: http://blog.safaribooksonline.com/2012/04/26/html5-canvas-games-tracking-transformation-matrices/

Here's code and a Fiddle: http://jsfiddle.net/m1erickson/btmLE/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>

<style>
    body{ background-color: ivory; }
    canvas{border:1px solid red;}
</style>

<script>
$(function(){

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");

    ctx.fillRect(25,25,100,100);

    var dataURL;

    $("#save").click(function(){
        dataURL=canvas.toDataURL();
        ctx.clearRect(0,0,canvas.width,canvas.height);
    });

    $("#reload").click(function(){
        var image=new Image();
        image.onload=function(){
            ctx.drawImage(image,0,0);
        }
        image.src=dataURL;
    });

}); // end $(function(){});
</script>

</head>

<body>
    <canvas id="canvas" width=300 height=300></canvas><br>
    <button id="save">Save</button>
    <button id="reload">Reload</button>
</body>
</html>


回答3:

No, .save() and .restore() do not save the canvas state in the JavaScript call stack. Returning from a JavaScript function won't affect that state—it's saved completely outside that world, in the canvas itself.

But I think you may be expecting these functions to do something other than what they actually do.

Here's a fiddle with your code.

It has a black rectangle, as expected from the .fillRect() call.

Were you thinking that the .restore() call would make the black rectangle go away? That's not what the function does. It doesn't restore the canvas bitmap to its previous state, only other canvas settings such as the clipping region, the stroke and fill styles, etc.

Here's an article that explains some of this.

If you want to save the actual bitmap you'll need to use other means to do that, perhaps using .getImageDataHD() and .setImageDataHD() - I'm not sure off the top of my head what would be best.