Skip to content

Commit

Permalink
Decompose splines with t flexibly updated
Browse files Browse the repository at this point in the history
This commit focuses on decomposing a spline into fewer segments by
finding an optimal value of t that meets our target tolerance for
flatness while ensuring it is at least as large as the maximum distance
from the line segment connecting the starting and ending points of the
original curve.

The approach employs the bisection method to determine the value of t.
The idea behind this method is based on the properties of curves. By
choosing the point of maximum curvature as the junction between two line
segments during curve fitting, we ensure that the curvature of all
points on both segments is less than the maximum curvature of the
original curve. This strategy guarantees that the two line segments fit
the original curve as well as possible.

Additionally, the properties of these two segments are such that the
slope of the tangent line at all points in one segment is relatively
positive compared to the slope of the line connecting the starting and
ending points of the original curve, while the slope in the other
line segment is relatively negative. Consequently, the point where the
tangent line's slope is zero relative to the connecting line results in
the maximum distance.

This distance is recorded to ensure we maximize it as much as possible.
Although this approach requires more computation to decompose the
spline into fewer segments, the extra effort is justified for improved
fit.

I have modified the implementation of font-edit to use fixed-point
arithmetic and used it as the evaluation testbed to do experiments as
follows:

original - Average number of _de_casteljau calls per point: 1.99
original - Average points per character: 18.89
flexibly update - Average number of _de_casteljau calls per point: 4.53
flexibly update - Average points per character: 16.30

See https://keithp.com/blogs/more-iterative-splines/ for details.
Close #2
  • Loading branch information
weihsinyeh committed Aug 18, 2024
1 parent 8af594f commit c782f22
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 23 deletions.
Empty file added expected-format
Empty file.
5 changes: 5 additions & 0 deletions include/twin_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ typedef int8_t twin_gfixed_t;
#define twin_get_8(v, i) ((uint16_t) (uint8_t) ((v) >> (i)))
#define twin_sat(t) ((uint8_t) ((t) | (0 - ((t) >> 8))))

#define twin_dfixed_mul(a, b) \
((twin_sfixed_t) (((twin_dfixed_t) (a) * (b)) >> 8))
#define twin_dfixed_div(a, b) \
((twin_sfixed_t) ((((twin_dfixed_t) (a)) << 8) / b))

#define twin_in(s, i, m, t) \
((twin_argb32_t) twin_int_mult(twin_get_8(s, i), (m), (t)) << (i))

Expand Down
76 changes: 53 additions & 23 deletions src/spline.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,38 @@ typedef struct _twin_spline {
} twin_spline_t;

/*
* Linearly interpolate between points 'a' and 'b' with a shift factor.
* The shift factor determines the position between 'a' and 'b'.
* Linearly interpolate between points 'a' and 'b' with a 't' factor.
* The 't' factor determines the position between 'a' and 'b'.
* The result is stored in 'result'.
*/
static void _lerp(twin_spoint_t *a,
twin_spoint_t *b,
int shift,
twin_dfixed_t t,
twin_spoint_t *result)
{
result->x = a->x + ((b->x - a->x) >> shift);
result->y = a->y + ((b->y - a->y) >> shift);
result->x = a->x + twin_dfixed_mul((b->x - a->x), t);
result->y = a->y + twin_dfixed_mul((b->y - a->y), t);
}

/*
* Perform the de Casteljau algorithm to split a spline at a given shift
* Perform the de Casteljau algorithm to split a spline at a given 't'
* factor. The spline is split into two new splines 's1' and 's2'.
*/
static void _de_casteljau(twin_spline_t *spline,
int shift,
twin_dfixed_t t,
twin_spline_t *s1,
twin_spline_t *s2)
{
twin_spoint_t ab, bc, cd;
twin_spoint_t abbc, bccd;
twin_spoint_t final;

_lerp(&spline->a, &spline->b, shift, &ab);
_lerp(&spline->b, &spline->c, shift, &bc);
_lerp(&spline->c, &spline->d, shift, &cd);
_lerp(&ab, &bc, shift, &abbc);
_lerp(&bc, &cd, shift, &bccd);
_lerp(&abbc, &bccd, shift, &final);
_lerp(&spline->a, &spline->b, t, &ab);
_lerp(&spline->b, &spline->c, t, &bc);
_lerp(&spline->c, &spline->d, t, &cd);
_lerp(&ab, &bc, t, &abbc);
_lerp(&bc, &cd, t, &bccd);
_lerp(&abbc, &bccd, t, &final);

s1->a = spline->a;
s1->b = ab;
Expand All @@ -56,10 +56,10 @@ static void _de_casteljau(twin_spline_t *spline,
}

/*
* Return an upper bound on the error (squared) that could result from
* Return an upper bound on the distance (squared) that could result from
* approximating a spline with a line segment connecting the two endpoints.
*/
static twin_dfixed_t _twin_spline_error_squared(twin_spline_t *spline)
static twin_dfixed_t _twin_spline_distance_squared(twin_spline_t *spline)
{
twin_dfixed_t berr, cerr;

Expand All @@ -72,12 +72,12 @@ static twin_dfixed_t _twin_spline_error_squared(twin_spline_t *spline)
}

/*
* Check if a spline is flat enough by comparing the error against the
* Check if a spline is flat enough by comparing the distance against the
* tolerance.
*/
static bool is_flat(twin_spline_t *spline, twin_dfixed_t tolerance_squared)
{
return _twin_spline_error_squared(spline) <= tolerance_squared;
return _twin_spline_distance_squared(spline) <= tolerance_squared;
}

/*
Expand All @@ -93,14 +93,44 @@ static void _twin_spline_decompose(twin_path_t *path,
_twin_path_sdraw(path, spline->a.x, spline->a.y);

while (!is_flat(spline, tolerance_squared)) {
int shift = 1;
twin_dfixed_t hi = TWIN_SFIXED_ONE * TWIN_SFIXED_ONE, lo = 0;
twin_dfixed_t t_optimal = 0, t;
twin_dfixed_t max_distance = 0, distance;
twin_spline_t left, right;

/* FIXME: Find the optimal shift value to decompose the spline */
do {
_de_casteljau(spline, shift, &left, &right);
shift++;
} while (!is_flat(&left, tolerance_squared));
while (true) {
t = (hi + lo) >> 1;
if (t_optimal == t)
break;

_de_casteljau(spline, t, &left, &right);

distance = _twin_spline_distance_squared(&left);
if (distance < max_distance)
break;

/* The left segment is close enough to fit the original spline. */
if (distance <= tolerance_squared) {
max_distance = distance;
t_optimal = t;
/* Try to find a better point such that the line segment
* connecting it to the previous point can fit the spline, while
* maximizing the distance from the spline as much as possible.
*/
lo = t;
} else {
/* The line segment connecting the previous point to the
* currently tested point with value 't' cannot fit the original
* spline. The point with value 't_optimal' is sufficient to fit
* the original spline.
*/
if (t_optimal)
break;
hi = t;
}
}

_de_casteljau(spline, t_optimal, &left, &right);

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

0 comments on commit c782f22

Please sign in to comment.