我如何在 fabricjs 圆圈周围绘制字母
How can i plot letter around a fabricjs circle
我在 canvas 中添加了一个圆圈,然后我想在圆圈周围添加一些文字。
这是我目前所拥有的
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();
}
我已经尝试了一些在网上发布的其他解决方案,但到目前为止 fabric 都无法正常工作。
循环文本。
我开始这个回答时以为它会很简单,但它变得有点难看。我将其回滚到更简单的版本。
我遇到的问题很基础,但没有简单的解决方案..
- 环绕圆圈的文字可能会颠倒过来。不利于
阅读
- 间距。因为 canvas 只给出了一个基本的二维变换,我可以
不单独缩放文本的顶部和底部导致
文本看起来间距太宽或太压扁。
我有一个完全不同的方法,因为它太重了,无法在这里回答。它涉及自定义扫描线渲染(某种 GPU hack),因此如果文本质量至关重要,您可以尝试沿着这些线寻找一些东西。
我遇到的问题是通过忽略它们来解决的,总是一个很好的解决方案。哈哈
如何在 2D 上呈现圆形文本 canvas。
由于无法在一次调用中完成,我编写了一个函数,一次渲染一个字符。我使用 ctx.measureText
获取要绘制的整个字符串的大小,然后将其转换为 angular 像素大小。然后对各种选项、对齐、拉伸和方向(镜像)进行一些调整,我一次一个地检查字符串中的每个字符,使用 ctx.measureText
测量它的大小,然后使用 ctx.setTransform
定位旋转和缩放角色,然后调用 ctx.fillText()
渲染那个角色。
它比 ctx.fillText()
方法慢一点,但是填充文本不能画在圆圈上可以吗。
需要一些计算。
针对给定半径锻炼像素的 angular 大小是微不足道的,但我经常看到做得不正确。因为 Javascript 以弧度工作 angular 像素大小只是
var angularPixelSize = 1 / radius; // simple
从而计算一些文本在圆或给定半径上占据的角度。
var textWidth = ctx.measureText("Hello").width;
var textAngularWidth = angularPixelSize * textWidth;
锻炼单个字符的大小。
var text = "This is some text";
var index = 2; // which character
var characterWidth = ctx.measureText(text[index]).width;
var characterAngularWidth = angularPixelSize * textWidth;
因此您拥有 angular 大小,您现在可以将圆上的文本居中、向右或向左对齐。有关详细信息,请参阅代码段。
然后您需要逐个遍历每个字符,一次计算转换、渲染文本、为下一个字符移动正确的 angular 距离,直到完成。
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;
}
繁琐的数学部分是设置变换。我发现直接使用变换矩阵更容易,这样我就可以处理缩放等问题,而不必使用太多变换调用。
Set transform取6个数,前两个是x轴方向,后两个是y轴方向,最后两个是从canvas原点的平移。
所以得到Y轴。对于每个字符,从圆心向外移动的线我们需要绘制字符的角度并减少错位(注意减少而不是消除)angular 宽度,以便我们可以使用字符的中心来对齐它。
// 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
现在我们有了将作为 x 轴的归一化向量。角色是沿着这个轴从左到右绘制的。我一次性构建了矩阵,但我将在下面对其进行分解。请注意,我在我的代码片段中用角度做了一个 boo boo,所以代码回到前面(X 是 Y,Y 是 X)
请注意,该代码段能够在两个角度之间调整文本,因此我缩放 x 轴以允许这样做。
// 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
);
这就是绘制圆形字符串的数学和逻辑。对不起,但我不使用 fabric.js 所以它可能有也可能没有选项。但是您可以创建自己的函数并直接渲染到与 fabric.js 相同的 canvas,因为它不排除访问。虽然保存和恢复 canvas 状态是值得的,因为 fabric.js 不知道状态变化。
下面是一个片段,展示了上面的实践。它远非理想,但大约是使用现有 canvas 2D API 可以快速完成的最佳效果。 Snippet有测量和绘图两个功能加上一些基本的使用例子。
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);
我在 canvas 中添加了一个圆圈,然后我想在圆圈周围添加一些文字。 这是我目前所拥有的
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();
}
我已经尝试了一些在网上发布的其他解决方案,但到目前为止 fabric 都无法正常工作。
循环文本。
我开始这个回答时以为它会很简单,但它变得有点难看。我将其回滚到更简单的版本。
我遇到的问题很基础,但没有简单的解决方案..
- 环绕圆圈的文字可能会颠倒过来。不利于 阅读
- 间距。因为 canvas 只给出了一个基本的二维变换,我可以 不单独缩放文本的顶部和底部导致 文本看起来间距太宽或太压扁。
我有一个完全不同的方法,因为它太重了,无法在这里回答。它涉及自定义扫描线渲染(某种 GPU hack),因此如果文本质量至关重要,您可以尝试沿着这些线寻找一些东西。
我遇到的问题是通过忽略它们来解决的,总是一个很好的解决方案。哈哈
如何在 2D 上呈现圆形文本 canvas。
由于无法在一次调用中完成,我编写了一个函数,一次渲染一个字符。我使用 ctx.measureText
获取要绘制的整个字符串的大小,然后将其转换为 angular 像素大小。然后对各种选项、对齐、拉伸和方向(镜像)进行一些调整,我一次一个地检查字符串中的每个字符,使用 ctx.measureText
测量它的大小,然后使用 ctx.setTransform
定位旋转和缩放角色,然后调用 ctx.fillText()
渲染那个角色。
它比 ctx.fillText()
方法慢一点,但是填充文本不能画在圆圈上可以吗。
需要一些计算。
针对给定半径锻炼像素的 angular 大小是微不足道的,但我经常看到做得不正确。因为 Javascript 以弧度工作 angular 像素大小只是
var angularPixelSize = 1 / radius; // simple
从而计算一些文本在圆或给定半径上占据的角度。
var textWidth = ctx.measureText("Hello").width;
var textAngularWidth = angularPixelSize * textWidth;
锻炼单个字符的大小。
var text = "This is some text";
var index = 2; // which character
var characterWidth = ctx.measureText(text[index]).width;
var characterAngularWidth = angularPixelSize * textWidth;
因此您拥有 angular 大小,您现在可以将圆上的文本居中、向右或向左对齐。有关详细信息,请参阅代码段。
然后您需要逐个遍历每个字符,一次计算转换、渲染文本、为下一个字符移动正确的 angular 距离,直到完成。
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;
}
繁琐的数学部分是设置变换。我发现直接使用变换矩阵更容易,这样我就可以处理缩放等问题,而不必使用太多变换调用。
Set transform取6个数,前两个是x轴方向,后两个是y轴方向,最后两个是从canvas原点的平移。
所以得到Y轴。对于每个字符,从圆心向外移动的线我们需要绘制字符的角度并减少错位(注意减少而不是消除)angular 宽度,以便我们可以使用字符的中心来对齐它。
// 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
现在我们有了将作为 x 轴的归一化向量。角色是沿着这个轴从左到右绘制的。我一次性构建了矩阵,但我将在下面对其进行分解。请注意,我在我的代码片段中用角度做了一个 boo boo,所以代码回到前面(X 是 Y,Y 是 X) 请注意,该代码段能够在两个角度之间调整文本,因此我缩放 x 轴以允许这样做。
// 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
);
这就是绘制圆形字符串的数学和逻辑。对不起,但我不使用 fabric.js 所以它可能有也可能没有选项。但是您可以创建自己的函数并直接渲染到与 fabric.js 相同的 canvas,因为它不排除访问。虽然保存和恢复 canvas 状态是值得的,因为 fabric.js 不知道状态变化。
下面是一个片段,展示了上面的实践。它远非理想,但大约是使用现有 canvas 2D API 可以快速完成的最佳效果。 Snippet有测量和绘图两个功能加上一些基本的使用例子。
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);