canvas提供了一个名为
bezierCurveTo
的方法,用来绘制贝塞尔曲线,提供三个控制点,但是我在实际应用中发现很难去找到对应的控制点,就无法达到想要实现的效果,那么在查阅了些资料后得知有人已经造过相关的轮子了,让我们一起来学习一下,这个好用的轮子,如何在canvas中穿点连线的吧!
一、生成多个点位完成贝塞尔曲线效果
function getCurvePoints(pts, tension, isClosed, numOfSegments) {
// use input value if provided, or use a default value
tension = typeof tension != "undefined" ? tension : 0.5;
isClosed = isClosed ? isClosed : false;
numOfSegments = numOfSegments ? numOfSegments : 16;
let _pts = [],
res = [], // clone array
x,
y, // our x,y coords
t1x,
t2x,
t1y,
t2y, // tension vectors
c1,
c2,
c3,
c4, // cardinal points
st,
t,
i; // steps based on num. of segments
// clone array so we don't change the original
_pts = pts.slice(0);
// The algorithm require a previous and next point to the actual point array.
// Check if we will draw closed or open curve.
// If closed, copy end points to beginning and first points to end
// If open, duplicate first points to befinning, end points to end
if (isClosed) {
_pts.unshift(pts[pts.length - 1]);
_pts.unshift(pts[pts.length - 2]);
_pts.unshift(pts[pts.length - 1]);
_pts.unshift(pts[pts.length - 2]);
_pts.push(pts[0]);
_pts.push(pts[1]);
} else {
_pts.unshift(pts[1]); //copy 1. point and insert at beginning
_pts.unshift(pts[0]);
_pts.push(pts[pts.length - 2]); //copy last point and append
_pts.push(pts[pts.length - 1]);
}
// ok, lets start..
// 1. loop goes through point array
// 2. loop goes through each segment between the 2 pts + 1e point before and after
for (i = 2; i < _pts.length - 4; i += 2) {
for (t = 0; t <= numOfSegments; t++) {
// calc tension vectors
t1x = (_pts[i + 2] - _pts[i - 2]) * tension;
t2x = (_pts[i + 4] - _pts[i]) * tension;
t1y = (_pts[i + 3] - _pts[i - 1]) * tension;
t2y = (_pts[i + 5] - _pts[i + 1]) * tension;
// calc step
st = t / numOfSegments;
// calc cardinals
c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1;
c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2);
c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st;
c4 = Math.pow(st, 3) - Math.pow(st, 2);
// calc x and y cords with common control vectors
x = c1 * _pts[i] + c2 * _pts[i + 2] + c3 * t1x + c4 * t2x;
y = c1 * _pts[i + 1] + c2 * _pts[i + 3] + c3 * t1y + c4 * t2y;
//store points in array
res.push(x);
res.push(y);
}
}
return res;
}
那么这个工具的核心部分就是这个函数,根据平面点位数pts([x,y,x1,y1])
,该算法需要前一个点和后一个点产生一个数组,那么这个数组的点位数可控,由numOfSegments
控制,但需要传2的倍数。
然后循环遍历点数组,循环遍历前后两个pts之间的每一个段,这里涉及到一个字段,tension,我的理解是柔和的程度,传值为0-1 ,0就是没有柔和感,1就是最柔和的效果
isClosed控制最后的点是否与开始点重合形成闭环,根据实际需求传值就好了,那么该函数最后抛出的数组就是我们提供点路径上,柔和的所有点路径数组集合啦。
二、封装传值便于使用
function drawCurve(
ctx,
ptsa,
tension,
isClosed,
numOfSegments,
showPoints
) {
return new Promise((resolve) => {
if (showPoints) {
ctx.beginPath();
ctx.strokeStyle = "red";
for (let i = 0; i < ptsa.length - 1; i += 2) {
ctx.rect(ptsa[i] - 2, ptsa[i + 1] - 2, 4, 4);
}
ctx.stroke();
}
resolve(getCurvePoints(ptsa, tension, isClosed, numOfSegments));
});
}
传入showPoints,利于自己知道原始数组的点位是哪几个点。
ctx: canvas对象、ptsa: 平面点位数组 [x,y,x1,y1]、tension: 柔和程度 0-1 1:最柔和、 isClosed: 是否形成闭合、numOfsGments: 生成的模型点位数量 必须是2的倍数、showPoints: 画出点位矩形框
四、曲线动画
我们都知道,CSS的 animation
提供了一些曲线动画,简单举一些例子。
线性曲线(linear curve)是最简单的一种曲线,元素的速度会在动画的整个过程中保持不变。它具有良好的适应性,因此通常用于简单的过渡效果。
缓入曲线(ease-in curve)表示元素的速度会在动画刚开始时较慢,然后逐渐加速,直到达到最终速度。它可以使动画在开始时表现得更加自然和柔和。
缓出曲线(ease-out curve)与缓入曲线相反,表示元素的速度在动画结束时变慢。它可以使动画在结束时表现得更加流畅和自然。
那么总的来说,CSS动画速度曲线是一种非常有用的工具,可以帮助开发者控制动画的速度和过渡效果,选择不同的曲线类型,可以使动画看起来更加自然、流畅,平滑,提升用户体验。
那么有这么一个网站,可以提供多种曲线,帮助我们在绘制贝塞尔时,更加流畅,观感更好
那么就很简单了,只需要简单的传递一个函数,判断是否做缓动动画,在绘制的时候干预一下,就能实现这个效果了。
// 偏移动画
const animate = async() => {
if (count > t) {
return resolve();
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.lineWidth = "3";
const gradient = ctx.createLinearGradient(
pts[0],
pts[1],
pts[pts.length - 2],
pts[pts.length - 1]
);
ctx.strokeStyle = gradient;
let st = count;
if (inching) {
st = inching(count / t) * count;
}
ctx.moveTo(pts[0], pts[1]);
for (let c = 1; c < (st * pts.length) / (t * 2); c++) {
ctx.lineTo(pts[c * 2], pts[c * 2 + 1]);
}
ctx.stroke();
count++;
canvas.requestAnimationFrame(animate);
};
canvas.requestAnimationFrame(animate);
const inching = (x) => {
return 1 - Math.pow(1 - x, 3);
};
await drawAnimationLines(canvas, simileLinePts, inching);
把曲线函数传入到绘制函数中,每一帧就会根据该函数去做曲线绘制啦
五、案例应用
<canvas id="myCanvas" width="400" height="400"></canvas>
const lMouth = { x: 75.85993743358597, y: 264.7501215463236 }
const rMouth = { x: 311.51496290332886, y: 256.4827779067491 }
const tUpperLip = { x: 187.6760622033568, y: 225.42848230318364 }
const bUpperLip = { x: 191.7290792380103, y: 250.9176251107116 }
const tUnderLip = { x: 194.79259738330217, y: 288.5975254073281 }
const bUnderLip = { x: 197.79055533854995, y: 333.12249512030405 }
const lPhiltrum = { x: 161.00346241972707, y: 220.74391369146161 }
const rPhiltrum = { x: 214.4383762577197, y: 217.61402350246559 }
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints,easeFn) {
showPoints = showPoints ? showPoints : false;
console.log(getCurvePoints(ptsa, tension, isClosed, numOfSegments))
console.log(getCurvePoints(ptsa, tension, isClosed, numOfSegments).length)
drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments),easeFn);
if (showPoints) {
ctx.stroke();
ctx.beginPath();
for (var i = 0; i < ptsa.length - 1; i += 2)
ctx.rect(ptsa[i] - 2, ptsa[i + 1] - 2, 4, 4);
}
}
function getCurvePoints(pts, tension, isClosed, numOfSegments) {
// use input value if provided, or use a default value
tension = (typeof tension != 'undefined') ? tension : 0.5;
isClosed = isClosed ? isClosed : false;
numOfSegments = numOfSegments ? numOfSegments : 16;
var _pts = [], res = [], // clone array
x, y, // our x,y coords
t1x, t2x, t1y, t2y, // tension vectors
c1, c2, c3, c4, // cardinal points
st, t, i; // steps based on num. of segments
// clone array so we don't change the original
//
_pts = pts.slice(0);
// The algorithm require a previous and next point to the actual point array.
// Check if we will draw closed or open curve.
// If closed, copy end points to beginning and first points to end
// If open, duplicate first points to befinning, end points to end
if (isClosed) {
_pts.unshift(pts[pts.length - 1]);
_pts.unshift(pts[pts.length - 2]);
_pts.unshift(pts[pts.length - 1]);
_pts.unshift(pts[pts.length - 2]);
_pts.push(pts[0]);
_pts.push(pts[1]);
}
else {
_pts.unshift(pts[1]); //copy 1. point and insert at beginning
_pts.unshift(pts[0]);
_pts.push(pts[pts.length - 2]); //copy last point and append
_pts.push(pts[pts.length - 1]);
}
// ok, lets start..
// 1. loop goes through point array
// 2. loop goes through each segment between the 2 pts + 1e point before and after
for (i = 2; i < (_pts.length - 4); i += 2) {
for (t = 0; t <= numOfSegments; t++) {
// calc tension vectors
t1x = (_pts[i + 2] - _pts[i - 2]) * tension;
t2x = (_pts[i + 4] - _pts[i]) * tension;
t1y = (_pts[i + 3] - _pts[i - 1]) * tension;
t2y = (_pts[i + 5] - _pts[i + 1]) * tension;
// calc step
// st = 1 - Math.pow(1 - t / numOfSegments, 3)
st = t / numOfSegments
// calc cardinals
c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1;
c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2);
c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st;
c4 = Math.pow(st, 3) - Math.pow(st, 2);
// calc x and y cords with common control vectors
x = c1 * _pts[i] + c2 * _pts[i + 2] + c3 * t1x + c4 * t2x;
y = c1 * _pts[i + 1] + c2 * _pts[i + 3] + c3 * t1y + c4 * t2y;
//store points in array
res.push(x);
res.push(y);
}
}
return res;
}
function drawLines(ctx, pts,easeFn) {
const t = 30;
let count = 1
// 偏移动画
const animate = () => {
if (count > t) {
return;
}
ctx.clearRect(0,0,canvas.width,canvas.height)
ctx.beginPath()
ctx.lineWidth = '3'
ctx.moveTo(pts[0], pts[1]);
const gradient = ctx.createLinearGradient(pts[0], pts[1],pts[pts.length-2],pts[pts.length-1])
gradient.addColorStop(0, 'rgba(59, 200, 151, 0)');
gradient.addColorStop(0.3, 'rgba(59, 200, 151, 0.6)');
gradient.addColorStop(0.4, 'rgba(59, 200, 151, 0.8)');
gradient.addColorStop(0.6, 'rgba(59, 200, 151, 0.8)');
gradient.addColorStop(0.7, 'rgba(59, 200, 151, 0.6)');
gradient.addColorStop(1, 'rgba(59, 200, 151, 0)');
ctx.strokeStyle = gradient;
let st = count
if(easeFn){
st = easeFn(count,t)
}
for (let c = 1; c < (st * pts.length /(t*2)); c++) {
ctx.lineTo(pts[c*2], pts[c*2 + 1]);
}
ctx.stroke()
count++;
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
}
const tension = 0.4;
// 笑线
const simileLip = {
x: (bUpperLip.x + tUnderLip.x) * 0.5,
y: (bUpperLip.y + tUnderLip.y) * 0.5
}
ctx.fillRect(bUpperLip.x, bUpperLip.y, 5, 5)
ctx.fillRect(tUnderLip.x, tUnderLip.y, 5, 5)
ctx.fillRect(simileLip.x, simileLip.y, 5, 5)
const simileArr = [
lMouth.x,
lMouth.y,
simileLip.x,
simileLip.y,
rMouth.x,
rMouth.y,
]
const easeFn=(count,t)=>{
let x = count / t
return (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2)*count;
}
ctx.strokeStyle = 'RGB(59, 200, 151)'
drawCurve(ctx, simileArr, tension, false, 24, false,easeFn);
ctx.stroke();
案例中的函数可能更细一点,也可能更冗余,但是和文章开头介绍的封装更精简的函数出入不大,
可以参照这个结合提供的Easing网站找到自定义封装自己需要的效果。