js+贝塞尔曲线+animate动画

一 介绍

贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。

下面是我们最常用到bezier曲线的地方

  • svg
  • canvas/webgl
  • css3 动画
  • animation

下面我们将用js来实现贝塞尔曲线的画制

通用的贝塞尔曲线公式:
贝塞尔曲线公式
由此公式可计算得到下面的n阶贝塞尔曲线各个坐标点

二 示例

1阶贝塞尔曲线

    /**
     * @desc 一阶贝塞尔
     * @param {number} t 当前百分比
     * @param {Array} p1 起点坐标
     * @param {Array} p2 终点坐标
     */
    oneBezier(t, p1, p2) {
    
    
        const [x1, y1] = p1;
        const [x2, y2] = p2;
        let x = x1 + (x2 - x1) * t;
        let y = y1 + (y2 - y1) * t;
        return [x, y];
    }

1阶贝塞尔曲线:
上图为1阶贝塞尔的绘制,其实只是从起点(-17,285)到终点(1920,89)的直线运动,中间并没有改变运动轨迹

2阶贝塞尔曲线

在这里插入图片描述

    /**
     * @desc 二阶贝塞尔
     * @param {number} t 当前百分比
     * @param {Array} p1 起点坐标
     * @param {Array} p2 终点坐标
     * @param {Array} cp 控制点
     */
    twoBezier(t, p1, cp, p2) {
    
    
        const [x1, y1] = p1;
        const [cx, cy] = cp;
        const [x2, y2] = p2;
        let x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * cx + t * t * x2;
        let y = (1 - t) * (1 - t) * y1 + 2 * t * (1 - t) * cy + t * t * y2;
        return [x, y];
    }

2阶贝塞尔曲线
上图为2阶贝塞尔的绘制,是小球从起点p0(180,22)到终点p2(1920,89)的匀速运动,中间受到p1(800,0)坐标影响改变运动轨迹,形成的曲线的运动轨迹

3阶贝塞尔曲线:

在这里插入图片描述

    /**
     * @desc 三阶贝塞尔
     * @param {number} t 当前百分比
     * @param {Array} p1 起点坐标
     * @param {Array} p2 终点坐标
     * @param {Array} cp1 控制点1
     * @param {Array} cp2 控制点2
     */
    threeBezier(t, p1, cp1, cp2, p2) {
    
    
        const [x1, y1] = p1;
        const [x2, y2] = p2;
        const [cx1, cy1] = cp1;
        const [cx2, cy2] = cp2;
        let x =
            x1 * (1 - t) * (1 - t) * (1 - t) +
            3 * cx1 * t * (1 - t) * (1 - t) +
            3 * cx2 * t * t * (1 - t) +
            x2 * t * t * t;
        let y =
            y1 * (1 - t) * (1 - t) * (1 - t) +
            3 * cy1 * t * (1 - t) * (1 - t) +
            3 * cy2 * t * t * (1 - t) +
            y2 * t * t * t;
        return [x, y];
    }

3阶贝塞尔曲线
上图为3阶贝塞尔的绘制,是小球从起点p0(0,500)到终点p3(1920,0)的匀速运动,中间受到p1(300,0),p2(1160,500)坐标影响改变运动轨迹,形成的曲线的运动轨迹

4/n阶贝塞尔曲线

在这里插入图片描述

    /**
     * 多阶贝塞尔曲线的生成
     * @param {*} anchorpoints 贝塞尔基点
     * @param {*} pointsAmount 生成的点数
     * @returns 路径点的Array
     */
    CreateBezierPoints(anchorpoints, pointsAmount) {
    
    
        let last = anchorpoints[anchorpoints.length-1]
        var points = [];
        for (var i = 0; i < pointsAmount; i++) {
    
    
            var point = this.MultiPointBezier(anchorpoints, i / pointsAmount);
            points.push(point);
        }
        return points;
    }

     MultiPointBezier(points, t) {
    
    
        var len = points.length;
        var x = 0, y = 0;
        var erxiangshi = function (start, end) {
    
    
            var cs = 1, bcs = 1;
            while (end > 0) {
    
    
                cs *= start;
                bcs *= end;
                start--;
                end--;
            }
            return (cs / bcs);
        };
        for (var i = 0; i < len; i++) {
    
    
            var point = points[i];
            x += point[0] * Math.pow((1 - t), (len - 1 - i)) * Math.pow(t, i) * (erxiangshi(len - 1, i));
            y += point[1] * Math.pow((1 - t), (len - 1 - i)) * Math.pow(t, i) * (erxiangshi(len - 1, i));
        }
        return [x,y];
    }

4阶贝塞尔曲线
上图为4阶贝塞尔的绘制,是小球从起点p0(-17,285)到终点p4(1920,89)的匀速运动,中间受到p1(180,22) p2(1160,1102) p3(1350,-44)坐标影响改变运动轨迹,形成的曲线的运动轨迹

下面为完整的封装和使用:

三 封装和使用

bezier.js

/**
 * @desc 贝塞尔曲线算法,包含了3阶贝塞尔
 */
class Bezier {
    
    
    /**
       * @desc 获取点,这里可以设置点的个数
       * @param {number} num 点个数
       * @param {Array} p1 起点坐标
       * @param {Array} p2 终点坐标
       * @param {Array} p3 点坐标
       * @param {Array} p4 点坐标
       * 如果参数是 num, p1, p2 为一阶贝塞尔
       * 如果参数是 num, p1, c1, p2 为二阶贝塞尔
       * 如果参数是 num, p1, c1, c2, p2 为三阶贝塞尔
       */
    getBezierPoints(num = 100, p1, p2, p3, p4) {
    
    
        let func;
        const points = [];
        if (!p3 && !p4) {
    
    
            func = this.oneBezier;
        } else if (p3 && !p4) {
    
    
            func = this.twoBezier;
        } else if (p3 && p4) {
    
    
            func = this.threeBezier;
        } else {
    
    
            return
        }
        for (let i = 0; i < num; i++) {
    
    
            points.push(func(i / num, p1, p2, p3, p4));
        }
        if (p4) {
    
    
            points.push([...p4]);
        } else if (p3) {
    
    
            points.push([...p3]);
        }
        return points;
    }

    /**
       * @desc 一阶贝塞尔
       * @param {number} t 当前百分比
       * @param {Array} p1 起点坐标
       * @param {Array} p2 终点坐标
       */
    oneBezier(t, p1, p2) {
    
    
        const [x1, y1] = p1;
        const [x2, y2] = p2;
        let x = x1 + (x2 - x1) * t;
        let y = y1 + (y2 - y1) * t;
        return [x, y];
    }

    /**
       * @desc 二阶贝塞尔
       * @param {number} t 当前百分比
       * @param {Array} p1 起点坐标
       * @param {Array} p2 终点坐标
       * @param {Array} cp 控制点
       */
    twoBezier(t, p1, cp, p2) {
    
    
        const [x1, y1] = p1;
        const [cx, cy] = cp;
        const [x2, y2] = p2;
        let x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * cx + t * t * x2;
        let y = (1 - t) * (1 - t) * y1 + 2 * t * (1 - t) * cy + t * t * y2;
        return [x, y];
    }

    /**
       * @desc 三阶贝塞尔
       * @param {number} t 当前百分比
       * @param {Array} p1 起点坐标
       * @param {Array} p2 终点坐标
       * @param {Array} cp1 控制点1
       * @param {Array} cp2 控制点2
       */
    threeBezier(t, p1, cp1, cp2, p2) {
    
    
        const [x1, y1] = p1;
        const [x2, y2] = p2;
        const [cx1, cy1] = cp1;
        const [cx2, cy2] = cp2;
        let x =
            x1 * (1 - t) * (1 - t) * (1 - t) +
            3 * cx1 * t * (1 - t) * (1 - t) +
            3 * cx2 * t * t * (1 - t) +
            x2 * t * t * t;
        let y =
            y1 * (1 - t) * (1 - t) * (1 - t) +
            3 * cy1 * t * (1 - t) * (1 - t) +
            3 * cy2 * t * t * (1 - t) +
            y2 * t * t * t;
        return [x, y];
    }

    /**
       * 多阶贝塞尔曲线的生成
       * @param {*} anchorpoints 贝塞尔基点数组
       * @param {*} pointsAmount 生成的点数
       * @returns 路径点的Array
       */
    CreateBezierPoints(anchorpoints, pointsAmount) {
    
    
        // let last = anchorpoints[anchorpoints.length - 1]
        let points = [];
        for (let i = 0; i < pointsAmount; i++) {
    
    
            let point = this.MultiPointBezier(anchorpoints, i / pointsAmount);
            points.push(point);
        }
        return points;
    }

