Events attached to object inside canvas - javascript

I have simply canvas code which draw rect on the canvas
var x=document.getElementById("canvas");
var ctx=x.getContext("2d");
ctx.rect(20,20,150,100);
ctx.stroke();
is it possible to add eventListener on said rect? For example, if I click on rect, it will turn red.

Regions
Depending on how well you want to support various and older browsers, there is addHitRegion() that you can use by enabling it through flags in Firefox and Chrome (at the moment of this being written):
Firefox: about:config -> search "hitregions" and set to true
Chrome: chrome://flags/ -> Enable experimental canvas features
This is the only technique that integrates directly with the event system. I would not recommend it for production quite yet though, and AFAIK there is not a polyfill for it either - but to show how easy it is to use:
var x=document.getElementById("canvas");
var ctx=x.getContext("2d");
ctx.rect(20,20,150,100);
ctx.addHitRegion({id: "demo"}); // enable in flags in Chrome/Firefox
ctx.stroke();
x.addEventListener("click", function(e) {
if (e.region && e.region === "demo") alert("Hit!");
})
<canvas id="canvas"></canvas>
Path: isPointInPath
The other techniques require one to manually implement a mechanism for hit-detection. One is by using isPointInPath(). You simply rebuild the paths you want to test, one by one, then run your (adjusted) x/y mouse coordinate against it:
var x=document.getElementById("canvas");
var ctx=x.getContext("2d");
generatePath();
ctx.stroke();
x.addEventListener("click", function(e) {
var r = this.getBoundingClientRect(),
x = e.clientX - r.left,
y = e.clientY - r.top;
// normally you would loop through your paths:
generatePath();
if (ctx.isPointInPath(x, y)) alert("Hit!");
})
function generatePath() {
ctx.beginPath(); // reset path
ctx.rect(20,20,150,100); // add region to draw/test
}
<canvas id="canvas"></canvas>
Path: Path2D objects
For the latter example there is also the new Path2D objects which can hold a path on their own - the advantage here is that you don't need to rebuild the paths, just pass in the path object with x/y to the isPointInPath() method.
The problem is that Path2D is not supported in all browsers yet, but there is this polyfill that will fix that for you,
var x=document.getElementById("canvas");
var ctx=x.getContext("2d");
var path1 = new Path2D();
path1.rect(20,20,150,100); // add rect to path object
ctx.stroke(path1);
x.addEventListener("click", function(e) {
var r = this.getBoundingClientRect(),
x = e.clientX - r.left,
y = e.clientY - r.top;
// normally you would loop through your paths objects:
if (ctx.isPointInPath(path1, x, y)) alert("Hit!");
})
<canvas id="canvas"></canvas>
Manually check boundary
And of course, there is the old technique of using manual boundary checks. This will work in all browsers. Here the advisable thing to do is to create objects that holds the bounds and can also be used to render it. This typically limits you to rectangular areas - more complex shapes will require more complex algorithms (such as the isPointInPath() embeds).
var x=document.getElementById("canvas");
var ctx=x.getContext("2d");
ctx.rect(20,20,150,100);
ctx.stroke();
x.addEventListener("click", function(e) {
var r = this.getBoundingClientRect(),
x = e.clientX - r.left,
y = e.clientY - r.top;
// normally you would loop through your region objects:
if (x >= 20 && x < 20+150 && y >= 20 && y < 20+100) alert("Hit!");
})
<canvas id="canvas"></canvas>

Shapes and paths are drawn to the canvas as side-effects, so there is no element to add an event listener to; you could, however, add an event listener to the entire canvas or to an element that shares a location with the canvas, and when it is clicked then redraw the canvas with the rectangle, but red (or anything else changed). (make sure to clear the canvas before redrawing it with the .clearRect() method).

