Create an arc distortion of an image with canvas - javascript

I would like to create an arc distortion of an image with canvas.
My goal is to do the same thing as imagemagick but in javascript with canvas: https://legacy.imagemagick.org/usage/distorts/#circular_distorts
Here is the expected result with the angle parameter that corresponds to the images below:
60°, 120°, 180°, 270°, 360°
I only found two interesting codes that go in the right direction:
This experimental script
which works directly on the pixel array but does not keep the aspect ratio of the original image and the angle given as a parameter does not work well:
https://github.com/sergiks/canvas-text-arc
This other script
which makes a rotation on each column of the image with drawimage but does not allow to configure the angle of the arc, it is a 360° rotation by default:
http://jsfiddle.net/hto1s6fy/
var cv = document.getElementById('cv');
var ig = document.getElementById('ig');
var ctx = cv.getContext('2d');
// draw the part of img defined by the rect (startX, startY, endX, endY) inside
// the circle of center (cx,cy) between radius (innerRadius -> outerRadius)
// - no check performed -
function drawRectInCircle(img, cx, cy, innerRadius, outerRadius, startX, startY, endX, endY) {
var angle = 0;
var step = 1 * Math.atan2(1, outerRadius);
var limit = 2 * Math.PI;
ctx.save();
ctx.translate(cx, cy);
while (angle < limit) {
ctx.save();
ctx.rotate(angle);
ctx.translate(innerRadius, 0);
ctx.rotate(-Math.PI / 2);
var ratio = angle / limit;
var x = startX + ratio * (endX - startX);
ctx.drawImage(img, x, startY, 1, (endY - startY), 0, 0, 1, (outerRadius - innerRadius));
ctx.restore();
angle += step;
}
ctx.restore();
}
var cx = 300,
cy = 300;
var innerRadius = 0;
var outerRadius = 300;
var startX = 0,
endX = 1361,
startY = 0,
endY = 681;
drawRectInCircle(ig, cx, cy, innerRadius, outerRadius, startX, startY, endX, endY);
Imagemagick source code
Finally, I also looked at the C source code of imagemagick but I don't have the skills to transpose it:
https://github.com/imagemagick/imagemagick/blob/main/magickcore/distort.c
(to see what concerns arc distortion, use the keyword "ArcDistortion")

Though this is an interesting topic and I also like to re-invent the wheel sometimes, it isn't necessary in this case. Someone else had a go at it yet and released a JavaScript library called lens, which replicates some of ImageMagick's filters. Luckily the 'Arc distortion' is among those.
Lens offers a method called distort() which accepts an input like a <canvas> element, applies the transformation requested and outputs raw pixel data, which you can then use to make another <canvas>.
Here's a quick example:
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.font = "48px sans";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#0";
ctx.fillText("Around the World", canvas.width / 2, canvas.height / 2);
async function makeArc(image, angle, rotate = 0) {
let result = await lens.distort(
image,
lens.Distortion.ARC, [angle, rotate], {
imageVirtualPixelMethod: angle === 360 ? lens.VirtualPixelMethod.HORIZONTAL_TILE : lens.VirtualPixelMethod.TRANSPARENT
}
);
let tempCanv = document.createElement("canvas");
let tempCtx = tempCanv.getContext("2d");
tempCanv.width = result.image.width;
tempCanv.height = result.image.height;
tempCtx.putImageData(result.image.imageData, 0, 0);
document.body.appendChild(tempCanv);
}
makeArc(canvas, 120, 0);
<script src="https://cdn.jsdelivr.net/npm/#alxcube/lens#1.0.0/dist/lens.min.js"></script>
<canvas id="canvas" width="450" height="50"></canvas>

Related

Why is the drawn line not clearing from canvas?