    MultiPointBezier(points, t) {
    
    
        let len = points.length;
        let x = 0; let y = 0;
        let erxiangshi = function (start, end) {
    
    
            let cs = 1; let bcs = 1;
            while (end > 0) {
    
    
                cs *= start;
                bcs *= end;
                start--;
                end--;
            }
            return (cs / bcs);
        };
        for (let i = 0; i < len; i++) {
    
    
            let point = points[i];
            x += point[0] * Math.pow((1 - t), (len - 1 - i)) * Math.pow(t, i) * (erxiangshi(len - 1, i));
            y += point[1] * Math.pow((1 - t), (len - 1 - i)) * Math.pow(t, i) * (erxiangshi(len - 1, i));
        }
        return [x, y];
    }
}

export default new Bezier();


App.jsx

import './App.scss';
import {
    
     useEffect, useState, Fragment } from 'react';
import bezier from './utils/bezier'
import logo from './logo.svg'
import {
    
     WOW } from 'wowjs'
import 'animate.css';
// import 'wowjs/css/libs/animate.css';

function App() {
    
    

  let [w_width, setWWidth] = useState(window.innerWidth)  //设置幕布宽度
  let [h_height, setHeight] = useState(500)  //设置幕布高度
  let [begin_n, setBeginN] = useState([0, 164])  //开始坐标
  let [end_n, setEndN] = useState([w_width, 40]) //结束坐标
  let [one_n, setOneN] = useState([100, 40]) //bezier坐标1
  let [two_n, setTwoN] = useState([750, 788]) //bezier坐标2
  let [three_n, setThreeN] = useState([800, -30]) //bezier坐标3
  let [one_dot_n, setOneDotN] = useState(40) //生成背景虚线坐标的数量
  let [two_dot_n, setTwoDotN] = useState(10) //生成上层圆的数量
  let [oneXY, setOneXY] = useState() //背景虚线坐标数组
  let [twoXY, setTwoXY] = useState() //上层圆坐标数组
  let [bezier_n, setBezierN] = useState(1) //bezier阶数

  useEffect(() => {
    
    

    window.addEventListener('resize', () => {
    
    
      // 只要窗口大小发生像素变化就会触发
      setWWidth(window.innerWidth)
    })
    return () => {
    
    
      window.removeEventListener('resize', () => {
    
     })
    }
  }, [])

  //根据当前屏幕宽度计算各个贝塞尔点坐标
  useEffect(() => {
    
    
    console.log('当前屏幕宽', w_width)
    setBeginN([(-17 / 1920) * w_width, (255 / 446) * h_height])
    setEndN([(1920 / 1920) * w_width, (80 / 446) * h_height])
    setOneN([(180 / 1920) * w_width, (20 / 446) * h_height])
    setTwoN([(1160 / 1920) * w_width, (983 / 446) * h_height])
    setThreeN([(1350 / 1920) * w_width, (-40 / 446) * h_height])
  }, [w_width, h_height])

  useEffect(() => {
    
    
    new WOW({
    
    
      live: false
    }).init()
    console.log('当前坐标组为:', [begin_n, one_n, two_n, three_n, end_n])
    let anchorpoints

    switch (bezier_n) {
    
    
      case 1:
        anchorpoints = [begin_n, end_n]
        break;
      case 2:
        anchorpoints = [begin_n, one_n, end_n]
        break;
      case 3:
        anchorpoints = [begin_n, one_n, two_n, end_n]
        break;
      default:
        anchorpoints = [begin_n, one_n, two_n, three_n, end_n]
        break;
    }

    let oneXY = bezier.CreateBezierPoints(
      anchorpoints,
      one_dot_n
    )

    //计算背景虚线的斜率
    oneXY.forEach((value, key) => {
    
    
      if (oneXY[key + 1]) {
    
    
        let nextX = oneXY[key + 1][0]
        let nextY = oneXY[key + 1][1]
        let thisX = value[0]
        let thisY = value[1]
        let xl = (((nextY - thisY) / (nextX - thisX)) * 100) / 2
        oneXY[key] = [...value, xl]
        // console.log(`第${key}个坐标斜率为:${xl}`)
        // console.log(value)
      }
    })
    // console.log(oneXY)
    setOneXY(oneXY)

    let twoXY = bezier.CreateBezierPoints(
      anchorpoints,
      two_dot_n
    )
    setTwoXY(twoXY)
  }, [begin_n, one_n, two_n, three_n, end_n, one_dot_n, two_dot_n, bezier_n])

  let arrayChange = (array, number, element) => {
    
    
    let newValue = [...array]
    newValue[number] = parseFloat(element.target.value)
    console.log(newValue)
    return newValue
  }


  return (
    <div className="App">
      <div className="inputNum">
        <h2>
          贝塞尔曲线参数:<br />
          幕布宽 <input type="number" value={
    
    w_width} onChange={
    
    (el) => {
    
     setWWidth(el.target.value) }} /><br />
          幕布高度 <input type="number" value={
    
    h_height} onChange={
    
    (el) => {
    
     setHeight(el.target.value) }} /><br />
          Bezier阶数 <input type="number" value={
    
    bezier_n} onChange={
    
    (el) => {
    
     setBezierN(parseInt(el.target.value)) }} />
        </h2>
        <div className="left">
          <div className="param_group">
            起点:
            <input type="number" placeholder="X轴" value={
    
    begin_n[0]} onChange={
    
    (el) => {
    
     setBeginN(arrayChange(begin_n, 0, el)) }} />
            <input type="number" placeholder="Y轴" value={
    
    begin_n[1]} onChange={
    
    (el) => {
    
     setBeginN(arrayChange(begin_n, 1, el)) }} />
          </div>
          <div className="param_group">
            1点:
            <input type="number" placeholder="X轴" value={
    
    one_n[0]} onChange={
    
    (el) => {
    
     setOneN(arrayChange(one_n, 0, el)) }} />
            <input type="number" placeholder="Y轴" value={
    
    one_n[1]} onChange={
    
    (el) => {
    
     setOneN(arrayChange(one_n, 1, el)) }} />
          </div>
          <div className="param_group">
            2点:
            <input type="number" placeholder="X轴" value={
    
    two_n[0]} onChange={
    
    (el) => {
    
     setTwoN(arrayChange(two_n, 0, el)) }} />
            <input type="number" placeholder="Y轴" value={
    
    two_n[1]} onChange={
    
    (el) => {
    
     setTwoN(arrayChange(two_n, 1, el)) }} />
          </div>
          <div className="param_group">
            3点:
            <input type="number" placeholder="X轴" value={
    
    three_n[0]} onChange={
    
    (el) => {
    
     setThreeN(arrayChange(three_n, 0, el)) }} />
            <input type="number" placeholder="Y轴" value={
    
    three_n[1]} onChange={
    
    (el) => {
    
     setThreeN(arrayChange(three_n, 1, el)) }} />
          </div>
          <div className="param_group">
            终点:
            <input type="number" placeholder="X轴" value={
    
    end_n[0]} onChange={
    
    (el) => {
    
     setEndN(arrayChange(end_n, 0, el)) }} />
            <input type="number" placeholder="Y轴" value={
    
    end_n[1]} onChange={
    
    (el) => {
    
     setEndN(arrayChange(end_n, 1, el)) }} />
          </div>
        </div>
        <div className="right">
          <div className="param_group">
            曲线数量:
            <input type="text" value={
    
    one_dot_n} onChange={
    
    (el) => {
    
     setOneDotN(el.target.value) }} />
          </div>
          <div className="param_group">
            元素数量:
            <input type="text" value={
    
    two_dot_n} onChange={
    
    (el) => {
    
     setTwoDotN(el.target.value) }} />
          </div>
        </div>
      </div>
      <div className="main-container" style={
    
    {
    
    
        width: `${
      
      w_width}px`,
        height: `${
      
      h_height}px`
      }}>
        <Fragment>
          {
    
    
            oneXY ? oneXY.map((v, k) => {
    
    
              return (
                <span
                  key={
    
    `${
      
      k}one`}
                  className={
    
    `dot${
      
      k} wow`}
                  data-wow-delay={
    
    `${
      
      k * 60}ms`}
                  data-wow-duration="1s"
                  style={
    
    {
    
    
                    left: `${
      
      v[0]}px`,
                    top: `${
      
      v[1]}px`,
                    transform: `rotate(${
      
      v[2]}deg) translate(-50%, -50%)`,
                  }}>
                </span>
              )
            }) : ''
          }
        </Fragment>

        <Fragment>
          {
    
    
            twoXY ? twoXY.map((v, k) => {
    
    
              return (
                <div
                  className={
    
    `domain-infos${
      
      k} wow`}
                  key={
    
    `${
      
      k}two`}
                  data-wow-delay={
    
    `${
      
      k * 300 + 2000}ms`}
                  data-wow-duration="4s"
                  style={
    
    {
    
    
                    left: `${
      
      v[0]}px`,
                    top: `${
      
      v[1]}px`,
                    display: k > 0 ? '' : 'none',
                    'flexDirection': k % 2 === 0 ? 'column-reverse' : 'column',
                  }}
                >
                  <div
                    className="domain-img_o"
                    data-wow-delay="2s"
                    data-wow-duration="2s">
                    <img className="domain-img_item" src={
    
    logo} alt="" />
                  </div>
                  <div className="domain-name">[{
    
    parseInt(v[0])},{
    
    parseInt(v[1])}]</div>
                </div>
              )
            }) : ''
          }
        </Fragment>
      </div>
    </div>
  );
}

export default App;


App.scss


@keyframes dotchange {
    
    
  0% {
    
    
    opacity: 0;
    visibility: hidden;
  }

  20% {
    
    
    opacity: .2;
    visibility: visible;
  }

  40% {
    
    
    opacity: .4;
    visibility: visible;
  }

  60% {
    
    
    opacity: .6;
    visibility: visible;
  }

  80% {
    
    
    opacity: .8;
    visibility: visible;
  }

  100% {
    
    
    opacity: 1;
    visibility: visible;
  }
}

@keyframes mymove {
    
    
  0% {
    
    
    top: 0px;
  }

  30% {
    
    
    top: -10px;
  }

  60% {
    
    
    top: -20px;
  }

  100% {
    
    
    top: -32px;
  }
}

.App {
    
    
  width: 100%;
  background: pink;
  height: 100vh;
  color: #000000;
}

h2 {
    
    
  font-size: 15px;
  line-height: 15px;
  // color: #ffffff;
  text-align: center;
}

.inputNum {
    
    
  width: 60%;
  padding-top: 50px;
  margin: 50px auto;
  display: flex;
  align-items: center;

  .right {
    
    
    margin-left: 40px;
  }

  .param_group {
    
    
    margin-top: 5px;
    display: flex;
    justify-content: center;
    align-items: center;

    input {
    
    
      width: 60px;
      z-index: 12;
    }
  }
}

.main-container {
    
    
  margin: 10px auto;
  width: 100%;
  height: 500px;
  background: #00022f;
  position: relative;
  overflow-x: clip;
}

[class^=dot] {
    
    
  position: absolute;
  width: 6px;
  height: 3px;
  border-radius: 4px;
  display: inline-block;
  background: #34ccff;
  font-size: 12px;
  color: #ccc;
  visibility: hidden;
  opacity: 0;
  animation: dotchange linear;
  animation-fill-mode: both;
}


[class^=domain-infos] {
    
    
  position: absolute;
  display: flex;
  align-items: center;
  flex-direction: column;
  width: 60px;
  height: 60px;
  visibility: hidden;
  opacity: 0;
  animation: dotchange linear;
  animation-fill-mode: both;
  transform: translate(-50%, -50%);

  &:hover {
    
    
    .domain-img_o {
    
    
      visibility: visible !important;
      opacity: 1 !important;

      .domain-img_item {
    
    
        visibility: visible !important;
      }
    }
  }

  &:hover {
    
    

    .domain-name {
    
    
      opacity: 1;
    }
  }

  .domain-img_o {
    
    
    border-radius: 50%;
    background: rgba(52, 204, 255, 0.2);
    animation: dotchange linear;
    animation-fill-mode: both;
    display: flex;
    align-items: center;
    justify-content: center;


    .domain-img_item {
    
    
      width: 60px;
      height: 60px;
      border-radius: 50%;
    }
  }

  .domain-name {
    
    
    padding: 20px 0;
    font-family: 'DINNextLTPro-Regular';
    font-weight: 700;
    font-size: 16px;
    line-height: 19px;
    color: #ffffff;
    opacity: 0.5;
    text-align: center;
  }
}

猜你喜欢

转载自blog.csdn.net/qq_36228377/article/details/127687964