i create an apps in HTML5 canvas, for cropping image. there are 3 canvases in the DOM, one for drawing the line, one for original image, and another one for the result.
and here is the code i use.
// copy image data to secondary canvas
var pixelData = ctx.getImageData(x-10, y-10, 20, 20);
var tmpCanvas = document.createElement("canvas");
tmpCanvas.width = 20; tmpCanvas.height = 20;
var tmpCtx = tmpCanvas.getContext("2d");
tmpCtx.putImageData(pixelData, x, y);
var tmpImageEl = document.createElement("img");
tmpImageEl.onload = function(){
ctxCopy.drawImage(this, x, y);
}
console.log(tmpCanvas.toDataURL());
tmpImageEl.src = tmpCanvas.toDataURL();
//ctxCopy.putImageData(pixelData, x, y);
//document.getElementsByTagName("body")[0].appendChild(tmpCanvas);
in this line
tmpImageEl.src = tmpCanvas.toDataURL();
i got nothing but blank/transparent image. what i expect is the imagedata from original source..what is wrong with the code? if there is rounded options for putImageData this could be easier for me (i aleady googling about rounded putImageData, no luck)
Your ctxCopy does not seem to be defined; you probably intend to do this:
var tmpCtx = tmpCanvas.getContext("2d");
...
tmpImageEl.onload = function(){
tmpCtx.drawImage(this, x, y); /// for your temporary canvas
//ctxCopy.drawImage(this, x, y);
}
There is also an issue in Chrome with data-uris as source; you can get around that by setting first an empty string, then the data-uri:
tmpImageEl.src = '';
tmpImageEl.src = tmpCanvas.toDataURL();
However, you can save yourselves a bit of trouble by simply using the drawImage() method directly on your temporary canvas to draw in the source canvas (it can take image, canvas and video as source input):
var tmpCanvas = document.createElement("canvas");
tmpCanvas.width = tmpCanvas.height = 20;
var tmpCtx = tmpCanvas.getContext("2d");
tmpCtx.drawImage(ctx.canvas, 0, 0);
No need for getPixelData()/putImageData()/image, and it's much faster this way.
Hope this helps.
Related
So I have this piece of code that I use for erasing and restoring parts of an image with a (for example) removed background. Erasing from the main canvas is simple and the user can erase a circular shape with a line between points.
if(removeMode) {
ctxs[index].globalCompositeOperation = 'destination-out';
ctxs[index].beginPath();
ctxs[index].arc(x, y, radius, 0, 2 * Math.PI);
ctxs[index].fill();
ctxs[index].lineWidth = 2 * radius;
ctxs[index].beginPath();
ctxs[index].moveTo(old.x, old.y);
ctxs[index].lineTo(x, y);
ctxs[index].stroke();
}
The problem is with the restoring. Currently I am able to copy parts of the original image to the main canvas but only in a rectangular shape using the getImageData() and putImageData() functions.
ctxs[index].globalCompositeOperation = 'source-out';
ctxs[0].putImageData(ctxs[1].getImageData(x-radius, y-radius, 2*radius, 2*radius), x-radius, y-radius);
Ideally I would like to clip a part of the original image canvas to the main canvas with a shape similar to the erasing feature. I have tried the clip() function but honestly I am not sure how to go about it. Here is what I initially tried to clip a part of a canvas.
ctxs[index].beginPath();
ctxs[index].arc(x, y, radius, 0, Math.PI * 2);
ctxs[index].fill();
ctxs[index].lineWidth = 2 * radius;
ctxs[index].beginPath();
ctxs[index].moveTo(old.x, old.y);
ctxs[index].lineTo(x, y);
ctxs[index].stroke();
ctxs[index].clip();
How do I copy a custom shape from a canvas to another canvas?
Thanks in advance,
Edit:
I have also thought of using a mask where I would create the mask as such (example using numpy in python):
Y, X = np.ogrid[:canvas_height, :canvas_width]
# Y, X are matrix values and x, y are coordinates of the cursor within the image
center_dist = np.sqrt((X - x)**2 + (Y-y)**2)
# create mask
mask = center_dist <= radius
# omit everything except circular shape from mask
circular_img = original_img.copy()
circular_img[~mask] = 0
# combine images
new_img = np.maximum(original_img, new_img)
Example of what I have now
Simpler solution
Every shape fits into a rectangle.
Proof Your canvas is a rectangle and already contains the shape.
As a result, you can determine the smallest possible rectangle that contains the full shape and store that. It will necessarily contain your shape. Upon reload you will need to know the shape's boundaries inside the copy though, so that info will also be needed.
Harder, but more precise solution
You can create a structure and store the content, point-by-point (yet, this will be not very performant):
const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
let myShape = [];
for (let x = 0; x < canvas.width; x++) {
for (let y = 0; y < canvas.height; y++) {
if (inShape(x, y, canvas)) {
myShape.push({x, y, content: data});
}
}
}
The snippet above assumes that you have properly implemented inShape.
Homogeneous shape
If all the points inside the shape are similar, then you will need to only know where the boundaries of the shape were. If you have a convex polygon, for example, then you will need to know where its center is and what the boundaries are. If you have a filled circle, then you will only need its center and radius. The geometrical data you need largely depend on what shape you have.
Keep using composite operations.
"destination-out" will indeed remove the previous pixels that do overlap with the new ones.
If you use the inverse "destination-in", only the previous pixels that do overlap with the new ones are kept.
So you store your original image intact, and then use one of these modes to render it given the action you want to perform.
Here since it seems we are in a paint-like configuration, I guess it makes more sense to erase the final result and restore the original canvas. For this we need a third canvas, detached where we'll draw the "restoration" part on its own before drawing that back to the visible canvas:
(async () => {
// the main, visible canvas
const canvas = document.querySelector("canvas");
canvas.width = 500;
canvas.height = 250;
const ctx = canvas.getContext("2d");
// a detached canvas context to do the compositing
const detached = canvas.cloneNode().getContext("2d");
// the "source" canvas (here just an ImageBitmap)
const originalCanvas = await loadImage();
ctx.lineWidth = detached.lineWidth = 8;
// we store every drawing in its own Path2D object
const paths = [];
let down = false;
const checkbox = document.querySelector("input");
canvas.onmousedown = (evt) => {
down = true;
const newPath = new Path2D();
newPath.isEraser = !checkbox.checked;
paths.push(newPath);
};
canvas.onmouseup = (evt) => { down = false; };
canvas.onmousemove = (evt) => {
if (!down) { return; }
const {x, y} = parseMouseEvent(evt);
paths[paths.length - 1].lineTo(x, y);
redraw();
};
redraw();
function redraw() {
// clear the visible context
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(originalCanvas, 0, 0);
paths.forEach((path) => {
if (path.isEraser) {
// erase the current content
ctx.globalCompositeOperation = "destination-out";
ctx.stroke(path);
}
else {
// to restore
// we do the compositing on the detached canvas
detached.globalCompositeOperation = "source-over";
detached.drawImage(originalCanvas, 0, 0);
detached.globalCompositeOperation = "destination-in";
detached.stroke(path);
// draw the result on the main context
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(detached.canvas, 0, 0);
}
});
}
})().catch(console.error);
async function loadImage() {
const url = "https://picsum.photos/500/250";
const req = await fetch(url);
const blob = req.ok && await req.blob();
return createImageBitmap(blob);
}
function parseMouseEvent(evt) {
const rect = evt.target.getBoundingClientRect();
return {x: evt.clientX - rect.left, y: evt.clientY - rect.top };
}
canvas { border: 1px solid; vertical-align: top }
<label>erase/restore <input type="checkbox"></label>
<canvas></canvas>
Note that here I do create new paths every time, but you could very well use the same ones for both erasing and restoring (and even any other graphic source).
I'm using HTML5 canvas in a project and occasionally need to draw drop shadows on SVGs within a canvas. I've noticed that, compared to Chrome, Safari does two things incorrectly when doing this:
Safari draws a shadow on each individual shape within an SVG
Safari crops off parts of the shadow that go beyond the SVG's bounds
These issues can be illustrated by the following code:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.shadowOffsetX = 10;
context.shadowOffsetY = 10;
context.shadowColor = 'red'
var image = new Image();
image.src = 'https://storage.googleapis.com/card-conjurer/img/manaSymbols/0.svg';
image.onload = function() {
context.drawImage(image, 10, 10, 100, 100);
}
<canvas id='canvas'></canvas>
I can't embed images yet, but here are some links to images that illustrate the problem:
SVG Shadows with Google Chrome
SVG Shadows with Safari
(they are screenshots of the code above)
The results from Safari are... quite ugly, as you can see. Is there a way make Safari to render SVGs with shadows on HTML5 canvas like Chrome does?
Any help would be greatly appreciated.
Thanks so much for your time!
That's a bug, you should report it to webkit's bug-tracker.
Though you can workaround it by first drawing the image on a second canvas just to rasterize that svg image and use that canvas as source for the shadowing:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var image = new Image();
image.src = 'https://storage.googleapis.com/card-conjurer/img/manaSymbols/0.svg';
image.onload = function() {
const off = canvas.cloneNode();
off.getContext('2d').drawImage(image, 10, 10, 100, 100);
context.shadowOffsetX = 10;
context.shadowOffsetY = 10;
context.shadowColor = 'red';
context.drawImage(off, 0, 0);
}
<canvas id='canvas'></canvas>
In order to use a single canvas, we need to use an offset trick, but it's not always easy to do since it requires knowing clearly the position of our drawing:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var image = new Image();
image.src = 'https://storage.googleapis.com/card-conjurer/img/manaSymbols/0.svg';
image.onload = function() {
// first pass without shadow
context.drawImage(image, 10, 10, 100, 100);
// set shadow offsets to the position in page of bottom-right corner
context.shadowOffsetX = 10 + 110;
context.shadowOffsetY = 10 + 110;
context.shadowColor = 'red';
// draw behind
context.globalCompositeOperation = "destination-over";
// draw with inverse offset, so that the image is not visible
// but the shadow is in-screen
context.drawImage(canvas, -110, -110);
}
<canvas id='canvas'></canvas>
I have a PNG graphic I want to use as a background for my canvas in JS. It should be displayed in a tiled format. For this I'm currently doing something like this.
const ratioX = Math.ceil(canvas.width / image.width);
const ratioY = Math.ceil(canvas.height / image.height);
for (var x = 0; x < ratioX; x++) {
for (var y = 0; y < ratioY; y++) {
ctx.drawImage(image, x*image.width, y*image.height, image.width, image.height);
}
}
It's working as intended, but the performance might get improved by not doing the calculations every frame, but buffering the bigger tiled version of my image. So I'm looking for a way to create this image as an instance of the Image class or any other class I can use for drawImage().
At first I might need to get the image data (so the rgba info for every pixel). I've seen a way to do it like here, using canvas's context, but do I really need to do that? Is there no simpler way?
The second step would be to create a drawable image object out of that data. How would I do that?
I would love to see a way that's equivalent to Java's bufferedImage.getRgb(x, y) and bufferedImage.setRgb(x, y, color). Does something like this exist in JS?
As shown here in W3Schools you can use ctx createPattern to achieve what you want:
var ctx = document.getElementById("canvas").getContext("2d");
var img = new Image();
img.src = 'http://placehold.it/50/50'
img.onload = function() {
var pat = ctx.createPattern(img, 'repeat');
ctx.rect(0, 0, 350, 350);
ctx.fillStyle = pat;
ctx.fill();
};
<canvas id='canvas' width=350 height=350/>
How can I save fragment in memory?
var img = document.getElementById('img1');
ctx.save();
ctx.rotate(Math.PI / 180 * 45);
ctx.drawImage(img, 100, 100, img.width, img.height);
ctx.restore();
And now I want save this fragment (already rotated) to some array, and then just call drawImage without new rotating.
Is it real in JS and canvas or I should rotate image every time, when I want draw it?
var url = canvas.toDataURL();
may assist you with storing the canvas data in memory. It can then be re-drawn from this url later. e.g.
img.src = url;
ctx.drawImage(img,0,0);
I assume what you want is to store a rotated image.
To do it, most easy way is to create a canvas, then draw rotated into this canvas : you now have your rotated image inside a canvas, that you might want to transform into an image.
However, the security policy prevents you from creating an image out of a canvas if you used an image coming from another domain. So be sure to use canvas only in such a case.
http://jsbin.com/gemijekewewu/1/edit?js,output
(thx #markE for careful reading).
function getRotatedImage(sourceImage, provideImage) {
var cv = document.createElement('canvas');
cv.width = sourceImage.height;
cv.height = sourceImage.width;
var ctx = cv.getContext('2d');
ctx.translate(cv.width/2, cv.height/2);
ctx.rotate(Math.PI / 2);
ctx.drawImage(sourceImage, -cv.width/2, -cv.height/2);
if (provideImage) {
var img=new Image();
img.src= cv.toDataURL();
return img;
} else
return cv;
}
use with :
// during the init ...
var rotatedImage=getRotatedImage(myImage);
// ... later ...
ctx.drawImage(rotatedImage, ... , ...);
i draw a canvas(aka canvas 1) with image() then rotate it 25 degree. then i take rotated canvas to make a pattern for another canvas(aka canvas 2). then i draw this . and fill the fillstyle with newly created pattern. i noticed if alert in the middle of below code
finalsleeve_ctx.globalCompositeOperation = "source-atop";
/*****************************************
alert(sleeve.toDataURL('image/png'));
*****************************************/
var pattern = finalsleeve_ctx.createPattern(sleeve, 'repeat');
then firefox gives a correct output but if i dont do alert it does not give me correct output. crome not showing me correct output.
do i need to delay ?
here is what i have tried.
HTML
<div >
<canvas id="sleeve" width=436 height=567></canvas>
<canvas id="finalsleeve" width=436 height=567 ></canvas>
</div>
JS
var sleeve = document.getElementById('sleeve');
var sleeve_ctx = sleeve.getContext('2d');
var finalsleeve = document.getElementById('finalsleeve');
var finalsleeve_ctx = finalsleeve.getContext('2d');
function rotator2(var2,var3)
{
sleeve.width=sleeve.width;
var imageObj_rotator2 = new Image();
imageObj_rotator2.onload = function ()
{
var pattern_rotator2 = sleeve_ctx.createPattern(imageObj_rotator2, "repeat");
sleeve_ctx.fillStyle = pattern_rotator2;
sleeve_ctx.rect(0, 0, sleeve.width, sleeve.height);
sleeve_ctx.rotate(var3 * Math.PI/180);
sleeve_ctx.fill();
}
imageObj_rotator2.src = var2;
}
function drawSleeve()
{
finalsleeve.width = finalsleeve.width;
var imgsleeve = new Image();
imgsleeve.src="http://i.stack.imgur.com/FoqGC.png";
finalsleeve_ctx.drawImage(imgsleeve,0,0);
finalsleeve_ctx.globalCompositeOperation = "source-atop";
alert(sleeve.toDataURL('image/png'));
var pattern = finalsleeve_ctx.createPattern(sleeve, 'repeat');
finalsleeve_ctx.rect(0, 0, sleeve.width, sleeve.height);
finalsleeve_ctx.fillStyle = pattern;
finalsleeve_ctx.fill();
finalsleeve_ctx.globalAlpha = .10;
finalsleeve_ctx.drawImage(imgsleeve, 0, 0);
finalsleeve_ctx.drawImage(imgsleeve, 0, 0);
finalsleeve_ctx.drawImage(imgsleeve, 0, 0);
}
rotator2('http://i.stack.imgur.com/fvpMN.png','25');
drawSleeve();
Here is fiddle.
http://jsfiddle.net/EbBHz/
EDITED
Sorry, I completely misunderstood your question. I just now went back and saw the last question you posted and the goal you are trying to achieve.
To get the functionality you desire you can just create one function, you don't need two. Instead of using a second canvas in the HTML I created a temporary one using javascript.
Here is the simplified and functional code
<canvas id="sleeve" width='436' height='567'></canvas>
var sleeve = document.getElementById('sleeve');
var ctx = sleeve.getContext('2d');
function rotator2(var2, var3) {
// Draw the original sleeves
var imageObj_rotator2 = new Image();
imageObj_rotator2.src = var2;
imageObj_rotator2.onload = function () {
var imgsleeve = new Image();
imgsleeve.src="http://i.stack.imgur.com/FoqGC.png";
ctx.drawImage(imgsleeve,0,0);
// Create a second temporary canvas
var pattern = document.createElement('canvas');
pattern.width = 500;
pattern.height = 500;
var pctx = pattern.getContext('2d');
// Make the pattern that fills the generated canvas
var pattern_rotator2 = pctx.createPattern(imageObj_rotator2, "repeat");
pctx.fillStyle = pattern_rotator2;
pctx.rotate(var3 * Math.PI / 180);
// Fill the generated canvas with the rotated image pattern we just created
pctx.fillRect(0, 0, pattern.width, pattern.height);
// Create a pattern of the generated canvas
var patterned = ctx.createPattern(pattern, "repeat");
// Fills in the non-transparent part of the image with whatever the
// pattern from the second canvas is
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = patterned;
ctx.fillRect(0, 0, sleeve.width, sleeve.height);
}
}
rotator2('http://i.stack.imgur.com/fvpMN.png', '45')
The technique works alright, but only for certain angles. Here is the demo set to 45 degrees. As you can see, there is a problem: part of the sleeve is whited out. However, if you change the degree to 15 like this it works just fine. This is because when the image is being rotated in the created canvas it leaves white space before repeating. To see this issue first hand, change the width and the height of the created canvas to 30 (the default width/height of the image) like this
Note: You may have to click run once the jsfiddle tab is open, canvases don't like generating content when another tab is focused
I tried problem solving the issue including
Making the generated canvas really large (which works but KILLS load
time/crashes page sometimes)
Translating the picture in the generated canvas after rotating it
which didn't work like I had hoped
Coming up with a function to change the width/height to cover the
entire first canvas based on the rotated second-canvas-dimensions, which is by far the most promising, but I don't have the time or desire to work out a good solution
All that being said if the angle HAS to be dynamic you can work on a function for it. Otherwise just use a workaround angle/generated canvas dimensions
final result> Here is a working solution for fill rotated pattern without white at any angle
var sleeve = document.getElementById('sleeve');
var ctx = sleeve.getContext('2d');
function rotator2(var2, var3) {
var x =0;
var y=0;
//pattern size should be grater than height and width of object so that white space does't appear.
var patternSize = sleeve.width+ sleeve.height;
// Draw the original sleeves
var imageObj_rotator2 = new Image();
imageObj_rotator2.src = var2;
imageObj_rotator2.onload = function () {
var imgsleeve = new Image();
imgsleeve.src="http://i.stack.imgur.com/FoqGC.png";
ctx.drawImage(imgsleeve,0,0);
// Create a second temporary canvas
var pattern = document.createElement('canvas');
pattern.width = sleeve.width;
pattern.height = sleeve.height;
var pctx = pattern.getContext('2d');
// Make the pattern that fills the generated canvas
var pattern_rotator2 = pctx.createPattern(imageObj_rotator2, "repeat");
pctx.fillStyle = pattern_rotator2;
//moving rotation point to center of target object.
pctx.translate(x+ sleeve.width/2, y+sleeve.height/2);
pctx.rotate(var3 * Math.PI / 180);
// Fill the generated canvas with the rotated image pattern we just created and expanding size from center of rotated angle
pctx.fillRect(-patternSize/2, -patternSize/2, patternSize, patternSize);
// Create a pattern of the generated canvas
var patterned = ctx.createPattern(pattern, "no-repeat");
// Fills in the non-transparent part of the image with whatever the
// pattern from the second canvas is
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = patterned;
ctx.fillRect(x, y, sleeve.width, sleeve.height);
}
}
rotator2('http://i.stack.imgur.com/fvpMN.png', '50')