比贝塞尔更好用的曲线函数


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网站找到自定义封装自己需要的效果。


文章作者: feico
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 feico !
评论
  目录