Say I have a simple canvas element, and draw a complicated, resource intensive picture on it. I then draw some simple lines on the picture. Would there be a way to "save" the state of the canvas (before the lines are drawn), and then redraw the state to erase any further changes made. I did try this with save() and restore() but I don't think the state for that includes the current shapes on the canvas. See my demo below.
var canvas = document.getElementById("canvas");
var context = canvas.getContext('2d');
function init() {
// This is some computationally intensive drawing we don't want to repeat
context.fillStyle = "rgb(150,29,28)";
context.fillRect(40, 40, 255, 200);
context.fillStyle = "rgb(150,83,28)";
context.fillRect(10, 10, 50, 50);
context.fillStyle = "rgb(17,90,90)";
context.fillRect(5, 100, 200, 120);
context.fillStyle = "rgb(22,120,22)";
context.fillRect(200, 200, 90, 90);
// Now we save the state so we can return to it
saveState();
}
function lines() {
// This is some drawing we will do and then want to get rid of
context.beginPath();
context.moveTo(125, 125);
context.lineTo(150, 45);
context.lineTo(200, 200);
context.closePath();
context.stroke();
}
function saveState() {
//copy the data into some variable
}
function loadState() {
//load the data from the variable and apply to canvas
}
init();
#canvas {
border: 1px solid #000;
}
<canvas id="canvas" width="300" height="300"></canvas>
<button onClick="lines()">Draw over image</button>
<button onClick="loadState()">Restore</button>
You can easily just copy the canvas to a new one.
// canvas is the canvas you want to copy.
var canvasBack = document.createElement("canvas");
canvasBack.width = canvas.width;
canvasBack.height = canvas.height;
canvasBack.ctx = canvasBack.getContext("2d");
canvasBack.ctx.drawImage(canvas,0,0);
You treat the new canvas as if it is another image and can copy it to the original with
ctx.drawImage(canvasBack,0,0);
Rendering an image is done in hardware so can be done easily in realtime many times per frame. Because of this you can treat the canvases as layers (like photoshop) and using globalCompositeOperation create a wide range of adjustable FX.
You can convert to a dataURL but that is a much slower process and not quick enough for realtime rendering. Also keeping a copy of the DataURL string and then decoding it to an image will place a larger strain on memory than just creating a canvas copy (base64 encodes 3 bytes (24bit) in every 4 characters. As JS characters are 16 bits long storing data in base64 is very inefficient (64bits of memory used to store 24bits)
The an alternative is to store the canvas as a typed array with ctx.getImageData but this is also very slow, and can not handle realtime needs.
You can create an <img> element, call canvas.toDataURL() to store original canvas at saveState(), usecontext.clearRect()to clearcanvas,context.drawImage()to restore savedcanvas`
var canvas = document.getElementById("canvas");
var context = canvas.getContext('2d');
var _canvas;
var img = new Image;
img.width = canvas.width;
img.height = canvas.height;
function init() {
// This is some computationally intensive drawing we don't want to repeat
context.fillStyle = "rgb(150,29,28)";
context.fillRect(40, 40, 255, 200);
context.fillStyle = "rgb(150,83,28)";
context.fillRect(10, 10, 50, 50);
context.fillStyle = "rgb(17,90,90)";
context.fillRect(5, 100, 200, 120);
context.fillStyle = "rgb(22,120,22)";
context.fillRect(200, 200, 90, 90);
// Now we save the state so we can return to it
saveState(canvas);
}
function lines() {
// This is some drawing we will do and then want to get rid of
context.beginPath();
context.moveTo(125, 125);
context.lineTo(150, 45);
context.lineTo(200, 200);
context.closePath();
context.stroke();
}
function saveState(c) {
_canvas = c.toDataURL();
//copy the data into some variable
}
function loadState() {
//load the data from the variable and apply to canvas
context.clearRect(0, 0, canvas.width, canvas.height);
img.onload = function() {
context.drawImage(img, 0, 0);
}
img.src = _canvas;
}
init();
#canvas {
border: 1px solid #000;
}
<canvas id="canvas" width="300" height="300"></canvas>
<button onClick="lines()">Draw over image</button>
<button onClick="loadState()">Restore</button>
Related
I created a canvas element with a colored background, and I came up with the following code to insert an image on top of this canvas:
const canvas = document.getElementById(id);
console.log(canvas);
if (canvas.getContext) {
var context = canvas.getContext('2d');
var img = new Image();
img.src = 'https://google.com/xyz.jpg';
img.onload = function () {
context.save();
context.rect(10, 2, 10, 10);
context.clip();
context.drawImage(img, 0, 0); // image won't resize
context.restore();
};
}
Now this successfully layers the image on top of the canvas (it's cut off however), but I can't find a way to resize the image to be of a certain size (for example 10x10). I tried changing changing the second last line in my code to context.drawImage(img, 0, 0, 10, 10); but this broke my code and the image would not display on the canvas at this point. Any help would be greatly appreciated!
To load and draw an image.
const ctx = can.getContext('2d');
const img = new Image;
img.src = 'https://pbs.twimg.com/media/DyhGubJX0AYjCkn.jpg';
img.addEventListener("load", () => {
// draw image at 10, 10, width and height 50,50
ctx.drawImage(img, 10, 10, 50, 50); // resizes image
}, {once: true});
canvas {
background: green;
}
<canvas id="can" width="200" height = "200"></canvas>
I am working on an app which uses canvas. I draw some shapes, one over another which can be filled with colours or images and overlap each other. I use clip() to clip images to fit shape, but when I change globalCompositeOperation to multiply it makes clip() stop working. I've created a simple example to present what my problem is.
Please try to open it in Google Chrome and in Mozilla Firefox. While in Chrome image can be clipped then set as a multiply of lower layer, in Mozilla after applying multiply, clipping stops working. Any ideas to solve this issue?
// Grab the Canvas and Drawing Context
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
// Create an image element
var img = document.createElement('IMG');
// When the image is loaded, draw it
img.onload = function () {
ctx.fillStyle="red"
ctx.fillRect(0,0,100,100)
// Save the state, so we can undo the clipping
ctx.save();
// Create a shape, of some sort
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(100, 30);
ctx.lineTo(180, 10);
ctx.lineTo(200, 60);
ctx.arcTo(180, 70, 120, 0, 10);
ctx.lineTo(200, 180);
ctx.lineTo(100, 150);
ctx.lineTo(70, 180);
ctx.lineTo(20, 130);
ctx.lineTo(50, 70);
ctx.closePath();
// Clip to the current path
ctx.clip();
ctx.globalCompositeOperation="multiply";
ctx.drawImage(img, 0, 0);
// Undo the clipping
ctx.restore();
}
// Specify the src to load the image
img.src = "http://i.imgur.com/gwlPu.jpg";
body {
background: #CEF;
}
<canvas id="c" width="200" height="158"></canvas>
This sounds like a known issue on Windows version of FireFox...
Only FF devs could have provided a real fix, but since the bug has been reported at least two years ago, I wouldn't expect it to be fixed anytime soon.
Now, to workaround the issue, you could draw in two steps:
on a first offscreen canvas do the clipping,
then do the blending on the main canvas using the now clipped one.
This may incur a little memory overhead, but having a second canvas ready might also come handy if you do a lot of compositing/blending.
// Grab the Canvas and Drawing Context
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
// create an off-screen copy of the context
var ctx2 = canvas.cloneNode().getContext('2d');
// Create an image element
var img = document.createElement('IMG');
// When the image is loaded, draw it
img.onload = function () {
// Do the clipping on the offscreen canvas
ctx2.save();
// Create a shape, of some sort
ctx2.beginPath();
ctx2.moveTo(10, 10);
ctx2.lineTo(100, 30);
ctx2.lineTo(180, 10);
ctx2.lineTo(200, 60);
ctx2.arcTo(180, 70, 120, 0, 10);
ctx2.lineTo(200, 180);
ctx2.lineTo(100, 150);
ctx2.lineTo(70, 180);
ctx2.lineTo(20, 130);
ctx2.lineTo(50, 70);
ctx2.closePath();
ctx2.clip();
// draw the image
ctx2.drawImage(img, 0, 0);
ctx2.restore();
// Now go back on the main canvas
ctx.fillStyle = "red";
ctx.fillRect(0,0,100,100);
// do the blending with our now clipped image
ctx.globalCompositeOperation="multiply";
ctx.drawImage(ctx2.canvas,0,0);
}
// Specify the src to load the image
img.src = "http://i.imgur.com/gwlPu.jpg";
body {
background: #CEF;
}
<canvas id="c" width="200" height="158"></canvas>
But for this exact case, i.e multiply blending, you can actually simply get rid of the clipping altogether on a single canvas.
Indeed, if I'm not mistaken, multiply doesn't really cares of which layer is top or bottom, the result will just be the same.
So you could simply do your compositing first, and as a final step do the blending over the composited image.
// Grab the Canvas and Drawing Context
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
// Create an image element
var img = document.createElement('IMG');
// When the image is loaded, draw it
img.onload = function () {
ctx.fillStyle="red";
// Create a shape, of some sort
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(100, 30);
ctx.lineTo(180, 10);
ctx.lineTo(200, 60);
ctx.arcTo(180, 70, 120, 0, 10);
ctx.lineTo(200, 180);
ctx.lineTo(100, 150);
ctx.lineTo(70, 180);
ctx.lineTo(20, 130);
ctx.lineTo(50, 70);
ctx.closePath();
// fill the current path
ctx.fill();
// draw only where our previous shape was drawn
ctx.globalCompositeOperation="source-atop";
ctx.drawImage(img, 0, 0);
// multiply blending should work the same in two directions
ctx.globalCompositeOperation="multiply";
ctx.fillRect(0,0,100,100);
}
// Specify the src to load the image
img.src = "http://i.imgur.com/gwlPu.jpg";
body {
background: #CEF;
}
<canvas id="c" width="200" height="158"></canvas>
And of course, you could very well get rid of clipping thanks to compositing and use a second offscreen canvas. (Probably my personal favorite).
I am trying to add an image to a canvas object, but for the life of me I cannot figure out how to get it to display properly. It shows the canvas object (a circle), but not the image.
Here is my canvas HTML:
<canvas id="leaf" width="60" height="60" style="border:1px solid #d3d3d3;"></canvas>
Below is the relevant JS:
let canvas = <HTMLCanvasElement>document.getElementById('leaf');
let context = canvas.getContext('2d');
let imageObj = new Image();
imageObj.onload = function()
{
context.save();
context.beginPath();
context.arc(30, 30, 28, 0, 2 * Math.PI);
context.clip();
context.drawImage(imageObj, 0, 0);
};
imageObj.src = 'http://www.html5canvastutorials.com/demos/assets/darth-vader.jpg';
context.restore();
context.beginPath();
context.arc(30, 30, 28, 0, 2 * Math.PI);
context.drawImage(imageObj, 0, 0);
context.strokeStyle = 'red';
context.stroke();
console.log(canvas.toDataURL());
return canvas.toDataURL();
But instead of showing the image within the circle, it just displays the circle, without the image...
I figured out that this was due to the return canvas.toDataURL(); being called preemptively, so I moved it into the image.load() function, and now it displays properly!
Is there a way to create an opacity map on a canvas element
I am trying to fade a generated image as shown below.
EXAMPLE:
I don't believe there is any way to directly draw an image with a gradiant mask, but you could pre-draw the image to a separate canvas, and use globalCompositeOperation to draw a masking linear gradient, then draw that canvas using drawImage to the main canvas.
Working Example:
var cvs = document.getElementById('cvs');
var ctx = cvs.getContext('2d');
// Draw some background colors.
ctx.fillStyle = "#FF6666";
ctx.fillRect(0, 0, 150, 200);
ctx.fillStyle = "#6666FF";
ctx.fillRect(150, 0, 150, 200);
// Load the image.
img = new Image();
img.onload = function() {
// Create a canvas in memory and draw the image to it.
var icvs = document.createElement('canvas');
icvs.width = img.width;
icvs.height = img.height;
var ictx = icvs.getContext('2d');
ictx.drawImage(img, 0, 0);
// For masking.
ictx.globalCompositeOperation = 'destination-out';
// Draw the masking gradient.
var gradient = ictx.createLinearGradient(0, 0, 0, icvs.height);
gradient.addColorStop(0, "transparent");
gradient.addColorStop(1, "white");
ictx.fillStyle = gradient;
ictx.fillRect(0, 0, icvs.width, icvs.height);
// Draw the separate canvas to the main canvas.
ctx.drawImage(icvs, 25, 25, 250, 150);
};
img.src = '//i.stack.imgur.com/dR8i9.jpg';
<canvas id="cvs" width="300" height="200"></canvas>
I'm working on a canvas element, and I thought I'd add some simple graphics elements, but for some reason they are grinding the fps to a halt. Without them it's 60fps, with them it slows down to 3-4 fps within a minute of it running:
ctx.rect(0, 0, cnv.width, cnv.height);
ctx.fillStyle = ctx.createPattern(albImg[8], "repeat");
ctx.fill();
ctx.lineWidth="1";
ctx.strokeStyle="#5d92de";
ctx.rect(173.5,638.5,623,98);
ctx.stroke();
What am I doing wrong?
Animation slows with each new animation loop
#DanielBengtsson, Yes, as you've discovered, use strokeRect.
Alternatively, you can add ctx.beginPath before ctx.rect. What's happening is that all previous rects are being redrawn since the last beginPath so you are really drawing hundreds of rects as you animate.
// alternative with beginPath so previous rects will not redraw and
// cause slowdowns.
ctx.lineWidth="1";
ctx.strokeStyle="#5d92de";
ctx.beginPath();
ctx.rect(173.5,638.5,623,98);
ctx.stroke();
Repeating Background Pattern -- wait for the image to fully load before trying to use it
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
var img = new Image();
img.onload = start;
img.src = "https://dl.dropboxusercontent.com/u/139992952/multple/emoticon1.png";
function start() {
ctx.fillStyle = ctx.createPattern(img, "repeat");
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
#canvas{border:1px solid red; margin:0 auto; }
<canvas id="canvas" width=300 height=300></canvas>