common: generalize bezier math

added new functionality for bezier curves:
1. generate bezier curve as an arc
2. estimate the number of points for optimal tessellation
3. check that bezier curve is close to a straight line
This functionality is useful for gl/wg renderers
This commit is contained in:
Sergii Liebodkin 2025-06-26 18:06:19 +03:00 committed by Hermet Park
parent 43f94f8b32
commit 8543099fcd
5 changed files with 91 additions and 108 deletions

View file

@ -293,6 +293,21 @@ void Line::split(float at, Line& left, Line& right) const
}
Bezier::Bezier(const Point& st, const Point& ed, float radius)
{
// Calculate the angle between the start and end points
auto angle = tvg::atan2(ed.y - st.y, ed.x - st.x);
// Calculate the control points of the cubic bezier curve
auto c = radius * PATH_KAPPA; // c = radius * (4/3) * tan(pi/8)
start = {st.x, st.y};
ctrl1 = {st.x + radius * cos(angle), st.y + radius * sin(angle)};
ctrl2 = {ed.x - c * cos(angle), ed.y - c * sin(angle)};
end = {ed.x, ed.y};
}
void Bezier::split(Bezier& left, Bezier& right) const
{
auto c = (ctrl1.x + ctrl2.x) * 0.5f;
@ -449,5 +464,31 @@ void Bezier::bounds(Point& min, Point& max) const
findMinMax(start.y, ctrl1.y, ctrl2.y, end.y, min.y, max.y);
}
bool Bezier::flatten() const
{
float diff1_x = fabsf((ctrl1.x * 3.f) - (start.x * 2.f) - end.x);
float diff1_y = fabsf((ctrl1.y * 3.f) - (start.y * 2.f) - end.y);
float diff2_x = fabsf((ctrl2.x * 3.f) - (end.x * 2.f) - start.x);
float diff2_y = fabsf((ctrl2.y * 3.f) - (end.y * 2.f) - start.y);
if (diff1_x < diff2_x) diff1_x = diff2_x;
if (diff1_y < diff2_y) diff1_y = diff2_y;
return (diff1_x + diff1_y <= 0.5f);
}
uint32_t Bezier::segments() const
{
if (flatten()) return 1;
Bezier left, right;
split(left, right);
return left.segments() + right.segments();
}
Bezier Bezier::operator*(const Matrix& m)
{
return Bezier{start * m, ctrl1* m, ctrl2 * m, end * m};
}
}

View file

@ -121,6 +121,12 @@ static inline constexpr const Matrix identity()
}
static inline float scaling(const Matrix& m)
{
return sqrtf(m.e11 * m.e11 + m.e21 * m.e21);
}
static inline void scale(Matrix* m, const Point& p)
{
m->e11 *= p.x;
@ -330,6 +336,21 @@ static inline Point operator-(const Point& a)
return {-a.x, -a.y};
}
enum class Orientation
{
Linear = 0,
Clockwise,
CounterClockwise,
};
static inline Orientation orientation(const Point& p1, const Point& p2, const Point& p3)
{
auto val = cross(p2 - p1, p3 - p1);
if (zero(val)) return Orientation::Linear;
else return val > 0 ? Orientation::Clockwise : Orientation::CounterClockwise;
}
static inline void log(const Point& pt)
{
@ -362,6 +383,13 @@ struct Bezier
Point ctrl2;
Point end;
Bezier() {}
Bezier(const Point& p0, const Point& p1, const Point& p2, const Point& p3):
start(p0), ctrl1(p1), ctrl2(p2), end(p3) {}
// Constructor that approximates a quarter-circle segment of arc between 'start' and 'end' points
// using a cubic Bezier curve with a given 'radius'.
Bezier(const Point& start, const Point& end, float radius);
void split(float t, Bezier& left);
void split(Bezier& left, Bezier& right) const;
void split(float at, Bezier& left, Bezier& right) const;
@ -372,8 +400,11 @@ struct Bezier
Point at(float t) const;
float angle(float t) const;
void bounds(Point& min, Point& max) const;
};
bool flatten() const;
uint32_t segments() const;
Bezier operator*(const Matrix& m);
};
/************************************************************************/
/* Geometry functions */

View file

@ -81,11 +81,6 @@
static inline float getScaleFactor(const Matrix& m)
{
return sqrtf(m.e11 * m.e11 + m.e21 * m.e21);
}
enum class GlStencilMode {
None,
FillNonZero,

View file

@ -214,7 +214,7 @@ void GlRenderer::drawPrimitive(GlShape& sdata, const RenderColor& c, RenderUpdat
auto a = MULTIPLY(c.a, sdata.opacity);
if (flag & RenderUpdateFlag::Stroke) {
float strokeWidth = sdata.rshape->strokeWidth() * getScaleFactor(sdata.geometry.matrix);
float strokeWidth = sdata.rshape->strokeWidth() * scaling(sdata.geometry.matrix);
if (strokeWidth < MIN_GL_STROKE_WIDTH) {
float alpha = strokeWidth / MIN_GL_STROKE_WIDTH;
a = MULTIPLY(a, static_cast<uint8_t>(alpha * 255));

View file

@ -25,55 +25,6 @@
namespace tvg
{
static bool _bezIsFlatten(const Bezier& bz)
{
float diff1_x = fabs((bz.ctrl1.x * 3.f) - (bz.start.x * 2.f) - bz.end.x);
float diff1_y = fabs((bz.ctrl1.y * 3.f) - (bz.start.y * 2.f) - bz.end.y);
float diff2_x = fabs((bz.ctrl2.x * 3.f) - (bz.end.x * 2.f) - bz.start.x);
float diff2_y = fabs((bz.ctrl2.y * 3.f) - (bz.end.y * 2.f) - bz.start.y);
if (diff1_x < diff2_x) diff1_x = diff2_x;
if (diff1_y < diff2_y) diff1_y = diff2_y;
if (diff1_x + diff1_y <= 0.5f) return true;
return false;
}
static int32_t _bezierCurveCount(const Bezier &curve)
{
if (_bezIsFlatten(curve)) {
return 1;
}
Bezier left{};
Bezier right{};
curve.split(left, right);
return _bezierCurveCount(left) + _bezierCurveCount(right);
}
static Bezier _bezFromArc(const Point& start, const Point& end, float radius)
{
// Calculate the angle between the start and end points
auto angle = tvg::atan2(end.y - start.y, end.x - start.x);
// Calculate the control points of the cubic bezier curve
auto c = radius * 0.552284749831f; // c = radius * (4/3) * tan(pi/8)
Bezier bz;
bz.start = {start.x, start.y};
bz.ctrl1 = {start.x + radius * cos(angle), start.y + radius * sin(angle)};
bz.ctrl2 = {end.x - c * cosf(angle), end.y - c * sinf(angle)};
bz.end = {end.x, end.y};
return bz;
}
static uint32_t _pushVertex(Array<float>& array, float x, float y)
{
@ -83,22 +34,6 @@ static uint32_t _pushVertex(Array<float>& array, float x, float y)
}
enum class Orientation
{
Linear,
Clockwise,
CounterClockwise,
};
static Orientation _calcOrientation(const Point& p1, const Point& p2, const Point& p3)
{
auto val = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x);
if (std::abs(val) < 0.0001f) return Orientation::Linear;
else return val > 0 ? Orientation::Clockwise : Orientation::CounterClockwise;
}
Stroker::Stroker(GlGeometryBuffer* buffer, const Matrix& matrix) : mBuffer(buffer), mMatrix(matrix)
{
}
@ -112,7 +47,7 @@ void Stroker::stroke(const RenderShape *rshape, const RenderPath& path)
mStrokeWidth = rshape->strokeWidth();
if (isinf(mMatrix.e11)) {
auto strokeWidth = rshape->strokeWidth() * getScaleFactor(mMatrix);
auto strokeWidth = rshape->strokeWidth() * scaling(mMatrix);
if (strokeWidth <= MIN_GL_STROKE_WIDTH) strokeWidth = MIN_GL_STROKE_WIDTH;
mStrokeWidth = strokeWidth / mMatrix.e11;
}
@ -265,22 +200,12 @@ void Stroker::strokeLineTo(const Point& curr)
void Stroker::strokeCubicTo(const Point& cnt1, const Point& cnt2, const Point& end)
{
Bezier curve{};
curve.start = {mStrokeState.prevPt.x, mStrokeState.prevPt.y};
curve.ctrl1 = {cnt1.x, cnt1.y};
curve.ctrl2 = {cnt2.x, cnt2.y};
curve.end = {end.x, end.y};
Bezier curve{ mStrokeState.prevPt, cnt1, cnt2, end };
Bezier relCurve {curve.start, curve.ctrl1, curve.ctrl2, curve.end};
relCurve.start *= mMatrix;
relCurve.ctrl1 *= mMatrix;
relCurve.ctrl2 *= mMatrix;
relCurve.end *= mMatrix;
auto count = _bezierCurveCount(relCurve);
auto count = (curve * mMatrix).segments();
auto step = 1.f / count;
for (int32_t i = 0; i <= count; i++) {
for (uint32_t i = 0; i <= count; i++) {
strokeLineTo(curve.at(step * i));
}
}
@ -299,9 +224,9 @@ void Stroker::strokeClose()
void Stroker::strokeJoin(const Point& dir)
{
auto orientation = _calcOrientation(mStrokeState.prevPt - mStrokeState.prevPtDir, mStrokeState.prevPt, mStrokeState.prevPt + dir);
auto orient = orientation(mStrokeState.prevPt - mStrokeState.prevPtDir, mStrokeState.prevPt, mStrokeState.prevPt + dir);
if (orientation == Orientation::Linear) {
if (orient == Orientation::Linear) {
if (mStrokeState.prevPtDir == dir) return; // check is same direction
if (mStrokeJoin != StrokeJoin::Round) return; // opposite direction
@ -318,7 +243,7 @@ void Stroker::strokeJoin(const Point& dir)
auto prevNormal = Point{-mStrokeState.prevPtDir.y, mStrokeState.prevPtDir.x};
Point prevJoin, currJoin;
if (orientation == Orientation::CounterClockwise) {
if (orient == Orientation::CounterClockwise) {
prevJoin = mStrokeState.prevPt + prevNormal * strokeRadius();
currJoin = mStrokeState.prevPt + normal * strokeRadius();
} else {
@ -335,7 +260,7 @@ void Stroker::strokeJoin(const Point& dir)
void Stroker::strokeRound(const Point &prev, const Point& curr, const Point& center)
{
if (_calcOrientation(prev, center, curr) == Orientation::Linear) return;
if (orientation(prev, center, curr) == Orientation::Linear) return;
mLeftTop.x = std::min(mLeftTop.x, std::min(center.x, std::min(prev.x, curr.x)));
mLeftTop.y = std::min(mLeftTop.y, std::min(center.y, std::min(prev.y, curr.y)));
@ -343,7 +268,7 @@ void Stroker::strokeRound(const Point &prev, const Point& curr, const Point& cen
mRightBottom.y = std::max(mRightBottom.y, std::max(center.y, std::max(prev.y, curr.y)));
// Fixme: just use bezier curve to calculate step count
auto count = _bezierCurveCount(_bezFromArc(prev, curr, strokeRadius()));
auto count = Bezier(prev, curr, strokeRadius()).segments();
auto c = _pushVertex(mBuffer->vertex, center.x, center.y);
auto pi = _pushVertex(mBuffer->vertex, prev.x, prev.y);
auto step = 1.f / (count - 1);
@ -375,7 +300,7 @@ void Stroker::strokeRound(const Point &prev, const Point& curr, const Point& cen
void Stroker::strokeRoundPoint(const Point &p)
{
// Fixme: just use bezier curve to calculate step count
auto count = _bezierCurveCount(_bezFromArc(p, p, strokeRadius())) * 2;
auto count = Bezier(p, p, strokeRadius()).segments() * 2;
auto c = _pushVertex(mBuffer->vertex, p.x, p.y);
auto step = 2 * MATH_PI / (count - 1);
@ -655,11 +580,7 @@ void DashStroke::dashLineTo(const Point& to, bool validPoint)
void DashStroke::dashCubicTo(const Point& cnt1, const Point& cnt2, const Point& end, bool validPoint)
{
Bezier cur;
cur.start = {mPtCur.x, mPtCur.y};
cur.ctrl1 = {cnt1.x, cnt1.y};
cur.ctrl2 = {cnt2.x, cnt2.y};
cur.end = {end.x, end.y};
Bezier cur{ mPtCur, cnt1, cnt2, end };
auto len = cur.length();
@ -721,23 +642,23 @@ void DashStroke::dashCubicTo(const Point& cnt1, const Point& cnt2, const Point&
void DashStroke::moveTo(const Point& pt)
{
mPts->push(Point{pt.x, pt.y});
mPts->push(pt);
mCmds->push(PathCommand::MoveTo);
}
void DashStroke::lineTo(const Point& pt)
{
mPts->push(Point{pt.x, pt.y});
mPts->push(pt);
mCmds->push(PathCommand::LineTo);
}
void DashStroke::cubicTo(const Point& cnt1, const Point& cnt2, const Point& end)
{
mPts->push({cnt1.x, cnt1.y});
mPts->push({cnt2.x, cnt2.y});
mPts->push({end.x, end.y});
mPts->push(cnt1);
mPts->push(cnt2);
mPts->push(end);
mCmds->push(PathCommand::CubicTo);
}
@ -782,13 +703,8 @@ void BWTessellator::tessellate(const RenderPath& path, const Matrix& matrix)
} break;
case PathCommand::CubicTo: {
Bezier curve{pts[-1], pts[0], pts[1], pts[2]};
Bezier relCurve {pts[-1], pts[0], pts[1], pts[2]};
relCurve.start *= matrix;
relCurve.ctrl1 *= matrix;
relCurve.ctrl2 *= matrix;
relCurve.end *= matrix;
auto stepCount = _bezierCurveCount(relCurve);
auto stepCount = (curve * matrix).segments();
if (stepCount <= 1) stepCount = 2;
float step = 1.f / stepCount;