This example shows how to render text along an arc. It includes how you can add functionality to the CanvasRenderingContext2D
by extending its prototype.
This examples is derived from the stackoverflow answer Circular Text.
The example adds 3 new text rendering functions to the 2D context prototype.
(function(){
const FILL = 0; // const to indicate filltext render
const STROKE = 1;
var renderType = FILL; // used internal to set fill or stroke text
const multiplyCurrentTransform = true; // if true Use current transform when rendering
// if false use absolute coordinates which is a little quicker
// after render the currentTransform is restored to default transform
// measure circle text
// ctx: canvas context
// text: string of text to measure
// 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 measure = function(ctx, text, radius){
var textWidth = ctx.measureText(text).width; // get the width of all the text
return {
width : textWidth,
angularWidth : (1 / radius) * textWidth,
pixelAngularSize : 1 / radius
};
}
// displays text along a circle
// ctx: canvas context
// text: string of text to measure
// x,y: position of circle center
// r: radius of circle in pixels
// start: angle in radians to start.
// [end]: optional. If included text align is ignored and the text is
// scaled to fit between start and end;
// [forward]: optional default true. if true text direction is forwards, if false direction is backward
var circleText = function (ctx, text, x, y, radius, start, end, forward) {
var i, textWidth, pA, pAS, a, aw, wScale, aligned, dir, fontSize;
if(text.trim() === "" || ctx.globalAlpha === 0){ // dont render empty string or transparent
return;
}
if(isNaN(x) || isNaN(y) || isNaN(radius) || isNaN(start) || (end !== undefined && end !== null && isNaN(end))){ //
throw TypeError("circle text arguments requires a number for x,y, radius, start, and end.")
}
aligned = ctx.textAlign; // save the current textAlign so that it can be restored at end
dir = forward ? 1 : forward === false ? -1 : 1; // set dir if not true or false set forward as true
pAS = 1 / radius; // get the angular size of a pixel in radians
textWidth = ctx.measureText(text).width; // get the width of all the text
if (end !== undefined && end !== null) { // if end is supplied then fit text between start and end
pA = ((end - start) / textWidth) * dir;
wScale = (pA / pAS) * dir;
} else { // if no end is supplied correct start and end for alignment
// if forward is not given then swap top of circle text to read the correct direction
if(forward === null || forward === undefined){
if(((start % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2) > Math.PI){
dir = -1;
}
}
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":// intentionally falls through to case "end"
case "end":
end = start;
start -= pA * textWidth;
break;
case "left": // intentionally falls through to case "start"
case "start":
end = start + pA * textWidth;
}
}
ctx.textAlign = "center"; // align for rendering
a = start; // set the start angle
for (var i = 0; i < text.length; i += 1) { // for each character
aw = ctx.measureText(text[i]).width * pA; // get the angular width of the text
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(multiplyCurrentTransform){ // transform multiplying current transform
ctx.save();
if (xDy < 0) { // is the text upside down. If it is flip it
ctx.transform(-xDy * wScale, xDx * wScale, -xDx, -xDy, xDx * radius + x, xDy * radius + y);
} else {
ctx.transform(-xDy * wScale, xDx * wScale, xDx, xDy, xDx * radius + x, xDy * radius + y);
}
}else{
if (xDy < 0) { // is the text upside down. If it is flip it
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);
}
}
if(renderType === FILL){
ctx.fillText(text[i], 0, 0); // render the character
}else{
ctx.strokeText(text[i], 0, 0); // render the character
}
if(multiplyCurrentTransform){ // restore current transform
ctx.restore();
}
a += aw; // step to the next angle
}
// all done clean up.
if(!multiplyCurrentTransform){
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore the transform
}
ctx.textAlign = aligned; // restore the text alignment
}
// define fill text
var fillCircleText = function(text, x, y, radius, start, end, forward){
renderType = FILL;
circleText(this, text, x, y, radius, start, end, forward);
}
// define stroke text
var strokeCircleText = function(text, x, y, radius, start, end, forward){
renderType = STROKE;
circleText(this, text, x, y, radius, start, end, forward);
}
// define measure text
var measureCircleTextExt = function(text,radius){
return measure(this, text, radius);
}
// set the prototypes
CanvasRenderingContext2D.prototype.fillCircleText = fillCircleText;
CanvasRenderingContext2D.prototype.strokeCircleText = strokeCircleText;
CanvasRenderingContext2D.prototype.measureCircleText = measureCircleTextExt;
})();
This example adds 3 functions to the CanvasRenderingContext2D prototype
. fillCircleText
, strokeCircleText
, and measureCircleText
ctx.textAlign
is ignored and the text is scaled to fit between start and end.Both functions use the textBaseline to position the text vertically around the radius. For the best results use ctx.TextBaseline
.
Functions will throw a TypeError
is any of the numerical arguments as NaN.
If the text
argument trims to an empty string or ctx.globalAlpha = 0
the function just drops through and does nothing.
- **text:** String of text to measure.
- **radius:** radius of circle in pixels.
Returns a Object containing various size metrics for rendering circular text
- **width:** Pixel width of text as it would normaly be rendered
- **angularWidth:** angular width of text in radians.
- **pixelAngularSize:** angular width of a pixel in radians.
const rad = canvas.height * 0.4;
const text = "Hello circle TEXT!";
const fontSize = 40;
const centX = canvas.width / 2;
const centY = canvas.height / 2;
ctx.clearRect(0,0,canvas.width,canvas.height)
ctx.font = fontSize + "px verdana";
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillStyle = "#000";
ctx.strokeStyle = "#666";
// Text under stretched from Math.PI to 0 (180 - 0 deg)
ctx.fillCircleText(text, centX, centY, rad, Math.PI, 0);
// text over top centered at Math.PI * 1.5 ( 270 deg)
ctx.fillCircleText(text, centX, centY, rad, Math.PI * 1.5);
// text under top centered at Math.PI * 1.5 ( 270 deg)
ctx.textBaseline = "top";
ctx.fillCircleText(text, centX, centY, rad, Math.PI * 1.5);
// text over top centered at Math.PI * 1.5 ( 270 deg)
ctx.textBaseline = "middle";
ctx.fillCircleText(text, centX, centY, rad, Math.PI * 1.5);
// Use measureCircleText to get angular size
var circleTextMetric = ctx.measureCircleText("Text to measure", rad);
console.log(circleTextMetric.width); // width of text if rendered normally
console.log(circleTextMetric.angularWidth); // angular width of text
console.log(circleTextMetric.pixelAngularSize); // angular size of a pixel
// Use measure text to draw a arc around the text
ctx.textBaseline = "middle";
var width = ctx.measureCircleText(text, rad).angularWidth;
ctx.fillCircleText(text, centX, centY, rad, Math.PI * 1.5);
// render the arc around the text
ctx.strokeStyle= "red";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(centX, centY, rad + fontSize / 2,Math.PI * 1.5 - width/2,Math.PI*1.5 + width/2);
ctx.arc(centX, centY, rad - fontSize / 2,Math.PI * 1.5 + width/2,Math.PI*1.5 - width/2,true);
ctx.closePath();
ctx.stroke();
NOTE: The text rendered is only an approximation of circular text. For example if two l's are rendered the two lines will not be parallel, but if you render a "H" the two edges will be parallel. This is because each character is rendered as close as possible to the required direction, rather than each pixel being correctly transformed to create circular text.
NOTE:
const multiplyCurrentTransform = true;
defined in this example is used to set the transformation method used. Iffalse
the transformation for circular text rendering is absolute and does not depend on the current transformation state. The text will not be effected by any previous scale, rotate, or translate transforms. This will increase the performance of the render function, after the function is called the transform will be set to the defaultsetTransform(1,0,0,1,0,0)
IfmultiplyCurrentTransform = true
(set as default in this example) the text will use the current transform so that the text can be scaled translated, skewed, rotated, etc but modifying the current transform befor calling thefillCircleText
andstrokeCircleText
functions. Depending on the current state of the 2D context this may be somewhat slower thenmultiplyCurrentTransform = false