// SPDX-License-Identifier: GPL-2.0-or-later /** @file * Interpolators for lists of points. */ /* Authors: * Johan Engelen * * Copyright (C) 2010-2011 Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #ifndef INKSCAPE_LPE_POWERSTROKE_INTERPOLATORS_H #define INKSCAPE_LPE_POWERSTROKE_INTERPOLATORS_H #include <2geom/path.h> #include <2geom/bezier-utils.h> #include <2geom/sbasis-to-bezier.h> #include "live_effects/spiro.h" /// @TODO Move this to 2geom? namespace Geom { namespace Interpolate { enum InterpolatorType { INTERP_LINEAR, INTERP_CUBICBEZIER, INTERP_CUBICBEZIER_JOHAN, INTERP_SPIRO, INTERP_CUBICBEZIER_SMOOTH, INTERP_CENTRIPETAL_CATMULLROM }; class Interpolator { public: Interpolator() = default;; virtual ~Interpolator() = default;; static Interpolator* create(InterpolatorType type); virtual Geom::Path interpolateToPath(std::vector const &points) const = 0; private: Interpolator(const Interpolator&) = delete; Interpolator& operator=(const Interpolator&) = delete; }; class Linear : public Interpolator { public: Linear() = default;; ~Linear() override = default;; Path interpolateToPath(std::vector const &points) const override { Path path; path.start( points.at(0) ); for (unsigned int i = 1 ; i < points.size(); ++i) { path.appendNew(points.at(i)); } return path; }; private: Linear(const Linear&) = delete; Linear& operator=(const Linear&) = delete; }; // this class is terrible class CubicBezierFit : public Interpolator { public: CubicBezierFit() = default;; ~CubicBezierFit() override = default;; Path interpolateToPath(std::vector const &points) const override { unsigned int n_points = points.size(); // worst case gives us 2 segment per point int max_segs = 8*n_points; Geom::Point * b = g_new(Geom::Point, max_segs); Geom::Point * points_array = g_new(Geom::Point, 4*n_points); for (unsigned i = 0; i < n_points; ++i) { points_array[i] = points.at(i); } double tolerance_sq = 0; // this value is just a random guess int const n_segs = Geom::bezier_fit_cubic_r(b, points_array, n_points, tolerance_sq, max_segs); Geom::Path fit; if ( n_segs > 0) { fit.start(b[0]); for (int c = 0; c < n_segs; c++) { fit.appendNew(b[4*c+1], b[4*c+2], b[4*c+3]); } } g_free(b); g_free(points_array); return fit; }; private: CubicBezierFit(const CubicBezierFit&) = delete; CubicBezierFit& operator=(const CubicBezierFit&) = delete; }; /// @todo invent name for this class class CubicBezierJohan : public Interpolator { public: CubicBezierJohan(double beta = 0.2) { _beta = beta; }; ~CubicBezierJohan() override = default;; Path interpolateToPath(std::vector const &points) const override { Path fit; fit.start(points.at(0)); for (unsigned int i = 1; i < points.size(); ++i) { Point p0 = points.at(i-1); Point p1 = points.at(i); Point dx = Point(p1[X] - p0[X], 0); fit.appendNew(p0+_beta*dx, p1-_beta*dx, p1); } return fit; }; void setBeta(double beta) { _beta = beta; } double _beta; private: CubicBezierJohan(const CubicBezierJohan&) = delete; CubicBezierJohan& operator=(const CubicBezierJohan&) = delete; }; /// @todo invent name for this class class CubicBezierSmooth : public Interpolator { public: CubicBezierSmooth(double beta = 0.2) { _beta = beta; }; ~CubicBezierSmooth() override = default;; Path interpolateToPath(std::vector const &points) const override { Path fit; fit.start(points.at(0)); unsigned int num_points = points.size(); for (unsigned int i = 1; i < num_points; ++i) { Point p0 = points.at(i-1); Point p1 = points.at(i); Point dx = Point(p1[X] - p0[X], 0); if (i == 1) { fit.appendNew(p0, p1-0.75*dx, p1); } else if (i == points.size() - 1) { fit.appendNew(p0+0.75*dx, p1, p1); } else { fit.appendNew(p0+_beta*dx, p1-_beta*dx, p1); } } return fit; }; void setBeta(double beta) { _beta = beta; } double _beta; private: CubicBezierSmooth(const CubicBezierSmooth&) = delete; CubicBezierSmooth& operator=(const CubicBezierSmooth&) = delete; }; class SpiroInterpolator : public Interpolator { public: SpiroInterpolator() = default;; ~SpiroInterpolator() override = default;; Path interpolateToPath(std::vector const &points) const override { Path fit; Coord scale_y = 100.; guint len = points.size(); Spiro::spiro_cp *controlpoints = g_new (Spiro::spiro_cp, len); for (unsigned int i = 0; i < len; ++i) { controlpoints[i].x = points[i][X]; controlpoints[i].y = points[i][Y] / scale_y; controlpoints[i].ty = 'c'; } controlpoints[0].ty = '{'; controlpoints[1].ty = 'v'; controlpoints[len-2].ty = 'v'; controlpoints[len-1].ty = '}'; Spiro::spiro_run(controlpoints, len, fit); fit *= Scale(1,scale_y); g_free(controlpoints); return fit; }; private: SpiroInterpolator(const SpiroInterpolator&) = delete; SpiroInterpolator& operator=(const SpiroInterpolator&) = delete; }; // Quick mockup for testing the behavior for powerstroke controlpoint interpolation class CentripetalCatmullRomInterpolator : public Interpolator { public: CentripetalCatmullRomInterpolator() = default;; ~CentripetalCatmullRomInterpolator() override = default;; Path interpolateToPath(std::vector const &points) const override { unsigned int n_points = points.size(); Geom::Path fit(points.front()); if (n_points < 3) return fit; // TODO special cases for 0,1 and 2 input points // return n_points-1 cubic segments // duplicate first point fit.append(calc_bezier(points[0],points[0],points[1],points[2])); for (std::size_t i = 0; i < n_points-2; ++i) { Point p0 = points[i]; Point p1 = points[i+1]; Point p2 = points[i+2]; Point p3 = (i < n_points-3) ? points[i+3] : points[i+2]; fit.append(calc_bezier(p0, p1, p2, p3)); } return fit; }; private: CubicBezier calc_bezier(Point p0, Point p1, Point p2, Point p3) const { // create interpolating bezier between p1 and p2 // Part of the code comes from StackOverflow user eriatarka84 // http://stackoverflow.com/a/23980479/2929337 // calculate time coords (deltas) of points // the factor 0.25 can be generalized for other Catmull-Rom interpolation types // see alpha in Yuksel et al. "On the Parameterization of Catmull-Rom Curves", // --> http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf double dt0 = powf(distanceSq(p0, p1), 0.25); double dt1 = powf(distanceSq(p1, p2), 0.25); double dt2 = powf(distanceSq(p2, p3), 0.25); // safety check for repeated points double eps = Geom::EPSILON; if (dt1 < eps) dt1 = 1.0; if (dt0 < eps) dt0 = dt1; if (dt2 < eps) dt2 = dt1; // compute tangents when parameterized in [t1,t2] Point tan1 = (p1 - p0) / dt0 - (p2 - p0) / (dt0 + dt1) + (p2 - p1) / dt1; Point tan2 = (p2 - p1) / dt1 - (p3 - p1) / (dt1 + dt2) + (p3 - p2) / dt2; // rescale tangents for parametrization in [0,1] tan1 *= dt1; tan2 *= dt1; // create bezier from tangents (this is already in 2geom somewhere, or should be moved to it) // the tangent of a bezier curve is: B'(t) = 3(1-t)^2 (b1 - b0) + 6(1-t)t(b2-b1) + 3t^2(b3-b2) // So we have to make sure that B'(0) = tan1 and B'(1) = tan2, and we already know that b0=p1 and b3=p2 // tan1 = B'(0) = 3 (b1 - p1) --> p1 + (tan1)/3 = b1 // tan2 = B'(1) = 3 (p2 - b2) --> p2 - (tan2)/3 = b2 Point b0 = p1; Point b1 = p1 + tan1 / 3; Point b2 = p2 - tan2 / 3; Point b3 = p2; return CubicBezier(b0, b1, b2, b3); } CentripetalCatmullRomInterpolator(const CentripetalCatmullRomInterpolator&) = delete; CentripetalCatmullRomInterpolator& operator=(const CentripetalCatmullRomInterpolator&) = delete; }; inline Interpolator* Interpolator::create(InterpolatorType type) { switch (type) { case INTERP_LINEAR: return new Geom::Interpolate::Linear(); case INTERP_CUBICBEZIER: return new Geom::Interpolate::CubicBezierFit(); case INTERP_CUBICBEZIER_JOHAN: return new Geom::Interpolate::CubicBezierJohan(); case INTERP_SPIRO: return new Geom::Interpolate::SpiroInterpolator(); case INTERP_CUBICBEZIER_SMOOTH: return new Geom::Interpolate::CubicBezierSmooth(); case INTERP_CENTRIPETAL_CATMULLROM: return new Geom::Interpolate::CentripetalCatmullRomInterpolator(); default: return new Geom::Interpolate::Linear(); } } } //namespace Interpolate } //namespace Geom #endif /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :