Skip to content

Commit 7e25ecd

Browse files
committed
Find optimal shift value to decompose the spline
The previous method for decomposing a spline into fewer segments aimed to identify the optimal value of 't', which corresponds to the location of maximum curvature on the curve. However, using a bisection method to locate this point increases the average number of '_de_casteljau' calls per point, as demonstrated in Experiment 2. Even with the early stopping method applied, the improvement is little. Moreover, given the high cost of finding points with the bisection method, we have decided to abandon the flexible update of 't' using bisection. Therefore, we can reduce computation cost by only using shift operation to determine the coefficients of lerp in the '_de_casteljau' function. Based on the previous experiments, we have identified that the primary issue is the necessity to call the '_de_casteljau' function twice to generate a point, even under the original settings. On average, the number of _de_casteljau calls per point is 1.99. Ideally, we want the rendering process to call the '_de_casteljau' function only once for each point in every iteration, indicating that the first call is redundant. Also, we've observed that during the initial 60 percent of the rendering process, the original method on average requires more than two shift attempts to determine the optimal value in every iteration. Therefore, we propose adjusting the initial 't' value from 0.5 to 0.25 by applying an initial shift of 2. Additionally, as the spline rendering process progresses to the later stages, the amount of shift gradually decreases. Hence, we store the amount of shift used in each iteration as a global variable to record the value of the last shift change. This way allows us to use this value directly in the next iteration, eliminating the need to start from an initial shift of 2 again. Furthermore, instead of merely decreasing 't' by adding the amount of shift, we also reduce the amount of shift to a minimum of 1. Based on Experiment 3, this limitation of the scope can decrease the average number of function '_de_casteljau' calls per point and increase the number of points when rendering. This means that these points do not always have the maximum curvature, and some optimal points with maximum curvature will be overlooked while limiting the scope of 't' to [0, 0.25]. In summary, we have chosen the Original (shift2) setting in this pull request, as demonstrated in Experiment 1. The modified implementation of font-edit, which utilizes fixed-point arithmetic, serves as the evaluation testbed for the following experiments: Experiment 1: Original - Average number of _de_casteljau calls per point: 1.99 Original - Average points per character: 18.89 Original (shift2) - Average number of _de_casteljau calls per point: 1.51 Original (shift2) - Average points per character: 18.98 Experiment 2: Flexible - Average number of _de_casteljau calls per point: 4.53 Flexible - Average points per character: 16.30 Flexible (shift2) - Average number of _de_casteljau calls per point: 4.40 Flexible (shift2) - Average points per character: 21.16 Flexible (early stopping) - Average number of _de_casteljau calls per point: 4.23 Flexible (early stopping) - Average points per character: 16.18 Flexible (early stopping) (shift2) - Average number of _de_casteljau calls per point: 3.99 Flexible (early stopping) (shift2) - Average points per character: 21.09 Experiment 3: Original (shift2) (limit scope) - Average number of _de_casteljau calls per point: 1.18 Original (shift2) (limit scope) - Average points per character: 22.57
1 parent b75d5ca commit 7e25ecd

File tree

1 file changed

+37
-22
lines changed

1 file changed

+37
-22
lines changed

src/spline.c

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ typedef struct _twin_spline {
1111
} twin_spline_t;
1212

