How to rotate image in Canvas - javascript

I have built a canvas project with https://github.com/petalvlad/angular-canvas-ext
<canvas width="640" height="480" ng-show="activeateCanvas" ap-canvas src="src" image="image" zoomable="true" frame="frame" scale="scale" offset="offset"></canvas>
I am successfully able to zoom and pan the image using following code
scope.zoomIn = function() {
scope.scale *= 1.2;
}
scope.zoomOut = function() {
scope.scale /= 1.2;
}
Additionally I want to rotate the image. any help i can get with which library i can use and how can i do it inside angularjs.

You can rotate an image using context.rotate function in JavaScript.
Here is an example of how to do this:
var canvas = null;
var ctx = null;
var angleInDegrees = 0;
var image;
var timerid;
function imageLoaded() {
image = document.createElement("img");
canvas = document.getElementById("canvas");
ctx = canvas.getContext('2d');
image.onload = function() {
ctx.drawImage(image, canvas.width / 2 - image.width / 2, canvas.height / 2 - image.height / 2);
};
image.src = "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcT7vR66BWT_HdVJpwxGJoGBJl5HYfiSKDrsYrzw7kqf2yP6sNyJtHdaAQ";
}
function drawRotated(degrees) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(degrees * Math.PI / 180);
ctx.drawImage(image, -image.width / 2, -image.height / 2);
ctx.restore();
}
<button onclick="imageLoaded();">Load Image</button>
<div>
<canvas id="canvas" width=360 height=360></canvas><br>
<button onclick="javascript:clearInterval(timerid);
timerid = setInterval(function() {
angleInDegrees += 2;
drawRotated(angleInDegrees);
}, 100);">
Rotate Left
</button>
<button onclick="javascript:clearInterval(timerid);
timerid = setInterval(function() {
angleInDegrees -= 2;
drawRotated(angleInDegrees);
}, 100);">
Rotate Right
</button>
</div>

With curtesy to this page!
Once you can get your hands on the canvas context:
// save the context's co-ordinate system before
// we screw with it
context.save();
// move the origin to 50, 35 (for example)
context.translate(50, 35);
// now move across and down half the
// width and height of the image (which is 128 x 128)
context.translate(64, 64);
// rotate around this point
context.rotate(0.5);
// then draw the image back and up
context.drawImage(logoImage, -64, -64);
// and restore the co-ordinate system to its default
// top left origin with no rotation
context.restore();

To do it in a single state change. The ctx transformation matrix has 6 parts. ctx.setTransform(a,b,c,d,e,f); (a,b) represent the x,y direction and scale the top of the image will be drawn along. (c,d) represent the x,y direction and scale the side of the image will be drawn along. (e,f) represent the x,y location the image will be draw.
The default matrix (identity matrix) is ctx.setTransform(1,0,0,1,0,0) draw the top in the direction (1,0) draw the side in the direction (0,1) and draw everything at x = 0, y = 0.
Reducing state changes improves the rendering speed. When its just a few images that are draw then it does not matter that much, but if you want to draw 1000+ images at 60 frames a second for a game you need to minimise state changes. You should also avoid using save and restore if you can.
The function draws an image rotated and scaled around its center point that will be at x,y. Scale less than 1 makes the images smaller, greater than one makes it bigger. ang is in radians with 0 having no rotation, Math.PI is 180deg and Math.PI*0.5 Math.PI*1.5 are 90 and 270deg respectively.
function drawImage(ctx, img, x, y, scale, ang){
var vx = Math.cos(ang) * scale; // create the vector along the image top
var vy = Math.sin(ang) * scale; //
// this provides us with a,b,c,d parts of the transform
// a = vx, b = vy, c = -vy, and d = vx.
// The vector (c,d) is perpendicular (90deg) to (a,b)
// now work out e and f
var imH = -(img.Height / 2); // get half the image height and width
var imW = -(img.Width / 2);
x += imW * vx + imH * -vy; // add the rotated offset by mutliplying
y += imW * vy + imH * vx; // width by the top vector (vx,vy) and height by
// the side vector (-vy,vx)
// set the transform
ctx.setTransform(vx, vy, -vy, vx, x, y);
// draw the image.
ctx.drawImage(img, 0, 0);
// if needed to restore the ctx state to default but should only
// do this if you don't repeatably call this function.
ctx.setTransform(1, 0, 0, 1, 0, 0); // restores the ctx state back to default
}

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 Javascript Canvas Rotate Sprite

I have already looked for many questions like this and his answers on stackoverflow but it seems that I never complety have the exact same problem:
Player, created at x = canvas.width /2, y = canvas.height /2
The code that I use to generate the various sprites on the canvas is:
class Sprite {
constructor({
position,
imageSrc,
scale,
framesMax = 1,
offset = { x: 0, y: 0 },
}) {
this.position = position
this.width = 50
this.height = 150
this.image = new Image()
this.image.src = imageSrc
this.scale = scale
this.framesMax = framesMax
this.framesCurrent = 0
this.framesElapsed = 0
this.framesHold = 5
this.offset = offset
}
draw() {
c.drawImage(
this.image,
this.framesCurrent * (this.image.width / this.framesMax),
0,
this.image.width / this.framesMax,
this.image.height,
this.position.x - this.offset.x,
this.position.y - this.offset.y,
(this.image.width / this.framesMax) * this.scale,
this.image.height * this.scale
)
}
animateFrames() {
this.framesElapsed++
if (this.framesElapsed % this.framesHold === 0) {
if (this.framesCurrent < this.framesMax - 1) {
this.framesCurrent++
} else {
this.framesCurrent = 0
}
}
}
update() {
this.draw()
this.animateFrames()
}
}
Then what I want to do is to create the "Fishnet" that you see in the first picture, and position the image on a certain angle, but keep the starting point of the coordinates, another image is possible useful.
Fishnet:
I have tried many things, but the most common that I see everywhere, is to draw de image, save the canvas context, translate the canvas, rotate the canvas, and draw the image.
For reasons that I can't get in to, I never could rotate the image and maintain the starting position.
I wrote another sprite class specific for this rotation, and added the rotate method:
rotate(){
c.save();
c.translate(x,y)
c.rotate(this.angle)
c.drawImage(
this.image,
-(this.image.width),
-(this.image.height),
150,
50,
this.position.x - this.offset.x,
this.position.y - this.offset.y,
(this.image.width / this.framesMax) * this.scale,
this.image.height * this.scale
)
c.restore();
}
The x and y continue to be the (canvas.width / 2) and (canvas.height / 2) in witch I have my doubts of working...
The angle is calculate by the position of the yellow (projetile) and the center of the canvas:
angle = Math.atan2(projectile.position.y - canvas.height / 2, projectile.position.x - canvas.width / 2)
One of the few attempts this has """worked""", was by an example that I saw online, but I had to remove most of my parameters on the drawImage as I rotated it, like this:
draw() {
c.drawImage(
this.image,
this.framesCurrent * (this.image.width / this.framesMax),
0,
this.image.width / this.framesMax,
this.image.height,
this.position.x - this.offset.x,
this.position.y - this.offset.y,
(this.image.width / this.framesMax) * this.scale,
this.image.height * this.scale
)
}
Rotate:
rotate(){
c.save();
c.translate(x,y)
c.rotate(this.angle)
c.drawImage(
this.image,
-(this.image.width),
-(this.image.height)
)
c.restore();
Then I called the Draw method and after the Rotate method. It generated me 2 images. If I only call the rotate method it is obvious that I create only one image, but idk why, can't get the drawImage from rotate to work with the scale and position's x,y. Only works with those 3 paramaters
(this.image, -(this.image.width),-(this.image.height))
Here is the result of working only with the rotate method:
Rotation working with no scaling or proper angle (maybe translate is wrong?)
It can be a problem only on the angle, and I will try to figure it out (I do not think so anyway, because I have another solution launching a circle at that angle to check if its right, and it is.)
Still can't get it to work with scaling.. like I do in the draw() method above.
I know this will be very confusing but I have little knowledge of canvas in JavaScript (and overall..), feel free to comment on more information.
Thanks in advance.
Expanding on my comment, you can just draw the net yourself no need for an image, it does not look too complicated, an arc and a few lines should be a good starting point to test the concept
And the most complicated part I see you are already using:
c.translate(x,y)
c.rotate(angle)
Here is a quick example:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var angle = 0
function drawNet(x, y, w, h, angle) {
ctx.beginPath();
ctx.translate(x,y)
ctx.rotate(angle)
ctx.moveTo(0, 0);
ctx.arc(0, h, w / 2, 0, Math.PI)
ctx.lineTo(0, 0);
ctx.stroke();
ctx.rotate(-angle)
ctx.translate(-x,-y)
}
function draw() {
ctx.clearRect(0,0, c.width, c.height)
drawNet(70, 50, 55, 50, angle)
drawNet(200, 80 + Math.sin(angle)*30, 45, 30, -angle*2)
angle += 0.1
}
setInterval(draw, 60)
<canvas id="myCanvas" style="border:1px solid;">
In my function drawNet I'm translating and rotating to the given parameters then rotating back and translating back to the original position, that allows all following drawing to be at the correct location

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>

Rotation Matrix Spiraling inward

I am trying to make a square that rotates in place, however my square is spiraling inward, and I have no idea why. Here is the code, if someone could please explain what is happening as to why it is not just spinning in place.
var angle = 2 * (Math.PI / 180);
var rotate = [
[Math.cos(angle),Math.sin(angle)],
[-Math.sin(angle),Math.cos(angle)]
];
var points = [[300,0],[0,300],[-300,0],[0,-300]];
init.ctx.translate(init.canvas.width/2,init.canvas.height/2);
function loop(){
draw();
}
setInterval(loop,10);
function draw(){
init.ctx.beginPath();
init.ctx.moveTo(points[0][0],points[0][1]);
init.ctx.lineTo(points[1][0],points[1][1]);
init.ctx.lineTo(points[2][0],points[2][1]);
init.ctx.lineTo(points[3][0],points[3][1]);
init.ctx.closePath();
init.ctx.stroke();
for(let i=0;i<points.length;i++){
init.ctx.beginPath();
init.ctx.fillStyle = "red";
init.ctx.fillRect(points[i][0],points[i][1],5,5);
points[i][0] = points[i][0]*rotate[0][0] + points[i][1]*rotate[0][1];
points[i][1] = points[i][0]*rotate[1][0] + points[i][1]*rotate[1][1];
}
}
So, you are applying a small rotation each time draw is called, specifically 1/180th of a full rotation. Problem is that you are relying on floating point math to give you exact values, and it's not because it doesn't. This is compounded by the points array being calculated by iterations. I suggest calculate the new points on each step through draw by applying the correct rotate matrix for your current angle to the starting points.
var angle = 0;
var startPoints = [[300,0],[0,300],[-300,0],[0,-300]];
var points = [[300,0],[0,300],[-300,0],[0,-300]];
init.ctx.translate(init.canvas.width/2,init.canvas.height/2);
function loop(){
draw();
}
setInterval(loop,10);
function draw(){
init.ctx.beginPath();
init.ctx.moveTo(points[0][0],points[0][1]);
init.ctx.lineTo(points[1][0],points[1][1]);
init.ctx.lineTo(points[2][0],points[2][1]);
init.ctx.lineTo(points[3][0],points[3][1]);
init.ctx.closePath();
init.ctx.stroke();
angle = angle + Math.PI / 90;
var rotate = [
[Math.cos(angle),Math.sin(angle)],
[-Math.sin(angle),Math.cos(angle)]
];
for(let i=0;i<points.length;i++){
init.ctx.beginPath();
init.ctx.fillStyle = "red";
init.ctx.fillRect(points[i][0],points[i][1],5,5);
points[i][0] = startPoints[i][0]*rotate[0][0] + startPoints[i][1]*rotate[0][1];
points[i][1] = startPoints[i][0]*rotate[1][0] + startPoints[i][1]*rotate[1][1];
}
}
Some tips to improve your code.
As a beginner I can see some bad habits creeping in and as there is already an answer I thought I would just give some tips to improve your code.
Don't use setInterval to create animations. requestAnimationFrame gives much better quality animations.
Arrays were created in high level languages to make life easier, not harder.
You have painfully typed out
init.ctx.beginPath();
init.ctx.moveTo(points[0][0],points[0][1]);
init.ctx.lineTo(points[1][0],points[1][1]);
init.ctx.lineTo(points[2][0],points[2][1]);
init.ctx.lineTo(points[3][0],points[3][1]);
init.ctx.closePath();
init.ctx.stroke();
That would be a nightmare if you had 100 points. Much better to create a generic function to do that for you.
function drawShape(ctx,shape){
ctx.beginPath();
for(var i = 0; i < shape.length; i++){
ctx.lineTo(shape[i][0], shape[i][1]);
}
ctx.closePath();
ctx.stroke();
}
Now you can render any shape on any canvas context with the same code.
drawShape(init.ctx,points); // how to draw your shape.
If you use a uniform scale then you can shorten the transform a little by reusing the x axis of the transformation
var rotate = [
[Math.cos(angle),Math.sin(angle)],
[-Math.sin(angle),Math.cos(angle)]
];
Note how the second two values are just the first two swapped with the new x negated. You can also include a scale in that and just hold the first two values.
var angle = ?
var scale = 1; // can be anything
// now you only need two values for the transform
var xAx = Math.cos(angle) * scale; // direction and size of x axis
var xAy = Math.sin(angle) * scale;
And you apply the transform to a point as follows
var px = ?; // point to transform
var py = ?;
var tx = px * xAx - py * xAy;
var ty = px * xAy + py * xAx;
And to add a origin
var tx = px * xAx - py * xAy + ox; // ox,oy is the origin
var ty = px * xAy + py * xAx + oy;
But is is much better to let the canvas 2D API do the transformation for you. The example below shows the various methods described above to render your box and animate the box.
Example using best practice.
const ctx = canvas.getContext("2d");
var w = canvas.width; // w,h these are set if canvas is resized
var h = canvas.height;
var cw = w / 2; // center width
var ch = h / 2; // center height
var globalScale = 1; // used to scale shape to fit the canvas
var globalTime;
var angle = Math.PI / 2;
var rotateRate = 90; // deg per second
var points = [
[300, 0],
[0, 300],
[-300, 0],
[0, -300]
];
var maxSize = Math.hypot(600, 600); // diagonal size used to caculate scale
// so that shape fits inside the canvas
// Add path to the current path
// shape contains path points
// x,y origin of shape
// scale is the scale of the shape
// angle is the amount of rotation in radians.
function createShape(shape, x, y, scale, angle) {
var i = 0;
ctx.setTransform(scale, 0, 0, scale, x, y); // set the scale and origin
ctx.rotate(angle); // set the rotation
ctx.moveTo(shape[i][0], shape[i++][1]);
while (i < shape.length) { // create a line to each point
ctx.lineTo(shape[i][0], shape[i++][1]);
}
}
// draws fixed scale axis aligned boxes at vertices.
// shape contains the vertices
// vertSize size of boxes drawn at verts
// x,y origin of shape
// scale is the scale of the shape
// angle is the amount of rotation in radians.
function drawVertices(shape, vertSize, x, y, scale, angle) {
ctx.setTransform(1, 0, 0, 1, x, y);
const xAx = Math.cos(angle) * scale; // direction and size of x axis
const xAy = Math.sin(angle) * scale;
var i = 0;
while (i < shape.length) {
const vx = shape[i][0]; // get vert coordinate
const vy = shape[i++][1]; // IMPORTANT DONT forget i++ in the while loop
ctx.fillRect(
vx * xAx - vy * xAy - vertSize / 2, // transform and offset by half box size
vx * xAy + vy * xAx - vertSize / 2,
vertSize, vertSize
);
}
}
// draws shape outline and vertices
function drawFullShape(shape, scale, angle, lineCol, vertCol, lineWidth, vertSize) {
// draw outline of shape
ctx.strokeStyle = lineCol;
ctx.lineWidth = lineWidth / scale; // to ensure that the line with is 1 pixel
// set the width to in inverse scale
ctx.beginPath();
// shape origin at cw,ch
createShape(shape, cw, ch, scale, angle);
ctx.closePath();
ctx.stroke();
// draw the vert boxes.
ctx.fillStyle = vertCol;
drawVertices(shape, vertSize, cw, ch, scale, angle);
}
function loop(timer) {
globalTime = timer;
if (w !== innerWidth || h !== innerHeight) { // check if canvas need resize
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
globalScale = Math.min(w / maxSize, h / maxSize);
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.clearRect(0, 0, w, h);
const innerAngle = globalTime * (rotateRate * (Math.PI / 180)) / 1000;
drawFullShape(points, globalScale, angle, "black", "red", 2, 6);
drawFullShape(points, globalScale * 0.5, innerAngle, "black", "red", 2, 6);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>

How can i plot letter around a fabricjs circle

i have a circle added to canvas and then some text i would like wrapped around a circle.
here is what i have so far
var circle = new fabric.Circle({
top: 100,
left: 100,
radius: 100,
fill: '',
stroke: 'green',
});
canvas.add(circle);
var obj = "some text"
for(letter in obj){
var newLetter = new fabric.Text(obj[letter], {
top: 100,
left: 100
});
canvas.add(newLetter);
canvas.renderAll();
}
i have a tried a couple other solutions posted around the web but nothing working properly so far with fabric.
Circular Text.
I started this answer thinking it would be easy but it turned a little ugly. I rolled it back to a simpler version.
The problems I encountered are basic but there are no simple solutions..
Text going around the circle can end up upside down. Not good for
reading
The spacing. Because the canvas only gives a basic 2D transform I can
not scale the top and bottom of the text independently resulting in
text that either looks too widely spaced or too squashed.
I have an altogether alternative approch by it is way too heavy for an answer here. It involves a custom scan line render (a GPU hack of sorts) so you may try looking for something along those lines if text quality is paramount.
The problem I encounter were fixed by just ignoring them, always a good solution. LOL
How to render circular text on 2D canvas.
As there is no way to do it in one call I wrote a function that renders each character one at a time. I use ctx.measureText to get the size of the whole string to be drawn and then convert that into an angular pixel size. Then with a little adjustments for the various options, alignment, stretching, and direction (mirroring) I go through each character in the string one at a time, use ctx.measureText to measure it's size and then use ctx.setTransform to position rotate and scale the character, then just call ctx.fillText() rendering just that character.
It is a little slower than just the ctx.fillText()method but then fill text can't draw on circles can it.
Some calculations required.
To workout the angular size of a pixel for a given radius is trivial but often I see not done correctly. Because Javascript works in radians angular pixel size is just
var angularPixelSize = 1 / radius; // simple
Thus to workout what angle some text will occupy on a circle or given radius.
var textWidth = ctx.measureText("Hello").width;
var textAngularWidth = angularPixelSize * textWidth;
To workout the size of a single character.
var text = "This is some text";
var index = 2; // which character
var characterWidth = ctx.measureText(text[index]).width;
var characterAngularWidth = angularPixelSize * textWidth;
So you have the angular size you can now align the text on the circle, either centered, right or left. See the snippet code for details.
Then you need to loop through each character one at a time calculating the transformation, rendering the text, moving the correct angular distance for the next character until done.
var angle = ?? // the start angle
for(var i = 0; i < text.length; i += 1){ // for each character in the string
var c = text[i]; // get character
// get character angular width
var w = ctx.measureText(c).width * angularPixelSize;
// set the matrix to align the text. See code under next paragraph
...
...
// matrix set
ctx.fillText(c,0,0); // as the matrix set the origin just render at 0,0
angle += w;
}
The fiddly math part is setting the transform. I find it easier to work directly with the transformation matrix and that allows me to mess with scaling etc with out having to use too many transformation calls.
Set transform takes 6 numbers, first two are the direction of the x axis, the next two are the direction of the y axis and the last two are the translation from the canvas origin.
So to get the Y axis. The line from the circle center moving outward for each character we need the angle at which the character is being draw and to reduce misalignment (NOTE reduce not eliminate) the angular width so we can use the character's center to align it.
// assume angle is position and w is character angular width from above code
var xDx = Math.cos(angle + w / 2); // get x part of X axis direction
var xDy = Math.sin(angle + w / 2); // get y part of X axis direction
Now we have the normalised vector that will be the x axis. The character is draw from left to right along this axis. I construct the matrix in one go but I'll break it up below. Please NOTE that I made a boo boo in my snippet code with angles so the code is back to front (X is Y and Y is X)
Note that the snippet has the ability to fit text between two angles so I scale the x axis to allow this.
// assume scale is how much the text is squashed along its length.
ctx.setTransform(
xDx * scale, xDy * scale, // set the direction and size of a pixel for the X axis
-xDy, xDx, // the direction ot the Y axis is perpendicular so switch x and y
-xDy * radius + x, xdx * radius + y // now set the origin by scaling by radius and translating by the circle center
);
Well thats the math and logic to drawing a circular string. I am sorry but I dont use fabric.js so it may or may not have the option. But you can create your own function and render directly to the same canvas as fabric.js, as it does not exclude access. Though it will pay to save and restore the canvas state as fabric.js does not know of the state change.
Below is a snippet showing the above in practice. It is far from ideal but is about the best that can be done quickly using the existing canvas 2D API. Snippet has the two functions for measuring and drawing plus some basic usage examples.
function showTextDemo(){
/** Include fullScreenCanvas.js begin **/
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
canvas = (function () {
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
} ) ();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
// measure circle text
// ctx: canvas context
// text: string of text to measure
// x,y: position of center
// r: radius in pixels
//
// returns the size metrics of the text
//
// width: Pixel width of text
// angularWidth : angular width of text in radians
// pixelAngularSize : angular width of a pixel in radians
var measureCircleText = function(ctx, text, x, y, radius){
var textWidth;
// get the width of all the text
textWidth = ctx.measureText(text).width;
return {
width :textWidth,
angularWidth : (1 / radius) * textWidth,
pixelAngularSize : 1 / radius
}
}
// displays text alon a circle
// ctx: canvas context
// text: string of text to measure
// x,y: position of center
// r: radius in pixels
// start: angle in radians to start.
// [end]: optional. If included text align is ignored and the text is
// scalled to fit between start and end;
// direction
var circleText = function(ctx,text,x,y,radius,start,end,direction){
var i, textWidth, pA, pAS, a, aw, wScale, aligned, dir;
// save the current textAlign so that it can be restored at end
aligned = ctx.textAlign;
dir = direction ? 1 : -1;
// get the angular size of a pixel in radians
pAS = 1 / radius;
// get the width of all the text
textWidth = ctx.measureText(text).width;
// if end is supplied then fit text between start and end
if(end !== undefined){
pA = ((end - start) / textWidth) * dir;
wScale = (pA / pAS) * dir;
}else{ // if no end is supplied corret start and end for alignment
pA = -pAS * dir;
wScale = -1 * dir;
switch(aligned){
case "center": // if centered move around half width
start -= pA * (textWidth / 2);
end = start + pA * textWidth;
break;
case "right":
end = start;
start -= pA * textWidth;
break;
case "left":
end = start + pA * textWidth;
}
}
// some code to help me test. Left it here incase someone wants to underline
// rmove the following 3 lines if you dont need underline
ctx.beginPath();
ctx.arc(x,y,radius,end,start,end>start?true:false);
ctx.stroke();
ctx.textAlign = "center"; // align for rendering
a = start; // set the start angle
for (var i = 0; i < text.length; i += 1) { // for each character
// get the angular width of the text
aw = ctx.measureText(text[i]).width * pA;
var xDx = Math.cos(a + aw / 2); // get the yAxies vector from the center x,y out
var xDy = Math.sin(a + aw / 2);
if (xDy < 0) { // is the text upside down. If it is flip it
// sets the transform for each character scaling width if needed
ctx.setTransform(-xDy * wScale, xDx * wScale,-xDx,-xDy, xDx * radius + x,xDy * radius + y);
}else{
ctx.setTransform(-xDy * wScale, xDx * wScale, xDx, xDy, xDx * radius + x, xDy * radius + y);
}
// render the character
ctx.fillText(text[i],0,0);
a += aw;
}
ctx.setTransform(1,0,0,1,0,0);
ctx.textAlign = aligned;
}
// set up canvas
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // centers
var ch = h / 2;
var rad = (h / 2) * 0.9; // radius
// clear
ctx.clearRect(0, 0, w, h)
// the font
var fontSize = Math.floor(h/20);
if(h < 400){
var fontSize = 10;
}
ctx.font = fontSize + "px verdana";
// base settings
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillStyle = "#666";
ctx.strokeStyle = "#666";
// Text under stretched
circleText(ctx, "Test of circular text rendering", cw, ch, rad, Math.PI, 0, true);
// Text over stretchered
ctx.fillStyle = "Black";
circleText(ctx, "This text is over the top", cw, ch, rad, Math.PI, Math.PI * 2, true);
// Show centered text
rad -= fontSize + 4;
ctx.fillStyle = "Red";
// Use measureCircleText to get angular size
var tw = measureCircleText(ctx, "Centered", cw, ch, rad).angularWidth;
// centered bottom and top
circleText(ctx, "Centered", cw, ch, rad, Math.PI / 2, undefined, true);
circleText(ctx, "Centered", cw, ch, rad, -Math.PI * 0.5, undefined, false);
// left align bottom and top
ctx.textAlign = "left";
circleText(ctx, "Left Align", cw, ch, rad, Math.PI / 2 - tw * 0.6, undefined, true);
circleText(ctx, "Left Align Top", cw, ch, rad, -Math.PI / 2 + tw * 0.6, undefined, false);
// right align bottom and top
ctx.textAlign = "right";
circleText(ctx, "Right Align", cw, ch, rad, Math.PI / 2 + tw * 0.6, undefined, true);
circleText(ctx, "Right Align Top", cw, ch, rad, -Math.PI / 2 - tw * 0.6, undefined, false);
// Show base line at middle
ctx.fillStyle = "blue";
rad -= fontSize + fontSize;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
circleText(ctx, "Baseline Middle", cw, ch, rad, Math.PI / 2, undefined, true);
circleText(ctx, "Baseline Middle", cw, ch, rad, -Math.PI / 2, undefined, false);
// show baseline at top
ctx.fillStyle = "Green";
rad -= fontSize + fontSize;
ctx.textAlign = "center";
ctx.textBaseline = "top";
circleText(ctx, "Baseline top", cw, ch, rad, Math.PI / 2, undefined, true);
circleText(ctx, "Baseline top", cw, ch, rad, -Math.PI / 2, undefined, false);
}
showTextDemo();
window.addEventListener("resize",showTextDemo);

Categories

Resources