Link to JSFiddle for entire code: https://jsfiddle.net/u4mk0gdt/
I read the Mozilla docs on save() and restore() and I thought that "save" saved the current state of the entire canvas and "restore" restored the canvas to the most recent "save" state. Hence I placed the saves and restores in such a way that it should clear the white line that is drawn to canvas after is is drawn. However when I run this code the white line is never cleared from the canvas and is drawn continually without clearing.
ctx.restore();
ctx.save(); // <--should save blank canvas
//DRAW LINE
ctx.moveTo(tMatrix.x1, tMatrix.y1);
ctx.lineTo(w/2,h/2);
ctx.strokeStyle = "white";
ctx.stroke();
ctx.restore(); // <-- should restore to the "save()" above
ctx.save(); // <-- <--should save blank canvas again
As you can see, I made a lot of modifications to your code:
console.log("rotating_recs");
// create canvas and add resize
var canvas, ctx;
function createCanvas() {
canvas = document.createElement("canvas");
canvas.style.position = "absolute";
canvas.style.left = "0px";
canvas.style.top = "0px";
canvas.style.zIndex = 1000;
document.body.appendChild(canvas);
}
function resizeCanvas() {
if (canvas === undefined) {
createCanvas();
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
}
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
var Player = function(x, y, height, width, rot) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.rot = rot;
this.objWinX = 0; //translate the window object and then apply to this
this.objWinY = 0;
this.draw = function() {
//rotate by user.rot degrees, from the players center
ctx.translate(this.x + this.width / 2, this.y + this.height / 2)
ctx.rotate(this.rot * Math.PI / 180)
ctx.translate(-this.x - this.width / 2, -this.y - this.height / 2)
ctx.fillStyle = "grey";
ctx.fillRect(this.x, this.y, this.height, this.width);
ctx.translate(this.x + this.width / 2, this.y + this.height / 2)
ctx.rotate(-this.rot * Math.PI / 180)
ctx.translate(-this.x - this.width / 2, -this.y - this.height / 2)
}
}
var user = new Player(0, 0, 40, 40, 0);
var user2 = new Player(0, 0, 40, 40, 0);
let rot = 0;
function update(time) {
var w, h;
w = canvas.width; // get canvas size incase there has been a resize
h = canvas.height;
ctx.clearRect(0, 0, w, h); // clear the canvas
//MIDDLE RECT
/*
if you don't want this you can just translate by w/2 and h/2, but I would recommend just making the p layers position the middle
*/
user.x = w / 2 - 20;
user.y = h / 2 - 20;
user.rot += 0.5 // or whatever speed
user.draw(); //draw player -- look at the draw function I added some stuff
//LINE
/*
I don't know what you are trying to do, but I just drew the line to the user2's position,
if this doesn't work for your scenario you can change it back
*/
ctx.beginPath()
ctx.moveTo(user2.x + user2.width/2, user2.y + user2.height/2);
ctx.lineTo(w / 2, h / 2);
ctx.strokeStyle = "white";
ctx.stroke();
//FAST SPIN RECT
/*
There are multiple ways to do this, the one that I think you should do, is actually change the position of user two, this uses some very simple trigonometry, if you know this, this is a great way to do this, if not, you can do it how you did previously, and just translate to the center, rotate, and translate back. Similar to what I did with the player draw function. I am going to demonstrate the trig way here:
*/
user2.rot += 5
rot += 2;
user2.x = w/2 + (w/2) * Math.cos(rot * (Math.PI/180))
user2.y = h/2 + (w/2) * Math.sin(rot * (Math.PI/180))
user2.draw();
//RED RECT
ctx.fillStyle = 'red';
ctx.fillRect(140, 60, 40, 40);
requestAnimationFrame(update); // do it all again
}
requestAnimationFrame(update);
While I think you should add some of these modifications into you code, they are not super necessary. To fix you line problem, all you had to do was add ctx.beginPath() before you drew it. The demonstration that I made was not very good (hence demonstration), and you probably shouldn't use it exactly, but definitely look over it. The modified code for you line drawing would look like:
//LINE
ctx.beginPath()
ctx.moveTo(tMatrix.x1, tMatrix.y1);
ctx.lineTo(w/2,h/2);
ctx.strokeStyle = "white";
ctx.stroke();
ctx.restore();
ctx.save();
Hope this helps :D
Sorry for bad spelling

Why are multiple rotate() needed to place every number on the right spot? - Canvas Clock Numbers

