Canvas: rotate image with 'canvas' - javascript

Rotating image and resize canvas.
var img = document.getElementById("i");
width = img.width;
height = img.height;
canvasH.width = width;
canvasH.height = height;
ctxH.clearRect(0, 0, width, height);
ctxH.save();
ctxH.translate(width / 2, height / 2);
ctxH.rotate(90 * Math.PI / 180);
ctxH.translate(-(width / 2), -(height / 2));
ctxH.drawImage(img, 0, 0);
ctxH.restore();
var w = canvasH.width;
// Resize canvas to meet rotated image size
// Comment last two lines and see how image rotated
canvasH.width = height;
canvasH.height = w;
Canvas rotated (resized) but image out of visible area.
What can I do to get rotated image?
FIDDLE: http://jsfiddle.net/ouh5845c/1/

It is important to know that changing the width/height of canvas implicitly clears the canvas. So, whatever you have to do related to sizing the canvas, do it before rendering to it.
Here's a trigonometrically correct approach, working for any angle:
var img = document.getElementById("i"),
angrad = angle * Math.PI /180,
sin = Math.sin(angrad),
cos = Math.cos(angrad);
width = Math.abs(img.width*cos)+Math.abs(img.height*sin);
height = Math.abs(img.height*cos)+Math.abs(img.width*sin);
console.log(img.width,img.height,width,height);
canvasH.width = width;
canvasH.height = height;
ctxH.clearRect(0, 0, width, height);
ctxH.save();
ctxH.translate(width / 2, height / 2);
ctxH.rotate(angrad);
ctxH.translate(-img.width / 2, -img.height / 2);
ctxH.drawImage(img, 0, 0);
ctxH.restore();
http://jsfiddle.net/ouh5845c/5/
===========================
and the following story you can forget:
Now, because you need to rotate, width and height are interpreted differently before rotation and after rotation. (Of course, for a "hairy" situation when the angle is not 90 degrees, some trigonometry would come in handy).
I believe this fiddle does what you needed:
var img = document.getElementById("i");
width = img.width;
height = img.height;
canvasH.width = height;
canvasH.height = width;
ctxH.clearRect(0, 0, width, height);
ctxH.save();
ctxH.translate(height / 2, width / 2);
ctxH.rotate(90 * Math.PI / 180);
ctxH.translate(-width / 2, -height / 2);
ctxH.drawImage(img, 0, 0);
ctxH.restore();
http://jsfiddle.net/ouh5845c/4/

If you are trying to rotate the image 90 degrees this should work.
var canvasH = document.getElementById("canvasH"),
ctxH = canvasH.getContext("2d"),
x = 0,
y = 0,
width = 0,
height = 0,
angle = 180,
timeOut = null;
function loaded() {
var img = document.getElementById("i");
canvasH.width = img.height;
canvasH.height = img.width;
ctxH.clearRect(0, 0, img.width, img.height);
ctxH.save();
ctxH.translate(img.height, 0);
ctxH.rotate(1.57079633);
ctxH.drawImage(img, 0, 0);
ctxH.restore();
}
jsFiddle

Related

Canvas drawImage: Proportion Issue when linearly interpolating from "fit"- to "fill"-style settings

