Smooth curve generation is a very practical technique. Most of the time, we need to draw some broken lines and let the computer connect them smoothly. First, let’s look at the final effect (red is the straight line we input, blue is the curve after fitting)
The idea is to use Bezier curve to fit
Introduction to Bezier curves
Bezier curve (English: Bezier curve) is an important parameter curve in computer graphics.
Quadratic Bessel curve
The path of the quadratic Bezier curve is traced by B (t) of the given points P0, P1, and P2:
Cubic Bezier curves
For cubic curves, the intermediate points Q0, Q1, Q2 described by linear Bezier curve and points R0, R1 described by quadratic curve can be constructed
Bezier curves compute functions
According to the above formula we can get the calculation function
The second order
/ * * * * *@param {number} p0
* @param {number} p1
* @param {number} p2
* @param {number} t
* @return {*}
* @memberof Path* /
bezier2P(p0: number, p1: number, p2: number, t: number) {
const P0 = p0 * Math.pow(1 - t, 2);
const P1 = p1 * 2 * t * (1 - t);
const P2 = p2 * t * t;
return P0 + P1 + P2;
}
/ * * * * *@param {Point} p0
* @param {Point} p1
* @param {Point} p2
* @param {number} num
* @param {number} tick
* @return {*} {Point}
* @memberof Path* /
getBezierNowPoint2P(
p0: Point,
p1: Point,
p2: Point,
num: number.tick: number,
): Point {
return {
x: this.bezier2P(p0.x, p1.x, p2.x, num * tick),
y: this.bezier2P(p0.y, p1.y, p2.y, num * tick),
};
}
/** * Generates the quadratic Bezier curve vertex data **@param {Point} p0
* @param {Point} p1
* @param {Point} p2
* @param {number} [num=100]
* @param {number} [tick=1]
* @return {*}
* @memberof Path* /
create2PBezier(
p0: Point,
p1: Point,
p2: Point,
num: number = 100,
tick: number = 1.) {
const t = tick / (num - 1);
const points = [];
for (let i = 0; i < num; i++) {
const point = this.getBezierNowPoint2P(p0, p1, p2, i, t);
points.push({x: point.x, y: point.y});
}
return points;
}
Copy the code
The third order
/** * the third power of the Searle curve formula **@param {number} p0
* @param {number} p1
* @param {number} p2
* @param {number} p3
* @param {number} t
* @return {*}
* @memberof Path* /
bezier3P(p0: number, p1: number, p2: number, p3: number, t: number) {
const P0 = p0 * Math.pow(1 - t, 3);
const P1 = 3 * p1 * t * Math.pow(1 - t, 2);
const P2 = 3 * p2 * Math.pow(t, 2) * (1 - t);
const P3 = p3 * Math.pow(t, 3);
return P0 + P1 + P2 + P3;
}
/** * get coordinates **@param {Point} p0
* @param {Point} p1
* @param {Point} p2
* @param {Point} p3
* @param {number} num
* @param {number} tick
* @return {*}
* @memberof Path* /
getBezierNowPoint3P(
p0: Point,
p1: Point,
p2: Point,
p3: Point,
num: number,
tick: number.) {
return {
x: this.bezier3P(p0.x, p1.x, p2.x, p3.x, num * tick),
y: this.bezier3P(p0.y, p1.y, p2.y, p3.y, num * tick),
};
}
/** * Generates a cubic Bezier curve vertex data **@param {Point} P0 starts at {x: number, y: number} *@param {Point} P1 Control point 1 {x: number, y: number} *@param {Point} P2 Control point 2 {x: number, y: number} *@param {Point} P3 End point {x: number, y: number} *@param {number} [num=100]
* @param {number} [tick=1]
* @return {Point []}
* @memberof Path* /
create3PBezier(
p0: Point,
p1: Point,
p2: Point,
p3: Point,
num: number = 100,
tick: number = 1.) {
const pointMum = num;
const _tick = tick;
const t = _tick / (pointMum - 1);
const points = [];
for (let i = 0; i < pointMum; i++) {
const point = this.getBezierNowPoint3P(p0, p1, p2, p3, i, t);
points.push({x: point.x, y: point.y});
}
return points;
}
Copy the code
Fitting method
The question is how do we get control points, do we do it in a simpler way
- Take the angular bisector C1C2 of P1-Pt-P2 perpendicular to this angular bisector C2 as the projection point of P2
- Take the short side as the length of C1-pt c2-pt
- I’m going to scale this length and this length can be thought of roughly as the curvature of the curve
Here, the ab segment is simply treated with a second-order curve generation -> 🌈. Here, the BC segment can be treated according to personal ideas using control points c2 calculated by ABC and C3 calculated by BCD, and so on
/** * The control points needed to generate a smooth curve **@param {Vector2D} p1
* @param {Vector2D} pt
* @param {Vector2D} p2
* @param {number} [thewire = 0.3] *@return {*}
* @memberof Path* /
createSmoothLineControlPoint(
p1: Vector2D,
pt: Vector2D,
p2: Vector2D,
ratio: number = 0.3.) {
const vec1T: Vector2D = vector2dMinus(p1, pt);
const vecT2: Vector2D = vector2dMinus(p1, pt);
const len1: number = vec1T.length;
const len2: number = vecT2.length;
const v: number = len1 / len2;
let delta;
if (v > 1) {
delta = vector2dMinus(
p1,
vector2dPlus(pt, vector2dMinus(p2, pt).scale(1 / v)),
);
} else {
delta = vector2dMinus(
vector2dPlus(pt, vector2dMinus(p1, pt).scale(v)),
p2,
);
}
delta = delta.scale(ratio);
const control1: Point = {
x: vector2dPlus(pt, delta).x,
y: vector2dPlus(pt, delta).y,
};
const control2: Point = {
x: vector2dMinus(pt, delta).x,
y: vector2dMinus(pt, delta).y,
};
return {control1, control2};
}
/** * Smooth curve generates **@param {Point []} points
* @param {number} ratio
* @return {*}
* @memberof Path* /
createSmoothLine(points: Point[], ratio: number = 0.3) {
const len = points.length;
let resultPoints = [];
const controlPoints = [];
if (len < 3) return;
for (let i = 0; i < len - 2; i++) {
const {control1, control2} = this.createSmoothLineControlPoint(
new Vector2D(points[i].x, points[i].y),
new Vector2D(points[i + 1].x, points[i + 1].y),
new Vector2D(points[i + 2].x, points[i + 2].y),
ratio,
);
controlPoints.push(control1);
controlPoints.push(control2);
let points1;
let points2;
// Use only one header control point
if (i === 0) {
points1 = this.create2PBezier(points[i], control1, points[i + 1].50);
} else {
console.log(controlPoints);
points1 = this.create3PBezier(
points[i],
controlPoints[2 * i - 1],
control1,
points[i + 1].50,); }// The end part
if (i + 2 === len - 1) {
points2 = this.create2PBezier(
points[i + 1],
control2,
points[i + 2].50,); }if (i + 2 === len - 1) {
resultPoints = [...resultPoints, ...points1, ...points2];
} else{ resultPoints = [...resultPoints, ...points1]; }}return resultPoints;
}
Copy the code
Case code
const input = [
{ x: 0.y: 0 },
{ x: 150.y: 150 },
{ x: 300.y: 0 },
{ x: 400.y: 150 },
{ x: 500.y: 0 },
{ x: 650.y: 150},]const s = path.createSmoothLine(input);
let ctx = document.getElementById('cv').getContext('2d');
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.moveTo(0.0);
for (let i = 0; i < s.length; i++) {
ctx.lineTo(s[i].x, s[i].y);
}
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0.0);
for (let i = 0; i < input.length; i++) {
ctx.lineTo(input[i].x, input[i].y);
}
ctx.strokeStyle = 'red';
ctx.stroke();
document.getElementById('btn').addEventListener('click'.() = > {
let app = document.getElementById('app');
let index = 0;
let move = () = > {
if (index < s.length) {
app.style.left = s[index].x - 10 + 'px';
app.style.top = s[index].y - 10 + 'px';
index++;
requestAnimationFrame(move)
}
}
move()
})
Copy the code
Appendix: Vector2D related code
/ * * * * *@class Vector2D
* @extends {Array}* /
class Vector2D extends Array {
/**
* Creates an instance of Vector2D.
* @param {number} [x=1]
* @param {number} [y=0]
* @memberof Vector2D* * /
constructor(x: number = 1, y: number = 0) {
super(a);this.x = x;
this.y = y;
}
/ * * * *@param {number} v
* @memberof Vector2D* /
set x(v) {
this[0] = v;
}
/ * * * *@param {number} v
* @memberof Vector2D* /
set y(v) {
this[1] = v;
}
/ * * * * *@readonly
* @memberof Vector2D* /
get x() {
return this[0];
}
/ * * * * *@readonly
* @memberof Vector2D* /
get y() {
return this[1];
}
/ * * * * *@readonly
* @memberof Vector2D* /
get length() {
return Math.hypot(this.x, this.y);
}
/ * * * * *@readonly
* @memberof Vector2D* /
get dir() {
return Math.atan2(this.y, this.x);
}
/ * * * * *@return {*}
* @memberof Vector2D* /
copy() {
return new Vector2D(this.x, this.y);
}
/ * * * * *@param {*} v
* @return {*}
* @memberof Vector2D* /
add(v) {
this.x += v.x;
this.y += v.y;
return this;
}
/ * * * * *@param {*} v
* @return {*}
* @memberof Vector2D* /
sub(v) {
this.x -= v.x;
this.y -= v.y;
return this;
}
/ * * * * *@param {*} a
* @return {Vector2D}
* @memberof Vector2D* /
scale(a) {
this.x *= a;
this.y *= a;
return this;
}
/ * * * * *@param {*} rad
* @return {*}
* @memberof Vector2D* /
rotate(rad) {
const c = Math.cos(rad);
const s = Math.sin(rad);
const [x, y] = this;
this.x = x * c + y * -s;
this.y = x * s + y * c;
return this;
}
/ * * * * *@param {*} v
* @return {*}
* @memberof Vector2D* /
cross(v) {
return this.x * v.y - v.x * this.y;
}
/ * * * * *@param {*} v
* @return {*}
* @memberof Vector2D* /
dot(v) {
return this.x * v.x + v.y * this.y;
}
/** ** ** *@return {*}
* @memberof Vector2D* /
normalize() {
return this.scale(1 / this.length); }}/** * vector addition **@param {*} vec1
* @param {*} vec2
* @return {Vector2D}* /
function vector2dPlus(vec1, vec2) {
return new Vector2D(vec1.x + vec2.x, vec1.y + vec2.y);
}
/** * subtraction of vectors **@param {*} vec1
* @param {*} vec2
* @return {Vector2D}* /
function vector2dMinus(vec1, vec2) {
return new Vector2D(vec1.x - vec2.x, vec1.y - vec2.y);
}
export {Vector2D, vector2dPlus, vector2dMinus};
Copy the code