The definition and derivation of Bessel curve
The Bezier curve was widely published in 1962 by Pierre Bezier, a French engineer who used it to design the main body of a car. Bessel curve was first developed by Paul de Castelio in 1959 using de Castelio algorithm to solve the Bessel curve with stable numerical method. The Bezier curve consists of n control points corresponding to the bezier curve of order N-1, and can be drawn in a recursive way.
Let’s first give the formula for the n-order Bessel curve
First order Bessel curve
Set the moving point in the graph as PtP_tPt, TTT as the moving time, t∈(0,1t∈(0,1t∈(0,1 t∈(0,1), and the following formula can be obtained
Second order Bessel curve
The second-order Bessel curve is determined by P0P_0P0, P1P_1P1 and P2P_2P2, where P0P_0P0 is the starting point, P2P_2P2 is the end point, and P1P_1P1 is the control point. The equation of the curve is:
- I have three points, P0P_0P0, P1P_1P1, and P2P_2P2, and I connect the segments P0P1P_0P_1P0P1 and P1P2P_1P_2P1P2,
- PaP_aPa on P0P1P_0P_1P0P1 moves from P0P_0P0 to P1P_1P1 with time t, such that P0Pa/P0P1=tP_0P_a/P_0P_1=tP0Pa/P0P1=t;
- PbP_bPb on P1P2P_1P_2P1P2 moves from P1P_1P1 to P2P_2P2 with time t, such that P1Pb/P1P2=tP_1P_b/P_1P_2=tP1Pb/P1P2=t;
- Connect the line segment PaPbP_aP_bPaPb,
- PtP_tPt on PaPbP_aP_bPaPb moves from PaP_aPa to PbP_bPb over time, such that PaPt/PaPb=tP_aP_t/P_aP_b=tPaPt/PaPb=t;
- As TTT changes from 0 to 1, all PtP_tPt points form a second order Bessel curve.
It can be obtained by formula (1)
By substituting formula (2) and Formula (3) into formula (4), it can be obtained
Third order Bessel curve
The third-order Bessel curve is determined by four points P0P_0P0, P1P_1P1, P2P_2P2 and P3P_3P3, where P0P_0P0 is the starting point, P3P_3P3 is the end point, and P1P_1P1 and P2P_2P2 are the control points. The equation of the curve is:
- Given three points P0P_0P0, P1P_1P1, P2P_2P2, P3P_3P3, connect the segments P0P1P_0P_1P0P1, P1P2P_1P_2P1P2, and P2P3P_2P_3P2P3;
- PaP_aPa on P0P1P_0P_1P0P1 moves from P0P_0P0 to P1P_1P1 with time t, such that P0Pa/P0P1=tP_0P_a/P_0P_1=tP0Pa/P0P1=t;
- PbP_bPb on P1P2P_1P_2P1P2 moves from P1P_1P1 to P2P_2P2 with time t, such that P1Pb/P1P2=tP_1P_b/P_1P_2=tP1Pb/P1P2=t;
- PcP_cPc on P2P3P_2P_3P2P3 moves from P2P_2P2 to P3P_3P3 with time t, making P2Pc/P2P3=tP_2P_c/P_2P_3=tP2Pc/P2P3=t;
- Connect line segment PaPbP_aP_bPaPb, PbPcP_bP_cPbPc;
- PdP_dPd on PaPbP_aP_bPaPb moves from PaP_aPa to PbP_bPb over time t, such that PaPd/PaPb=tP_aP_d/P_aP_b=tPaPd/PaPb=t;
- On PbPcP_bP_cPbPc, PeP_ePe moves from PbP_bPb to PcP_cPc with time t, making PbPe/PbPc=tP_bP_e/P_bP_c=tPbPe/PbPc=t;
- On PdPeP_dP_ePdPe, PtP_tPt moves from PdP_dPd to PeP_ePe with time t, making PdPt/PdPe=tP_dP_t/P_dP_e=tPdPt/PdPe=t;
- When TTT changes from 0 to 1, all PtP_tPt points form the third-order Bessel curve.
From the previous formula, we can get:
Substitute the above formula into PtP_tPt
The recursive nature
Carefully observe the above construction process, after the change of the fifth step, the solution of the third-order Bessel curve becomes the solution of the second-order Bessel curve equation with PaP_aPa as the starting point, PcP_cPc as the end point and PbP_bPb as the control point.
First, there are four control points; Four control points form three line segments, and one point on each line segment is moving, so you get three points; The three control points form two line segments, and one point on each line segment is moving, so you get two points; Two points form a line segment, and one point on that line segment is moving, so you get a point; The last point’s trajectory forms a Bezier curve!
And what we find is that it’s actually n points in each round, forming an N-1 segment, and one point in each segment is moving, so let’s just focus on those n-1 points and keep going. When there is only one point left, its trajectory is the result.
This is the recursion property of Bessel curves that we mentioned earlier.
By reading the definition and derivation process of bezier curve above, we have a preliminary understanding of what Bezier curve is and how to obtain the curve formula of corresponding order. Then how do we define and use Bezier curve in daily development? UIBezierPath is a graph drawing class in UIKit. It is a wrapper around CGPathRef, allowing us to draw rectangles, ellipses, or a combination of lines and curves. Let’s take a quick look at the UIBezierPath API.
UIBezierPath
UIBezierPath is used to define a combined path of lines and curves that can be rendered in a custom view.
Commonly used API
Create UIBezierPath.
// Create and return a new bezierPath object
+ (instancetype)bezierPath;
// Create and return a rectangular bezierPath object with a rectangular rect
+ (instancetype)bezierPathWithRect:(CGRect)rect;
// Create and return an elliptical bezierPath object enclosed by a rectangle rect
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
// Create a rounded rectangle with size of CGRect and radius of cornerRadius
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius;
// Create a rounded rectangle path with CGRect as size, Corners as position and cornerRadii as radius
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
// create a clockwise path with center as the center of the circle, radius as the radius, startAngle as the startAngle of the circle, endAngle as the endAngle of the circle, which is the direction of the path drawing
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
// Return a reverse path of an existing path
- (UIBezierPath *)bezierPathByReversingPath;
Copy the code
2. Draw the path
// Move currentPoint of path to the specified location
- (void)moveToPoint:(CGPoint)point;
// Add a line to the path from currentPoint to the specified location
- (void)addLineToPoint:(CGPoint)point;
Add a clockwise arc to the path. Center is the center of the circle, radius is the radius, startAngle is the starting Angle of the circle, endAngle is the ending Angle of the circle, which is the direction of the path drawing
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
Copy the code
// add a second order bezier curve to the path, with endPoint as the endPoint and controlPoint as the controlPoint
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
Copy the code
// add a third order bezier curve to the path, with endPoint as the endPoint, controlPoint1 and controlPoint2 as the two control points,
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
Copy the code
// Close the path and add a line from currentPoint to the start of the path
- (void)closePath;
// Remove all points from the path, i.e. remove all subpaths
- (void)removeAllPoints;
// Add another path to the path
- (void)appendPath:(UIBezierPath *)bezierPath;
// Get the immutable CGPathRef object for the path
@property(nonatomic) CGPathRef CGPath;
// The current point in the path drawing process, that is, the starting point for the next drawing. If the path is empty, the value of this property is CGPointZero
@property(nonatomic.readonly) CGPoint currentPoint;
Copy the code
3. Drawing properties
// Path line width, default value 1.0
@property(nonatomic) CGFloat lineWidth;
Copy the code
// The start and end styles of the path curve are valid only for open paths, but not for closed paths. The default value is kCGLineCapButt
@property(nonatomic) CGLineCap lineCapStyle;
typedef CF_ENUM(int32_t, CGLineCap) {
kCGLineCapButt,// Square end, end position in exact position
kCGLineCapRound,// Round end, ending half a line width beyond the exact position
kCGLineCapSquare// Square end, end position more than half line width of the exact position
};
Copy the code
// Join point style of the path segment. The default value is kCGLineJoinMiter
@property(nonatomic) CGLineJoin lineJoinStyle;
typedef CF_ENUM(int32_t, CGLineJoin) {
kCGLineJoinMiter,/ / Angle
kCGLineJoinRound,/ / the rounded
kCGLineJoinBevel/ / cutting Angle
};
Copy the code
// Transform all points on the path using the specified affine transformation matrix
- (void)applyTransform:(CGAffineTransform)transform;
// After path calls addClip, changes the visible drawing area of the current graph context, and subsequent drawing outside path will not be visible. If you want to remove the clipping area in the next draw, you can call CGContextSaveGState to save the current graph context state before clipping. When the clipping area is no longer needed, you can restore it with CGContextRestoreGState
- (void)addClip;
// Fill the path
- (void)fill;
// Draw the path
- (void)stroke;
Copy the code
How do I draw the path to the screen once I’ve defined it?
- The UIView
- (void)drawRect:(CGRect)rect
Method to draw a graph - Using CAShapeLayer
CAShapeLayer and drawRect:
- CAShapeLayer: belongs to the framework of CoreAnimation. It uses GPU to render graphics, saving performance and using memory efficiently.
- DrawRect: Belongs to the Core Graphics framework and consumes a lot of CPU and performance.
CAShapeLayer
The CAShapeLayer is inherited from the CALayer. CAShapeLayer belongs to the framework of CoreAnimation and uses GPU to render graphics to save performance. Animation rendering is submitted directly to the mobile GPU, efficient use of memory. Each CAShapeLayer object represents an arbitrary shape that will be rendered to the screen. The specific shape is specified by its PATH (type CGPathRef) attribute. A normal CALayer is a rectangle and requires the frame property. CAShapeLayer initializes with a frame value, but it has no shape. Its shape comes from its path property. Shape in CAShapeLayer requires a shape to be effective. UIBezierPath can give it a path to draw the shape.
Commonly used API
// The path rendered by the layer is Animatable. When animating the path, make sure that the two paths have the same number of control points
@property(nullable) CGPathRef path;
// Layer path color
@property(nullable) CGColorRef strokeColor;
// the relative position between the start and end of the path rendering, between [0,1], can be animated
@property CGFloat strokeStart;
@property CGFloat strokeEnd;
// Start drawing the position within the length of the dotted line
@property CGFloat lineDashPhase;
The array defines the width of the solid line and Spaces
@property(nullable.copy) NSArray<NSNumber *> *lineDashPattern;
// UIBezierPath is the same as UIBezierPath
@property CGFloat lineWidth;
// Start and end styles, meaning the same as UIBezierPath
@property(copy) CAShapeLayerLineCap lineCap;
// Join point style, meaning same as UIBezierPath
@property(copy) CAShapeLayerLineJoin lineJoin;
Copy the code
For example
Below, we will use the second-order and third-order Bessel curves to achieve the above wave GIF effect, as shown in the diagram above. The wave curve is decomposed, and two identical complete “sine waves” are drawn, and then the curve is constantly moved to the left and reset to achieve the effect of wave fluctuation.
The second order to achieve
// P0P1, P1P2, P2p3, p3P4 are four second-order curves, and C1, C2, C3, C4 are the control points of their corresponding second-order Bessel curves
- (void)p_creatWavePath {
CGFloat waterHeight = 300.f;
CGFloat waveWidth = self.view.frame.size.width / 2.f;
CGFloat waveControlHeight = 50.f;
UIBezierPath *wavePath = [UIBezierPath bezierPath];
[wavePath moveToPoint:CGPointMake(0 - self.waveOffsetX, 0)];
[wavePath addLineToPoint:CGPointMake(0 - self.waveOffsetX, waterHeight)];
// Calculate the starting point and control point
CGFloat waveHeight = 20.f;
CGPoint p0 = CGPointMake(0 - self.waveOffsetX, waterHeight);
CGPoint p1 = CGPointMake(waveWidth - self.waveOffsetX, waterHeight);
CGPoint p2 = CGPointMake(waveWidth * 2 - self.waveOffsetX, waterHeight);
CGPoint p3 = CGPointMake(waveWidth * 3 - self.waveOffsetX, waterHeight);
CGPoint p4 = CGPointMake(waveWidth * 4 - self.waveOffsetX, waterHeight);
CGPoint c1 = CGPointMake(waveWidth * 1 / 2 - self.waveOffsetX, waterHeight - waveControlHeight);
CGPoint c2 = CGPointMake(waveWidth * 3 / 2 - self.waveOffsetX, waterHeight + waveControlHeight);
CGPoint c3 = CGPointMake(waveWidth * 5 / 2 - self.waveOffsetX, waterHeight - waveControlHeight);
CGPoint c4 = CGPointMake(waveWidth * 7 / 2 - self.waveOffsetX, waterHeight + waveControlHeight);
[wavePath addQuadCurveToPoint:p1 controlPoint:c1];
[wavePath addQuadCurveToPoint:p2 controlPoint:c2];
[wavePath addQuadCurveToPoint:p3 controlPoint:c3];
[wavePath addQuadCurveToPoint:p4 controlPoint:c4];
[wavePath addLineToPoint:CGPointMake(waveWidth * 4 - self.waveOffsetX, 0)];
[wavePath closePath];
self.waveLayer.path = wavePath.CGPath;
// Add the left offset of the curve
self.waveOffsetX = (self.waveOffsetX + self.waveStepX) % (NSInteger) (self.view.frame.size.width);
}
Copy the code
The third order to achieve
// P0p1p2 and P2P3P4 are two third-order curves, c1 and C2, c3 and C4 are the control points of their corresponding third-order Bessel curves
- (void)p_creatWavePath {
CGFloat waterHeight = 300.f;
CGFloat waveWidth = self.view.frame.size.width / 2.f;
CGFloat waveControlHeight = 50.f;
UIBezierPath *wavePath = [UIBezierPath bezierPath];
[wavePath moveToPoint:CGPointMake(0 - self.waveOffsetX, 0)];
[wavePath addLineToPoint:CGPointMake(0 - self.waveOffsetX, waterHeight)];
// Calculate the starting point and control point
CGPoint p2 = CGPointMake(waveWidth * 2 - self.waveOffsetX, waterHeight);
CGPoint p4 = CGPointMake(waveWidth * 4 - self.waveOffsetX, waterHeight);
CGPoint c1 = CGPointMake(waveWidth * 1 / 2 - self.waveOffsetX, waterHeight - waveControlHeight);
CGPoint c2 = CGPointMake(waveWidth * 3 / 2 - self.waveOffsetX, waterHeight + waveControlHeight);
CGPoint c3 = CGPointMake(waveWidth * 5 / 2 - self.waveOffsetX, waterHeight - waveControlHeight);
CGPoint c4 = CGPointMake(waveWidth * 7 / 2 - self.waveOffsetX, waterHeight + waveControlHeight);
[wavePath addCurveToPoint:p2 controlPoint1:c1 controlPoint2:c2];
[wavePath addCurveToPoint:p4 controlPoint1:c3 controlPoint2:c4];
[wavePath addLineToPoint:CGPointMake(waveWidth * 4 - self.waveOffsetX, 0)];
[wavePath closePath];
self.waveLayer.path = wavePath.CGPath;
self.waveOffsetX = (self.waveOffsetX + self.waveStepX) % (NSInteger) (self.view.frame.size.width);
}
Copy the code
Push back control points
From the previous introduction, to draw a Bezier curve, in addition to the starting point and the end point, it is necessary to know the corresponding number of control points. But in everyday development, we don’t always know the control points, but instead the points through which the curve passes, what do we do? The following takes the third-order curve as an example to deduce the calculation process of control points
The movement equation can be obtained:
Assuming that the starting point P0P_0P0 and the ending point P3P_3P3 of the third-order Bessel curve are known, the point PaP_aPa on the curve when t=1/4 and the point PbP_bPb on the curve when t=3/4 can be obtained by putting the point PaP_aPa at t=1/4 into the formula:
Let Pc=64Pa−27P0−P3P_c=64P_a- 27p_0-p_3pc =64Pa−27P0−P3. Since PaP_aPa, P0P_0P0, P3P_3P3 are known, PcP_cPc can also be calculated. namely
Similarly, the point PbP_bPb at t=3/4 can be substituted into the formula:
Set Pd=64Pb−P0−27P3P_d= 64p_B-p_0-27p_3pd =64Pb−P0−27P3. Since PbP_bPb, P0P_0P0, P3P_3P3 are known, PdP_dPd can also be calculated. namely
By substituting formula (5) and formula (6) into the formula for simplification, we can get:
The following is a functional implementation of the control point solution in the above example
// Obtain the control point of the third-order curve
P0: the starting point, p3: the end point, Pa: t=t1, pb: t= T2
- (NSArray<NSValue *> *)p_calculateControlPointsWithPoint0:(CGPoint)p0 pointA:(CGPoint)pa t1:(CGFloat)t1 pointB:(CGPoint)pb t2:(CGFloat)t2 point3:(CGPoint)p3 {
// ax + by = c
// dx + ey = f
// x = (b * f - c * e) / (b * d - a * e)
// y = (c * d - a * f) / (b * d - a * e)
CGFloat fa = 3 * t1 * pow((1 - t1), 2);
CGFloat fb = 3 * (1 - t1) * pow(t1, 2);
CGFloat fd = 3 * t2 * pow((1 - t2), 2);
CGFloat fe = 3 * (1 - t2) * pow(t2, 2);
CGFloat fcx = pa.x - pow((1 - t1), 3) * p0.x - pow(t1, 3) * p3.x;
CGFloat fcy = pa.y - pow((1 - t1), 3) * p0.y - pow(t1, 3) * p3.y;
CGFloat ffx = pb.x - pow((1 - t2), 3) * p0.x - pow(t2, 3) * p3.x;
CGFloat ffy = pb.y - pow((1 - t2), 3) * p0.y - pow(t2, 3) * p3.y;
CGPoint p1 = CGPointZero;
CGPoint p2 = CGPointZero;
p1.x = (fb * fcx - ffx * fe) / (fb * fd - fa * fe);
p1.y = (fb * fcy - ffy * fe) / (fb * fd - fa * fe);
p2.x = (fd * fcx - ffx * fa) / (fb * fd - fa * fe);
p2.y = (fd * fcy - ffy * fa) / (fb * fd - fa * fe);
return@ [[NSValue valueWithCGPoint:p1], [NSValue valueWithCGPoint:p2]];
}
Copy the code
Hi, I’m a keyboard breaker from Kuaishou e-commerce
Kuaishou e-commerce wireless technology team is recruiting talents 🎉🎉🎉! We are the core business line of the company, here gathered all kinds of experts, but also full of opportunities and challenges. With the rapid development of the business, the team is also expanding rapidly. Welcome to join us and create world-class e-commerce products together
Hot positions: Android/iOS Senior Development, Android/iOS Expert, Java Architect, Product Manager (E-commerce background), Test Development… A lot of HC waiting for you ~
For internal recommendation, please send your resume to >>> our email: [email protected] <<<. Note that my roster success rate is higher ~ 😘