I have a canvas and I want to be able to draw an image in different sizes from "fit" (like CSS "contain") to "fill" (like CSS "cover"). I use drawImage() with different source and destination properties for fit and fill. Both extremes work perfectly as expected, but in between the image proportions are way off, and the image looks flat. I use linear interpolation to calculate the between source and destination properties.
"fit/contain" properties:
ctx.drawImage(
img, // image
0, // source x
0, // source y
img.width, // source width
img.height, // source height
(canvas.width - canvas.height * imageAspect) / 2, // destination x
0, // destination y
canvas.height * imageAspect, // destination width
canvas.height // destination height
)
"fill/cover" Properties:
ctx.drawImage(
img, // image
0, // source x
(image.height - img.width / canvasAspect) / 2, // source y
img.width, // source width
img.width / canvasAspect, // source height
0, // destination x
0, // destination y
canvas.width, // destination width
canvas.height // destination height
)
These are both fine, but linear interpolation of all the values get the wrong proportions of the image. Here's a quick demo that is not working as expected, I animated the interpolation so that you can see the squished effect more clearly:
Code Pen
The desired result would be keeping the image's proportions right in every step between 0 (fit) and 1 (fill). What am I missing here?
EDIT: The easiest solution would be to always take the full source image (not crop it with sX, sY, sWidth, and sHeight) and then draw the destination with negative coordinate values on the canvas when the image is bigger than the canvas. This is working but it is not the desired behavior. Because further on I need to be able to draw only to a certain sub-rectangle in the canvas, where the overlapping ("negative values") would be seen. I don't want to draw outside the rectangle. I am quite sure it is just a small mathematical issue here that needs to be solved.
For me, the solution in your "Edit" is the way to go.
If later on you want to clip the image in a smaller rectangle than the canvas, use the clip() method:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 800;
canvas.height = 150;
let step = 1;
let direction = 1;
// control the clipping rect position with the mouse
const mouse = {x: 400, y: 75};
onmousemove = (evt) => {
const rect = canvas.getBoundingClientRect();
mouse.x = evt.clientX - rect.left;
mouse.y = evt.clientY - rect.top;
};
function getBetweenValue(from, to, stop) {
return from + (to - from) * stop;
}
const image = new Image();
image.src =
"https://w7.pngwing.com/pngs/660/154/png-transparent-perspective-grid-geometry-grid-perspective-grid-geometric-grid-grid.png";
let imageAspect = 0;
let canvasAspect = canvas.width / canvas.height;
let source;
let containDestination;
let coverDestination;
function draw(image) {
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Clip the context in a sub-rectangle
ctx.beginPath();
ctx.rect(mouse.x - 150, mouse.y - 50, 300, 100);
ctx.stroke();
ctx.clip();
// Since our image scales from the middle of the canvas,
// set the context's origin there, that makes our BBox values simpler
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.drawImage(
image,
source.x,
source.y,
source.width,
source.height,
getBetweenValue(containDestination.x, coverDestination.x, step),
getBetweenValue(containDestination.y, coverDestination.y, step),
getBetweenValue(containDestination.width, coverDestination.width, step),
getBetweenValue(containDestination.height, coverDestination.height, step)
);
ctx.restore(); // remove clip & transform
}
image.addEventListener("load", () => {
imageAspect = image.width / image.height;
source = {
x: 0,
y: 0,
width: image.width,
height: image.height
};
containDestination = {
x: -(canvas.height * imageAspect) / 2,
y: -(canvas.height / 2),
width: canvas.height * imageAspect,
height: canvas.height
};
coverDestination = {
x: -image.width / 2,
y: -image.height / 2,
width: image.width,
height: image.height
};
raf();
});
function raf() {
draw(image);
step += .005 * direction;
if (step > 1 || step < 0) {
direction *= -1;
}
window.requestAnimationFrame(raf);
}
canvas {
border:1px solid red;
}
img {
max-width:30em;
height:auto;
}
Use your mouse to move the clipping rectangle<br>
<canvas></canvas><br><br>
Original image proportions:<br>
<img src="https://w7.pngwing.com/pngs/660/154/png-transparent-perspective-grid-geometry-grid-perspective-grid-geometric-grid-grid.png" alt="">

HTML5 Canvas animate the rotation of image around Y axis

