| /* |
| * Copyright 2018 Google Inc. |
| * Copyright 2022 Rive |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "gm.hpp" |
| #include "gmutils.hpp" |
| #include "rive/math/bezier_utils.hpp" |
| #include "rive/renderer.hpp" |
| #include "common/rand.hpp" |
| |
| using namespace rivegm; |
| using namespace rive; |
| |
| static constexpr float kStrokeWidth = 30; |
| static constexpr int kCellSize = 200; |
| static constexpr int kNumCols = 5; |
| static constexpr int kNumRows = 5; |
| static constexpr int kTestWidth = kNumCols * kCellSize; |
| static constexpr int kTestHeight = kNumRows * kCellSize; |
| |
| enum class CellFillMode |
| { |
| kStretch, |
| kCenter |
| }; |
| |
| struct TrickyCubic |
| { |
| Vec2D fPoints[4]; |
| int fNumPts; |
| CellFillMode fFillMode; |
| float fScale = 1; |
| }; |
| |
| // This is a compilation of cubics that have given strokers grief. Feel free to |
| // add more. |
| static const TrickyCubic kTrickyCubics[] = { |
| {{{122, 737}, {348, 553}, {403, 761}, {400, 760}}, |
| 4, |
| CellFillMode::kStretch}, |
| {{{244, 520}, {244, 518}, {1141, 634}, {394, 688}}, |
| 4, |
| CellFillMode::kStretch}, |
| {{{550, 194}, {138, 130}, {1035, 246}, {288, 300}}, |
| 4, |
| CellFillMode::kStretch}, |
| {{{226, 733}, {556, 779}, {-43, 471}, {348, 683}}, |
| 4, |
| CellFillMode::kStretch}, |
| {{{268, 204}, {492, 304}, {352, 23}, {433, 412}}, |
| 4, |
| CellFillMode::kStretch}, |
| {{{172, 480}, {396, 580}, {256, 299}, {338, 677}}, |
| 4, |
| CellFillMode::kStretch}, |
| {{{731, 340}, {318, 252}, {1026, -64}, {367, 265}}, |
| 4, |
| CellFillMode::kStretch}, |
| {{{475, 708}, {62, 620}, {770, 304}, {220, 659}}, |
| 4, |
| CellFillMode::kStretch}, |
| {{{0, 0}, {128, 128}, {128, 0}, {0, 128}}, |
| 4, |
| CellFillMode::kCenter}, // Perfect cusp |
| {{{0, .01f}, {128, 127.999f}, {128, .01f}, {0, 127.99f}}, |
| 4, |
| CellFillMode::kCenter}, // Near-cusp |
| {{{0, -.01f}, {128, 128.001f}, {128, -.01f}, {0, 128.001f}}, |
| 4, |
| CellFillMode::kCenter}, // Near-cusp |
| {{{0, 0}, {0, -10}, {0, -10}, {0, 10}}, |
| 4, |
| CellFillMode::kCenter, |
| 1.098283f}, // Flat line with 180 |
| {{{10, 0}, {0, 0}, {20, 0}, {10, 0}}, |
| 4, |
| CellFillMode::kStretch}, // Flat line with 2 180s |
| {{{39, -39}, {40, -40}, {40, -40}, {0, 0}}, |
| 4, |
| CellFillMode::kStretch}, // Flat diagonal with 180 |
| {{{40, 40}, {0, 0}, {200, 200}, {0, 0}}, |
| 4, |
| CellFillMode::kStretch}, // Diag w/ an internal 180 |
| {{{0, 0}, {1e-2f, 0}, {-1e-2f, 0}, {0, 0}}, |
| 4, |
| CellFillMode::kCenter}, // Circle |
| {{{400.75f, 100.05f}, |
| {400.75f, 100.05f}, |
| {100.05f, 300.95f}, |
| {100.05f, 300.95f}}, |
| 4, |
| CellFillMode::kStretch}, // Flat line with no turns |
| {{{0.5f, 0}, {0, 0}, {20, 0}, {10, 0}}, |
| 4, |
| CellFillMode::kStretch}, // Flat line with 2 180s |
| {{{10, 0}, {0, 0}, {10, 0}, {10, 0}}, |
| 4, |
| CellFillMode::kStretch}, // Flat line with a 180 |
| {{{1, 1}, {2, 1}, {1, 1}, {1, std::numeric_limits<float>::quiet_NaN()}}, |
| 3, |
| CellFillMode::kStretch}, // Flat QUAD with a cusp |
| {{{1, 1}, |
| {100, 1}, |
| {25, 1}, |
| {.3f, std::numeric_limits<float>::quiet_NaN()}}, |
| 3, |
| CellFillMode::kStretch}, // Flat CONIC with a cusp |
| {{{1, 1}, |
| {70, 1}, |
| {25, 1}, |
| {1.5f, std::numeric_limits<float>::quiet_NaN()}}, |
| 3, |
| CellFillMode::kStretch}, // Flat CONIC with a cusp |
| }; |
| |
| static AABB calc_tight_cubic_bounds(const Vec2D P[4], int depth = 5) |
| { |
| if (0 == depth) |
| { |
| AABB bounds; |
| bounds.minX = |
| std::min(std::min(P[0].x, P[1].x), std::min(P[2].x, P[3].x)); |
| bounds.minY = |
| std::min(std::min(P[0].y, P[1].y), std::min(P[2].y, P[3].y)); |
| bounds.maxX = |
| std::max(std::max(P[0].x, P[1].x), std::max(P[2].x, P[3].x)); |
| bounds.maxY = |
| std::max(std::max(P[0].y, P[1].y), std::max(P[2].y, P[3].y)); |
| return bounds; |
| } |
| |
| Vec2D chopped[7]; |
| math::chop_cubic_at(P, chopped, .5f); |
| AABB bounds0 = calc_tight_cubic_bounds(chopped, depth - 1); |
| AABB bounds1 = calc_tight_cubic_bounds(chopped + 3, depth - 1); |
| if (bounds1.isEmptyOrNaN()) |
| { |
| return bounds0; |
| } |
| if (bounds0.isEmptyOrNaN()) |
| { |
| return bounds1; |
| } |
| bounds0.expand(bounds1); |
| return bounds0; |
| } |
| |
| static Vec2D lerp(const Vec2D& a, const Vec2D& b, float T) |
| { |
| assert(1 != T); // The below does not guarantee lerp(a, b, 1) === b. |
| return (b - a) * T + a; |
| } |
| |
| enum class FillMode |
| { |
| kCenter, |
| kScale |
| }; |
| |
| class TrickyCubicsGM : public GM |
| { |
| public: |
| TrickyCubicsGM(StrokeCap cap, StrokeJoin join, float feather) : |
| GM(kTestWidth, kTestHeight), |
| m_Cap(cap), |
| m_Join(join), |
| m_feather(feather) |
| {} |
| |
| ColorInt clearColor() const override { return 0xff000000; } |
| |
| void onDraw(rive::Renderer* renderer) override |
| { |
| Rand rand; |
| |
| Paint strokePaint; |
| strokePaint->style(RenderPaintStyle::stroke); |
| strokePaint->thickness(kStrokeWidth); |
| strokePaint->cap(m_Cap); |
| strokePaint->join(m_Join); |
| |
| for (size_t i = 0; i < std::size(kTrickyCubics); ++i) |
| { |
| auto [originalPts, numPts, fillMode, scale] = kTrickyCubics[i]; |
| |
| assert(numPts <= 4); |
| Vec2D p[4]; |
| memcpy(p, originalPts, sizeof(Vec2D) * numPts); |
| for (int j = 0; j < numPts; ++j) |
| { |
| p[j] *= scale; |
| } |
| |
| auto cellRect = AABB::fromLTWH((i % kNumCols) * kCellSize, |
| (int)(i / kNumCols) * kCellSize, |
| kCellSize, |
| kCellSize); |
| |
| AABB strokeBounds; |
| if (numPts == 4) |
| { |
| strokeBounds = calc_tight_cubic_bounds(p); |
| } |
| else |
| { |
| assert(numPts == 3); |
| Vec2D asCubic[4] = {p[0], |
| lerp(p[0], p[1], 2 / 3.f), |
| lerp(p[1], p[2], 1 / 3.f), |
| p[2]}; |
| strokeBounds = calc_tight_cubic_bounds(asCubic); |
| } |
| strokeBounds = strokeBounds.inset(-kStrokeWidth, -kStrokeWidth); |
| |
| Mat2D matrix; |
| if (fillMode == CellFillMode::kStretch) |
| { |
| matrix = rive::computeAlignment(Fit::contain, |
| Alignment::center, |
| cellRect, |
| strokeBounds); |
| } |
| else |
| { |
| matrix = Mat2D::fromTranslate( |
| cellRect.minX + kStrokeWidth + |
| (cellRect.width() - strokeBounds.width()) / 2, |
| cellRect.minY + kStrokeWidth + |
| (cellRect.height() - strokeBounds.height()) / 2); |
| } |
| |
| renderer->save(); |
| renderer->transform(matrix); |
| strokePaint->thickness(kStrokeWidth / matrix.findMaxScale()); |
| strokePaint->color(rand.u32() | 0xff808080); |
| Path path; |
| path->moveTo(p[0].x, p[0].y); |
| if (numPts == 4) |
| { |
| path->cubicTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y); |
| } |
| else |
| { |
| assert(numPts == 3); |
| Vec2D c0 = p[0] + (p[1] - p[0]) * (2 / 3.f); |
| Vec2D c1 = p[2] + (p[1] - p[2]) * (2 / 3.f); |
| path->cubicTo(c0.x, c0.y, c1.x, c1.y, p[2].x, p[2].y); |
| } |
| if (m_feather != 0) |
| { |
| strokePaint->feather(m_feather / matrix.findMaxScale()); |
| } |
| renderer->drawPath(path, strokePaint); |
| renderer->restore(); |
| } |
| } |
| |
| private: |
| StrokeCap m_Cap; |
| StrokeJoin m_Join; |
| float m_feather; |
| }; |
| |
| GMREGISTER(trickycubicstrokes, |
| return new TrickyCubicsGM(StrokeCap::butt, StrokeJoin::miter, 0)) |
| GMREGISTER(trickycubicstrokes_roundcaps, |
| return new TrickyCubicsGM(StrokeCap::round, StrokeJoin::round, 0)) |
| // Feathers ignore cap and join. |
| GMREGISTER(trickycubicstrokes_feather, |
| return new TrickyCubicsGM(StrokeCap::butt, StrokeJoin::miter, 20)) |