blob: 4495446048c22aa4f3fec4cda2621cb258c35889 [file] [log] [blame]
/*
* Copyright 2022 Rive
*/
#include "viewer/viewer_content.hpp"
#include "utils/rive_utf.hpp"
#include "rive/math/raw_path.hpp"
#include "rive/refcnt.hpp"
#include "rive/factory.hpp"
#include "rive/render_text.hpp"
#include "rive/math/contour_measure.hpp"
#include "rive/text/line_breaker.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;
}
static void warp_in_place(ContourMeasure* meas, RawPath* path) {
for (auto& pt : path->points()) {
pt = meas->warp(pt);
}
}
////////////////////////////////////////////////////////////////////////////////////
static std::unique_ptr<RenderPath> make_rpath(RawPath& path) {
return ViewerContent::RiveFactory()->makeRenderPath(path, FillRule::nonZero);
}
static void stroke_path(Renderer* renderer, RawPath& path, float size, ColorInt color) {
auto paint = ViewerContent::RiveFactory()->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);
}
static RenderTextRun
append(std::vector<Unichar>* unichars, rcp<RenderFont> font, float size, const char text[]) {
const uint8_t* ptr = (const uint8_t*)text;
uint32_t n = 0;
while (*ptr) {
unichars->push_back(rive::UTF::NextUTF8(&ptr));
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;
AABB m_gbounds;
std::vector<Vec2D> m_pathpts;
Vec2D m_lastPt = {0, 0};
int m_trackingIndex = -1;
Mat2D m_trans;
Mat2D m_oneLineXform;
bool m_trackingOneLine = false;
float m_oneLineX = 0;
float m_flareRadius = 50;
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(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, "ne漢字asy"));
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 compute_bounds = [](const std::vector<RenderGlyphRun>& gruns) {
AABB bounds = {};
for (const auto& gr : gruns) {
bounds.minY = std::min(bounds.minY, gr.font->lineMetrics().ascent * gr.size);
bounds.maxY = std::max(bounds.maxY, gr.font->lineMetrics().descent * gr.size);
}
bounds.minX = gruns.front().xpos.front();
bounds.maxX = gruns.back().xpos.back();
printf("%g %g %g %g\n", bounds.left(), bounds.top(), bounds.right(), bounds.bottom());
return bounds;
};
auto truns = this->make_truns(ViewerContent::DecodeFont);
m_gruns = truns[0].font->shapeText(m_unichars, truns);
m_gbounds = compute_bounds(m_gruns);
m_oneLineXform = Mat2D::fromScale(2.5, 2.5) * Mat2D::fromTranslate(20, 80);
m_paint = ViewerContent::RiveFactory()->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, RawPath& warp) {
stroke_path(renderer, warp, 0.5, 0xFF00FF00);
auto paint = ViewerContent::RiveFactory()->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(m_pathpts);
this->draw_warp(renderer, warp);
auto meas = ContourMeasureIter(warp).next();
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 = get_path(gr, i, offset);
warp_in_place(meas.get(), &path);
renderer->drawPath(make_rpath(path).get(), m_paint.get());
glyphIndex += 1;
m_scaleY = scaleY;
}
}
renderer->restore();
}
void drawOneLine(Renderer* renderer) {
auto paint = ViewerContent::RiveFactory()->makeRenderPaint();
paint->color(0xFF88FFFF);
if (m_trackingOneLine) {
float mx = m_oneLineX / m_gbounds.width();
const ColorInt colors[] = {0xFF88FFFF, 0xFF88FFFF, 0xFFFFFFFF, 0xFF88FFFF, 0xFF88FFFF};
const float stops[] = {0, mx / 2, mx, (1 + mx) / 2, 1};
paint->shader(ViewerContent::RiveFactory()->makeLinearGradient(m_gbounds.left(),
0,
m_gbounds.right(),
0,
colors,
stops,
5));
}
struct EaseWindow {
float center, radius;
float map(float x) const {
float dist = std::abs(center - x);
if (dist > radius) {
return 0;
}
float t = (radius - dist) / radius;
return t * t * (3 - 2 * t);
}
};
auto wrap_path = [](const RawPath& src, float x, float rad) {
return src.morph([x, rad](Vec2D p) {
Vec2D newpt = p;
newpt.y = p.y * 4 + 18;
const float t = EaseWindow{x, rad}.map(p.x);
return Vec2D::lerp(p, newpt, t);
});
};
visit(m_gruns, {0, 0}, [&](RawPath& rp) {
RawPath* ptr = &rp;
RawPath storage;
if (m_trackingOneLine) {
storage = wrap_path(rp, m_oneLineX, m_flareRadius);
ptr = &storage;
}
renderer->drawPath(make_rpath(*ptr).get(), paint.get());
});
}
void handleDraw(rive::Renderer* renderer, double) override {
renderer->save();
this->draw(renderer, m_gruns);
renderer->restore();
renderer->save();
renderer->transform(m_oneLineXform);
this->drawOneLine(renderer);
renderer->restore();
}
void handlePointerMove(float x, float y) override {
auto contains = [](const AABB& r, Vec2D p) {
return r.left() <= p.x && p.x < r.right() && r.top() <= p.y && p.y < r.bottom();
};
// are we on onLine?
{
m_trackingOneLine = false;
auto pos = m_oneLineXform.invertOrIdentity() * Vec2D{x, y};
if (contains(m_gbounds.inset(-8, 0), pos)) {
m_trackingOneLine = true;
m_oneLineX = pos.x;
return;
}
}
// are we on the path?
m_lastPt = m_trans.invertOrIdentity() * Vec2D{x, y};
if (m_trackingIndex >= 0) {
m_pathpts[m_trackingIndex] = m_lastPt;
}
}
void handlePointerDown(float x, float y) 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(float x, float y) 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::SliderFloat("Flare radius", &m_flareRadius, 10, 100);
ImGui::End();
}
};
std::unique_ptr<ViewerContent> ViewerContent::TextPath(const char filename[]) {
return std::make_unique<TextPathContent>();
}