blob: 55cbd7f53ad5a0fa53323c29409bbd648b6adba5 [file] [log] [blame] [edit]
/*
* 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))