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);
Related
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>
Problem: Im drawing a spaceship on the canvas. Upon hovering over it's x/y, im drawing an arc on the canvas, indicating the starships weapons angle and range (considering the starships current Baring/facing). Currently the determined angle is being drawn in green and extends as far as the weapons range value allows.
However, i would like to use a gradiant to fill the determined arc to indicate a drop-off in accuracy (i.e. gradiant begins at green, moves to orange, turns red the further away from the starships Position the angle is).
However, i dont know how i could replace my stock ctx.fill() on the drawn arc with a gradiant.
var ship {
loc: {x, y}, // lets say 100, 100
facing: facing // lets say facing 0, i.e. straight right
weapons: objects (range, startArc, endArc) // lets say 50, 300, 60 -> 120 degree angle, so -60 and +60 from facing (0/360)
}
for (var i = 0; i < weapon.arc.length; i++){
var p1 = getPointInDirection(weapon.range, weapon.arc[i][0] + angle, pos.x, pos.y);
var p2 = getPointInDirection(weapon.range, weapon.arc[i][1] + angle, pos.x, pos.y)
var dist = getDistance( {x: pos.x, y: pos.y}, p1);
var rad1 = degreeToRadian(weapon.arc[i][0] + angle);
var rad2 = degreeToRadian(weapon.arc[i][1] + angle);
fxCtx.beginPath();
fxCtx.moveTo(pos.x, pos.y);
fxCtx.lineTo(p1.x, p1.y);
fxCtx.arc(pos.x, pos.y, dist, rad1, rad2, false);
fxCtx.closePath();
fxCtx.globalAlpha = 0.3;
fxCtx.fillStyle = "green";
fxCtx.fill();
fxCtx.globalAlpha = 1;
}
is it possible to replace the arc/globalalpha/fill so use a gradiant flow instead of it being colored fixed and if so, how ?
thanks
To fill an arc with a gradient, animated just for the fun.
Uses a radial gradient and set colour stops as a fraction of distance.
The function createRadialGradient takes 6 numbers the position x,y and start radius and the position x,y and end radius of the gradient.
Colour stops are added via the gradient object addColorStop function that takes a value 0 inner to 1 outer part of the gradient and the colour as a CSS color string. "#F00" or "rgba(200,0,0,0.5)" or "RED"
Then just use the gradient as the fill style.
var canvas = document.createElement("canvas");
document.body.appendChild(canvas);
var ctx = canvas.getContext("2d");
function update(time) {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// position of zones in fractions
var posRed = 0.8 + Math.sin(time / 100) * 0.091;
var posOrange = 0.5 + Math.sin(time / 200) * 0.2;
var posGreen = 0.1 + Math.sin(time / 300) * 0.1;
var pos = {
x: canvas.width / 2,
y: canvas.height / 2
};
var dist = 100;
var ang1 = 2 + Math.sin(time / 1000) * 0.5;
var ang2 = 4 + Math.sin(time / 1300) * 0.5;
var grad = ctx.createRadialGradient(pos.x, pos.y, 0, pos.x, pos.y, dist);
grad.addColorStop(0, "#0A0");
grad.addColorStop(posGreen, "#0A0");
grad.addColorStop(posOrange, "#F80");
grad.addColorStop(posRed, "#F00");
grad.addColorStop(1, "#000");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
ctx.arc(pos.x, pos.y, dist, ang1, ang2);
ctx.fill();
requestAnimationFrame(update);
}
requestAnimationFrame(update);
suppose that there is a text to be drawn inside a rotated bounding rectangle (not aligned to normal axes x-y), and that text can be also rotated,
given the max width of the bounding box, how to select the best font size to use to draw a wrapped text inside that bounding box in html5 canvas and javascript?
I know that method: measureText() can measure dimensions of give font size, but I need the inverse of that: using a known width to get the problem font size.
thanks
You do not have to find the font point size to make it fit. The font will smoothly scale up and down according to the current transformation scale.
All you do is measureText to find its textWidth, get the pointSize from the context.font attribute then if you have the width and height of the box you need to fit then find the minimum of the width / textWidth and height / pointSize and you have the scale that you need to render the font at.
As a function
var scale2FitCurrentFont = function(ctx, text, width, height){
var points, fontWidth;
points = Number(ctx.font.split("px")[0]); // get current point size
points += points * 0.2; // As point size does not include hanging tails and
// other top and bottom extras add 20% to the height
// to accommodate the extra bits
var fontWidth = ctx.measureText(text).width;
// get the max scale that will allow the text to fi the current font
return Math.min(width / fontWidth, height / points);
}
The arguments are
ctx is current context to draw to
text the text to draw
width the width to fit the text to
height the height to fit the text to
Returns the scale to fit the text within the width and height.
The demo has it all integrated and it draws random boxes and fills with random text from your question. It keeps the font selection and point size separate from the font scaling so you can see it will work for any font and any point size.
var demo = function(){
/** fullScreenCanvas.js begin **/
var canvas = (function(){
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// 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.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** FrameUpdate.js begin **/
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;
var ch = h / 2;
var PI2 = Math.PI * 2; // 360 to save typing
var PIh = Math.PI / 2; // 90
// draws a rounded rectangle path
function roundedRect(ctx,x, y, w, h, r){
ctx.beginPath();
ctx.arc(x + r, y + r, r, PIh * 2, PIh * 3);
ctx.arc(x + w - r, y + r, r, PIh * 3, PI2);
ctx.arc(x + w - r, y + h - r, r, 0, PIh);
ctx.arc(x + r, y + h - r, r, PIh, PIh * 2);
ctx.closePath();
}
// random words
var question = "Suppose that there is a text to be drawn inside a rotated bounding rectangle (not aligned to normal axes x-y), and that text can be also rotated, given the max width of the bounding box, how to select the best font size to use to draw a wrapped text inside that bounding box in html5 canvas and javascript? I know that method: measureText() can measure dimensions of give font size, but I need the inverse of that: using a known width to get the problem font size. thanks.";
question = question.split(" ");
var getRandomWords= function(){
var wordCount, firstWord, s, i, text;
wordCount = Math.floor(rand(4)+1);
firstWord = Math.floor(rand(question.length - wordCount));
text = "";
s = "";
for(i = 0; i < wordCount; i++){
text += s + question[i + firstWord];
s = " ";
}
return text;
}
// fonts to use?? Not sure if these are all safe for all OS's
var fonts = "Arial,Arial Black,Verdanna,Comic Sans MS,Courier New,Lucida Console,Times New Roman".split(",");
// creates a random font with random points size in pixels
var setRandomFont = function(ctx){
var size, font;
size = Math.floor(rand(10, 40));
font = fonts[Math.floor(rand(fonts.length))];
ctx.font = size + "px " + font;
}
var scale2FitCurrentFont = function(ctx, text, width, height){
var points, fontWidth;
var points = Number(ctx.font.split("px")[0]); // get current point size
points += points * 0.2;
var fontWidth = ctx.measureText(text).width;
// get the max scale that will allow the text to fi the current font
return Math.min(width / fontWidth, height / points);
}
var rand = function(min, max){
if(max === undefined){
max = min;
min = 0;
}
return Math.random() * (max - min)+min;
}
var randomBox = function(ctx){
"use strict";
var width, height, rot, dist, x, y, xx, yy,cx, cy, text, fontScale;
// get random box
width = rand(40, 400);
height = rand(10, width * 0.4);
rot = rand(-PIh,PIh);
dist = Math.sqrt(width * width + height * height)
x = rand(0, ctx.canvas.width - dist);
y = rand(0, ctx.canvas.height - dist);
xx = Math.cos(rot);
yy = Math.sin(rot);
ctx.fillStyle = "white";
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
// rotate the box
ctx.setTransform(xx, yy, -yy, xx, x, y);
// draw the box
roundedRect(ctx, 0, 0, width, height, Math.min(width / 3, height / 3));
ctx.fill();
ctx.stroke();
// get some random text
text = getRandomWords();
// get the scale that will fit the font
fontScale = scale2FitCurrentFont(ctx, text, width - textMarginLeftRigth * 2, height - textMarginTopBottom * 2);
// get center of rotated box
cx = x + width / 2 * xx + height / 2 * -yy;
cy = y + width / 2 * yy + height / 2 * xx;
// scale the transform
xx *= fontScale;
yy *= fontScale;
// set the font transformation to fit the box
ctx.setTransform(xx, yy, -yy, xx, cx, cy);
// set up the font render
ctx.fillStyle = "Black";
ctx.textAlign = "center";
ctx.textBaseline = "middle"
// draw the text to fit the box
ctx.fillText(text, 0, 0);
}
var textMarginLeftRigth = 8; // margin for fitted text in pixels
var textMarginTopBottom = 4; // margin for fitted text in pixels
var drawBoxEveryFrame = 60; // frames between drawing new box
var countDown = 1;
// update function will try 60fps but setting will slow this down.
function update(){
// restore transform
ctx.setTransform(1, 0, 0, 1, 0, 0);
// fade clears the screen
ctx.fillStyle = "white"
ctx.globalAlpha = 1/ (drawBoxEveryFrame * 1.5);
ctx.fillRect(0, 0, w, h);
// reset the alpha
ctx.globalAlpha = 1;
// count frames
countDown -= 1;
if(countDown <= 0){ // if frame count 0 the draw another text box
countDown = drawBoxEveryFrame;
setRandomFont(ctx);
randomBox(ctx);
}
if(!STOP){ // do until told to stop.
requestAnimationFrame(update);
}else{
STOP = false;
}
}
update();
}
// demo code to restart on resize
var STOP = false; // flag to tell demo app to stop
function resizeEvent(){
var waitForStopped = function(){
if(!STOP){ // wait for stop to return to false
demo();
return;
}
setTimeout(waitForStopped,200);
}
STOP = true;
setTimeout(waitForStopped,100);
}
window.addEventListener("resize",resizeEvent);
demo();
/** FrameUpdate.js end **/
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
}
Updated what is wrong with the code? I know it doesnt rotate but why is the text screwy.
Does anyone know why
I am tearing my hair out trying to figure this out
function showCircularNameRotating(string, startAngle, endAngle){
//context.clearRect(0, 0, canvas.width, canvas.height);
context.font = '32pt Sans-Serif';
context.fillStyle = '#1826B0';
circle = {
x: canvas.width/2,
y: canvas.height/2,
radius: 200
};
var radius = circle.radius,
angleDecrement = (startAngle - endAngle/string.length-1),
angle = parseFloat(startAngle),
index = 0,
character;
context.save();
while(index <string.length){
character = string.charAt(index);
context.save();
context.beginPath();
context.translate(circle.x + Math.cos(angle) * radius,
circle.y - Math.sin(angle) * radius);
context.rotate(Math.PI/2 - angle);
context.fillText(character, 0,0);
context.strokeText(character,0,0);
angle -= angleDecrement;
index++;
context.restore();
}
context.restore();
}
Yes, it's possible.
Here is a simple approach which you can build upon (I made it right now so it can certainly be optimized and tweaked in various ways).
This uses two objects, one for the text itself and one for each char.
The string is split into char objects in the text object's constructor
The canvas is rotated
The chars are each drawn relative to each other in a circular pattern
Live demo
Text object
function Text(ctx, cx, cy, txt, font, radius) {
this.radius = radius; // expose so we can alter it live
ctx.textBaseline = 'bottom'; // use base of char for rotation
ctx.textAlign = 'center'; // center char around pivot
ctx.font = font;
var charsSplit = txt.split(''), // split string to chars
chars = [], // holds Char objects (see below)
scale = 0.01, // scales the space between the chars
step = 0.05, // speed in steps
i = 0, ch;
for(; ch = charsSplit[i++];) // create Char objects for each char
chars.push(new Char(ctx, ch));
// render the chars
this.render = function() {
var i = 0, ch, w = 0;
ctx.translate(cx, cy); // rotate the canvas creates the movement
ctx.rotate(-step);
ctx.translate(-cx, -cy);
for(; ch = chars[i++];) { // calc each char's position
ch.x = cx + this.radius * Math.cos(w);
ch.y = cy + this.radius * Math.sin(w);
ctx.save(); // locally rotate the char
ctx.translate(ch.x, ch.y);
ctx.rotate(w + 0.5 * Math.PI);
ctx.translate(-ch.x, -ch.y);
ctx.fillText(ch.char, ch.x, ch.y);
ctx.restore();
w += ch.width * scale;
}
};
}
The Char object
function Char(ctx, ch) {
this.char = ch; // current char
this.width = ctx.measureText('W').width; // width of char or widest char
this.x = 0; // logistics
this.y = 0;
}
Now all we need to do is to create a Text object and then call the render method in a loop:
var text = new Text(ctx, cx, cy, 'CIRCULAR TEXT', '32px sans-serif', 170);
(function loop() {
ctx.clearRect(0, 0, w, h);
text.render();
requestAnimationFrame(loop);
})();
As said, there is plenty of room for optimizations here. The most expensive parts are:
Text rendering (render text to images first)
Local rotation for each char using save/restore
Minor things
I'll leave that as an exercise for OP though :)
In Canvas, not so sure. But would be trivial in SVG if you can use that?
Place the text along a path representing the circle.
Use an animateTransform to spin the path