I have an image on a canvas:
var canvas = document.getElementsByTagName("canvas")[0];
var ctx = canvas.getContext("2d");
var img = new Image();
img.onload = function(){
ctx.drawImage(img, 0, 0, 200, 350);
}
img.src = "/img.png";
Can an image be rotated around the Y axis? The 2d Canvas API only seems to have a rotate function that rotates around the Z-axis.
You can rotate around whatever axis you want. Using the save-transform-restore method covered in the linked question, we can do something similar by transforming a DOMMatrix object and applying it to the canvas.
Sample:
// Untransformed draw position
const position = {x: 0, y: 0};
// In degrees
const rotation = { x: 0, y: 0, z: 0};
// Rotation relative to here (this is the center of the image)
const rotPt = { x: img.width / 2, y: img.height / 2 };
ctx.save();
ctx.setTransform(new DOMMatrix()
.translateSelf(position.x + rotPt.x, position.y + rotPt.y)
.rotateSelf(rotation.x, rotation.y, rotation.z)
);
ctx.drawImage(img, -rotPt.x, -rotPt.y);
ctx.restore();
This isn't a "true" 3d, of course (the rendering context is "2d" after all). I.e. It's not a texture applied to some polygons. All it's doing is rotating and scaling the image to give the illusion. If you want that kind of functionality, you'll want to look at a WebGL library.
Demo:
I drew a cyan rectangle around the image to show the untransformed position.
Image source from MDN (see snippet for url).
const canvas = document.getElementsByTagName("canvas")[0];
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = draw;
img.src = "https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage/canvas_drawimage.jpg";
let rotCounter = 0;
let motionCounter = 0;
function draw() {
// Untransformed draw position
const position = {
x: motionCounter % canvas.width,
y: motionCounter % canvas.height
};
// In degrees
const rotation = {
x: rotCounter * 1.2,
y: rotCounter,
z: 0
};
// Rotation relative to here
const rotPt = {
x: img.width / 2,
y: img.height / 2
};
ctx.save();
ctx.setTransform(new DOMMatrix()
.translateSelf(position.x + rotPt.x, position.y + rotPt.y)
.rotateSelf(rotation.x, rotation.y, rotation.z)
);
// Rotate relative to this point
ctx.drawImage(img, -rotPt.x, -rotPt.y);
ctx.restore();
// Position
ctx.strokeStyle = 'cyan';
ctx.strokeRect(position.x, position.y, img.width, img.height);
ctx.strokeStyle = 'black';
rotCounter++;
motionCounter++;
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
draw();
requestAnimationFrame(render);
}
render();
canvas {
border: 1px solid black;
}
<canvas width=600 height=400></canvas>
Rotate without perceptive
If you only want to rotate around the y Axis without perspective then you can easily do it on the 2D canvas as follows.
Assuming you have loaded the image the following will rotate around the image Y axis and have the y axis align to an arbitrary line.
The arguments for rotateImg(img, axisX, axisY, rotate, centerX, centerY)
img a valid image.
axisX, axisY vector that is the direction of the y axis. To match image set to - axisX = 0, axisY = 1
rotate amount to rotate around image y axis in radians. 0 has image facing screen.
centerX & centerY where to place the center of the image
function rotateImg(img, axisX, axisY, rotate, centerX, centerY) {
const iw = img.naturalWidth;
const ih = img.naturalHeight;
// Normalize axis
const axisLen = Math.hypot(axisX, axisY);
const nAx = axisX / axisLen;
const nAy = axisY / axisLen;
// Get scale along image x to match rotation
const wScale = Math.cos(rotate);
// Set transform to draw content
ctx.setTransform(nAy * wScale, -nAx * wScale, nAx, nAy, centerX, centerY);
// Draw image normally relative to center. In this case half width and height up
// to the left.
ctx.drawImage(img, -iw * 0.5, -ih * 0.5, iw, ih);
// to reset transform use
// ctx.setTransform(1,0,0,1,0,0);
}
Demo
The demo uses a slightly modified version of the above function. If given an optional color as the last argument. It will shade the front face using the color and render the back face as un-shaded with that color.
The demo rotates the image and slowly rotates the direction of the image y axis.
const ctx = canvas.getContext("2d");
var W = canvas.width, H = canvas.height;
const img = new Image;
img.src = "https://i.stack.imgur.com/C7qq2.png?s=256&g=1";
img.addEventListener("load", () => requestAnimationFrame(renderLoop), {once:true});
function rotateImg(img, axisX, axisY, rotate, centerX, centerY, backCol) {
const iw = img.naturalWidth;
const ih = img.naturalHeight;
const axisLen = Math.hypot(axisX, axisY);
const nAx = axisX / axisLen;
const nAy = axisY / axisLen;
const wScale = Math.cos(rotate);
ctx.setTransform(nAy * wScale, -nAx * wScale, nAx, nAy, centerX, centerY);
ctx.globalAlpha = 1;
ctx.drawImage(img, -iw * 0.5, -ih * 0.5, iw, ih);
if (backCol) {
ctx.globalAlpha = wScale < 0 ? 1 : 1 - wScale;
ctx.fillStyle = backCol;
ctx.fillRect(-iw * 0.5, -ih * 0.5, iw, ih);
}
}
function renderLoop(time) {
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0, 0, W, H);
rotateImg(img, Math.cos(time / 4200), Math.sin(time / 4200), time / 500, W * 0.5, H * 0.5, "#268C");
requestAnimationFrame(renderLoop);
}
canvas {border: 1px solid black; background: #147;}
<canvas id="canvas" width="300" height="300"></canvas>

How to recalculate BoundingBox coordinates from image after scaleToFill transformation in HTML Canvas

I have a boundingBox selecting a zone in a image and i would like to scale this boundingBox to my canvas ratio.
I would like to recalculate the ratio of my boundingBox to correctly target a zone of the resized image in the canvas.
Here an example + jsFiddle : ( this is an example, the real project use multiple boundingBox with a big range of images)``
The boundingBox coordinate / width and height are calculated from the image but after the transformation i dont know how to convert the coordinate / ratio.
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext("2d");
//bbox
let [bx, by, bw, bh] = [146, 58, 82, 87]
console.log(bx, by, bw, bh)
function bboxDraw(){
// Draw the bounding box.
ctx.strokeStyle = "#00FFFF";
ctx.lineWidth = 4;
ctx.strokeRect(bx, by, bw, bh);
// Draw the label background.
ctx.fillStyle = "#00FFFF";
}
function scaleToFill(img, callback){
canvas.width = window.innerWidth
canvas.height = window.innerHeight
// get the scale
var scale = Math.max(canvas.width / img.width, canvas.height / img.height);
// get the top left position of the image
var x = (canvas.width / 2) - (img.width / 2) * scale;
var y = (canvas.height / 2) - (img.height / 2) * scale;
ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
bboxDraw()
}
let img = new Image()
try {
img.src = "https://via.placeholder.com/1280x720"
} catch (error) {
console.log("image URL", error);
}
img.onload = function() {
scaleToFill(this)
}
JSFiddle
Any good idea to preserve the ratio after the scale2fill transformation and correctly target the boundingBox area ?
To recalculate BoundingBox coordinates from image after scale to fill transformation in HTML Canvas.
We need to recalculate the width/height/x/y of the boundingBox using the naturalWidth and naturalHeight of the image.
let [bx, by, bw, bh] = [146, 58, 82, 87]
function bboxRatioDraw(img) {
// Use percent to correctly adapt the coordinate to the scaled image
let percentBx = (100 * (bx / img.naturalWidth)), // x %
percentBy = (100 * (by / img.naturalHeight)), // y %
percentBw = (bw * 100) / img.naturalWidth, // width%
percentBh = (bh * 100) / img.naturalHeight; // height%
// then map the values to the current canvas
let finalBx = (percentBx * canvas.width) / 100, // x en pixel
finalBy = (percentBy * canvas.height) / 100, // y en pixel
finalBw = (percentBw * canvas.width) / 100, // width en pixel
finalBh = (percentBh * canvas.height) / 100; // height en pixel
// Draw the bounding box.
ctx.strokeStyle = "green";
ctx.lineWidth = 4;
ctx.strokeRect(finalBx, finalBy, finalBw, finalBh);
// Draw the label background.
ctx.fillStyle = "#00FFFF";
}
Updated JSFiddle

Fabric Canvas - Image resolution is very poor

I'm implementing Fabricjs in my application for an editing tool. I need to set a high resolution image into the canvas. If I use setBackgroundImage method, it is only working for small sized (Size very less than canvas) images. So I need to reduce the size of the image (but need to keep the ratio) to have something good looking.
My first idea is down sampling which works fine for some pictures but not for all. Here is the code :
Method 1)
var steps = 2;
var imgAspect = img.width / img.height;
var oc = document.createElement('canvas');
octx = oc.getContext('2d');
for(var i = 0; i < steps; i++){
if(i == 0){
oc.width = img.width * 0.5;
oc.height = img.height * 0.5;
octx.drawImage(img, 0, 0, oc.width, oc.height);
}
else{
octx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5);
}
}
_w = canvas.width;
_h = canvas.height;
ctx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5, 0, 0, _w, _h);
var data = ctx.getImageData(0, 0, canvas.width, canvas.height);
var c = document.createElement('canvas');
c.setAttribute('id', '_temp_canvas');
c.width = canvas.width;
c.height = canvas.height;
c.getContext('2d').putImageData(data, 0, 0);
var img = fabric.Image.fromURL(c.toDataURL(), function(img) {
img.left = 00;
img.top = 00;
img.isFixed = true;
img.selectable = false;
canvas.add(img);
c = null;
$('#_temp_canvas').remove();
canvas.renderAll();
});
My second idea, I'm following from here which looks very simple
var canvasCopy = document.createElement("canvas")
var copyContext = canvasCopy.getContext("2d")
var ratio = 1;
if(img.width > canvas.width){
ratio = canvas.width / img.width;
}
else if(img.height > canvas.height){
ratio = canvas.height / img.height;
}
canvasCopy.width = img.width;
canvasCopy.height = img.height;
copyContext.drawImage(img, 0, 0);
canvas.width = img.width * ratio;
canvas.height = img.height * ratio;
ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
The issue is none of them producing good resolution. Method 1 works well for some pictures but it is failed for few pictures. If I try the method 2 it is working for the failed pictures from method 1. Can somebody help what is missing regarding the resolution ?
See the fiddle1 and fiddle2 with 2 different images. You can see the difference by calling the method1 and method2 in the _img.onload function to see the difference.
I don't know if this is your case, but for me worked imageSmoothingEnabled=false like
var canvasCopy = document.createElement("canvas")
var copyContext = canvasCopy.getContext("2d")
copyContext.imageSmoothingEnabled = false;
reference http://fabricjs.com/lanczos-webgl

