javascript - Scale canvas in all direction - javascript

I have two layers. One layer is big and another one is small. So to align the small layer with the big layer I am scaling the small layer to size of the big layer.
Scaling works on top/left but on bottom/right it's not working as I had hoped: it looks like it's not scaling equally in all directions.
I'm doing it like:
var canvas=document.getElementById("canvas");
var context=canvas.getContext("2d");
var imageObject1=new Image();
imageObject1.onload=function(){
context1.clearRect(0,0,imageObject1.width,imageObject1.width);
context1.scale(1.0927,1.0956);
context1.drawImage(imageObject1,0,0);
}
imageObject1.src=canvas.toDataURL();
JsFiddle of problem: https://jsfiddle.net/7cufxf6d/
Any idea how to solve this problem?

Your offsets and ratio's seem wrong. In your code they are hardcoded and then shifted again using hardcoded values. You might want to re-investigate the values and how you obtain them.
Scaling (using canvas's transformation matrix) when using floating point values can introduce more rounding errors as opposed to using more fixed boundaries as provided by drawImage. As you are already hardcoding these values you might save some work (and memory) and both shift and scale the image when drawing it (see: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage):
ctx.drawImage(image, dx, dy, dWidth, dHeight)
I slightly modified your fiddle:
// Silver Layer
var canvas0 = document.getElementById("layer1");
var context0 = canvas0.getContext("2d");
var imageObject0 = new Image();
imageObject0.crossOrigin = "anonymous";
imageObject0.onload = function() {
document.getElementById('layer1').width = imageObject0.width;
document.getElementById('layer1').height = imageObject0.height;
context0.clearRect(0, 0, imageObject0.width, imageObject0.width);
// context.scale(1.5,1.5);
context0.drawImage(imageObject0, 0, 0);
};
imageObject0.src = 'https://i.imgur.com/tIaNJku.png';
// Black Layer
var canvas1 = document.getElementById("layer2");
var context1 = canvas1.getContext("2d");
var imageObject1 = new Image();
imageObject1.crossOrigin = "anonymous";
imageObject1.onload = function() {
document.getElementById('layer2').width = imageObject1.width;
document.getElementById('layer2').height = imageObject1.height;
context1.clearRect(0, 0, imageObject1.width, imageObject1.width);
//context1.scale(1.0927,1.0956);
context1.drawImage(imageObject1, -65, -10, 3020, 2680); // MODIFIED LINE
//REMOVED SHIFT
};
imageObject1.src = 'https://i.imgur.com/29aRnzv.png'; //black
<div style="position: relative;">
<canvas id="layer1" width="100" height="100" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
<canvas id="layer2" width="100" height="100" style="position: absolute; left: 0; top: 0; z-index: 1;"></canvas>
</div>

Related

Clearing previous positions of canvas object and not the entire canvas

l believe to have a logic error in the way of which my logic is meant to find the previous coordinates of my canvas object, a moving image, and delete the frame drawn before it so that it does not duplicated the image every time it is drawn onto the canvas.
There is a reproducible demo below and l added comments where l believe the problem occurs.
var canvas = document.getElementById("c");
var ctx = canvas.getContext("2d");
var the_background = document.getElementById("the_background");
var imgTag = new Image();
var X_POS = canvas.width;
var Y_POS = 0;
imgTag.src = "http://i.stack.imgur.com/Rk0DW.png"; // load image
function animate() {
/* Error resides from here */
var coords = {};
coords.x = Math.floor(this.X_POS - imgTag);
coords.y = Math.floor(this.Y_POS - imgTag);
ctx.clearRect(coords.x, coords.y, X_POS, Y_POS);
/* To here */
ctx.drawImage(imgTag, X_POS, Y_POS);
X_POS -= 5;
if (X_POS > 200) requestAnimationFrame(animate)
}
window.onload = function() {
ctx.drawImage(the_background, 0, 0, canvas.width, canvas.height);
animate();
}
html,
body {
width: 100%;
height: 100%;
margin: 0px;
border: 0;
overflow: hidden;
display: block;
}
<html>
<canvas id="c" width="600" height="400"></canvas>
<img style="display: none;" id="the_button" src="https://i.imgur.com/wO7Wc2w.png" />
<img style="display: none;" id="the_background" src="https://img.freepik.com/free-photo/hand-painted-watercolor-background-with-sky-clouds-shape_24972-1095.jpg?size=626&ext=jpg" />
</html>
It seems logical to only clear the subset of the canvas that's changing, but the normal approach is to clear and redraw the entire canvas per frame. After the car moves, the background that's cleared underneath it needs to be filled in, and trying to redraw only that subset will lead to visual artifacts and general fussiness. Clearing small portions of the screen is a premature optimization.
Canvases can't keep track of much of anything other than pixels, so an animation is more like a flipbook of stationary frames and less like a cardboard cutout animation, where the same pieces of cardboard move along and overlap one another.
Math.floor(this.X_POS - imgTag) looks wrong -- imgTag is an image object, which doesn't make sense to subtract from a number. You may have meant to grab the x property from this.
Use image.onload to ensure the image is actually loaded before running the loop. It's a bit odd to use image tags in the DOM just to load images for a canvas. I'd do that programmatically from JS which saves some typing and makes it easier to manage the data.
const loadImg = url => new Promise((resolve, reject) => {
const img = new Image();
img.onerror = reject;
img.onload = () => resolve(img);
img.src = url;
});
const images = [
"https://img.freepik.com/free-photo/hand-painted-watercolor-background-with-sky-clouds-shape_24972-1095.jpg?size=626&ext=jpg",
"http://i.stack.imgur.com/Rk0DW.png",
];
Promise
.all(images.map(loadImg))
.then(([backgroundImg, carImg]) => {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const car = {x: canvas.width, y: 0};
(function update() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(
backgroundImg, 0, 0, canvas.width, canvas.height
);
ctx.drawImage(
carImg, car.x, car.y, carImg.width, carImg.height
);
car.x -= 5;
if (car.x > 200) {
requestAnimationFrame(update);
}
})();
})
.catch(err => console.error(err))
;
<canvas id="canvas" width="600" height="400"></canvas>

