/*
 * Copyright 2020 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "imgui.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkPath.h"
#include "include/core/SkPathBuilder.h"
#include "include/core/SkPathMeasure.h"
#include "include/core/SkPathUtils.h"
#include "include/utils/SkParsePath.h"
#include "src/core/SkGeometry.h"
#include "tools/viewer/ClickHandlerSlide.h"

#include <stack>
#include <vector>

namespace {

//////////////////////////////////////////////////////////////////////////////

constexpr inline SkPoint rotate90(const SkPoint& p) { return {p.fY, -p.fX}; }
inline SkPoint rotate180(const SkPoint& p) { return p * -1; }
inline bool isClockwise(const SkPoint& a, const SkPoint& b) { return a.cross(b) > 0; }

static SkPoint checkSetLength(SkPoint p, float len, const char* file, int line) {
    if (!p.setLength(len)) {
        SkDebugf("%s:%d: Failed to set point length\n", file, line);
    }
    return p;
}

/** Version of setLength that prints debug msg on failure to help catch edge cases */
#define setLength(p, len) checkSetLength(p, len, __FILE__, __LINE__)

constexpr uint64_t choose(uint64_t n, uint64_t k) {
    SkASSERT(n >= k);
    uint64_t result = 1;
    for (uint64_t i = 1; i <= k; i++) {
        result *= (n + 1 - i);
        result /= i;
    }
    return result;
}

//////////////////////////////////////////////////////////////////////////////

/**
 * A scalar (float-valued weights) Bezier curve of arbitrary degree.
 */
class ScalarBezCurve {
public:
    inline static constexpr int kDegreeInvalid = -1;

    /** Creates an empty curve with invalid degree. */
    ScalarBezCurve() : fDegree(kDegreeInvalid) {}

    /** Creates a curve of the specified degree with weights initialized to 0. */
    explicit ScalarBezCurve(int degree) : fDegree(degree) {
        SkASSERT(degree >= 0);
        fWeights.resize(degree + 1, {0});
    }

    /** Creates a curve of specified degree with the given weights. */
    ScalarBezCurve(int degree, const std::vector<float>& weights) : ScalarBezCurve(degree) {
        SkASSERT(degree >= 0);
        SkASSERT(weights.size() == (size_t)degree + 1);
        fWeights.insert(fWeights.begin(), weights.begin(), weights.end());
    }

    /** Returns the extreme-valued weight */
    float extremumWeight() const {
        float f = 0;
        int sign = 1;
        for (float w : fWeights) {
            if (std::abs(w) > f) {
                f = std::abs(w);
                sign = w >= 0 ? 1 : -1;
            }
        }
        return sign * f;
    }

    /** Evaluates the curve at t */
    float eval(float t) const { return Eval(*this, t); }

    /** Evaluates the curve at t */
    static float Eval(const ScalarBezCurve& curve, float t) {
        // Set up starting point of recursion (k=0)
        ScalarBezCurve result = curve;

        for (int k = 1; k <= curve.fDegree; k++) {
            // k is level of recursion, k-1 has previous level's values.
            for (int i = curve.fDegree; i >= k; i--) {
                result.fWeights[i] = result.fWeights[i - 1] * (1 - t) + result.fWeights[i] * t;
            }
        }

        return result.fWeights[curve.fDegree];
    }

    /** Splits this curve at t into two halves (of the same degree) */
    void split(float t, ScalarBezCurve* left, ScalarBezCurve* right) const {
        Split(*this, t, left, right);
    }

    /** Splits this curve into the subinterval [tmin,tmax]. */
    void split(float tmin, float tmax, ScalarBezCurve* result) const {
        // TODO: I believe there's a more efficient algorithm for this
        const float tRel = tmin / tmax;
        ScalarBezCurve ll, rl, rr;
        this->split(tmax, &rl, &rr);
        rl.split(tRel, &ll, result);
    }

    /** Splits the curve at t into two halves (of the same degree) */
    static void Split(const ScalarBezCurve& curve,
                      float t,
                      ScalarBezCurve* left,
                      ScalarBezCurve* right) {
        // Set up starting point of recursion (k=0)
        const int degree = curve.fDegree;
        ScalarBezCurve result = curve;
        *left = ScalarBezCurve(degree);
        *right = ScalarBezCurve(degree);
        left->fWeights[0] = curve.fWeights[0];
        right->fWeights[degree] = curve.fWeights[degree];

        for (int k = 1; k <= degree; k++) {
            // k is level of recursion, k-1 has previous level's values.
            for (int i = degree; i >= k; i--) {
                result.fWeights[i] = result.fWeights[i - 1] * (1 - t) + result.fWeights[i] * t;
            }

            left->fWeights[k] = result.fWeights[k];
            right->fWeights[degree - k] = result.fWeights[degree];
        }
    }

    /**
     * Increases the degree of the curve to the given degree. Has no effect if the
     * degree is already equal to the given degree.
     *
     * This process is always exact (NB the reverse, degree reduction, is not exact).
     */
    void elevateDegree(int newDegree) {
        if (newDegree == fDegree) {
            return;
        }

        fWeights = ElevateDegree(*this, newDegree).fWeights;
        fDegree = newDegree;
    }

    /**
     * Increases the degree of the curve to the given degree. Has no effect if the
     * degree is already equal to the given degree.
     *
     * This process is always exact (NB the reverse, degree reduction, is not exact).
     */
    static ScalarBezCurve ElevateDegree(const ScalarBezCurve& curve, int newDegree) {
        SkASSERT(newDegree >= curve.degree());
        if (newDegree == curve.degree()) {
            return curve;
        }

        // From Farouki, Rajan, "Algorithms for polynomials in Bernstein form" 1988.
        ScalarBezCurve elevated(newDegree);
        const int r = newDegree - curve.fDegree;
        const int n = curve.fDegree;

        for (int i = 0; i <= n + r; i++) {
            elevated.fWeights[i] = 0;
            for (int j = std::max(0, i - r); j <= std::min(n, i); j++) {
                const float f =
                        (choose(n, j) * choose(r, i - j)) / static_cast<float>(choose(n + r, i));
                elevated.fWeights[i] += curve.fWeights[j] * f;
            }
        }

        return elevated;
    }

    /**
     * Returns the zero-set of this curve, which is a list of t values where the curve crosses 0.
     */
    std::vector<float> zeroSet() const { return ZeroSet(*this); }