Following this tutorial which shows how to make an analog clock using HTML canvas, I've had a hard time understanding what is going on when placing numbers on the clock face.
The code is here, and the following is the part that I'd like to ask.
function drawNumbers(ctx, radius) {
var ang;
var num;
ctx.font = radius * 0.15 + "px arial";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
for(num = 1; num < 13; num++){
ang = num * Math.PI / 6;
ctx.rotate(ang);
ctx.translate(0, -radius * 0.85);
ctx.rotate(-ang);
ctx.fillText(num.toString(), 0, 0);
ctx.rotate(ang);
ctx.translate(0, radius * 0.85);
ctx.rotate(-ang);
}
}
In a for loop, the first ctx.rotate(ang) sets the number on the place it's supposed to be.
The next rotate ctx.rotate(-ang) puts the number back to upright because it's tilted. (although I don't know why it works like this not putting the number back to the first position.)
Then, after ctx.fillText(…) shows the number up, it seems to do the same again.
Why are these two rotate() needed? Do they work differently from the ones in the upper? If do, how?
What this code tries to do is to go back to its previous position, the center of the canvas.
Think of the context as a sheet of paper that you can rotate and move (translate), with a fixed pen over it.
First they do rotate that sheet of paper so that tracing a vertical line will go in the desired direction.
Then they move the sheet of paper vertically, so that the pen is at the correct position. However here, the sheet of paper is still rotated, so if they were to draw the text horizontally from here, the drawing would be oblique.
So they do rotate again in the other way for the text to be at correct angle.
They draw the text.
Now they want to go back to point 1 to be able to draw the next tick. For this they do the same route but in the other way: rotate back the sheet of paper to the desired angle so that they can move vertically to the center.
Move vertically to the center
Finally rotate back so that the sheet of paper is in its original orientation for the next tick.
However you should not do this. rotate() may end up having rounding issues, so doing rotate(angle); rotate(-angle) can not come back to the initial orientation, but to some slightly rotated state, which can be catastrophic for your application since now when you'll try to draw pixel perfect lines, you won't be able and you will kill the whole performances of your app.
Instead use the absolute setTransform method to go back to the original position:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var radius = canvas.height / 2;
radius = radius * 0.90
drawNumbers(ctx, radius);
function drawNumbers(ctx, radius) {
var ang;
var num;
ctx.font = radius * 0.15 + "px arial";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
for(num = 1; num < 13; num++){
ang = num * Math.PI / 6;
// go (back) to center
ctx.setTransform(1, 0, 0, 1, radius, radius);
ctx.rotate(ang);
ctx.translate(0, -radius * 0.85);
ctx.rotate(-ang);
ctx.fillText(num.toString(), 0, 0);
}
// reset to identity matrix;
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
canvas {
background-color: white;
}
<canvas id="canvas" width="400" height="400">
</canvas>
Here is another implementation without using rotate.
Instead I calculate the x, y with a bit of trigonometry.
The starting angle is var ang = Math.PI;
Then in the loop we decrease it ang -= Math.PI / 6;
Calculating the position is easy once you know the formula:
let x = radius * Math.sin(ang)
let y = radius * Math.cos(ang)
Below is a fully functional example
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.font = "16px arial";
ctx.textAlign = "center";
var radius = 60
var ang = Math.PI;
for (let num = 1; num < 13; num++) {
ang -= Math.PI / 6;
let x = radius * Math.sin(ang)
let y = radius * Math.cos(ang)
ctx.fillText(num.toString(), x, y);
ctx.beginPath()
ctx.arc(x, y - 6, 12, 0, 2 * Math.PI);
ctx.stroke();
ctx.beginPath()
ctx.arc(x, y - 6, 45, -ang-2,-ang);
ctx.stroke();
}
<canvas id="canvas" width="160" height="160"></canvas>
I personally never been a fan of using rotate, for a small static canvas image might be fine, but as we move to more complex animations with multiple object, when I have to debug with multiple rotation it quickly becomes painful and quite hard to follow.

canvas rotate img horizontally

I am working on animation optimisation and i wanted to try out canvas to see how it performs but i am not experienced well in canvas and i dont know how to prepare concept of this kind of animation.
this is the gif that shows how animation should rotate like:
this is my current code of js:
var cvs = document.getElementById('coin-spin'),
ctx = cvs.getContext('2d'),
w = cvs.width = 400,
h = cvs.height = 400,
cx = w / 2,
cy = h / 2,
a = 0;
var img = new Image();
var loop = function() {
// BG
ctx.fillStyle = '#ccc';
ctx.fillRect(0, 0, w, h);
// draw image
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(Math.PI / 180 * a);
ctx.translate(-cx, -cy);
ctx.drawImage(img, cx - (img.width / 2), cy - (img.height / 2));
ctx.restore();
// axis
ctx.strokeStyle = '#000';
ctx.beginPath();
ctx.moveTo(cx, 0);
ctx.lineTo(cx, h);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, cy);
ctx.lineTo(w, cy);
ctx.stroke();
//mod angle
a++;
window.requestAnimationFrame(loop);
};
img.onload = function() {
loop();
};
img.src = 'https://image.ibb.co/gqkeXx/coin.png';
and the working demo on fiddle.
Could someone show how to add to the code so the image would rotate horizontally like on the gif?
EDIT ----
I added the spin, as it was also something to do, but still struggling on how to rotate it.
To get around the problem of rotating the object along two axes (faking one by mapping width to a sine wave), you can use an offscreen canvas to render the coin rotating around one axis, then render that canvas applying the second rotation ;
//make an offscreen canvas for rendering the coin rotating around one axis
var offscreenCanvas = document.createElement('canvas');
var cvs = document.getElementById('coin-spin'),
ctx = cvs.getContext('2d'),
w = cvs.width = 400,
h = cvs.height = 400,
cx = w / 2,
cy = h / 2,
a = 0;
var img = new Image();
var frameCount = 0;
var loop = function() {
frameCount++;
// BG
ctx.fillStyle = '#ccc';
ctx.fillRect(0, 0, w, h);
offscreenContext.fillStyle = '#ccc';
offscreenContext.fillRect(0, 0, w, h);
//determine how wide to render the offscreen canvas so we can fake
//rotation around the second axis
var imgRenderWidth = offscreenCanvas.width * Math.sin(frameCount/10.0)
//render the coin rotating around one axis to the offscreen canvas
offscreenContext.save();
offscreenContext.translate(img.width/2, img.height/2);
offscreenContext.rotate(Math.PI / 180 * a);
offscreenContext.translate((0-img.width)/2, (0-img.height)/2);
offscreenContext.drawImage(img, 0,0);
offscreenContext.restore();
// draw offscreen canvas to the screen with our precalculated width
ctx.save();
ctx.drawImage(offscreenCanvas, cx - (imgRenderWidth / 2), cy - (offscreenCanvas.height / 2), imgRenderWidth, offscreenCanvas.height);
ctx.restore();
// axis
ctx.strokeStyle = '#000';
ctx.beginPath();
ctx.moveTo(cx, 0);
ctx.lineTo(cx, h);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, cy);
ctx.lineTo(w, cy);
ctx.stroke();
//mod angle
a++;
window.requestAnimationFrame(loop);
};
//once the image has loaded, we know what size our offscreen canvas needs to be
img.onload = function() {
offscreenCanvas.width = img.width;
offscreenCanvas.height = img.height;
loop();
};
img.src = 'https://image.ibb.co/gqkeXx/coin.png';
//prepare the offscreen context so we can render to it later
var offscreenContext = offscreenCanvas.getContext('2d');
https://jsfiddle.net/ay3h5vuo/