How can I animate a gradient with large pixels?

I am working on a project where I would like to have darkness covering the screen and the character glowing in the darkness. I tried to animate the scene then draw darkness over it using this code:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var canvasWidth = canvas.width;
var canvasHeight = canvas.height;
var pixelSize = 30;
var width = canvasWidth/pixelSize;
var height = canvasHeight/pixelSize;
var lightX = canvasWidth/2;
var lightY = canvasHeight/2;
var lightDiameter = 100;
var a = lightDiameter*pixelSize;
for(var x = 0; x < width; x++) {
for(var y = 0; y < height; y++) {
var alpha = 1.25 - a/(Math.pow(x*30 - lightX, 2) + Math.pow(y*30 -
lightY, 2));
ctx.fillStyle = "rgba( 25, 25, 30," + alpha + ")";
ctx.fillRect(x*pixelSize, y*pixelSize, pixelSize, pixelSize);
}
}
This worked pretty well and I liked the way it looked, but when this was repeatedly animated alongside the other code it slowed the rest down significantly. I think a possible solution may be to somehow draw a gradient with a lower "quality?", another solution I have considered is to save this drawing in a separate canvas and drawing it translated to the players location but that would make it impossible to add multiple sources of light, which I would like to do by simply adding their effect. I may just have to deal with the lag and I'm a noob at this stuff, but if anyone can help me that would be wonderful.
To clarify, I am using this code in the drawing loop, and also it is re-calculated in every iteration. I would prefer to recalculate this way so I can have multiple moving sources of light.
This is because fillRect is pretty slow compared to other methods. You could probably speed things up by using ImageData objects instead.
The way to do this would be to render everything to the canvas, get the corresponding ImageData, modify its contents and put it back onto the canvas:
var ctx = canvas.getContext("2d");
// render stuff here
var imageData = ctx.getImageData(0,0,canvasWidth,canvasHeight);
for (let x=0;x<canvasWidth;x++){
for (let y=0;y<canvasHeight;y++){
let i = (x+y*canvasWidth)*4;
let alpha = calculateAlpha(x,y); // your method here (should result in a value between 0 and 1)
imageData.data[i] = (1-alpha)*imageData.data[i]+alpha*25;
imageData.data[i+1] = (1-alpha)*imageData.data[i+1]+alpha*25;
imageData.data[i+2] = (1-alpha)*imageData.data[i+2]+alpha*30;
imageData.data[i+3] = 1-(1-alpha)*(1-imageData.data[i+3]);
}
}
ctx.putImageData(imageData,0,0);
This should do the lighting on a per-pixel basis, and much faster than using clearRect all the time. However, it might still slow things down, as you're doing a lot of calculations each frame. In that case, you could speed thing up by doing the lighting in a second canvas that is positioned over your main canvas using css:
<div id="container">
<canvas id="canvas"></canvas>
<canvas id="lightingCanvas"></canvas>
</div>
Css:
#container {
position: relative;
}
#canvas, #lightingCanvas {
position: absolute;
top: 0;
left: 0;
}
#container, #canvas, #lightingCanvas {
width: 480px;
height: 360px;
}
Javascript:
var canvas = document.getElementById("lightingCanvas")
var ctx = canvas.getContext("2d");
ctx.fillStyle = "rgb(25,25,30)";
ctx.fillRect(0,0,canvas.width,canvas.height);
var imageData = ctx.getImageData(0,0,canvasWidth,canvasHeight);
for (let x=0;x<canvasWidth;x++){
for (let y=0;y<canvasHeight;y++){
let i = (x+y*canvasWidth)*4;
let alpha = calculateAlpha(x,y); // your method here (should result in a value between 0 and 1)
imageData.data[i+3] = 255*alpha;
}
}
ctx.putImageData(imageData,0,0);
This way the browser takes care of the blending for you and you just need to plug in the correct alpha values - so rendering should be even faster now.
This will also allow you to bring the large pixels back in - just use a lower resolution on the second canvas and use some css effect like image-rendering: -webkit-crisp-edges to make the canvas pixelated when scaled up.

