Stacking states with Canvas
Introduction
This is the first post of a series about HTML5 Canvas. In this article I will explain the meaning of context.save() and context.restore(). Simply said, these methods are reponsible for stacking the contexts states. But what does it mean?
What are states in Canvas?
First, it is necessary to know, what states are. The simplified answer is: Everything that does not draw!The Canvas API provides a set of methods which can be distinguished between drawing methods and auxiliary methods. A good part (not all) of these auxiliary methods are used to define the appearance of drawn areas, and/or paths. For example, strokeStyle, fillStyle, globalAlpha, lineWidth, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, font, textAlign, textBaseline etc. are all methods which modify states. Also considered as a state is the transformation matrix, which is modified by the methods translate, rotate, scale, setTransform. Another kind of state is a defined clipping region, modified by clip; Everything modified by these methods are states, which can be stacked.
What can I do with stacking?
Obviously, it is easy to recover already set states by simply popping it from the stack, because sometimes it is quite cumbersome to define a proper state. This also keeps your code cleaner. Stacking can even improve runtime performance, as demonstrated here at JsPerf. Another important advantages is the "isolatation" of state dependent operations. In the next paragraph I'll explain this concept more precisely.
Isolation of state dependent operations
Using state stacking you can isolate some and group other state operations quite easily. Imagine a car whose wheels shall rotate, while the car is moving forward. You can isolate the rotation of the wheels during their painting by stacking the "translation matrix" and apply the rotation. Afterwards, you restore the "translation matrix" and paint the next frame. The following snippet demonstrate this principle by transforming texts. Here is the visual result. And here comes the code.function main(){ var context = document.getElementById('myCanvas').getContext('2d'); var painter = new Painter(context); painter.setFillColor(255,0,0,1); painter.drawText("Text 1", 50); painter.pushState(); painter.rotate(320, 100, 45); painter.setFillColor(0,0,255,1); painter.drawText("Text 2", 100); painter.popState(); painter.drawText("Text 3", 150); }
function Painter(ctx){ var context = ctx; var DEG2RAD = Math.PI/180.0; var center = {}; var init = function(ctx){ context = ctx; center[0] = context.canvas.width/2; center[1] = context.canvas.height/2; }; this.pushState = function(){ context.save(); }; this.popState = function(){ context.restore(); }; this.rotate = function(posX, posY, angle){ context.translate(posX, posY); context.rotate(angle * DEG2RAD); context.translate(-posX, -posY); }; this.setFillColor = function(r,g,b,a){ context.fillStyle = "rgba(" + r + "," + g + "," + b + "," + a +")"; }; this.drawText = function(text, ypos){ context.save(); context.font = "30px Arial"; context.textAlign = "center"; context.fillText(text, center[0], ypos); context.restore(); }; init(ctx); }