    /**
     * Returns the zero-set of the curve, which is a list of t values where the curve crosses 0.
     */
    static std::vector<float> ZeroSet(const ScalarBezCurve& curve) {
        constexpr float kTol = 0.001f;
        std::vector<float> result;
        ZeroSetRec(curve, 0, 1, kTol, &result);
        return result;
    }

    /** Multiplies the curve's weights by a constant value */
    static ScalarBezCurve Mul(const ScalarBezCurve& curve, float f) {
        ScalarBezCurve result = curve;
        for (int k = 0; k <= curve.fDegree; k++) {
            result.fWeights[k] *= f;
        }
        return result;
    }

    /**
     * Multiplies the two curves and returns the result.
     *
     * Degree of resulting curve is the sum of the degrees of the input curves.
     */
    static ScalarBezCurve Mul(const ScalarBezCurve& a, const ScalarBezCurve& b) {
        // From G. Elber, "Free form surface analysis using a hybrid of symbolic and numeric
        // computation". PhD thesis, 1992. p.11.
        const int n = a.degree(), m = b.degree();
        const int newDegree = n + m;
        ScalarBezCurve result(newDegree);

        for (int k = 0; k <= newDegree; k++) {
            result.fWeights[k] = 0;
            for (int i = std::max(0, k - n); i <= std::min(k, m); i++) {
                const float f =
                        (choose(m, i) * choose(n, k - i)) / static_cast<float>(choose(m + n, k));
                result.fWeights[k] += a.fWeights[i] * b.fWeights[k - i] * f;
            }
        }

        return result;
    }

    /** Returns a^2 + b^2. This is a specialized method because the loops are easily fused. */
    static ScalarBezCurve AddSquares(const ScalarBezCurve& a, const ScalarBezCurve& b) {
        const int n = a.degree(), m = b.degree();
        const int newDegree = n + m;
        ScalarBezCurve result(newDegree);

        for (int k = 0; k <= newDegree; k++) {
            float aSq = 0, bSq = 0;
            for (int i = std::max(0, k - n); i <= std::min(k, m); i++) {
                const float f =
                        (choose(m, i) * choose(n, k - i)) / static_cast<float>(choose(m + n, k));
                aSq += a.fWeights[i] * a.fWeights[k - i] * f;
                bSq += b.fWeights[i] * b.fWeights[k - i] * f;
            }
            result.fWeights[k] = aSq + bSq;
        }

        return result;
    }

    /** Returns a - b. */
    static ScalarBezCurve Sub(const ScalarBezCurve& a, const ScalarBezCurve& b) {
        ScalarBezCurve result = a;
        result.sub(b);
        return result;
    }

    /** Subtracts the other curve from this curve */
    void sub(const ScalarBezCurve& other) {
        SkASSERT(other.fDegree == fDegree);
        for (int k = 0; k <= fDegree; k++) {
            fWeights[k] -= other.fWeights[k];
        }
    }

    /** Subtracts a constant from this curve */
    void sub(float f) {
        for (int k = 0; k <= fDegree; k++) {
            fWeights[k] -= f;
        }
    }

    /** Returns the curve degree */
    int degree() const { return fDegree; }

    /** Returns the curve weights */
    const std::vector<float>& weights() const { return fWeights; }

    float operator[](size_t i) const { return fWeights[i]; }
    float& operator[](size_t i) { return fWeights[i]; }

private:
    /** Recursive helper for ZeroSet */
    static void ZeroSetRec(const ScalarBezCurve& curve,
                           float tmin,
                           float tmax,
                           float tol,
                           std::vector<float>* result) {
        float lenP = 0;
        bool allPos = curve.fWeights[0] >= 0, allNeg = curve.fWeights[0] < 0;
        for (int i = 1; i <= curve.fDegree; i++) {
            lenP += std::abs(curve.fWeights[i] - curve.fWeights[i - 1]);
            allPos &= curve.fWeights[i] >= 0;
            allNeg &= curve.fWeights[i] < 0;
        }
        if (lenP <= tol) {
            result->push_back((tmin + tmax) * 0.5);
            return;
        } else if (allPos || allNeg) {
            // No zero crossings possible if the coefficients don't change sign (convex hull
            // property)
            return;
        } else if (SkScalarNearlyZero(tmax - tmin)) {
            return;
        } else {
            ScalarBezCurve left(curve.fDegree), right(curve.fDegree);
            Split(curve, 0.5f, &left, &right);

            const float tmid = (tmin + tmax) * 0.5;
            ZeroSetRec(left, tmin, tmid, tol, result);
            ZeroSetRec(right, tmid, tmax, tol, result);
        }
    }

    int fDegree;
    std::vector<float> fWeights;
};

//////////////////////////////////////////////////////////////////////////////

/** Helper class that measures per-verb path lengths. */
class PathVerbMeasure {
public:
    explicit PathVerbMeasure(const SkPath& path) : fPath(path), fIter(path, false) { nextVerb(); }

    SkScalar totalLength() const;

    SkScalar currentVerbLength() { return fMeas.getLength(); }

    void nextVerb();

private:
    const SkPath& fPath;
    SkPoint fFirstPointInContour;
    SkPoint fPreviousPoint;
    SkPath fCurrVerb;
    SkPath::Iter fIter;
    SkPathMeasure fMeas;
};

SkScalar PathVerbMeasure::totalLength() const {
    SkPathMeasure meas(fPath, false);
    return meas.getLength();
}

void PathVerbMeasure::nextVerb() {
    SkPoint pts[4];
    SkPath::Verb verb = fIter.next(pts);

    while (verb == SkPath::kMove_Verb || verb == SkPath::kClose_Verb) {
        if (verb == SkPath::kMove_Verb) {
            fFirstPointInContour = pts[0];
            fPreviousPoint = fFirstPointInContour;
        }
        verb = fIter.next(pts);
    }

    SkPathBuilder builder;
    builder.moveTo(fPreviousPoint);
    switch (verb) {
        case SkPath::kLine_Verb:
            builder.lineTo(pts[1]);
            break;
        case SkPath::kQuad_Verb:
            builder.quadTo(pts[1], pts[2]);
            break;
        case SkPath::kCubic_Verb:
            builder.cubicTo(pts[1], pts[2], pts[3]);
            break;
        case SkPath::kConic_Verb:
            builder.conicTo(pts[1], pts[2], fIter.conicWeight());
            break;
        case SkPath::kDone_Verb:
            break;
        case SkPath::kClose_Verb:
        case SkPath::kMove_Verb:
            SkASSERT(false);
            break;
    }
    fCurrVerb = builder.detach();

    fPreviousPoint = fCurrVerb.getLastPt().value_or(SkPoint{0, 0});
    fMeas.setPath(&fCurrVerb, false);
}

//////////////////////////////////////////////////////////////////////////////

// Several debug-only visualization helpers
namespace viz {
std::unique_ptr<ScalarBezCurve> outerErr;
SkPath outerFirstApprox;
}  // namespace viz

/**
 * Prototype variable-width path stroker.
 *
 * Takes as input a path to be stroked, and two distance functions (inside and outside).
 * Produces a fill path with the stroked path geometry.
 *
 * The algorithms in use here are from:
 *
 * G. Elber, E. Cohen. "Error bounded variable distance offset operator for free form curves and
 * surfaces." International Journal of Computational Geometry & Applications 1, no. 01 (1991)
 *
 * G. Elber. "Free form surface analysis using a hybrid of symbolic and numeric computation."
 * PhD diss., Dept. of Computer Science, University of Utah, 1992.
 */
class SkVarWidthStroker {
public:
    /** Metric to use for interpolation of distance function across path segments. */
    enum class LengthMetric {
        /** Each path segment gets an equal interval of t in [0,1] */
        kNumSegments,
        /** Each path segment gets t interval equal to its percent of the total path length */
        kPathLength,
    };

    /**
     * Strokes the path with a fixed-width distance function. This produces a traditional stroked
     * path.
     */
    SkPath getFillPath(const SkPath& path, const SkPaint& paint) {
        return getFillPath(path, paint, identityVarWidth(paint.getStrokeWidth()),
                           identityVarWidth(paint.getStrokeWidth()));
    }

    /**
     * Strokes the given path using the two given distance functions for inner and outer offsets.
     */
    SkPath getFillPath(const SkPath& path,
                       const SkPaint& paint,
                       const ScalarBezCurve& varWidth,
                       const ScalarBezCurve& varWidthInner,
                       LengthMetric lengthMetric = LengthMetric::kNumSegments);

private:
    /** Helper struct referring to a single segment of an SkPath */
    struct PathSegment {
        SkPath::Verb fVerb;
        std::array<SkPoint, 4> fPoints;
    };

    struct OffsetSegments {
        std::vector<PathSegment> fInner;
        std::vector<PathSegment> fOuter;
    };

    /** Initialize stroker state */
    void initForPath(const SkPath& path, const SkPaint& paint);

    /** Strokes a path segment */
    OffsetSegments strokeSegment(const PathSegment& segment,
                                 const ScalarBezCurve& varWidth,
                                 const ScalarBezCurve& varWidthInner,
                                 bool needsMove);

    /**
     * Strokes the given segment using the given distance function.
     *
     * Returns a list of quad segments that approximate the offset curve.
     * TODO: no reason this needs to return a vector of quads, can just append to the path
     */
    std::vector<PathSegment> strokeSegment(const PathSegment& seg,
                                           const ScalarBezCurve& distanceFunc) const;

    /** Adds an endcap to fOuter */
    enum class CapLocation { Start, End };
    void endcap(CapLocation loc);

    /** Adds a join between the two segments */
    void join(const SkPoint& common,
              float innerRadius,
              float outerRadius,
              const OffsetSegments& prev,
              const OffsetSegments& curr);

    /** Appends path in reverse to result */
    static void appendPathReversed(const SkPath& path, SkPathBuilder* result);

    /** Returns the segment unit normal and unit tangent if not nullptr */
    static SkPoint unitNormal(const PathSegment& seg, float t, SkPoint* tangentOut);

    /** Returns the degree of a segment curve */
    static int segmentDegree(const PathSegment& seg);

    /** Splits a path segment at t */
    static void splitSegment(const PathSegment& seg, float t, PathSegment* segA, PathSegment* segB);

    /**
     * Returns a quadratic segment that approximates the given segment using the given distance
     * function.
     */
    static void approximateSegment(const PathSegment& seg,
                                   const ScalarBezCurve& distFnc,
                                   PathSegment* approxQuad);

    /** Returns a constant (deg 0) distance function for the given stroke width */
    static ScalarBezCurve identityVarWidth(float strokeWidth) {
        return ScalarBezCurve(0, {strokeWidth / 2.0f});
    }

    float fRadius;
    SkPaint::Cap fCap;
    SkPaint::Join fJoin;
    SkPathBuilder fInner, fOuter;
    ScalarBezCurve fVarWidth, fVarWidthInner;
    float fCurrT;
};

void SkVarWidthStroker::initForPath(const SkPath& path, const SkPaint& paint) {
    fRadius = paint.getStrokeWidth() / 2;
    fCap = paint.getStrokeCap();
    fJoin = paint.getStrokeJoin();
    fInner.reset();
    fOuter.reset();
    fCurrT = 0;
}

SkPath SkVarWidthStroker::getFillPath(const SkPath& path,
                                      const SkPaint& paint,
                                      const ScalarBezCurve& varWidth,
                                      const ScalarBezCurve& varWidthInner,
                                      LengthMetric lengthMetric) {
    const auto appendStrokes = [this](const OffsetSegments& strokes, bool needsMove) {
        if (needsMove) {
            fOuter.moveTo(strokes.fOuter.front().fPoints[0]);
            fInner.moveTo(strokes.fInner.front().fPoints[0]);
        }

        for (const PathSegment& seg : strokes.fOuter) {
            fOuter.quadTo(seg.fPoints[1], seg.fPoints[2]);
        }

        for (const PathSegment& seg : strokes.fInner) {
            fInner.quadTo(seg.fPoints[1], seg.fPoints[2]);
        }
    };

    initForPath(path, paint);
    fVarWidth = varWidth;
    fVarWidthInner = varWidthInner;

    // TODO: this assumes one contour:
    PathVerbMeasure meas(path);
    const float totalPathLength = lengthMetric == LengthMetric::kPathLength
                                          ? meas.totalLength()
                                          : (path.countVerbs() - 1);

    // Trace the inner and outer paths simultaneously. Inner will therefore be
    // recorded in reverse from how we trace the outline.
    SkPath::Iter it(path, false);
    PathSegment segment, prevSegment;
    OffsetSegments offsetSegs, prevOffsetSegs;
    bool firstSegment = true, prevWasFirst = false;

    float lenTraveled = 0;
    while ((segment.fVerb = it.next(&segment.fPoints[0])) != SkPath::kDone_Verb) {
        const float verbLength = lengthMetric == LengthMetric::kPathLength
                                         ? (meas.currentVerbLength() / totalPathLength)
                                         : (1.0f / totalPathLength);
        const float tmin = lenTraveled;
        const float tmax = lenTraveled + verbLength;

        // Subset the distance function for the current interval.
        ScalarBezCurve partVarWidth, partVarWidthInner;
        fVarWidth.split(tmin, tmax, &partVarWidth);
        fVarWidthInner.split(tmin, tmax, &partVarWidthInner);
        partVarWidthInner = ScalarBezCurve::Mul(partVarWidthInner, -1);

        // Stroke the current segment
        switch (segment.fVerb) {
            case SkPath::kLine_Verb:
            case SkPath::kQuad_Verb:
            case SkPath::kCubic_Verb:
                offsetSegs = strokeSegment(segment, partVarWidth, partVarWidthInner, firstSegment);
                break;
            case SkPath::kMove_Verb:
                // Don't care about multiple contours currently
                continue;
            default:
                SkDebugf("Unhandled path verb %d\n", segment.fVerb);
                SkASSERT(false);
                break;
        }

        // Join to the previous segment
        if (!firstSegment) {
            // Append prev inner and outer strokes
            appendStrokes(prevOffsetSegs, prevWasFirst);

            // Append the join
            const float innerRadius = varWidthInner.eval(tmin);
            const float outerRadius = varWidth.eval(tmin);
            join(segment.fPoints[0], innerRadius, outerRadius, prevOffsetSegs, offsetSegs);
        }

        std::swap(segment, prevSegment);
        std::swap(offsetSegs, prevOffsetSegs);
        prevWasFirst = firstSegment;
        firstSegment = false;
        lenTraveled += verbLength;
        meas.nextVerb();
    }

    // Finish appending final offset segments
    appendStrokes(prevOffsetSegs, prevWasFirst);

    // Open contour => endcap at the end
    const bool isClosed = path.isLastContourClosed();
    if (isClosed) {
        SkDebugf("Unhandled closed contour\n");
        SkASSERT(false);
    } else {
        endcap(CapLocation::End);
    }

    // Walk inner path in reverse, appending to result
    appendPathReversed(fInner.snapshot(), &fOuter);
    endcap(CapLocation::Start);

    return fOuter.snapshot();
}

SkVarWidthStroker::OffsetSegments SkVarWidthStroker::strokeSegment(
        const PathSegment& segment,
        const ScalarBezCurve& varWidth,
        const ScalarBezCurve& varWidthInner,
        bool needsMove) {
    viz::outerErr.reset(nullptr);

    std::vector<PathSegment> outer = strokeSegment(segment, varWidth);
    std::vector<PathSegment> inner = strokeSegment(segment, varWidthInner);
    return {inner, outer};
}

std::vector<SkVarWidthStroker::PathSegment> SkVarWidthStroker::strokeSegment(
        const PathSegment& seg, const ScalarBezCurve& distanceFunc) const {
    // Work item for the recursive splitting stack.
    struct Item {
        PathSegment fSeg;
        ScalarBezCurve fDistFnc, fDistFncSqd;
        ScalarBezCurve fSegX, fSegY;

        Item(const PathSegment& seg,
             const ScalarBezCurve& distFnc,
             const ScalarBezCurve& distFncSqd)
                : fSeg(seg), fDistFnc(distFnc), fDistFncSqd(distFncSqd) {
            const int segDegree = segmentDegree(seg);
            fSegX = ScalarBezCurve(segDegree);
            fSegY = ScalarBezCurve(segDegree);
            for (int i = 0; i <= segDegree; i++) {
                fSegX[i] = seg.fPoints[i].fX;
                fSegY[i] = seg.fPoints[i].fY;
            }
        }
    };

    // Push the initial segment and distance function
    std::stack<Item> stack;
    stack.push(Item(seg, distanceFunc, ScalarBezCurve::Mul(distanceFunc, distanceFunc)));

    std::vector<PathSegment> result;
    constexpr int kMaxIters = 5000; /** TODO: this is completely arbitrary */
    int iter = 0;
    while (!stack.empty()) {
        if (iter++ >= kMaxIters) break;
        const Item item = stack.top();
        stack.pop();

        const ScalarBezCurve& distFnc = item.fDistFnc;
        ScalarBezCurve distFncSqd = item.fDistFncSqd;
        const float kTol = std::abs(0.5f * distFnc.extremumWeight());

        // Compute a quad that approximates stroke outline
        PathSegment quadApprox;
        approximateSegment(item.fSeg, distFnc, &quadApprox);
        ScalarBezCurve quadApproxX(2), quadApproxY(2);
        for (int i = 0; i < 3; i++) {
            quadApproxX[i] = quadApprox.fPoints[i].fX;
            quadApproxY[i] = quadApprox.fPoints[i].fY;
        }

        // Compute control polygon for the delta(t) curve. First must elevate to a common degree.
        const int deltaDegree = std::max(quadApproxX.degree(), item.fSegX.degree());
        ScalarBezCurve segX = item.fSegX, segY = item.fSegY;
        segX.elevateDegree(deltaDegree);
        segY.elevateDegree(deltaDegree);
        quadApproxX.elevateDegree(deltaDegree);
        quadApproxY.elevateDegree(deltaDegree);

        ScalarBezCurve deltaX = ScalarBezCurve::Sub(quadApproxX, segX);
        ScalarBezCurve deltaY = ScalarBezCurve::Sub(quadApproxY, segY);

        // Compute psi(t) = delta_x(t)^2 + delta_y(t)^2.
        ScalarBezCurve E = ScalarBezCurve::AddSquares(deltaX, deltaY);

        // Promote E and d(t)^2 to a common degree.
        const int commonDeg = std::max(distFncSqd.degree(), E.degree());
        distFncSqd.elevateDegree(commonDeg);
        E.elevateDegree(commonDeg);

        // Subtract dist squared curve from E, resulting in:
        //   eps(t) = delta_x(t)^2 + delta_y(t)^2 - d(t)^2
        E.sub(distFncSqd);

        // Purely for debugging/testing, save the first approximation and error function:
        if (viz::outerErr == nullptr) {
            using namespace viz;
            outerErr = std::make_unique<ScalarBezCurve>(E);
            outerFirstApprox = SkPathBuilder()
                .moveTo(quadApprox.fPoints[0])
                .quadTo(quadApprox.fPoints[1], quadApprox.fPoints[2])
                .detach();
        }

        // Compute maxErr, which is just the max coefficient of eps (using convex hull property
        // of bez curves)
        float maxAbsErr = std::abs(E.extremumWeight());

        if (maxAbsErr > kTol) {
            PathSegment left, right;
            splitSegment(item.fSeg, 0.5f, &left, &right);

            ScalarBezCurve distFncL, distFncR;
            distFnc.split(0.5f, &distFncL, &distFncR);

            ScalarBezCurve distFncSqdL, distFncSqdR;
            distFncSqd.split(0.5f, &distFncSqdL, &distFncSqdR);

            stack.push(Item(right, distFncR, distFncSqdR));
            stack.push(Item(left, distFncL, distFncSqdL));
        } else {
            // Approximation is good enough.
            quadApprox.fVerb = SkPath::kQuad_Verb;
            result.push_back(quadApprox);
        }
    }
    SkASSERT(!result.empty());
    return result;
}

void SkVarWidthStroker::endcap(CapLocation loc) {
    const auto buttCap = [this](CapLocation loc) {
        if (loc == CapLocation::Start) {
            // Back at the start of the path: just close the stroked outline
            fOuter.close();
        } else {
            // Inner last pt == first pt when appending in reverse
            SkPoint innerLastPt = fInner.getLastPt().value_or(SkPoint{0, 0});
            fOuter.lineTo(innerLastPt);
        }
    };

    switch (fCap) {
        case SkPaint::kButt_Cap:
            buttCap(loc);
            break;
        default:
            SkDebugf("Unhandled endcap %d\n", fCap);
            buttCap(loc);
            break;
    }
}

void SkVarWidthStroker::join(const SkPoint& common,
                             float innerRadius,
                             float outerRadius,
                             const OffsetSegments& prev,
                             const OffsetSegments& curr) {
    const auto miterJoin = [this](const SkPoint& common,
                                  float leftRadius,
                                  float rightRadius,
                                  const OffsetSegments& prev,
                                  const OffsetSegments& curr) {
        // With variable-width stroke you can actually have a situation where both sides
        // need an "inner" or an "outer" join. So we call the two sides "left" and
        // "right" and they can each independently get an inner or outer join.
        const auto makeJoin = [this, &common, &prev, &curr](bool left, float radius) {
            SkPathBuilder* path = left ? &fOuter : &fInner;
            const auto& prevSegs = left ? prev.fOuter : prev.fInner;
            const auto& currSegs = left ? curr.fOuter : curr.fInner;
            SkASSERT(!prevSegs.empty());
            SkASSERT(!currSegs.empty());
            const SkPoint afterEndpt = currSegs.front().fPoints[0];
            SkPoint before = unitNormal(prevSegs.back(), 1, nullptr);
            SkPoint after = unitNormal(currSegs.front(), 0, nullptr);

            // Don't create any join geometry if the normals are nearly identical.
            const float cosTheta = before.dot(after);
            if (!SkScalarNearlyZero(1 - cosTheta)) {
                bool outerJoin;
                if (left) {
                    outerJoin = isClockwise(before, after);
                } else {
                    before = rotate180(before);
                    after = rotate180(after);
                    outerJoin = !isClockwise(before, after);
                }

                if (outerJoin) {
                    // Before and after have the same origin and magnitude, so before+after is the
                    // diagonal of their rhombus. Origin of this vector is the midpoint of the miter
                    // line.
                    SkPoint miterVec = before + after;

                    // Note the relationship (draw a right triangle with the miter line as its
                    // hypoteneuse):
                    //     sin(theta/2) = strokeWidth / miterLength
                    // so miterLength = strokeWidth / sin(theta/2)
                    // where miterLength is the length of the miter from outer point to inner
                    // corner. miterVec's origin is the midpoint of the miter line, so we use
                    // strokeWidth/2. Sqrt is just an application of half-angle identities.
                    const float sinHalfTheta = sqrtf(0.5 * (1 + cosTheta));
                    const float halfMiterLength = radius / sinHalfTheta;
                    // TODO: miter length limit
                    miterVec = setLength(miterVec, halfMiterLength);

                    // Outer join: connect to the miter point, and then to t=0 of next segment.
                    path->lineTo(common + miterVec);
                    path->lineTo(afterEndpt);
                } else {
                    // Connect to the miter midpoint (common path endpoint of the two segments),
                    // and then to t=0 of the next segment. This adds an interior "loop"
                    // of geometry that handles edge cases where segment lengths are shorter than
                    // the stroke width.
                    path->lineTo(common);
                    path->lineTo(afterEndpt);
                }
            }
        };

        makeJoin(true, leftRadius);
        makeJoin(false, rightRadius);
    };

    switch (fJoin) {
        case SkPaint::kMiter_Join:
            miterJoin(common, innerRadius, outerRadius, prev, curr);
            break;
        default:
            SkDebugf("Unhandled join %d\n", fJoin);
            miterJoin(common, innerRadius, outerRadius, prev, curr);
            break;
    }
}

void SkVarWidthStroker::appendPathReversed(const SkPath& path, SkPathBuilder* result) {
    SkSpan<const SkPoint> points = path.points();
    SkSpan<const SkPathVerb> verbs = path.verbs();
    const int numPoints = SkToInt(points.size());
    const int numVerbs = SkToInt(verbs.size());

    for (int i = numVerbs - 1, j = numPoints; i >= 0; i--) {
        auto verb = verbs[i];
        switch (verb) {
            case SkPathVerb::kLine: {
                j -= 1;
                SkASSERT(j >= 1);
                result->lineTo(points[j - 1]);
                break;
            }
            case SkPathVerb::kQuad: {
                j -= 1;
                SkASSERT(j >= 2);
                result->quadTo(points[j - 1], points[j - 2]);
                j -= 1;
                break;
            }
            case SkPathVerb::kMove:
                // Ignore
                break;
            default:
                SkASSERT(false);
                break;
        }
    }
}

int SkVarWidthStroker::segmentDegree(const PathSegment& seg) {
    static constexpr int lut[] = {
            -1,  // move,
            1,   // line
            2,   // quad
            -1,  // conic
            3,   // cubic
            -1   // done
    };
    const int deg = lut[static_cast<uint8_t>(seg.fVerb)];
    SkASSERT(deg > 0);
    return deg;
}

void SkVarWidthStroker::splitSegment(const PathSegment& seg,
                                     float t,
                                     PathSegment* segA,
                                     PathSegment* segB) {
    // TODO: although general, this is a pretty slow way to do this
    const int degree = segmentDegree(seg);
    ScalarBezCurve x(degree), y(degree);
    for (int i = 0; i <= degree; i++) {
        x[i] = seg.fPoints[i].fX;
        y[i] = seg.fPoints[i].fY;
    }

    ScalarBezCurve leftX(degree), rightX(degree), leftY(degree), rightY(degree);
    x.split(t, &leftX, &rightX);
    y.split(t, &leftY, &rightY);

    segA->fVerb = segB->fVerb = seg.fVerb;
    for (int i = 0; i <= degree; i++) {
        segA->fPoints[i] = {leftX[i], leftY[i]};
        segB->fPoints[i] = {rightX[i], rightY[i]};
    }
}

void SkVarWidthStroker::approximateSegment(const PathSegment& seg,
                                           const ScalarBezCurve& distFnc,
                                           PathSegment* approxQuad) {
    // This is a simple control polygon transformation.
    // From F. Yzerman. "Precise offsetting of quadratic Bezier curves". 2019.
    // TODO: detect and handle more degenerate cases (e.g. linear)
    // TODO: Tiller-Hanson works better in many cases but does not generalize well
    SkPoint tangentStart, tangentEnd;
    SkPoint offsetStart = unitNormal(seg, 0, &tangentStart);
    SkPoint offsetEnd = unitNormal(seg, 1, &tangentEnd);
    SkPoint offsetMid = offsetStart + offsetEnd;

    const float radiusStart = distFnc.eval(0);
    const float radiusMid = distFnc.eval(0.5f);
    const float radiusEnd = distFnc.eval(1);

    offsetStart = radiusStart == 0 ? SkPoint::Make(0, 0) : setLength(offsetStart, radiusStart);
    offsetMid = radiusMid == 0 ? SkPoint::Make(0, 0) : setLength(offsetMid, radiusMid);
    offsetEnd = radiusEnd == 0 ? SkPoint::Make(0, 0) : setLength(offsetEnd, radiusEnd);

    SkPoint start, mid, end;
    switch (segmentDegree(seg)) {
        case 1:
            start = seg.fPoints[0];
            end = seg.fPoints[1];
            mid = (start + end) * 0.5f;
            break;
        case 2:
            start = seg.fPoints[0];
            mid = seg.fPoints[1];
            end = seg.fPoints[2];
            break;
        case 3:
            start = seg.fPoints[0];
            mid = (seg.fPoints[1] + seg.fPoints[2]) * 0.5f;
            end = seg.fPoints[3];
            break;
        default:
            SkDebugf("Unhandled degree for segment approximation");
            SkASSERT(false);
            break;
    }

    approxQuad->fPoints[0] = start + offsetStart;
    approxQuad->fPoints[1] = mid + offsetMid;
    approxQuad->fPoints[2] = end + offsetEnd;
}

SkPoint SkVarWidthStroker::unitNormal(const PathSegment& seg, float t, SkPoint* tangentOut) {
    switch (seg.fVerb) {
        case SkPath::kLine_Verb: {
            const SkPoint tangent = setLength(seg.fPoints[1] - seg.fPoints[0], 1);
            const SkPoint normal = rotate90(tangent);
            if (tangentOut) {
                *tangentOut = tangent;
            }
            return normal;
        }
        case SkPath::kQuad_Verb: {
            SkPoint tangent;
            if (t == 0) {
                tangent = seg.fPoints[1] - seg.fPoints[0];
            } else if (t == 1) {
                tangent = seg.fPoints[2] - seg.fPoints[1];
            } else {
                tangent = ((seg.fPoints[1] - seg.fPoints[0]) * (1 - t) +
                           (seg.fPoints[2] - seg.fPoints[1]) * t) *
                          2;
            }
            if (!tangent.normalize()) {
                SkDebugf("Failed to normalize quad tangent\n");
                SkASSERT(false);
            }
            if (tangentOut) {
                *tangentOut = tangent;
            }
            return rotate90(tangent);
        }
        case SkPath::kCubic_Verb: {
            SkPoint tangent;
            SkEvalCubicAt(seg.fPoints.data(), t, nullptr, &tangent, nullptr);
            if (!tangent.normalize()) {
                SkDebugf("Failed to normalize cubic tangent\n");
                SkASSERT(false);
            }
            if (tangentOut) {
                *tangentOut = tangent;
            }
            return rotate90(tangent);
        }
        default:
            SkDebugf("Unhandled verb for unit normal %d\n", seg.fVerb);
            SkASSERT(false);
            return {};
    }
}

}  // namespace

//////////////////////////////////////////////////////////////////////////////

class VariableWidthStrokerSlide : public ClickHandlerSlide {
public:
    VariableWidthStrokerSlide()
            : fShowHidden(true)
            , fShowSkeleton(true)
            , fShowStrokePoints(false)
            , fShowUI(false)
            , fDifferentInnerFunc(false)
            , fShowErrorCurve(false) {
        resetToDefaults();

        fPtsPaint.setAntiAlias(true);
        fPtsPaint.setStrokeWidth(10);
        fPtsPaint.setStrokeCap(SkPaint::kRound_Cap);

        fStrokePointsPaint.setAntiAlias(true);
        fStrokePointsPaint.setStrokeWidth(5);
        fStrokePointsPaint.setStrokeCap(SkPaint::kRound_Cap);

        fStrokePaint.setAntiAlias(true);
        fStrokePaint.setStyle(SkPaint::kStroke_Style);
        fStrokePaint.setColor(0x80FF0000);

        fNewFillPaint.setAntiAlias(true);
        fNewFillPaint.setColor(0x8000FF00);

        fHiddenPaint.setAntiAlias(true);
        fHiddenPaint.setStyle(SkPaint::kStroke_Style);
        fHiddenPaint.setColor(0xFF0000FF);

        fSkeletonPaint.setAntiAlias(true);
        fSkeletonPaint.setStyle(SkPaint::kStroke_Style);
        fSkeletonPaint.setColor(SK_ColorRED);

        fName = "VariableWidthStroker";
    }

    void load(SkScalar w, SkScalar h) override { fWinSize = {w, h}; }

    void resize(SkScalar w, SkScalar h) override { fWinSize = {w, h}; }

    bool onChar(SkUnichar uni) override {
        switch (uni) {
            case '0':
                this->toggle(fShowUI);
                return true;
            case '1':
                this->toggle(fShowSkeleton);
                return true;
            case '2':
                this->toggle(fShowHidden);
                return true;
            case '3':
                this->toggle(fShowStrokePoints);
                return true;
            case '4':
                this->toggle(fShowErrorCurve);
                return true;
            case '5':
                this->toggle(fLengthMetric);
                return true;
            case 'x':
                resetToDefaults();
                return true;
            case '-':
                fWidth -= 5;
                return true;
            case '=':
                fWidth += 5;
                return true;
            default:
                break;
        }
        return false;
    }

    void draw(SkCanvas* canvas) override {
        canvas->drawColor(0xFFEEEEEE);

        SkPath path = this->makePath();

        fStrokePaint.setStrokeWidth(fWidth);

        // Elber-Cohen stroker result
        ScalarBezCurve distFnc = makeDistFnc(fDistFncs, fWidth);
        ScalarBezCurve distFncInner =
                fDifferentInnerFunc ? makeDistFnc(fDistFncsInner, fWidth) : distFnc;
        SkVarWidthStroker stroker;
        SkPath fillPath =
                stroker.getFillPath(path, fStrokePaint, distFnc, distFncInner, fLengthMetric);
        fillPath.setFillType(SkPathFillType::kWinding);
        canvas->drawPath(fillPath, fNewFillPaint);

        if (fShowHidden) {
            canvas->drawPath(fillPath, fHiddenPaint);
        }

        if (fShowSkeleton) {
            canvas->drawPath(path, fSkeletonPaint);
            canvas->drawPoints(SkCanvas::kPoints_PointMode, fPathPts, fPtsPaint);
        }

        if (fShowStrokePoints) {
            drawStrokePoints(canvas, fillPath);
        }

        if (fShowUI) {
            drawUI();
        }

        if (fShowErrorCurve && viz::outerErr != nullptr) {
            SkPaint firstApproxPaint;
            firstApproxPaint.setStrokeWidth(4);
            firstApproxPaint.setStyle(SkPaint::kStroke_Style);
            firstApproxPaint.setColor(SK_ColorRED);
            canvas->drawPath(viz::outerFirstApprox, firstApproxPaint);
            drawErrorCurve(canvas, *viz::outerErr);
        }
    }

protected:
    Click* onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey modi) override {
        const SkScalar tol = 4;
        const SkRect r = SkRect::MakeXYWH(x - tol, y - tol, tol * 2, tol * 2);
        for (size_t i = 0; i < fPathPts.size(); ++i) {
            if (r.intersects(SkRect::MakeXYWH(fPathPts[i].fX, fPathPts[i].fY, 1, 1))) {
                return new Click([this, i](Click* c) {
                    fPathPts[i] = c->fCurr;
                    return true;
                });
            }
        }
        return nullptr;
    }

    bool onClick(ClickHandlerSlide::Click *) override { return false; }

private:
    /** Selectable menu item for choosing distance functions */
    struct DistFncMenuItem {
        std::string fName;
        int fDegree;
        bool fSelected;
        std::vector<float> fWeights;

        DistFncMenuItem(const std::string& name, int degree, bool selected) {
            fName = name;
            fDegree = degree;
            fSelected = selected;
            fWeights.resize(degree + 1, 1.0f);
        }
    };

    void toggle(bool& value) { value = !value; }
    void toggle(SkVarWidthStroker::LengthMetric& value) {
        value = value == SkVarWidthStroker::LengthMetric::kPathLength
                        ? SkVarWidthStroker::LengthMetric::kNumSegments
                        : SkVarWidthStroker::LengthMetric::kPathLength;
    }

    void resetToDefaults() {
        fPathPts[0] = {300, 400};
        fPathPts[1] = {500, 400};
        fPathPts[2] = {700, 400};
        fPathPts[3] = {900, 400};
        fPathPts[4] = {1100, 400};

        fWidth = 175;

        fLengthMetric = SkVarWidthStroker::LengthMetric::kPathLength;
        fDistFncs = fDefaultsDistFncs;
        fDistFncsInner = fDefaultsDistFncs;
    }

    SkPath makePath() {
        return SkPathBuilder()
               .moveTo(fPathPts[0])
               .quadTo(fPathPts[1], fPathPts[2])
               .quadTo(fPathPts[3], fPathPts[4])
               .detach();
    }

    static ScalarBezCurve makeDistFnc(const std::vector<DistFncMenuItem>& fncs, float strokeWidth) {
        const float radius = strokeWidth / 2;
        for (const auto& df : fncs) {
            if (df.fSelected) {
                return ScalarBezCurve::Mul(ScalarBezCurve(df.fDegree, df.fWeights), radius);
            }
        }
        SkASSERT(false);
        return ScalarBezCurve(0, {radius});
    }

    void drawStrokePoints(SkCanvas* canvas, const SkPath& fillPath) {
        SkPath::Iter it(fillPath, false);
        SkPoint points[4];
        SkPath::Verb verb;
        std::vector<SkPoint> pointsVec, ctrlPts;
        while ((verb = it.next(&points[0])) != SkPath::kDone_Verb) {
            switch (verb) {
                case SkPath::kLine_Verb:
                    pointsVec.push_back(points[1]);
                    break;
                case SkPath::kQuad_Verb:
                    ctrlPts.push_back(points[1]);
                    pointsVec.push_back(points[2]);
                    break;
                case SkPath::kMove_Verb:
                    pointsVec.push_back(points[0]);
                    break;
                case SkPath::kClose_Verb:
                    break;
                default:
                    SkDebugf("Unhandled path verb %d for stroke points\n", verb);
                    SkASSERT(false);
                    break;
            }
        }

        canvas->drawPoints(SkCanvas::kPoints_PointMode, pointsVec, fStrokePointsPaint);
        fStrokePointsPaint.setColor(SK_ColorBLUE);
        fStrokePointsPaint.setStrokeWidth(3);
        canvas->drawPoints(SkCanvas::kPoints_PointMode, ctrlPts, fStrokePointsPaint);
        fStrokePointsPaint.setColor(SK_ColorBLACK);
        fStrokePointsPaint.setStrokeWidth(5);
    }

    void drawErrorCurve(SkCanvas* canvas, const ScalarBezCurve& E) {
        const float winW = fWinSize.width() * 0.75f, winH = fWinSize.height() * 0.25f;
        const float padding = 25;
        const SkRect box = SkRect::MakeXYWH(padding, fWinSize.height() - winH - padding,
                                            winW - 2 * padding, winH);
        constexpr int nsegs = 100;
        constexpr float dt = 1.0f / nsegs;
        constexpr float dx = 10.0f;
        const int deg = E.degree();
        SkPathBuilder builder;
        for (int i = 0; i < nsegs; i++) {
            const float tmin = i * dt, tmax = (i + 1) * dt;
            ScalarBezCurve left(deg), right(deg);
            E.split(tmax, &left, &right);
            const float tRel = tmin / tmax;
            ScalarBezCurve rl(deg), rr(deg);
            left.split(tRel, &rl, &rr);

            const float x = i * dx;
            if (i == 0) {
                builder.moveTo(x, -rr[0]);
            }
            builder.lineTo(x + dx, -rr[deg]);
        }
        SkPath path = builder.detach();

        SkPaint paint;
        paint.setStyle(SkPaint::kStroke_Style);
        paint.setAntiAlias(true);
        paint.setStrokeWidth(0);
        paint.setColor(SK_ColorRED);
        const SkRect pathBounds = path.computeTightBounds();
        constexpr float yAxisMax = 8000;
        const float sx = box.width() / pathBounds.width();
        const float sy = box.height() / (2 * yAxisMax);
        canvas->save();
        canvas->translate(box.left(), box.top() + box.height() / 2);
        canvas->scale(sx, sy);
        canvas->drawPath(path, paint);

        SkPath axes = SkPathBuilder()
                      .moveTo(0, 0)
                      .lineTo(pathBounds.width(), 0)
                      .moveTo(0, -yAxisMax)
                      .lineTo(0, yAxisMax)
                      .detach();
        paint.setColor(SK_ColorBLACK);
        paint.setAntiAlias(false);
        canvas->drawPath(axes, paint);

        canvas->restore();
    }

    void drawUI() {
        static constexpr auto kUIOpacity = 0.35f;
        static constexpr float kUIWidth = 200.0f, kUIHeight = 400.0f;
        ImGui::SetNextWindowBgAlpha(kUIOpacity);
        if (ImGui::Begin("E-C Controls", nullptr,
                         ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize |
                                 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
                                 ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) {
            const SkRect uiArea = SkRect::MakeXYWH(10, 10, kUIWidth, kUIHeight);
            ImGui::SetWindowPos(ImVec2(uiArea.x(), uiArea.y()));
            ImGui::SetWindowSize(ImVec2(uiArea.width(), uiArea.height()));

            const auto drawControls = [](std::vector<DistFncMenuItem>& distFncs,
                                         const std::string& menuPfx,
                                         const std::string& ptPfx) {
                std::string degreeMenuLabel = menuPfx + ": ";
                for (const auto& df : distFncs) {
                    if (df.fSelected) {
                        degreeMenuLabel += df.fName;
                        break;
                    }
                }
                if (ImGui::BeginMenu(degreeMenuLabel.c_str())) {
                    for (size_t i = 0; i < distFncs.size(); i++) {
                        if (ImGui::MenuItem(distFncs[i].fName.c_str(), nullptr,
                                            distFncs[i].fSelected)) {
                            for (size_t j = 0; j < distFncs.size(); j++) {
                                distFncs[j].fSelected = j == i;
                            }
                        }
                    }
                    ImGui::EndMenu();
                }

                for (auto& df : distFncs) {
                    if (df.fSelected) {
                        for (int i = 0; i <= df.fDegree; i++) {
                            const std::string label = ptPfx + std::to_string(i);
                            ImGui::SliderFloat(label.c_str(), &(df.fWeights[i]), 0, 1);
                        }
                    }
                }
            };

            const std::array<std::pair<std::string, SkVarWidthStroker::LengthMetric>, 2> metrics = {
                    std::make_pair("% path length", SkVarWidthStroker::LengthMetric::kPathLength),
                    std::make_pair("% segment count",
                                   SkVarWidthStroker::LengthMetric::kNumSegments),
            };
            if (ImGui::BeginMenu("Interpolation metric:")) {
                for (const auto& metric : metrics) {
                    if (ImGui::MenuItem(metric.first.c_str(), nullptr,
                                        fLengthMetric == metric.second)) {
                        fLengthMetric = metric.second;
                    }
                }
                ImGui::EndMenu();
            }

            drawControls(fDistFncs, "Degree", "P");

            if (ImGui::CollapsingHeader("Inner stroke", true)) {
                fDifferentInnerFunc = true;
                drawControls(fDistFncsInner, "Degree (inner)", "Q");
            } else {
                fDifferentInnerFunc = false;
            }
        }
        ImGui::End();
    }

    bool fShowHidden, fShowSkeleton, fShowStrokePoints, fShowUI, fDifferentInnerFunc,
            fShowErrorCurve;
    float fWidth = 175;
    SkPaint fPtsPaint, fStrokePaint, fNewFillPaint, fHiddenPaint, fSkeletonPaint,
            fStrokePointsPaint;
    inline static constexpr int kNPts = 5;
    std::array<SkPoint, kNPts> fPathPts;
    SkSize fWinSize;
    SkVarWidthStroker::LengthMetric fLengthMetric;
    const std::vector<DistFncMenuItem> fDefaultsDistFncs = {
            DistFncMenuItem("Linear", 1, true), DistFncMenuItem("Quadratic", 2, false),
            DistFncMenuItem("Cubic", 3, false), DistFncMenuItem("One Louder (11)", 11, false),
            DistFncMenuItem("30?!", 30, false)};
    std::vector<DistFncMenuItem> fDistFncs = fDefaultsDistFncs;
    std::vector<DistFncMenuItem> fDistFncsInner = fDefaultsDistFncs;
};

DEF_SLIDE(return new VariableWidthStrokerSlide;)