If you draw something to a canvas, the shape that is drawn is not a javascript object, but rather changes the particular state that the canvas is in. Therefore, you cannot attach an event listener to it, and should instead attach the event to the canvas itself.
Your javascript could then check the co-ordinates of the click, and find whether or not it is inside the rectangle. Bear in mind that if you draw something on top of the rectangle or shape, the code will have to be adjusted to check the new area formed. You might also find it difficult to check the area if it is not a rectangle, but it will still be possible.
If you want to redraw the rectangle as red, you should repaint the canvas, changing the colour of the new rectangle that you redraw (the rectangle is not an object, so you cannot change the colour directly). This would also involve repainting all the other shapes on the canvas.

Related

Is it possible to detect if a mouse is over a text in html5 canvas? [duplicate]

I am building a web application that draws a set of letters in different fonts on an HTML 5 Canvas using fillText. The user will click somewhere on that canvas and I need to check which letter they clicked on (or if they clicked on a letter at all).
I think I will need to:
Get the vector path for each letter (I have no clue how to do this).
Check if the click point is inside the letter path using some simple collision-detection algorithm.
Is there some easy function to do this that I am missing? Or maybe a library for things like this? If there aren't any libraries, how do I get the path for the letter in a specific font to do the checking myself?
I need to use the actual shape of the letter and not just its bounding box as I don't want the user to be able to click in the middle of an O and it register as a hit.
Any hints in this direction would be appreciated.
Logic
You can't handle separate letters on canvas without providing custom logic for it. Everything drawn to the canvas is merged to a soup of pixels.
And unfortunately you can't add text as pure path so you will have to check pixel values. Otherwise you could simply add the text to a new path and use the isPointInPath method for each letter.
One approach
We can't provide full solutions here on SO but here is a basis you can hopefully built on top of to provide basic logic to click single letters on a canvas:
Each letter is stored as object incl. its position, size, font and char, but also with a rectangle hit region (see below)
Define an array with these objects and then pass them to a render function
When you register a click iterate through the array and test against the rectangular hit-region and if inside check the pixel (*)
*) To distinguish between overlapping letters you need to check by priority. You can also render this char onto a separate canvas so you get pixels of only this char. I am not showing this in the demo but you'll get the idea.
Demo
var ltrs = []; /// stores the letter objects
/// Create some random objects
for(;i < 20; i++) {
/// build the object
var o = {char: alpha[((alpha.length - 1) * Math.random())|0],
x: ((w - 20) * Math.random())|0,
y: ((h - 20) * Math.random())|0,
size: (50 * Math.random() + 16)|0,
font: fonts[((fonts.length - 1) * Math.random())|0]};
/// store other things such as color etc.
/// store it in array
ltrs.push(o);
}
Then we have some function to render these characters (see demo).
When we then handle clicks we iterate through the object array and check by boundary first to check what letter we are at (picking just a pixel here won't enable us to ID the letter):
demo.onclick = function(e) {
/// adjust mouse position to be relative to canvas
var rect = demo.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top,
i = 0, o;
/// iterate
for(;o = ltrs[i]; i++) {
/// is in rectangle? "Older" letters has higher priority here...
if (x > o.x && x < (o.x + o.rect[2]) &&
y > o.y && y < (o.y + o.rect[3])) {
/// it is, check if we actually clicked a letter
/// This is what you would adopt to be on a separate canvas...
if (checkPixel(x, y) === true) {
setLetterObject(o, '#f00')
return;
}
}
}
}
The pixel check is straight forward, it picks a single pixel at x/y position and checks its alpha value (or color if you use solid backgrounds):
function checkPixel(x, y) {
var data = ctx.getImageData(x, y, 1, 1).data;
return (data[3] !== 0);
}
CLICK HERE FOR ONLINE DEMO
Updated check pixel function:
This updated check is capable of checking letters even if they are overlapping in the same region.
We create a separate canvas to draw the letter in. This isolates the letter and when we pick a pixel we can only get a pixel from that specific letter. It also doesn't matter what background color is as we our off-screen canvas only set pixels for the letter and not background during the check. The overhead is minimal.
function checkPixel(o, x, y) {
/// create off-screen canvas
var oc = document.createElement('canvas'),
octx = oc.getContext('2d'),
data,
oldX = o.x,
oldY = o.y;
/// default canvas is 300x150, adjust if letter size is larger *)
//oc.width = oc.height = 200;
/// this can be refactored to something better but for demo...
o.x = 0;
o.y = 0;
setLetterObject(octx, o, '#000');
o.x = oldX;
o.y = oldY;
data = octx.getImageData(x - oldX, y - oldY, 1, 1).data;
return (data[3] !== 0);
}
*) When we create a canvas the default size is 300x150. To avoid re-allocating a new bitmap we just leave it as it is as the memory is already allocated for it and we only need to pick a single pixel from it. If letters has larger pixel size than the default size we will of course need to re-allocate to make the letter fit.
In this demo we temporary override the x and y position. For production you should enable the setLetterObject method to somehow override this as that would be more elegant. But I'll leave it as-is here in the demo as the most important thing is to understand the principle.
I'd say that the best option is to actually use pixels that by the way are the most accurate thing you can do (remember that the user is seeing pixels when clicking, nothing more).
Since you cannot just use the color directly (because there can be many text objects with the same color (and may be also other primitives with the same color) you could use instead a separate "pick" canvas for that.
Basically when you draw your objects on the main canvas on the repaint function you also draw them in another hidden canvas with the exact same size, but you draw them using a unique color for each entity.
You can therefore have up to 16 millions entity (24-bit) on the canvas and know instantly which one was clicked by keeping a map between color code and the entity itself. By the way this sort of map is often used in CAD applications exactly to speed up picking.
The only somewhat annoying part is that there's no portable way to disable antialiasing when drawing on a canvas so it's possible that the color you'll get back from the pick canvas is not one of the colors you used or, even worse, it's possible that by clicking on the border of an entity a different unrelated entity is considered selected.
This should be a very rare event unless your display is really really crowded and picking is basically random anyway.

Text Collision Detection

I am building a web application that draws a set of letters in different fonts on an HTML 5 Canvas using fillText. The user will click somewhere on that canvas and I need to check which letter they clicked on (or if they clicked on a letter at all).
I think I will need to:
Get the vector path for each letter (I have no clue how to do this).
Check if the click point is inside the letter path using some simple collision-detection algorithm.
Is there some easy function to do this that I am missing? Or maybe a library for things like this? If there aren't any libraries, how do I get the path for the letter in a specific font to do the checking myself?
I need to use the actual shape of the letter and not just its bounding box as I don't want the user to be able to click in the middle of an O and it register as a hit.
Any hints in this direction would be appreciated.
Logic
You can't handle separate letters on canvas without providing custom logic for it. Everything drawn to the canvas is merged to a soup of pixels.
And unfortunately you can't add text as pure path so you will have to check pixel values. Otherwise you could simply add the text to a new path and use the isPointInPath method for each letter.
One approach
We can't provide full solutions here on SO but here is a basis you can hopefully built on top of to provide basic logic to click single letters on a canvas:
Each letter is stored as object incl. its position, size, font and char, but also with a rectangle hit region (see below)
Define an array with these objects and then pass them to a render function
When you register a click iterate through the array and test against the rectangular hit-region and if inside check the pixel (*)
*) To distinguish between overlapping letters you need to check by priority. You can also render this char onto a separate canvas so you get pixels of only this char. I am not showing this in the demo but you'll get the idea.
Demo
var ltrs = []; /// stores the letter objects
/// Create some random objects
for(;i < 20; i++) {
/// build the object
var o = {char: alpha[((alpha.length - 1) * Math.random())|0],
x: ((w - 20) * Math.random())|0,
y: ((h - 20) * Math.random())|0,
size: (50 * Math.random() + 16)|0,
font: fonts[((fonts.length - 1) * Math.random())|0]};
/// store other things such as color etc.
/// store it in array
ltrs.push(o);
}
Then we have some function to render these characters (see demo).
When we then handle clicks we iterate through the object array and check by boundary first to check what letter we are at (picking just a pixel here won't enable us to ID the letter):
demo.onclick = function(e) {
/// adjust mouse position to be relative to canvas
var rect = demo.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top,
i = 0, o;
/// iterate
for(;o = ltrs[i]; i++) {
/// is in rectangle? "Older" letters has higher priority here...
if (x > o.x && x < (o.x + o.rect[2]) &&
y > o.y && y < (o.y + o.rect[3])) {
/// it is, check if we actually clicked a letter
/// This is what you would adopt to be on a separate canvas...
if (checkPixel(x, y) === true) {
setLetterObject(o, '#f00')
return;
}
}
}
}
The pixel check is straight forward, it picks a single pixel at x/y position and checks its alpha value (or color if you use solid backgrounds):
function checkPixel(x, y) {
var data = ctx.getImageData(x, y, 1, 1).data;
return (data[3] !== 0);
}
CLICK HERE FOR ONLINE DEMO
Updated check pixel function:
This updated check is capable of checking letters even if they are overlapping in the same region.
We create a separate canvas to draw the letter in. This isolates the letter and when we pick a pixel we can only get a pixel from that specific letter. It also doesn't matter what background color is as we our off-screen canvas only set pixels for the letter and not background during the check. The overhead is minimal.
function checkPixel(o, x, y) {
/// create off-screen canvas
var oc = document.createElement('canvas'),
octx = oc.getContext('2d'),
data,
oldX = o.x,
oldY = o.y;
/// default canvas is 300x150, adjust if letter size is larger *)
//oc.width = oc.height = 200;
/// this can be refactored to something better but for demo...
o.x = 0;
o.y = 0;
setLetterObject(octx, o, '#000');
o.x = oldX;
o.y = oldY;
data = octx.getImageData(x - oldX, y - oldY, 1, 1).data;
return (data[3] !== 0);
}
*) When we create a canvas the default size is 300x150. To avoid re-allocating a new bitmap we just leave it as it is as the memory is already allocated for it and we only need to pick a single pixel from it. If letters has larger pixel size than the default size we will of course need to re-allocate to make the letter fit.
In this demo we temporary override the x and y position. For production you should enable the setLetterObject method to somehow override this as that would be more elegant. But I'll leave it as-is here in the demo as the most important thing is to understand the principle.
I'd say that the best option is to actually use pixels that by the way are the most accurate thing you can do (remember that the user is seeing pixels when clicking, nothing more).
Since you cannot just use the color directly (because there can be many text objects with the same color (and may be also other primitives with the same color) you could use instead a separate "pick" canvas for that.
Basically when you draw your objects on the main canvas on the repaint function you also draw them in another hidden canvas with the exact same size, but you draw them using a unique color for each entity.
You can therefore have up to 16 millions entity (24-bit) on the canvas and know instantly which one was clicked by keeping a map between color code and the entity itself. By the way this sort of map is often used in CAD applications exactly to speed up picking.
The only somewhat annoying part is that there's no portable way to disable antialiasing when drawing on a canvas so it's possible that the color you'll get back from the pick canvas is not one of the colors you used or, even worse, it's possible that by clicking on the border of an entity a different unrelated entity is considered selected.
This should be a very rare event unless your display is really really crowded and picking is basically random anyway.

HTML5 Canvas typewriter effect with word wrapping?

I'm trying to get an animated typewriter effect on a HTML5 Canvas but I'm really struggling with Word Wrapping.
Here's my Shapes.js in a Gist: https://gist.github.com/Jamesking56/0d7df54473085b3c5394
In there, I've created a Text object which has lots of methods. One of which is called typeText().
typeText() basically starts off the typewriting effect but it keeps on falling off the edge and I'm really struggling to find a way of fixing word wrapping.
Can anybody guide me on whats the best way to do this?
A solution I have used is roughly:
var maxWidth = 250;
var text = 'lorem ipsum dolor et sit amet...';
var words = text.split(' ');
var line = [words[0]]; //begin with a single word
for(var i=1; i<words.length; i++){
while(ctx.measureText(line.join(' ')) < maxWidth && i<words.length-1){
line.push(words[i++]);
}
if(i < words.length-1) { //Loop ended because line became too wide
line.pop(); //Remove last word
i--; //Take one step back
}
//Ouput the line
}
Since there seems to be no way to measure the height of the output text, you need to manually offset each line you output by some hardcoded line height.
I couldn't figure out how your Text constructor works. So I wrote an independent function (not belonging to any object). All you need to pass to this function is a string, the X and Y position, the line height and padding. It assumes the canvas to be assigned to a variable canvas and the 2d context to be assigned to variable ctx.
var canvas, ctx;
function typeOut(str, startX, startY, lineHeight, padding) {
var cursorX = startX || 0;
var cursorY = startY || 0;
var lineHeight = lineHeight || 32;
padding = padding || 10;
var i = 0;
$_inter = setInterval(function() {
var w = ctx.measureText(str.charAt(i)).width;
if(cursorX + w >= canvas.width - padding) {
cursorX = startX;
cursorY += lineHeight;
}
ctx.fillText(str.charAt(i), cursorX, cursorY);
i++;
cursorX += w;
if(i === str.length) {
clearInterval($_inter);
}
}, 75);
}
Check out the demo here.
Tips-
I was going through your code and found something link this-
function Rectangle(x,y,width,height,colour) {
//properties
this.draw = function() { //Don't assigns object methods through constructors
ctx.fillStyle = this.colour;
ctx.fillRect(this.x, this.y, this.width, this.height);
};
}
You shouldn't add methods to an object through the constructor as it creates the method every time the object is instantiated. Rather add methods to object's prototype, this way, they will be created only once and will be shared by all the instances. Like-
function Rectangle(x,y,width,height,colour) {
//properties
}
Rectangle.prototype.draw = function() {
ctx.fillStyle = this.colour;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
Also, I found that you are creating extra variables to point to some objects. Like -
var that = this;
var fadeIn = setInterval(function() {
//code
that.draw();
//code
}, time);
You shouldn't create an extra reference to the Text object. Use bind method so that this will point to the Text object. Like-
var fadeIn = setInterval(function() {
//code
this.draw(); // <-- Check this it is `this.draw` no need for `that.draw`
//code
}.bind(this), time); //bind the object
Read more about bind here.
Hope that helps.
PS: 75 millisecond for each character, that will be holy 800 characters per minute !!!
Update - If you want scalable graphics you should consider SVG. Canvas is raster-based, whereas SVG is vector-based. Which means SVG can easily be resized whereas when you resize a canvas the content of the canvas will start to pixelate and look fuzzy. Read more.
To resize the content of a canvas you need to repaint the whole canvas. Whenever something is drawn on a canvas, the browser draws and forgets about it. So if you want to change the size/position of the object you'll need to clear the canvas completely and redraw the object. In your case, you'll need to change the ctx.font according to the canvas's size and then update the canvas, which is going to be a really tedious task.
<canvas> is not designed for text handling. It is much, much, more easier doing this by overlaying transparent DOM elements on the top of your <canvas>. This, naturally, assumes what you don't need to post-process text pixels in <canvas>. Otherwise you need to re-implement the whole text handling stuff in Javascript and that's kind of reinventing the wheel, because browsers can simply do it for us.
Here are more information how to perform it using CSS position: absolute and position: relative.
Allowing the user to type text in a HTML5 Canvas game
I think you should have a DOM element which has all text with transparent colour, each letter wrapped with a <span>. Then you just start making those <span>s visible by adjusting opacity. This way the letter positions would not change.
Personally, I always use bitmap fonts when I need text in my canvas games. It has a couple of advantages:
FillText on canvas is slow. Bitmap fonts are much faster.
You know the width of each letter making wrapping and text alignment much easier.
You still get the benefit of selecting font color by using an cases compositing operations, and font size by dynamically adjusting the draw width height.

KineticJS click detection inside animated shapes

OK, I admit I tried to be clever: I thought if I overrode Shape's drawFunc property I could simply draw whatever inside a rectangle and still use KineticJS's click detection. Here's my attempt:
var shape = new Kinetic.Shape({
drawFunc: function(context) {
var id = 26; // Id of a region inside composite image.
context.beginPath();
context.rect(0, 0, w, h);
context.closePath();
this.fill(context);
this.stroke(context);
context.drawImage(copyCanvas, (id % 8) * w, flr(id / 8) * h,
w, h, 0, 0, w / 2, h / 2);
},
draggable: true
});
So, the idea was to draw a rectangle, and use drawImage() to draw something on top of the rectangle (like a texture, except it changes from time to time because copyCanvas itself changes). All the meanwhile, I expected event handling (drag-n-drop, in particular) to still 'just work'. Well, here's what happens: the part of the rectangle not covered by my drawImage() correctly detects clicks. However, the one fourth of the rectangle that is covered by the image refuses to respond to clicks! Now, my question is why? I dug into the KineticJS code, and looked to me that click detection simply means drawing to a buffer and seeing if a given x, y point has non-zero alpha. I can't see how this could be affected by my drawing an image on top of my rectangle.
Any ideas what's going on?
OK, so I went ahead and looked at the source code. Here's the definitive answer:
KineticJS assigns a random and unique RGB color to each shape that's created using a global map from RGB colors to shape objects. The draw() function of the shape is called twice: once with the 'real' canvas, and once with a 'buffer' canvas used for hit detection. When using the 'buffer' canvas, KineticJS switches the stroke and fill colors to the unique RGB color of the given shape. The same 'buffer' canvas is used for all shapes on a layer. Thus hit detection simply becomes reading the RGB value of a given point and looking up the corresponding shape in the global map. Now, in my example I drew an image in a way that circumvented KineticJS's juggling of colors used for hit detection. Thus, when I clicked on the image area, KineticJS saw some unknown RGB color on the buffer canvas with no known shape assigned to it.
The solution is not to draw the image for the 'buffer' (or 'hit detection') phase: a simple rectangle will do. In case you're wondering, here's the correct code for the drawFunc:
var width = 200;
var height = 100;
var myShape = new Kinetic.Shape({
drawFunc: function(context) {
if (layer.bufferCanvas.context == context) {
context.beginPath();
context.rect(0, 0, width, height);
context.closePath();
this.fill(context);
this.stroke(context);
} else {
context.drawImage(someCanvasWithAnythingOnIt, 0, 0, width, height,
0, 0, width, height);
}
}});
Can I collect my own reward?
I think your problem lies in the order. There is a depth associated with each object that you draw and the default ordering is like a stack, last drawn is on top.
Now that you have modified the code, making 2 draws inside the shape draw function, I still think the ordering is preserved and hence, the object is not able to detect the input. Try changing the order, i.e. draw image first and then the rectangle and see if the problem is solved.
Else, share a jsFiddle for an example.

Apply existing reference to CanvasRendingContext2D to an element

I am trying to store a canvas reference in a global object and then apply that reference to an element instead of regenerating the canvas. here is my existing code. I hope that makes sense. thanks in advance!
waveformCache is assumed to be a global
var cL = document.getElementById('track' + trackId + 'WaveformL');
var cR = document.getElementById('track' + trackId + 'WaveformR');
if (waveformCache.hasOwnProperty(track.path))
{
var waveformCacheItem = waveformCache[track.path];
if (waveformCacheItem.hasOwnProperty('left'))
{
// restore canvas data here to cL element
}
}
else
{
waveformCache[track.path] = {};
var left = track.data.getChannelData(0);
var ctx1 = cL.getContext('2d');
ctx1.save();
ctx1.strokeStyle = 'rgb(49,73,11)';
ctx1.translate(0, 55/2); //centers where the line drawing starts horizontally
for(var i = 0; i < left.length; i += 200) {
var x1 = Math.floor(track.waveformLength * i / left.length); //first parameter affects the length of the drawn waveform #ZOOM
var y1 = left[i] * 55/2;
ctx1.beginPath();
ctx1.moveTo(x1, 0);
ctx1.lineTo(x1 + 1, y1);
ctx1.stroke();
}
ctx1.restore();
waveformCache[track.path].left = ctx1;
}
An outline of how to serialize an html5 canvas CanvasRendingContext2D
The canvas context (CanvasRendingContext2D ) holds the canvas' properties (styling, current transformation, etc).
Important! The context does not hold all the executed drawing commands that created the canvas content.
Context Properties:
Coloring: strokeStyle, fillStyle(1), globalAlpha,
Line styles: lineWidth, lineCap, lineJoin, miterLimit,
Text Styles: font, textAlign, textBaseline,
Compositing: globalCompositeOperation,
Shadowing: shadowColor, shadowBlur, shadowOffsetX, shadowOffsetY
(1) fillStyle is usually a string ('#ff0000'), but it can alternatively hold a reference to a gradient object or pattern object. To save the context's fillStyle, you will have to either ignore gradients / patterns or also serialize the gradient / pattern properties.
Here's how to save context properties into an object
var properties=['strokeStyle','lineWidth','font','globalAlpha',
'globalCompositeOperation','shadowColor','shadowBlur',
'shadowOffsetX','shadowOffsetY','lineCap','lineJoin',
'miterLimit','textAlign','textBaseline'];
var serializedContext={}
for(var i=0;i<properties.length;i++){
var prop=properties[i];
serializedContext[prop]=context[prop];
}
// fillStyle can be a headache
if(typeof context.fillStyle === 'string'){
serializedContext['fillStyle']=context.fillStyle;
}else{
// do lots more work to serialize gradient or pattern :-O
}
Here's how to copy saved context properties into a new context:
var context=myOtherCanvas.getContext('2d');
for(var i=0;i<properties.length;i++){
var prop=properties[i];
context[prop]=serializedContext[prop];
}
// fillStyle can be a headache
if(typeof context.fillStyle === 'string'){
serializedContext['fillStyle']=context.fillStyle;
}else{
// do lots more work to re-establish gradient or pattern :-O
}
Re-executing the drawings
If you want to re-execute all the drawings commands, you must save the commands and their arguments.
From your example code, it looks like your drawings involve line segments(moveTo & lineTo) so you can save each segment as a segment-object in an array of segment-objects.
var segments=[];
segments.push({moveX:10, moveY:20, lineX:100, lineY:35});
... and push all the other line segments
And then you can "replay" the line-segment drawing commands after you've reset all the context properties:
// redraw every line segment
ctx.beginPath()
for(var i=0;i<segments.length;i++){
var s=segments[i];
ctx.moveTo(s.moveX,s.moveY);
ctx.lineTo(s.lineX,s.lineY);
}
ctx.stroke();
You can also serialize and replay all the common drawing commands (arc, beginPath, bezierCurveTo , clearRect, clip, closePath, fill, fillRect, fillText, lineTo, moveTo, quadraticCurveTo, rect, restore, rotate, save, scale, setTransform, stroke, strokeRect, strokeText, transform, translate). Save each command name & associated arguments in an object and save all those command-objects in an array.
These commands return values so you will need to do more work to handle them:
measureText, getImageData (putImageData), toDataURL, isPointInPath, isPointInStroke, createImageData, createLinearGradient, createRadialGradient, createPattern. Luckily, these commands are used are used less often than the more common (simpler) commands.
About portability
If you use this method of saving all properties & drawing commands into object arrays, you can easily serialize them all into JSON strings with JSON.stringify and you can easily deserialize them back into object arrays with JSON.parse.
Having your canvas properties & drawing commands serialized to strings means that you can easily transport them to a server for storage and then fetch them for replaying.
You can use a Path2D object to store your paths commands at. Then store the path in your global object. When you need to re-apply the path, simply stroke or fill using the stored path object.
For example:
var path = new Path2D();
...
path.moveTo(.. , ..);
path.lineTo(.. , ..);
etc.
Later when you need to recall the path:
ctx.stroke(path);
(A bonus is that you can initialize it using SVG paths. This means you can just define your path using SVG commands and store that as a single string. Reapply using the route of Path2D at a slight cost of performance when initializing.)
Path2D can be polyfilled for browsers which do not yet support it (see notes for special cases).

Categories