GetImageData not returning expected results for comparing 2 images

If I have 2 images that are the same except i change 1 pixel, it says there is 131 different changes in my array. Why is this happening? I'm creating 2 arrays using getcanvasdata and a loop to compare both.
var c = document.getElementById("myCanvas");
var c2 = document.getElementById("myCanvas2");
var ctx = c.getContext("2d");
var ctx2 = c2.getContext("2d");
var img = new Image();
var img2 = new Image();
var diffpixels = [];
var imgData;
var imgData2;
img.src = "avengers2.jpg";
img2.src = "avengers1.jpg";
img.onload = function() {
ctx.drawImage(img, 0, 0);
imgData = ctx.getImageData(0, 0, 1920, 1080).data;
img2.onload = function() {
ctx2.drawImage(img2, 0, 0);
imgData2 = ctx2.getImageData(0, 0, 1920, 1080).data;
console.log(imgData2.length);
for (var i = 0; i < imgData.length; i++) {
if (imgData[i] === imgData2[i]) {} else {
diffpixels.push(i);
}
}
console.log(diffpixels);
}
}
<canvas id="myCanvas" width="1920" height="1080" style="border:1px solid #d3d3d3"></canvas>
<canvas id="myCanvas2" width="1920" height="1080" style="border:1px solid #d3d3d3"></canvas>
I suspect your problem here is actually in the use of .jpgs for your image files, as these contain artifacts from compression. Changing even one pixel and/or recompressing a .jpg image by saving it as a new file frequently causes more than one pixel to change due to the rather lossy compression method. I suggest you try using .png images instead, since they are a (mostly, usually) lossless format. The rest of your method here seems solid.
Alternatively, for the purposes of testing your code to make absolutely sure it works, you could load just one of the images into both canvases, then pick an arbitrary point (for this example, the point (100, 100) in myCanvas2) on one and do a quick one-pixel rectangle of an unexpected colour (say, #FF0080)
ctx2.fillStyle = "#FF0080";
ctx2.fillRect(100, 100, 1, 1);
right before comparing the two canvases. That may not work for the eventual case you are going for, but it should accurately test the function you've written.

How to drawImage behind all other content on a canvas? [duplicate]

This question already has an answer here:
HTML Canvas: Drawing grid below a plot
(1 answer)
Closed 6 years ago.
I have a canvas, and I want to use drawImage to draw an image behind the current content on the canvas.
Due to the fact that there is content already on the canvas (I'm using Literally Canvas to create a canvas containing an image, so I can't really draw the image first), I cannot use drawImage before I render the rest of my content.
Is it possible to drawImage behind all other content on a canvas?
Yes you can just use globalCompositeOperation destination-over, but note that your first image needs some transparency, otherwise, you will obviously not see anything :
var img1 = new Image();
var img2 = new Image();
var loaded = 0;
var imageLoad = function(){
if(++loaded == 2){
draw();
}
};
img1.onload = img2.onload = imageLoad;
var draw = function(){
var ctx = c.getContext('2d');
ctx.drawImage(img1, 100,100);
// wait a little bit before drawing the background image
setTimeout(function(){
ctx.globalCompositeOperation = 'destination-over';
ctx.drawImage(img2, 0,0);
}, 500);
}
img1.src = "https://dl.dropboxusercontent.com/s/4e90e48s5vtmfbd/aaa.png";
img2.src = "https://picsum.photos/200/200";
<canvas id="c" width="200" height="200"></canvas>
Sorry about the previous post, I didn't properly read your post
Perhaps you could save the canvas, draw your image, and then reload the old content on top of your drawn image? Here's some JS psuedocode:
var imgData=ctx.getImageData(0,0,canvas.width,canvas.height);
ctx.drawImage('Your Image Watermark Stuff');
ctx.putImageData(imgData,0,0);
You can use KonvaJS. And then use layers for it.
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.rawgit.com/konvajs/konva/0.13.0/konva.min.js"></script>
<meta charset="utf-8">
<title>Konva Rect Demo</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #F0F0F0;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height
});
var layer = new Konva.Layer();
var imageObj = new Image();
imageObj.onload = function() {
var baseImage = new Konva.Image({
x: 50,
y: 50,
width: width,
height: height,
image: image
});
// add the shape to the layer
layer.add(rect);
// add the layer to the stage
stage.add(layer);
};
imageObj.src = 'url to your image'
</script>
</body>
</html>
A simple solution would be to use another canvas behind the first one.
Normally canvas pixels are initialized to transparent black and therefore are perfectly see-through.
If your first canvas is created opaque instead the only other option I can think to is
create a temporary canvas of the same size
draw your image in this temporary canvas
get the ImageData object of both the temporary canvas and of the original canvas
copy from the temporary canvas to the original canvas only where the original canvas is not set at the background color
In code:
var tmpcanvas = document.createElement("canvas");
tmpcanvas.width = canvas.width;
tmpcanvas.height = canvas.height;
var temp_ctx = tmpcanvas.getContext("2d");
// ... draw your image into temporary context ...
var temp_idata = temp_ctx.getImageData(0, 0, canvas.width, canvas.height);
var temp_data = temp_idata.data;
// Access the original canvas pixels
var ctx = canvas.getContext("2d");
var idata = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = idata.data;
// Find the background color (here I'll use first top-left pixel)
var br_r = data[0], bg_g = data[1], bg_b = data[2];
// Replace all background pixels with pixels from temp image
for (var i=0,n=canvas.width*canvas.height*4; i<n; i+=4) {
if (data[i] == bg_r && data[i+1] == bg_g && data[i+2] == bg_b) {
data[i] = tmp_data[i];
data[i+1] = tmp_data[i+1];
data[i+2] = tmp_data[i+2];
data[i+3] = tmp_data[i+3];
}
}
// Update the canvas
ctx.putImageData(idata, 0, 0);
this approach however will have a lower quality if the original canvas graphics has been drawn with antialiasing or if pixels of the background color are also used in the image (e.g. an object on #FFF white background where object highlights are also #FFF). Another problem is if the background color is not a perfectly uniform RGB value (this will happen if the image has been compressed with a lossy algorithm like jpeg).
All these problems could be mitigated with more sophisticated algorithms like range matching, morphological adjustments and color-to-alpha conversions (basically the same machinery used for chroma-keying).

HTML5 Canvas layer issue

I have similar issue with layers as described here html5 - canvas element - Multiple layers
But, accepted answer doesn't work for me, as for layer1 I have rendered image (drawImage)
And second layer - layer2 (gradient) always under layer1.
Sample code:
canvas = document.getElementById("layer1");
ctx = canvas.getContext("2d");
var img = new Image();
img.src = "/img/img.png";
img.onload = function() {
ctx.drawImage(img, 0, 0);
};
canvas2 = document.getElementById("layer2");
ctx2 = canvas.getContext("2d");
var my_gradient = ctx2.createLinearGradient(0, 0, 0, 400);
my_gradient.addColorStop(0, "black");
my_gradient.addColorStop(1, "white");
ctx2.fillStyle = my_gradient;
ctx2.fillRect(0, 0, 500, 555);
HTML:
<canvas id="layer1" width="1000" height="1000" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
<canvas id="layer2" width="1000" height="1000" style="position: absolute; left: 0; top: 0; z-index: 1;"></canvas>
You are setting ctx2 to layer1's context:
ctx2 = canvas.getContext("2d");
Of course, since the image loads asynchronously, the onload event fires after you've already drawn the gradient, and it gets drawn on the same canvas.

Categories

Resources