text on path experiment Diffs= 946d84479 text on path experiment
diff --git a/.rive_head b/.rive_head index c56723d..f3589ac 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -db49cdfaa1ec3a56ad7c5d26dd574e67498a96ae +946d84479e404dd8405dd7d91789b95c52cd40bc
diff --git a/include/rive/math/path_types.hpp b/include/rive/math/path_types.hpp index d6d1711..b3591d7 100644 --- a/include/rive/math/path_types.hpp +++ b/include/rive/math/path_types.hpp
@@ -23,13 +23,16 @@ }; enum class PathVerb : uint8_t { - move, - line, - quad, - conic_unused, // so we match skia's order - cubic, - close, + // These deliberately match Skia's values + move = 0, + line = 1, + quad = 2, + // conic + cubic = 4, + close = 5, }; + int path_verb_to_point_count(PathVerb); + } // namespace rive #endif
diff --git a/include/rive/math/raw_path.hpp b/include/rive/math/raw_path.hpp index 046f065..56e141d 100644 --- a/include/rive/math/raw_path.hpp +++ b/include/rive/math/raw_path.hpp
@@ -37,6 +37,9 @@ RawPath transform(const Mat2D&) const; void transformInPlace(const Mat2D&); + RawPath operator*(const Mat2D& mat) const { + return this->transform(mat); + } Span<const Vec2D> points() const { return toSpan(m_Points); } Span<Vec2D> points() { return toSpan(m_Points); } @@ -63,6 +66,27 @@ void addRect(const AABB&, PathDirection = PathDirection::cw); void addOval(const AABB&, PathDirection = PathDirection::cw); void addPoly(Span<const Vec2D>, bool isClosed); + + class Iter { + const Vec2D* m_currPts; + const PathVerb* m_currVerb; + const PathVerb* m_stopVerb; // 1 past last verb + public: + Iter(const RawPath& path) { + m_currPts = path.m_Points.data(); + m_currVerb = path.m_Verbs.data(); + m_stopVerb = path.m_Verbs.data() + path.m_Verbs.size(); + } + + struct Rec { + const Vec2D* pts; + int count; + PathVerb verb; + + operator bool() const { return pts != nullptr; } + }; + Rec next(); + }; }; } // namespace rive
diff --git a/skia/renderer/include/skia_factory.hpp b/skia/renderer/include/skia_factory.hpp index 22c432e..b9e0699 100644 --- a/skia/renderer/include/skia_factory.hpp +++ b/skia/renderer/include/skia_factory.hpp
@@ -11,6 +11,7 @@ namespace rive { class SkiaFactory : public Factory { +public: rcp<RenderBuffer> makeBufferU16(Span<const uint16_t>) override; rcp<RenderBuffer> makeBufferU32(Span<const uint32_t>) override; rcp<RenderBuffer> makeBufferF32(Span<const float>) override; @@ -44,7 +45,6 @@ // New virtual for access the platform's codecs // -public: enum class ColorType { rgba, bgra,
diff --git a/skia/renderer/include/to_skia.hpp b/skia/renderer/include/to_skia.hpp index e92649c..0ba14f8 100644 --- a/skia/renderer/include/to_skia.hpp +++ b/skia/renderer/include/to_skia.hpp
@@ -7,10 +7,12 @@ #include "include/core/SkMatrix.h" #include "include/core/SkPaint.h" +#include "include/core/SkPath.h" #include "include/core/SkPathTypes.h" #include "include/core/SkTileMode.h" #include "rive/math/mat2d.hpp" +#include "rive/math/raw_path.hpp" #include "rive/math/vec2d.hpp" #include "rive/renderer.hpp" #include "rive/shapes/paint/stroke_cap.hpp" @@ -91,6 +93,14 @@ assert(false); return SkBlendMode::kSrcOver; } + + static SkPath convert(const RawPath& rp) { + const auto pts = rp.points(); + const auto vbs = rp.verbsU8(); + return SkPath::Make((const SkPoint*)pts.data(), pts.size(), + vbs.data(), vbs.size(), + nullptr, 0, SkPathFillType::kWinding); + } // clang-format off }; } // namespace rive
diff --git a/skia/viewer/src/contour_measure.cpp b/skia/viewer/src/contour_measure.cpp new file mode 100644 index 0000000..dc97423 --- /dev/null +++ b/skia/viewer/src/contour_measure.cpp
@@ -0,0 +1,51 @@ + + +#include "contour_measure.hpp" +#include "to_skia.hpp" + +#include "include/core/SkContourMeasure.h" + +namespace rive { + +ContourMeasure::ContourMeasure(const RawPath& path) { + auto skpath = ToSkia::convert(path); + m_meas = SkContourMeasureIter(skpath, false).next().release(); +} + +ContourMeasure::~ContourMeasure() { + m_meas->unref(); +} + +float ContourMeasure::length() const { return m_meas->length(); } + +bool ContourMeasure::computePosTan(float distance, Vec2D* pos, Vec2D* tan) const { + return m_meas->getPosTan(distance, (SkPoint*)pos, (SkPoint*)tan); +} + +RawPath ContourMeasure::warp(const RawPath& src) const { + RawPath dst; + + RawPath::Iter iter(src); + while (auto rec = iter.next()) { + switch (rec.verb) { + case PathVerb::move: + dst.move(this->warp(rec.pts[0])); + break; + case PathVerb::line: + dst.line(this->warp(rec.pts[0])); + break; + case PathVerb::quad: + dst.quad(this->warp(rec.pts[0]), this->warp(rec.pts[1])); + break; + case PathVerb::cubic: + dst.cubic(this->warp(rec.pts[0]), this->warp(rec.pts[1]), this->warp(rec.pts[2])); + break; + case PathVerb::close: + dst.close(); + break; + } + } + return dst; +} + +}
diff --git a/skia/viewer/src/contour_measure.hpp b/skia/viewer/src/contour_measure.hpp new file mode 100644 index 0000000..564efcc --- /dev/null +++ b/skia/viewer/src/contour_measure.hpp
@@ -0,0 +1,43 @@ + + +#ifndef _RIVE_CONTOUR_MEASURE_HPP_ +#define _RIVE_CONTOUR_MEASURE_HPP_ + +#include "rive/math/raw_path.hpp" + +class SkContourMeasure; + +namespace rive { + + class ContourMeasure { + SkContourMeasure* m_meas; + public: + ContourMeasure(const RawPath& path); + ~ContourMeasure(); + + float length() const; + + bool computePosTan(float distance, Vec2D* pos, Vec2D* tan) const; + + bool warp(Vec2D src, Vec2D* dst) const { + Vec2D pos, tan; + if (this->computePosTan(src.x, &pos, &tan)) { + *dst = { + pos.x - tan.y * src.y, + pos.y + tan.x * src.y, + }; + return true; + } + return false; + } + + Vec2D warp(Vec2D point) const { + Vec2D result; + return this->warp(point, &result) ? result : Vec2D{0, 0}; + } + + RawPath warp(const RawPath&) const; + }; +} + +#endif
diff --git a/skia/viewer/src/text_content.cpp b/skia/viewer/src/text_content.cpp index cbae245..8f62b46 100644 --- a/skia/viewer/src/text_content.cpp +++ b/skia/viewer/src/text_content.cpp
@@ -110,6 +110,8 @@ std::vector<int> m_breaks; std::vector<RenderFontGlyphRuns> m_gruns; + rive::Mat2D m_xform; + float m_width = 300; RenderFontTextRuns make_truns(RenderFontFactory fact) { auto loader = [fact](const char filename[]) -> rive::rcp<rive::RenderFont> { @@ -158,14 +160,14 @@ m_gruns.push_back(truns[0].font->shapeText(rive::toSpan(m_unichars), rive::toSpan(truns))); } + + m_xform = rive::Mat2D::fromTranslate(10, 0) + * rive::Mat2D::fromScale(3, 3); } void draw(rive::Renderer* renderer, float width, const RenderFontGlyphRuns& gruns) { renderer->save(); - renderer->translate(10, 0); - - renderer->save(); - renderer->scale(3, 3); + renderer->transform(m_xform); auto lines = rive::RenderGlyphLine::BreakLines(rive::toSpan(gruns), rive::toSpan(m_breaks), width); @@ -173,30 +175,43 @@ draw_line(&skiaFactory, renderer, width); renderer->restore(); - - renderer->restore(); } void handleDraw(SkCanvas* canvas, double) override { rive::SkiaRenderer renderer(canvas); - static float width = 300; - static float dw = 1; - if (false) { - width += dw; if (width > 600) { dw = -dw; } if (width < 50) { dw = -dw; } - } - for (auto& grun : m_gruns) { - this->draw(&renderer, width, grun); + this->draw(&renderer, m_width, grun); renderer.translate(1200, 0); } } void handleResize(int width, int height) override {} - void handleImgui() override {} + void handleImgui() override { + ImGui::Begin("text", nullptr); + ImGui::SliderFloat("Alignment", &m_width, 10, 400); + ImGui::End(); + } }; +static bool ends_width(const char str[], const char suffix[]) { + size_t ln = strlen(str); + size_t lx = strlen(suffix); + if (lx > ln) { + return false; + } + for (size_t i = 0; i < lx; ++i) { + if (str[ln - lx + i] != suffix[i]) { + return false; + } + } + return true; +} + std::unique_ptr<ViewerContent> ViewerContent::Text(const char filename[]) { - return std::make_unique<TextContent>(); + if (ends_width(filename, ".svg")) { + return std::make_unique<TextContent>(); + } + return nullptr; }
diff --git a/skia/viewer/src/textpath_content.cpp b/skia/viewer/src/textpath_content.cpp new file mode 100644 index 0000000..b14b738 --- /dev/null +++ b/skia/viewer/src/textpath_content.cpp
@@ -0,0 +1,277 @@ +/* + * Copyright 2022 Rive + */ + +#include "viewer_content.hpp" + +#include "rive/refcnt.hpp" +#include "rive/render_text.hpp" + +#include "contour_measure.hpp" +#include "skia_factory.hpp" +#include "skia_renderer.hpp" +#include "line_breaker.hpp" +#include "to_skia.hpp" + +using namespace rive; + +using RenderFontTextRuns = std::vector<RenderTextRun>; +using RenderFontGlyphRuns = std::vector<RenderGlyphRun>; +using RenderFontFactory = rcp<RenderFont> (*)(const Span<const uint8_t>); + +template <typename Handler> +void visit(const std::vector<RenderGlyphRun>& gruns, Vec2D origin, Handler proc) { + for (const auto& gr : gruns) { + for (size_t i = 0; i < gr.glyphs.size(); ++i) { + auto path = gr.font->getPath(gr.glyphs[i]); + auto mx = Mat2D::fromTranslate(origin.x + gr.xpos[i], origin.y) + * Mat2D::fromScale(gr.size, gr.size); + path.transformInPlace(mx); + proc(path); + } + } +} + +static Vec2D ave(Vec2D a, Vec2D b) { + return (a + b) * 0.5f; +} + +static RawPath make_quad_path(Span<const Vec2D> pts) { + const int N = pts.size(); + RawPath path; + if (N >= 2) { + path.move(pts[0]); + if (N == 2) { + path.line(pts[1]); + } else if (N == 3) { + path.quad(pts[1], pts[2]); + } else { + for (int i = 1; i < N - 2; ++i) { + path.quad(pts[i], ave(pts[i], pts[i+1])); + } + path.quad(pts[N-2], pts[N-1]); + } + } + return path; +} + +//////////////////////////////////////////////////////////////////////////////////// + +#include "renderfont_skia.hpp" +#include "renderfont_hb.hpp" +#include "renderfont_coretext.hpp" + +static SkiaFactory skiaFactory; + +static std::unique_ptr<RenderPath> make_rpath(const RawPath& path) { + return skiaFactory.makeRenderPath(path.points(), path.verbsU8(), FillRule::nonZero); +} + +static void stroke_path(Renderer* renderer, const RawPath& path, float size, ColorInt color) { + auto paint = skiaFactory.makeRenderPaint(); + paint->color(color); + paint->thickness(size); + paint->style(RenderPaintStyle::stroke); + renderer->drawPath(make_rpath(path).get(), paint.get()); +} + +static void fill_rect(Renderer* renderer, const AABB& r, RenderPaint* paint) { + RawPath rp; + rp.addRect(r); + renderer->drawPath(make_rpath(rp).get(), paint); +} + +static void fill_point(Renderer* renderer, Vec2D p, float r, RenderPaint* paint) { + fill_rect(renderer, {p.x - r, p.y - r, p.x + r, p.y + r}, paint); +} + +typedef rcp<RenderFont> (*RenderFontFactory)(Span<const uint8_t>); + +static RenderTextRun append(std::vector<Unichar>* unichars, rcp<RenderFont> font, + float size, const char text[]) { + uint32_t n = 0; + while (text[n]) { + unichars->push_back(text[n]); // todo: utf8 -> unichar + n += 1; + } + return { std::move(font), size, n }; +} + +class TextPathContent : public ViewerContent { + std::vector<Unichar> m_unichars; + RenderFontGlyphRuns m_gruns; + std::unique_ptr<RenderPaint> m_paint; + + std::vector<Vec2D> m_pathpts; + Vec2D m_lastPt = {0,0}; + int m_trackingIndex = -1; + Mat2D m_trans; + + float m_alignment = 0, + m_scaleY = 1, + m_offsetY = 0, + m_windowWidth = 1, // % + m_windowOffset = 0; // % + + RenderFontTextRuns make_truns(RenderFontFactory fact) { + auto loader = [fact](const char filename[]) -> rcp<RenderFont> { + auto bytes = ViewerContent::LoadFile(filename); + if (bytes.size() == 0) { + assert(false); + return nullptr; + } + return fact(toSpan(bytes)); + }; + + const char* fontFiles[] = { + "../../test/assets/RobotoFlex.ttf", + "../../test/assets/LibreBodoni-Italic-VariableFont_wght.ttf", + }; + + auto font0 = loader(fontFiles[0]); + auto font1 = loader(fontFiles[1]); + assert(font0); + assert(font1); + + RenderFont::Coord c1 = {'wght', 100.f}, + c2 = {'wght', 800.f}; + + RenderFontTextRuns truns; + + truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 60, "U")); + truns.push_back(append(&m_unichars, font0->makeAtCoord(c1), 30, "neasy")); + truns.push_back(append(&m_unichars, font1, 30, " fits the crown")); + truns.push_back(append(&m_unichars, font1->makeAtCoord(c1), 30, " that often")); + truns.push_back(append(&m_unichars, font0, 30, " lies the head.")); + + return truns; + } + +public: + TextPathContent() { + auto truns = this->make_truns(CoreTextRenderFont::Decode); + m_gruns = truns[0].font->shapeText(toSpan(m_unichars), toSpan(truns)); + + m_paint = skiaFactory.makeRenderPaint(); + m_paint->color(0xFFFFFFFF); + + m_pathpts.push_back({ 20, 300}); + m_pathpts.push_back({220, 100}); + m_pathpts.push_back({420, 500}); + m_pathpts.push_back({620, 100}); + m_pathpts.push_back({820, 300}); + + m_trans = Mat2D::fromTranslate(200, 200) * Mat2D::fromScale(2, 2); + } + + void draw_warp(Renderer* renderer, const RawPath& warp) { + stroke_path(renderer, warp, 0.5, 0xFF00FF00); + + auto paint = skiaFactory.makeRenderPaint(); + paint->color(0xFF008800); + const float r = 4; + for (auto p : m_pathpts) { + fill_point(renderer, p, r, paint.get()); + } + } + + static size_t count_glyphs(const RenderFontGlyphRuns& gruns) { + size_t n = 0; + for (const auto& gr : gruns) { + n += gr.glyphs.size(); + } + return n; + } + + void modify(float amount) { + m_paint->color(0xFFFFFFFF); + } + + void draw(Renderer* renderer, const RenderFontGlyphRuns& gruns) { + auto get_path = [this](const RenderGlyphRun& run, int index, float dx) { + auto path = run.font->getPath(run.glyphs[index]); + path.transformInPlace(Mat2D::fromTranslate(run.xpos[index] + dx, m_offsetY) + * Mat2D::fromScale(run.size, run.size * m_scaleY)); + return path; + }; + + renderer->save(); + renderer->transform(m_trans); + + RawPath warp = make_quad_path(toSpan(m_pathpts)); + this->draw_warp(renderer, warp); + + ContourMeasure meas(warp); + const float warpLength = meas.length(); + const float textLength = gruns.back().xpos.back(); + const float offset = (warpLength - textLength) * m_alignment; + + const size_t glyphCount = count_glyphs(gruns); + size_t glyphIndex = 0; + float windowEnd = m_windowOffset + m_windowWidth; + + for (const auto& gr : gruns) { + for (size_t i = 0; i < gr.glyphs.size(); ++i) { + float percent = glyphIndex / (float)(glyphCount - 1); + float amount = (percent >= m_windowOffset && percent <= windowEnd); + + float scaleY = m_scaleY; + m_paint->color(0xFF666666); + m_paint->style(RenderPaintStyle::fill); + if (amount > 0) { + this->modify(amount); + } + + auto path = meas.warp(get_path(gr, i, offset)); + renderer->drawPath(make_rpath(path).get(), m_paint.get()); + glyphIndex += 1; + m_scaleY = scaleY; + } + } + renderer->restore(); + } + + void handleDraw(SkCanvas* canvas, double) override { + SkiaRenderer renderer(canvas); + + this->draw(&renderer, m_gruns); + } + + void handlePointerMove(float x, float y) override { + m_lastPt = m_trans.invertOrIdentity() * Vec2D{x, y}; + if (m_trackingIndex >= 0) { + m_pathpts[m_trackingIndex] = m_lastPt; + } + } + void handlePointerDown() override { + auto close_to = [](Vec2D a, Vec2D b) { + return Vec2D::distance(a, b) <= 10; + }; + for (size_t i = 0; i < m_pathpts.size(); ++i) { + if (close_to(m_lastPt, m_pathpts[i])) { + m_trackingIndex = i; + break; + } + } + } + + void handlePointerUp() override { + m_trackingIndex = -1; + } + + void handleResize(int width, int height) override {} + + void handleImgui() override { + ImGui::Begin("path", nullptr); + ImGui::SliderFloat("Alignment", &m_alignment, -3, 4); + ImGui::SliderFloat("Scale Y", &m_scaleY, 0.25f, 3.0f); + ImGui::SliderFloat("Offset Y", &m_offsetY, -100, 100); + ImGui::SliderFloat("Window Offset", &m_windowOffset, -1.1f, 1.1f); + ImGui::SliderFloat("Window Width", &m_windowWidth, 0, 1.2f); + ImGui::End(); + } +}; + +std::unique_ptr<ViewerContent> ViewerContent::TextPath(const char filename[]) { + return std::make_unique<TextPathContent>(); +}
diff --git a/skia/viewer/src/viewer_content.hpp b/skia/viewer/src/viewer_content.hpp index 3fd2697..a186911 100644 --- a/skia/viewer/src/viewer_content.hpp +++ b/skia/viewer/src/viewer_content.hpp
@@ -27,7 +27,7 @@ // Searches all handlers and returns a content if it is found. static std::unique_ptr<ViewerContent> FindHandler(const char filename[]) { - Factory factories[] = { Scene, Image, Text }; + Factory factories[] = { Scene, Image, Text, TextPath }; for (auto f : factories) { if (auto content = f(filename)) { return content; @@ -40,6 +40,7 @@ static std::unique_ptr<ViewerContent> Scene(const char[]); static std::unique_ptr<ViewerContent> Image(const char[]); static std::unique_ptr<ViewerContent> Text(const char[]); + static std::unique_ptr<ViewerContent> TextPath(const char[]); static std::vector<uint8_t> LoadFile(const char path[]); };
diff --git a/src/math/raw_path.cpp b/src/math/raw_path.cpp index 2461c3d..da05d50 100644 --- a/src/math/raw_path.cpp +++ b/src/math/raw_path.cpp
@@ -165,3 +165,34 @@ close(); } } + +////////////////////////////////////////////////////////////////////////// + +namespace rive { +int path_verb_to_point_count(PathVerb v) { + static uint8_t ptCounts[] = { + 1, // move + 1, // line + 2, // quad + 2, // conic (unused) + 3, // cubic + 0, // close + }; + size_t index = (size_t)v; + assert(index < sizeof(ptCounts)); + return ptCounts[index]; +} +} + +RawPath::Iter::Rec RawPath::Iter::next() { + Rec rec = {nullptr, 0, PathVerb::move}; + + if (m_currVerb < m_stopVerb) { + rec.pts = m_currPts; + rec.verb = *m_currVerb++; + rec.count = path_verb_to_point_count(rec.verb); + + m_currPts += rec.count; + } + return rec; +}
diff --git a/test/raw_path_test.cpp b/test/raw_path_test.cpp index 182fb12..e152351 100644 --- a/test/raw_path_test.cpp +++ b/test/raw_path_test.cpp
@@ -75,3 +75,81 @@ } } } + +////////////////////////////////////////////////////////////////////////// + +static bool is_move(const RawPath::Iter::Rec& rec) { + if (rec.verb == PathVerb::move) { + REQUIRE(rec.count == 1); + return true; + } + return false; +} + +static bool is_line(const RawPath::Iter::Rec& rec) { + if (rec.verb == PathVerb::line) { + REQUIRE(rec.count == 1); + return true; + } + return false; +} + +static bool is_quad(const RawPath::Iter::Rec& rec) { + if (rec.verb == PathVerb::quad) { + REQUIRE(rec.count == 2); + return true; + } + return false; +} + +static bool is_cubic(const RawPath::Iter::Rec& rec) { + if (rec.verb == PathVerb::cubic) { + REQUIRE(rec.count == 3); + return true; + } + return false; +} + +static bool is_close(const RawPath::Iter::Rec& rec) { + if (rec.verb == PathVerb::close) { + REQUIRE(rec.count == 0); + return true; + } + return false; +} + +TEST_CASE("rawpath-iter", "[rawpath]") { + auto eq = [](Vec2D p, float x, float y) { return p.x == x && p.y == y; }; + + { + RawPath rp; + RawPath::Iter iter(rp); + REQUIRE(iter.next() == false); + REQUIRE(iter.next() == false); // should be safe to call again + } + { + RawPath rp; + rp.moveTo(1, 2); + rp.lineTo(3, 4); + rp.quadTo(5, 6, 7, 8); + rp.cubicTo(9, 10, 11, 12, 13, 14); + rp.close(); + RawPath::Iter iter(rp); + auto rec = iter.next(); + REQUIRE((rec && is_move(rec) && eq(rec.pts[0], 1,2))); + rec = iter.next(); + REQUIRE((rec && is_line(rec) && eq(rec.pts[0], 3,4))); + rec = iter.next(); + REQUIRE((rec && is_quad(rec) && eq(rec.pts[0], 5,6) + && eq(rec.pts[1], 7,8))); + rec = iter.next(); + REQUIRE((rec && is_cubic(rec) && eq(rec.pts[0], 9,10) + && eq(rec.pts[1], 11,12) + && eq(rec.pts[2], 13,14))); + rec = iter.next(); + REQUIRE((rec && is_close(rec))); + rec = iter.next(); + REQUIRE(rec == false); + REQUIRE(iter.next() == false); // should be safe to call again + } +}