RawPath
diff --git a/include/rive/math/raw_path.hpp b/include/rive/math/raw_path.hpp new file mode 100644 index 0000000..2ac9709 --- /dev/null +++ b/include/rive/math/raw_path.hpp
@@ -0,0 +1,83 @@ +/* + * Copyright 2022 Rive + */ + +#ifndef _RIVE_RAW_PATH_HPP_ +#define _RIVE_RAW_PATH_HPP_ + +#include "rive/span.hpp" +#include "rive/math/aabb.hpp" +#include "rive/math/mat2d.hpp" +#include "rive/math/vec2d.hpp" + +#include <cmath> +#include <stdio.h> +#include <cstdint> +#include <vector> + +namespace rive { + +enum class PathDirection { + cw, + ccw, + // aliases + clockwise = cw, + counterclockwise = ccw, +}; + +enum class PathVerb : uint8_t { + move, + line, + quad, + conic_unused, // so we match skia's order + cubic, + close, +}; + +class RawPath { +public: + std::vector<Vec2D> m_Points; + std::vector<PathVerb> m_Verbs; + + RawPath() {} + ~RawPath() {} + + bool empty() const { return m_Points.empty(); } + AABB bounds() const; + + void move(Vec2D); + void line(Vec2D); + void quad(Vec2D, Vec2D); + void cubic(Vec2D, Vec2D, Vec2D); + void close(); + + RawPath transform(const Mat2D&) const; + void transformInPlace(const Mat2D&); + + Span<const Vec2D> points() const { return toSpan(m_Points); } + Span<Vec2D> points() { return toSpan(m_Points); } + + Span<const PathVerb> verbs() const { return toSpan(m_Verbs); } + Span<PathVerb> verbs() { return toSpan(m_Verbs); } + + // Syntactic sugar for x,y -vs- vec2d + + void moveTo(float x, float y) { move({x, y}); } + void lineTo(float x, float y) { line({x, y}); } + void quadTo(float x, float y, float x1, float y1) { + quad({x, y}, {x1, y1}); + } + void cubicTo(float x, float y, float x1, float y1, float x2, float y2) { + cubic({x, y}, {x1, y1}, {x2, y2}); + } + + // Helpers for adding new contours + + void addRect(const AABB&, PathDirection = PathDirection::cw); + void addOval(const AABB&, PathDirection = PathDirection::cw); + void addPoly(Span<const Vec2D>, bool isClosed); +}; + +} // namespace rive + +#endif
diff --git a/include/rive/renderer.hpp b/include/rive/renderer.hpp index adeff88..8c9a3f6 100644 --- a/include/rive/renderer.hpp +++ b/include/rive/renderer.hpp
@@ -8,6 +8,7 @@ #include "rive/span.hpp" #include "rive/math/aabb.hpp" #include "rive/math/mat2d.hpp" +#include "rive/math/raw_path.hpp" #include "rive/shapes/paint/blend_mode.hpp" #include "rive/shapes/paint/stroke_cap.hpp" #include "rive/shapes/paint/stroke_join.hpp" @@ -144,6 +145,11 @@ const AABB& content); }; + // Returns a full-formed RenderPath -- can be treated as immutable + extern RenderPath* makeRenderPath(Span<const Vec2D> points, + Span<const uint8_t> verbs, + FillRule); + extern RenderPath* makeRenderPath(); extern RenderPaint* makeRenderPaint(); extern RenderImage* makeRenderImage();
diff --git a/skia/renderer/include/skia_renderer.hpp b/skia/renderer/include/skia_renderer.hpp index d7af5f7..0f63a40 100644 --- a/skia/renderer/include/skia_renderer.hpp +++ b/skia/renderer/include/skia_renderer.hpp
@@ -14,6 +14,9 @@ SkPath m_Path; public: + SkiaRenderPath() {} + SkiaRenderPath(SkPath&& path) : m_Path(std::move(path)) {} + const SkPath& path() const { return m_Path; } void reset() override; void addRenderPath(RenderPath* path, const Mat2D& transform) override;
diff --git a/skia/renderer/include/to_skia.hpp b/skia/renderer/include/to_skia.hpp index 6420106..28f3aab 100644 --- a/skia/renderer/include/to_skia.hpp +++ b/skia/renderer/include/to_skia.hpp
@@ -32,6 +32,15 @@ return SkTileMode::kClamp; } + static SkPathFillType convert(FillRule value) { + switch (value) { + case FillRule::evenOdd: return SkPathFillType::kEvenOdd; + case FillRule::nonZero: return SkPathFillType::kWinding; + } + assert(false); + return SkPathFillType::kWinding; + } + static SkPaint::Cap convert(rive::StrokeCap cap) { switch (cap) { case rive::StrokeCap::butt:
diff --git a/skia/renderer/src/skia_renderer.cpp b/skia/renderer/src/skia_renderer.cpp index 4a8572f..b635126 100644 --- a/skia/renderer/src/skia_renderer.cpp +++ b/skia/renderer/src/skia_renderer.cpp
@@ -54,14 +54,7 @@ }; void SkiaRenderPath::fillRule(FillRule value) { - switch (value) { - case FillRule::evenOdd: - m_Path.setFillType(SkPathFillType::kEvenOdd); - break; - case FillRule::nonZero: - m_Path.setFillType(SkPathFillType::kWinding); - break; - } + m_Path.setFillType(ToSkia::convert(value)); } void SkiaRenderPath::reset() { m_Path.reset(); } @@ -237,6 +230,14 @@ return make_buffer(data); } + RenderPath* makeRenderPath(Span<const Vec2D> points, Span<const uint8_t> verbs, + FillRule fillrule) { + return new SkiaRenderPath(SkPath::Make((const SkPoint*)points.data(), points.size(), + verbs.data(), verbs.size(), + nullptr, 0, // conics + ToSkia::convert(fillrule), false)); + } + RenderPath* makeRenderPath() { return new SkiaRenderPath(); } RenderPaint* makeRenderPaint() { return new SkiaRenderPaint(); } RenderImage* makeRenderImage() { return new SkiaRenderImage(); }
diff --git a/src/math/raw_path.cpp b/src/math/raw_path.cpp new file mode 100644 index 0000000..775b94b --- /dev/null +++ b/src/math/raw_path.cpp
@@ -0,0 +1,169 @@ +/* + * Copyright 2022 Rive + */ + +#include "rive/math/raw_path.hpp" +#include <cmath> + +using namespace rive; + +AABB RawPath::bounds() const { + if (this->empty()) { + return {0, 0, 0, 0}; + } + + float l, t, r, b; + l = r = m_Points[0].x(); + t = b = m_Points[0].y(); + for (size_t i = 1; i < m_Points.size(); ++i) { + const float x = m_Points[i].x(); + const float y = m_Points[i].y(); + l = std::min(l, x); + r = std::max(r, x); + t = std::min(t, y); + b = std::max(b, y); + } + return {l, t, r, b}; +} + +void RawPath::move(Vec2D a) { + const auto n = m_Verbs.size(); + if (n > 0 && m_Verbs[n-1] == PathVerb::move) { + m_Points[n-1] = a; // replace previous move position + } else { + m_Points.push_back(a); + m_Verbs.push_back(PathVerb::move); + } +} + +void RawPath::line(Vec2D a) { + m_Points.push_back(a); + m_Verbs.push_back(PathVerb::line); +} + +void RawPath::quad(Vec2D a, Vec2D b) { + m_Points.push_back(a); + m_Points.push_back(b); + m_Verbs.push_back(PathVerb::quad); +} + +void RawPath::cubic(Vec2D a, Vec2D b, Vec2D c) { + m_Points.push_back(a); + m_Points.push_back(b); + m_Points.push_back(c); + m_Verbs.push_back(PathVerb::cubic); +} + +void RawPath::close() { + const auto n = m_Verbs.size(); + if (n > 0 && m_Verbs[n-1] != PathVerb::close) { + m_Verbs.push_back(PathVerb::close); + } +} + +RawPath RawPath::transform(const Mat2D& m) const { + RawPath path; + + path.m_Verbs = m_Verbs; + + path.m_Points.resize(m_Points.size()); + for (size_t i = 0; i < m_Points.size(); ++i) { + const float x = m_Points[i].x(); + const float y = m_Points[i].y(); + path.m_Points[i] = { + m[0] * x + m[2] * y + m[4], + m[1] * x + m[3] * y + m[5], + }; + } + return path; +} + +void RawPath::transformInPlace(const Mat2D& m) { + for (auto& p : m_Points) { + const float x = p.x(); + const float y = p.y(); + p = { + m[0] * x + m[2] * y + m[4], + m[1] * x + m[3] * y + m[5], + }; + } +} + +void RawPath::addRect(const AABB& r, PathDirection dir) { + // We manually close the rectangle, in case we want to stroke + // this path. We also call close() so we get proper joins + // (and not caps). + + m_Points.reserve(5); + m_Verbs.reserve(6); + + moveTo(r.left(), r.top()); + if (dir == PathDirection::clockwise) { + lineTo(r.right(), r.top()); + lineTo(r.right(), r.bottom()); + lineTo(r.left(), r.bottom()); + } else { + lineTo(r.left(), r.bottom()); + lineTo(r.right(), r.bottom()); + lineTo(r.right(), r.top()); + } + close(); +} + +void RawPath::addOval(const AABB& r, PathDirection dir) { + // see https://spencermortensen.com/articles/bezier-circle/ + constexpr float C = 0.5519150244935105707435627f; + // precompute clockwise unit circle, starting and ending at {1, 0} + constexpr rive::Vec2D unit[] = { + { 1, 0}, { 1, C}, { C, 1}, // quadrant 1 ( 4:30) + { 0, 1}, {-C, 1}, {-1, C}, // quadrant 2 ( 7:30) + {-1, 0}, {-1, -C}, {-C, -1}, // quadrant 3 (10:30) + { 0, -1}, { C, -1}, { 1, -C}, // quadrant 4 ( 1:30) + { 1, 0}, + }; + + const auto center = r.center(); + const float dx = center.x(); + const float dy = center.y(); + const float sx = r.width() * 0.5f; + const float sy = r.height() * 0.5f; + + auto map = [dx, dy, sx, sy](rive::Vec2D p) { + return rive::Vec2D(p.x() * sx + dx, p.y() * sy + dy); + }; + + m_Points.reserve(13); + m_Verbs.reserve(6); + + if (dir == PathDirection::clockwise) { + move(map(unit[0])); + for (int i = 1; i <= 12; i += 3) { + cubic(map(unit[i+0]), map(unit[i+1]), map(unit[i+2])); + } + } else { + move(map(unit[12])); + for (int i = 11; i >= 0; i -= 3) { + cubic(map(unit[i-0]), map(unit[i-1]), map(unit[i-2])); + } + } + close(); +} + +void RawPath::addPoly(Span<const Vec2D> span, bool isClosed) { + if (span.size() == 0) { + return; + } + + // should we permit must moveTo() or just moveTo()/close() ? + + m_Points.reserve(span.size() + isClosed); + m_Verbs.reserve(span.size() + isClosed); + + move(span[0]); + for (size_t i = 1; i < span.size(); ++i) { + line(span[i]); + } + if (isClosed) { + close(); + } +}
diff --git a/test/raw_path_test.cpp b/test/raw_path_test.cpp new file mode 100644 index 0000000..dc3fa85 --- /dev/null +++ b/test/raw_path_test.cpp
@@ -0,0 +1,74 @@ +/* + * Copyright 2022 Rive + */ + +#include <rive/math/aabb.hpp> +#include <rive/math/raw_path.hpp> +#include "no_op_renderer.hpp" + +#include <catch.hpp> +#include <cstdio> + +using namespace rive; + +TEST_CASE("rawpath-basics", "[rawpath]") { + RawPath path; + + REQUIRE(path.empty()); + REQUIRE(path.bounds() == AABB{0, 0, 0, 0}); + + path.move({1, 2}); + REQUIRE(!path.empty()); + REQUIRE(path.bounds() == AABB{1, 2, 1, 2}); + + path = RawPath(); + REQUIRE(path.empty()); + REQUIRE(path.bounds() == AABB{0, 0, 0, 0}); + + path.move({1, -2}); + path.line({3, 4}); + path.line({-1, 5}); + REQUIRE(!path.empty()); + REQUIRE(path.bounds() == AABB{-1, -2, 3, 5}); +} + +TEST_CASE("rawpath-add-helpers", "[rawpath]") { + RawPath path; + + path.addRect({1, 1, 5, 6}); + REQUIRE(!path.empty()); + REQUIRE(path.bounds() == AABB{1, 1, 5, 6}); + REQUIRE(path.points().size() == 4); + REQUIRE(path.verbs().size() == 5); // move, line, line, line, close + + path = RawPath(); + path.addOval({0, 0, 3, 6}); + REQUIRE(!path.empty()); + REQUIRE(path.bounds() == AABB{0, 0, 3, 6}); + REQUIRE(path.points().size() == 13); + REQUIRE(path.verbs().size() == 6); // move, cubic, cubic, cubic, cubic, close + + const Vec2D pts[] = { + {1, 2}, {4, 5}, {3, 2}, {100, -100}, + }; + constexpr auto size = sizeof(pts) / sizeof(pts[0]); + + for (auto isClosed : {false, true}) { + path = RawPath(); + path.addPoly({pts, size}, isClosed); + REQUIRE(path.bounds() == AABB{1, -100, 100, 5}); + REQUIRE(path.points().size() == size); + REQUIRE(path.verbs().size() == size + isClosed); + + for (size_t i = 0; i < size; ++i) { + REQUIRE(path.points()[i] == pts[i]); + } + REQUIRE(path.verbs()[0] == PathVerb::move); + for (size_t i = 1; i < size; ++i) { + REQUIRE(path.verbs()[i] == PathVerb::line); + } + if (isClosed) { + REQUIRE(path.verbs()[size] == PathVerb::close); + } + } +}