Calculate rotation of canvas gradient

I'm trying to use a gradient to fill an area of a canvas, but I would like to be able to set the angle of the gradient.
I know this is possible by using different values in the creation of the gradient (ctx.createLinearGradient(x1, y1, x2, y2)) as seen here:
But I can't seem to get my head around the maths needed to convert an angle (radians) to a gradient size that will produce the same angle (The angle I'm referring to is perpendicular to the direction of the gradient, so a 0 radian angle would show the gradient on the right)
In short, how can I convert (quantity) of radians into an X by Y shape?
$(document).ready(function(){
var canvas = document.getElementById("test");
var ctx = canvas.getContext("2d");
var angle = 0.5;
ctx.beginPath();
ctx.moveTo(100, 100);
ctx.arc(100, 100, 100, 0, -angle, true);
ctx.lineTo(100, 100);
ctx.closePath();
// Convert angle into coordinates to tilt the grad
// grad should be perpendicular to the top edge of the arc
var grad = ctx.createLinearGradient(0, 0, 0, 100);
grad.addColorStop(0, "rgba(0,0,0,0)");
grad.addColorStop(1, "rgba(0,0,0,0.8)");
ctx.fillStyle = grad;
ctx.fill();
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas id="test" width="500" height="500"></canvas>
(So no one wastes their time: I specifically don't want to use a context.rotate() in this case)
You can use the angle with cos and sin to define the line that gives the gradient. The only thing left then is to give the length:
var length = 100, angle = 0;
ctx.createLinearGradient(x, y, x + Math.cos(angle) * length, y + Math.sin(angle) * length);
The gradient will be rendered along (perpendicular) to the line given.
Not stated, but if you need to calculate the length of the line depending on the angle and box you can use the law of sines to do so (used in this way). The example below uses a fixed radius. You can also use max length from (x1, x2) by calculating the hypotenuse: length = Math.sqrt(diffX*diffX + diffY*diffY);.
Example
var ctx = c.getContext("2d"),
x1 = 150, y1 = 150, x2, y2, angle,
length = 150;
render();
cAngle.oninput = render;
function render() {
angle = +cAngle.value / 180 * Math.PI;
// calculate gradient line based on angle
x2 = x1 + Math.cos(angle) * length;
y2 = y1 + Math.sin(angle) * length;
// create and render gradient
ctx.fillStyle = ctx.createLinearGradient(x1, y1, x2, y2);
ctx.fillStyle.addColorStop(0, "#fff");
ctx.fillStyle.addColorStop(1, "#07f");
ctx.fillRect(0, 0, 300, 300);
// show definition line
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
<label>Angle: <input id=cAngle max=359 type=range value=0></label><br>
<canvas id=c height=300></canvas>

Rotation logic in html5 canvas

I have this animation, but i cant get over the logic. I hope someone can help me here.
Basicly i need this: http://jsfiddle.net/PDE85/9/ but without the arrow doing such crazy moves. It should be attached to the front of the open circle to simulate an expanding arrow.
I got the triangle to turn right here but it doesnt work when i mix it with position logic as seen in the first example.
Here is the code for reference
(function() {
var size = ($(window).height()/5)*4;
$("#intro-container").css('width',size);
$("#intro-canvas").css('width',size);
$("#intro-canvas").css('height',size);
var interval = window.setInterval(draw, 30);
var degrees = 0.0;
var offset = 20;
var rotate = 0;
var canvas = document.getElementById('intro-canvas');
var ctx = canvas.getContext('2d');
canvas.width = size;
canvas.height = size;
draw();
function draw() {
if (canvas.getContext) {
ctx.fillStyle="white";
ctx.strokeStyle="white";
ctx.clearRect(0, 0, size, size);
ctx.save();
ctx.translate(size/2, size/2);
ctx.rotate(-90 * Math.PI / 180);
ctx.beginPath();
ctx.lineWidth = size/8;
ctx.arc(0, 0, size/3, 0, rotate * Math.PI / 180);
//ctx.shadowBlur=1;
//ctx.shadowColor="black";
ctx.stroke();
ctx.restore();
ctx.beginPath();
ctx.save();
// moving logic
ctx.translate(size/2, size/2);
ctx.rotate(-Math.PI / 180 * -rotate+1);
ctx.translate(-size/3, -size/3);
// rotating logic
ctx.translate(size/2, size/2);
ctx.rotate((rotate * Math.PI + 420) / 180);
ctx.moveTo(0,0);
ctx.lineTo(size/6,0);
ctx.lineTo(0,size/6);
ctx.lineTo(0,0);
ctx.fill();
ctx.restore();
rotate += 1;
if(rotate > 360){
window.clearInterval(interval)
}
}
}
})();
I believe you are looking for this : http://jsfiddle.net/PDE85/12/
The rotation comes from, the rotate call which is unnecessary.
Plus you need an inverted triangle, hence the coordinates needed an update:
...
// ctx.rotate((rotate * Math.PI + 420) / 180);
ctx.moveTo(0,0);
ctx.lineTo(-size/6,0);
ctx.lineTo(0,-size/6);
...

Categories