实现效果:
文字变形
为了简化问题,我们先从一条直线开始。
第一步:画一条直线路径并均匀分段。
效果
// 绘制文字路径
const drawTextRoad = () =>{
ctx.beginPath();
// 绘制路径
ctx.moveTo(myCanvas.width/2 -200, myCanvas.height/2);
ctx.lineTo(myCanvas.width/2 + 200, myCanvas.height/2)
// 绘制圆
let step = 400/(textContent.value.length - 1);
for(var i = 0;i< textContent.value.length ; i++){
ctx.arc(myCanvas.width/2 -200 + step *i ,myCanvas.height/2,4,0,2*Math.PI)
}
ctx.closePath();
ctx.strokeStyle = 'black';
ctx.stroke();
}
第二步:opentype生成文字
首先绘制文字路径需要使用opentype.js。
1、下载依赖
npm install opentype.js
2、在你的画面上导入依赖
import opentype from "opentype.js";
3、下载一个字体包
这个字体包相当于是一个字体的路径资源,你们可以在网上找一些自己喜欢的字体,或者直接问deepseek要(我就是这么干的)我放在了public文件夹下面,你们可以根据自己的文件路径进行修改。
我把我的资源包压缩打包了,有需要可以自取。
4、获取文字路径并绘制画布
opentype的API:
getAdvanceWidth(text,fontsize);获取文字的宽度
getPath(text,x,y,fontsize);获取文字路径(x,y)文字生成的位置坐标
getBoundingBox();获取文字边界,他有四个参数x1,y1对应边界左上角;x2,y2对应边界右下角。
let fontComands = reactive([]);
let textWidth = 0;
// 获取文字路径
const getTextPath = (character)=>{
opentype.load('/fonts/open-sans/OpenSans-Regular.ttf').then((font)=>{
// 当前文字宽度
let advanceWidth = font.getAdvanceWidth(character,74);
// 文字路径
let path = font.getPath(character,myCanvas.width/2 - 200,myCanvas.height/2,74);
// 文本宽度
textWidth += advanceWidth;
//文字路径数组
fontComands.push({
commands:path.commands,
width: advanceWidth ,
bounding:path.getBoundingBox(),
});
}).catch((err)=>{
console.log("获取文字路径失败"+err);
})
}
每个文字路径的起点都是线段上第一个小圆的坐标,由于每个文字起点都一样,生成的文字最后都在同一坐标,所以需要对文字进行偏移从而实现均匀分布的效果。
效果:
代码:
//400是线段的总长度 num是当前字母下标
offsetX = 400/(textContent.value.length-1) * num;
第三步:弯曲路径
接下来,我们需要将这条直线路径弯曲,也就是绘制一个扇面,canvas绘制一个扇面需要三个重要参数,圆心坐标,半径,角度。大家可以仔细观察视频里面,可以发现随着角度越来越大,最终围成了一个完整的圆,所以文本路径的宽度就是扇面的曲线长度,由于这个扇面的角度是可输入的,根据扇面的曲线长度和角度我们可以得到圆的半径;而且这个圆是关于画布中心对称的那么圆心的X坐标 = 画布的中心坐标;根据半径和角度就可以得到圆心的Y坐标=R*Math.cos(扇面角度/2);
效果
代码
// 绘制文字路径
const drawTextRoad = () =>{
// 清空画布
ctx.clearRect(0,0,myCanvas.width,myCanvas.height);
ctx.beginPath();
// 绘制路径 400是扇形的曲线长度
let R = Math.abs((400*360)/(2*Math.PI*curvature.value));
let x0 = myCanvas.width/2;
let y0 = myCanvas.height/2 + R * Math.cos(Math.PI*curvature.value/360);
// 扇面的起始角度和结束角度
let startAngle = Math.PI*(90 + curvature.value/2)/180;
let endAngle = Math.PI*(90 - curvature.value/2)/180;
// 绘制扇面
ctx.arc(x0,y0,R,-startAngle,-endAngle);
// 绘制圆
let step = 400/(textContent.value.length - 1);
for(var i = 0;i< textContent.value.length ; i++){
ctx.arc(myCanvas.width/2 -200 + step *i ,myCanvas.height/2,4,0,2*Math.PI)
}
ctx.closePath();
ctx.strokeStyle = 'black';
ctx.stroke();
}
这里我说明下,这个扇形的绘制角度,看图。
第四步:偏移文字路径
到这一步我们可以通过改变角度从而实现直线的弯曲,接下来就是将文字放置到曲线上,这一步需要用到canvas的translate方法。translate方法就是将画布原点由(0,0)移动到指定位置,从而实现移动画布内容的效果。
比如translate(2,2)就是将坐标原点移动到(2,2)那么我之前的点(3,4)如果不想改变显示的位置就需要将(3,4)移到(1,2)计算方式就是(3-2,4-2)。而原来的坐标其实也是相对(0,0)计算得到的,计算方式是(3-0,4-0)。
因为是均匀放置的,所以这些位置坐标就很好计算了。
计算过程:
因为这些点的位置是平均的,所以点和圆心之间的连线,它与X轴正方向的夹角我们是可以计算出来的。而圆的半径我们也是知道的,那么根据圆心坐标以及半径就可以得到所有圆上的点的坐标。
代码:
// 夹角个数
let includeAngles = textContent.value.length-1;
// 平均的角度
let avg_angle = curvature.value/includeAngles;
// let step = 400/(textContent.value.length - 1);
for(var i = 0;i< textContent.value.length ; i++){
let x = x0- Math.cos(Math.PI * (avg_angle * i)/180 + endAngle)*R;
let y = y0 - Math.sin(Math.PI * (avg_angle * i)/180 + endAngle)*R;
ctx.arc(x,y,4,0,2*Math.PI)
}
接下来就是偏移文字路径
fontComands.forEach((character,num)=>{
// 偏移量
offsetY = - character.bounding.y2;
offsetX = - (character.bounding.x2 + character.bounding.x1)/2;
//扇形曲线上的坐标
let x = x0 - Math.cos(Math.PI * (avg_angle * num)/180 + endAngle)*R;
let y = y0 - Math.sin(Math.PI * (avg_angle * num)/180 + endAngle)*R;
ctx.arc(x,y,4,0,2*Math.PI)
// console.log(offsetX)
character.commands.forEach((data)=>{
switch (data.type){
case 'M':
ctx.save();
ctx.translate(x,y);
ctx.moveTo(data.x + offsetX,data.y + offsetY);
break;
case 'L':
ctx.lineTo(data.x + offsetX,data.y + offsetY);
break;
case 'C':
ctx.bezierCurveTo(data.x2 + offsetX,data.y2 + offsetY,data.x1 + offsetX,data.y1 + offsetY,data.x + offsetX,data.y + offsetY);
break;
case 'Q':
ctx.quadraticCurveTo(data.x1 + offsetX,data.y1 + offsetY,data.x + offsetX,data.y + offsetY);
break;
case 'Z':
ctx.closePath();
ctx.restore();
break;
default:
break
}
})
这里为了显示美观,我根据文字bounding的下边界来作为基准,所以偏移量直接根据boundingBox的参数来计算。
旋转效果,加上这段代码就能实现旋转效果
// 当前字母与中间字母的夹角个数
let gap = textContent.value.length/2-0.5 - num;
// 旋转角度
let angel = gap == 0?0: 180*includeAngles/( curvature.value* gap);
// 旋转
ctx.rotate(-Math.PI/angel);
到这里文字弯曲的基本实现原理已经说完了。大家可以练习下实现视频中向内凹陷的弯曲效果。非常简单,就是以boundingBox的上边界为基准偏移。这里就不详细赘述了。
offsetY = -character.bounding.y1
那么缩放效果呢,其实就是改变文字路径的长度,前面我设置了400的长度如果你修改这个参数就可以实现缩进。
这是200路径长度的效果: