文章目录
- 一 介绍
- 二 示例
- 1阶贝塞尔曲线
- 2阶贝塞尔曲线
- 3阶贝塞尔曲线:
- 4/n阶贝塞尔曲线
- 三 封装和使用
- bezier.js
- App.jsx
- App.scss
一 介绍
贝塞尔曲线(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阶贝塞尔的绘制,其实只是从起点(-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阶贝塞尔的绘制,是小球从起点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阶贝塞尔的绘制,是小球从起点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阶贝塞尔的绘制,是小球从起点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;
}
}