Getting ugly image while simulating cover in canvas

I'm trying to display the image using cover simulation in canvas. I've found some cool answer on how to do it.
The thing is when I do it with a large picture, it's being displayed ugly. How to fix that?
Here's my Codepen
HTML
<canvas id="canvas"></canvas>
CSS
canvas {
width: 100%;
height: 100vh;
}
JS
var ctx = canvas.getContext('2d'),
img = new Image;
img.onload = draw;
img.src = 'https://upload.wikimedia.org/wikipedia/commons/0/0f/2010-02-19_3000x2000_chicago_skyline.jpg';
function draw() {
drawImageProp(ctx, this, 0, 0, canvas.width, canvas.height);
//drawImageProp(ctx, this, 0, 0, canvas.width, canvas.height, 0.5, 0.5);
}
/**
* By Ken Fyrstenberg
*
* drawImageProp(context, image [, x, y, width, height [,offsetX, offsetY]])
*
* If image and context are only arguments rectangle will equal canvas
*/
function drawImageProp(ctx, img, x, y, w, h, offsetX, offsetY) {
if (arguments.length === 2) {
x = y = 0;
w = ctx.canvas.width;
h = ctx.canvas.height;
}
/// default offset is center
offsetX = offsetX ? offsetX : 0.5;
offsetY = offsetY ? offsetY : 0.5;
/// keep bounds [0.0, 1.0]
if (offsetX < 0) offsetX = 0;
if (offsetY < 0) offsetY = 0;
if (offsetX > 1) offsetX = 1;
if (offsetY > 1) offsetY = 1;
var iw = img.width,
ih = img.height,
r = Math.min(w / iw, h / ih),
nw = iw * r, /// new prop. width
nh = ih * r, /// new prop. height
cx, cy, cw, ch, ar = 1;
/// decide which gap to fill
if (nw < w) ar = w / nw;
if (nh < h) ar = h / nh;
nw *= ar;
nh *= ar;
/// calc source rectangle
cw = iw / (nw / w);
ch = ih / (nh / h);
cx = (iw - cw) * offsetX;
cy = (ih - ch) * offsetY;
/// make sure source rectangle is valid
if (cx < 0) cx = 0;
if (cy < 0) cy = 0;
if (cw > iw) cw = iw;
if (ch > ih) ch = ih;
/// fill image in dest. rectangle
ctx.drawImage(img, cx, cy, cw, ch, x, y, w, h);
}
To accomplish this you could use several techniques like HTML/CSS, CSS Only, Jquery or JS/Canvas. For more on this look here.
You do not have to set the width and height of your canvas in HTML like David Skx mentioned. You do have to erase your CSS, remove it completely.
In your JS you should set your canvas size (just define it in 1 place, don't let different languages interfere):
var canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
Window means the whole browser, Use pixels here if you want to limit it to just a canvas and not the whole background
That's all.
You have to specify the width and height in pixels directly on the <canvas>-Element, else it will distort it:
<canvas id="canvas" width="500" height="500"></canvas>
Use JavaScript to measure the window width and height and set it dynamically. Something like:
var canvas = document.getElementById('canvas');
canvas.setAttribute('width', window.innerWidth);
canvas.setAttribute('height', window.innerHeight);
UPDATE:
As Matthijs van Hest pointed out, the width and height attributes on the <canvas>-element are just optional.

Categories

Resources