I want to save canvas image as a bitmap to be able to later use it in drawImage.
Goal is to be able to change (eg. resize) canvas and keep it's contents (eg. scaled - canvas is always a square).
I tried:
var tmp = createImageBitmap(canvas)
ctx.drawImage(tmp,0,0)
What I got was an error that said 'tmp' is not a bitmap.
createImageBitmap returns a Promise, so the result is asynchronous and you have to wait for it with await:
const img = await createImageBitmap(canvas);
ctx.drawImage(img, 0, 0);
Or if you can't use await because you target ES5 then do it the old fashioned way:
createImageBitmap(canvas).then(img => {
ctx.drawImage(img, 0, 0);
});
An HTMLCanvasElement is part of the CanvasImageSource mixin, meaning you can directly use it with drawImage():
const [canvas1, canvas2] = document.querySelectorAll("canvas");
const ctx1 = canvas1.getContext("2d");
const ctx2 = canvas2.getContext("2d");
ctx1.fillStyle = "green"
ctx1.fillRect(0, 0, 50, 50);
ctx2.fillStyle = "red"
ctx2.fillRect(0, 0, 50, 50);
// draw canvas1 at (60, 0), with a 2x scale
ctx2.drawImage(canvas1, 60, 0, 600, 300);
canvas { border: 1px solid }
<canvas></canvas>
<canvas></canvas>
Related
I am using this jQuery plugin to make free drawing over a canvas.
I want to clear the canvas for redrawing but after I do and when I click inside canvas for redrawing the old drawing that I cleared appears again
$('#canvasFirst').sketch();
$('button').on("click", (evt) => {
var canvas = document.getElementById('canvasFirst');
let context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
});
canvas { border: 1px solid; vertical-align: top }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/mobomo/sketch.js#master/lib/sketch.min.js"></script>
<p>Draw on the below canvas, then press "clear" and try to draw again.</p>
<canvas id="canvasFirst">
</canvas>
<button>clear</button>
This plugin stores all the drawing commands in an actions array, and at each redraw it will go through that whole list and draw the full thing again. (Which allows to avoid having holes in your stroke).
The docs are very hard to grasp, but you can set this options's content through the .sketch("options", value) method.
As hinted in this issue, setting it to an empty Array will thus remove all the previous commands. All you have to do then is to redraw the whole scene, now empty:
const $canvas = $('#canvasFirst');
$canvas.sketch();
$('button').on("click", (evt) => {
$canvas.sketch("actions", []);
$canvas.sketch("redraw");
});
canvas { border: 1px solid; vertical-align: top }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/mobomo/sketch.js#master/lib/sketch.min.js"></script>
<p>Draw on the below canvas, then press "clear" and try to draw again.</p>
<canvas id="canvasFirst">
</canvas>
<button>clear</button>
You can try to save the state of the canvas as a blob. For example, as image/png.
There are two bad things here:
A small inconvenience. Methods for converting a blob to an image and back are asynchronous. Promises have to be used.
Speed. I strongly doubt that this solution is suitable for tasks that require speed - games, videos, etc.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const saveCanvasState = (canvas) => {
const ctx = canvas.getContext('2d');
ctx.save(); // save state
let blob;
return new Promise((r) => {
canvas.toBlob( // toBlob is async method
(b) => {
blob = b;
r(() =>
new Promise((r) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.restore();
const img = new Image();
img.addEventListener('load', () => {
URL.revokeObjectURL(img.src);
ctx.drawImage(img, 0, 0);
r();
});
img.src = URL.createObjectURL(blob);
}));
},
'image/png',
1
);
});
};
ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 100, 100);
saveCanvasState(canvas).then((restore) => {
ctx.fillStyle = 'black';
ctx.fillRect(150, 40, 100, 100);
ctx.fillRect(100, 40, 100, 100);
restore().then(() => {
console.log('restored, can draw');
});
});
<canvas id="canvas"></canvas>
I want save canvas in every 2 minutes.
document.getElementById('canvasUrl').value = canvas.toDataURL();
I try:
document.getElementById('canvasUrl').value += canvas.toDataURL();
//or
var canvasValue += canvas.toDataURL();
document.getElementById('canvasUrl').value = canvasValue;
both can't work! Why?
You can but you need to combine the image data first prior to using a dataURL here is an example. Please note it's very verbose I imagine you'd want multiple canvases in an array or something along those lines.
How this works is it takes the data from the first 3 canvases then draws them to the 4th, the 4th canvases dataURL is what's used which has the data from the previous 3 canvases combined.
const canvas1 = document.querySelector('#first');
const ctx1 = canvas1.getContext('2d');
const canvas2 = document.querySelector('#second');
const ctx2 = canvas2.getContext('2d');
const canvas3 = document.querySelector('#third');
const ctx3 = canvas3.getContext('2d');
ctx1.fillStyle = 'red';
ctx1.fillRect(0, 0, 100, 100);
ctx2.fillStyle = 'blue';
ctx2.fillRect(100, 100, 100, 100);
ctx3.fillStyle = 'green';
ctx3.fillRect(10, 10, 100, 10);
const saveCanvas = document.querySelector('#saved');
const ctxSave = saveCanvas.getContext('2d');
// Draw each of our other canvases onto the canvas we're going to save from.
ctxSave.drawImage(canvas1, 0, 0);
ctxSave.drawImage(canvas2, 0, 0);
ctxSave.drawImage(canvas3, 0, 0);
document.getElementById('canvasUrl').value = saveCanvas.toDataURL();
canvas {
border: 1px solid black;
}
<div>
<label>Saved Canvas Data</label>
<input id="canvasUrl" type="text">
</div>
<div>
<canvas id="first"></canvas>
<canvas id="second"></canvas>
<canvas id="third"></canvas>
<canvas id="saved"></canvas>
</div>
I want to take a screenshot of the following canvas:
<div class="A">
<canvas width="700" height="500" style="filter: brightness(200%)"></canvas>
<div class="A">
I have the following method:
takeSnapshot() {
const canvas = document.getElementsByClassName('A')[0].childNodes[0] as HTMLCanvasElement;
console.log(canvas);
const dataUrl = canvas.toDataURL('png');
const link = document.getElementsByClassName('link')[0];
link.setAttribute('download', 'snapshot-' + this.convertTimeStamp(Date.now()) + '.png');
link.setAttribute('href', dataUrl.replace('image/png', 'image/octet-stream'));
link.click();
}
But the style style="filter: brightness(200%)" is not keepling on the png dowloaded image. How can i do?
Filter canvas pixels
The filter applied by the canvas style does not effect the pixels held by the canvas. This style is applied while compositing the canvas with the rest of the page.
To change the pixels on the canvas you need to set CanvasRenderingContext2D filter property and then render what you want the filter applied to.
Note that filter is experimental so check support.
Note that some filters will taint the canvas and thus not let you access the pixels, toDataURL will throw an error and you will not be able to download the canvas.
This does not apply to the standard named CSS filters like functions blur(), brightness(), contrast(), drop-shadow(), grayscale(), hue-rotate(), invert(), opacity(), saturate(), and sepia()
Example
Applying brightness filter before downloading canvas content.
takeSnapshot() {
const canvas = document.getElementsByClassName('A')[0].childNodes[0];
const ctx = canvas.getContext("2d");
// Set important canvas state to defaults
ctx.globalAlpha = 1;
ctx.setTransform(1, 0, 0, 1, 0, 0);
// Set the filter function
ctx.filter = "brightness(200%)";
// Make sure all pixels are completely replaced. Important if there are transparent pixels
ctx.globalCompositeOperation = "copy";
// Copy canvas to itself applying the filter as it does so.
ctx.drawImage(canvas, 0, 0);
// Turn off filter
ctx.filter = "none";
// Restore default comp op
ctx.globalCompositeOperation = "source-over";
// Download
const link = document.getElementsByClassName('link')[0];
link.download = 'snapshot-' + this.convertTimeStamp(Date.now()) + '.png';
link.href = canvas.toDataURL('png').replace('image/png', 'image/octet-stream');
link.click();
}
Note This requires the CanvasRenderingContext2D. If the canvas content was created using another API you will not be able to get a 2d context. You thus need to create a temp canvas, the same size and draw the old canvas onto the new one (using the same method as in the example above) then download the new canvas.
If canvas.getContext("2d") returns null
As I don't know how your canvas was rendered the more robust version of the function checks to see if a 2D context can be obtained, if not then copy (using filter) to a new canvas and save that.
takeSnapshot() {
var dlCanvas, ctx;
const can = document.getElementsByClassName('A')[0].childNodes[0];
ctx = can.getContext("2d"); // ctx may be null (eg the canvas was rendered using webGL
if (!ctx) { // failed to get 2D API create a second canvas to apply the filter
dlCanvas = Object.assign(document.createElement("canvas"), {
width: can.width, height: can.height
});
ctx = dlCanvas.getContext("2d");
} else {
dlCanvas = can;
}
ctx.globalAlpha = 1;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.filter = "brightness(200%)";
ctx.globalCompositeOperation = "copy";
ctx.drawImage(can, 0, 0);
ctx.filter = "none";
ctx.globalCompositeOperation = "source-over";
const link = document.getElementsByClassName('link')[0];
link.download = 'snapshot-' + this.convertTimeStamp(Date.now()) + '.png';
link.href = dlCanvas.toDataURL('png').replace('image/png', 'image/octet-stream');
link.click();
}
I want to free-draw shapes with fabric.js. The outline is, say, 20px (such that the user sees it clearly).
After the user has drawn it, the shape should been filled with the same color as the outline.
The whole thing should be semi-transparent. Unfortunately, this causes the overlap between outline and fill to be less transparent and draws a strange "inner outline" to the shape.
Is there a way to make shape uniquely semi-transparent?
Maybe a trick would be: after the user has drawn the shape, "widen" the shape by half of outline thickness and set outline thickness to 1. Would that be possible?
See this https://jsfiddle.net/4ypdwe9o/ or below for an example.
var canvas = new fabric.Canvas('c', {
isDrawingMode: true,
});
canvas.freeDrawingBrush.width = 10;
canvas.freeDrawingBrush.color = 'rgb(255, 0, 0)';
canvas.on('mouse:up', function() {
canvas.getObjects().forEach(o => {
o.fill = 'rgb(255, 0, 0)';
o.opacity = 0.5;
});
canvas.renderAll();
})
canvas {
border: 1px solid #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.3/fabric.js"></script>
<canvas id="c" width="600" height="600"></canvas>
It's a bit tricky but can be solved using a temporary canvas. So you first render the path using a solid color fill on the temporary canvas, then copy it to the main canvas like this:
//create temporary canvas
var tmpCanvas = document.createElement("canvas");
tmpCanvas.width = canvas.width;
tmpCanvas.height = canvas.height;
var tmpCtx = tmpCanvas.getContext("2d");
//remember the original render function
var pathRender = fabric.Path.prototype.render;
//override the Path render function
fabric.util.object.extend(fabric.Path.prototype, {
render: function(ctx, noTransform) {
var opacity = this.opacity;
//render the path with solid fill on the temp canvas
this.opacity = 1;
tmpCtx.clearRect(0, 0, tmpCanvas.width, tmpCanvas.height);
pathRender.apply(this, [tmpCtx]);
this.opacity = opacity;
//copy the path from the temp canvas
ctx.globalAlpha = opacity;
ctx.drawImage(tmpCanvas, 0, 0);
}
});
See plunker here: https://plnkr.co/edit/r1Gs2wIoWSB0nSS32SrL?p=preview
In fabric.js 1.7.3, they have another implementation. When I use
fabricCanvasObject.getObjects('path').slice(-1)[0].setFill("red");
fabricCanvasObject.getObjects('path').slice(-1)[0].setOpacity(0.5);
instead of
fabricCanvasObject.getObjects('path').slice(-1)[0].fill = "red";
fabricCanvasObject.getObjects('path').slice(-1)[0].opacity = 0.5;
The boundary is painted correctly, without overlap. So, the temporary canvas from Janusz's answer is not needed anymore. For my former fabric.js 1.5.0, the answer from Janusz solved the problem.
Jetic,
You are almost finished your logic. Instead of using "opacity" use rgba:
canvas.getObjects().forEach(o => {
o.fill = 'rgba(255, 0, 0, 0.5)';
o.stroke = 'rgba(255, 0, 0, 0)';
// o.opacity = 0.5;
});
canvas.renderAll();
I'm cropping an image using canvas
function crop(img) {
var canvas = document.createElement("canvas");
canvas.width = 20;
canvas.height = 20;
var context = canvas.getContext('2d');
context.drawImage(img, 0, 0, 20, 20, 0, 0, 20, 20);
return canvas.toDataURL();
}
However, when I compare the base64 data return by Chrome and PhantomJs I noticed they're different. Does anyone know why this is ?
It's a Phantomjs issue and there's a working-arround solution. Take a look this:
https://github.com/ariya/phantomjs/issues/10455