1313
/*
14-
* Linearly interpolate between points 'a' and 'b' with a shift factor.
15-
* The shift factor determines the position between 'a' and 'b'.
14+
* Linearly interpolate between points 'a' and 'b' with a 'shift' factor.
15+
* The 'shift' factor determines the position between 'a' and 'b'.
1616
* The result is stored in 'result'.
1717
*/
1818
static void _lerp(const twin_spoint_t *a,
@@ -25,17 +25,15 @@ static void _lerp(const twin_spoint_t *a,
2525
}
2626

2727
/*
28-
* Perform the de Casteljau algorithm to split a spline at a given shift
28+
* Perform the de Casteljau algorithm to split a spline at a given 'shift'
2929
* factor. The spline is split into two new splines 's1' and 's2'.
3030
*/
3131
static void _de_casteljau(twin_spline_t *spline,
3232
int shift,
3333
twin_spline_t *s1,
3434
twin_spline_t *s2)
3535
{
36-
twin_spoint_t ab, bc, cd;
37-
twin_spoint_t abbc, bccd;
38-
twin_spoint_t final;
36+
twin_spoint_t ab, bc, cd, abbc, bccd, final;
3937

4038
_lerp(&spline->a, &spline->b, shift, &ab);
4139
_lerp(&spline->b, &spline->c, shift, &bc);
@@ -56,28 +54,31 @@ static void _de_casteljau(twin_spline_t *spline,
5654
}
5755

5856
/*
59-
* Return an upper bound on the error (squared) that could result from
60-
* approximating a spline with a line segment connecting the two endpoints.
57+
* Return an upper bound on the distance (squared) that could result from
58+
* approximating a spline with a line segment connecting the two endpoints,
59+
* which is based on the Convex Hull Property of Bézier Curves: The Bézier Curve
60+
* lies completely in the convex hull of the given control points. Therefore, we
61+
* can use control points B and C to approximate the actual spline.
6162
*/
62-
static twin_dfixed_t _twin_spline_error_squared(twin_spline_t *spline)
63+
static twin_dfixed_t _twin_spline_distance_squared(twin_spline_t *spline)
6364
{
64-
twin_dfixed_t berr, cerr;
65+
twin_dfixed_t bdist, cdist;
6566

66-
berr = _twin_distance_to_line_squared(&spline->b, &spline->a, &spline->d);
67-
cerr = _twin_distance_to_line_squared(&spline->c, &spline->a, &spline->d);
67+
bdist = _twin_distance_to_line_squared(&spline->b, &spline->a, &spline->d);
68+
cdist = _twin_distance_to_line_squared(&spline->c, &spline->a, &spline->d);
6869

69-
if (berr > cerr)
70-
return berr;
71-
return cerr;
70+
if (bdist > cdist)
71+
return bdist;
72+
return cdist;
7273
}
7374

7475
/*
75-
* Check if a spline is flat enough by comparing the error against the
76+
* Check if a spline is flat enough by comparing the distance against the
7677
* tolerance.
7778
*/
7879
static bool is_flat(twin_spline_t *spline, twin_dfixed_t tolerance_squared)
7980
{
80-
return _twin_spline_error_squared(spline) <= tolerance_squared;
81+
return _twin_spline_distance_squared(spline) <= tolerance_squared;
8182
}
8283

8384
/*
@@ -91,16 +92,30 @@ static void _twin_spline_decompose(twin_path_t *path,
9192
{
9293
/* Draw starting point */
9394
_twin_path_sdraw(path, spline->a.x, spline->a.y);
94-
95+
/*
96+
* It on average requires over two shift attempts per iteration to find the
97+
* optimal value. To reduce redundancy in shift 1, adjust the initial 't'
98+
* value from 0.5 to 0.25 by applying an initial shift of 2. As spline
99+
* rendering progresses, the shift amount decreases. Store the last shift
100+
* value as a global variable to use directly in the next iteration,
101+
* avoiding a reset to an initial shift of 2.
102+
*/
103+
int shift = 2;
95104
while (!is_flat(spline, tolerance_squared)) {
96-
int shift = 1;
97105
twin_spline_t left, right;
98106

99-
/* FIXME: Find the optimal shift value to decompose the spline */
100-
do {
107+
while (true) {
101108
_de_casteljau(spline, shift, &left, &right);
109+
if (is_flat(&left, tolerance_squared)) {
110+
/* Limiting the scope of 't' may overlook optimal points with
111+
* maximum curvature. Therefore, dynamically reduce the shift
112+
* amount to a minimum of 1. */
113+
if (shift > 1)
114+
shift--;
115+
break;
116+
}
102117
shift++;
103-
} while (!is_flat(&left, tolerance_squared));
118+
}
104119

105120
/* Draw the left segment */
106121
_twin_path_sdraw(path, left.d.x, left.d.y);

0 commit comments

Comments
 (0)