| #include "rive/core_context.hpp" | |
| #include "rive/math/math_types.hpp" | |
| #include "rive/shapes/path.hpp" | |
| #include "rive/shapes/shape.hpp" | |
| #include "rive/text/text.hpp" | |
| #include "rive/text/text_follow_path_modifier.hpp" | |
| #include "rive/text/text_modifier_group.hpp" | |
| #include "rive/transform_component.hpp" | |
| using namespace rive; | |
| // Path -> this -> text | |
| void TextFollowPathModifier::buildDependencies() | |
| { | |
| auto* tgt = target(); | |
| if (tgt != nullptr && tgt->is<Shape>()) | |
| { | |
| Shape* shape = static_cast<Shape*>(tgt); | |
| shape->pathComposer()->addDependent(this); | |
| } | |
| else if (tgt != nullptr && tgt->is<Path>()) | |
| { | |
| Path* path = static_cast<Path*>(tgt); | |
| path->addDependent(this); | |
| } | |
| Text* text = textComponent(); | |
| if (text != nullptr) | |
| { | |
| addDependent(text); | |
| } | |
| } | |
| StatusCode TextFollowPathModifier::onAddedClean(CoreContext* context) | |
| { | |
| if (auto* tgt = target()) | |
| { | |
| if (tgt->is<Shape>()) | |
| { | |
| Shape* shape = static_cast<Shape*>(tgt); | |
| shape->addFlags(PathFlags::followPath); | |
| } | |
| else if (tgt->is<Path>()) | |
| { | |
| Path* path = static_cast<Path*>(tgt); | |
| path->addFlags(PathFlags::followPath); | |
| } | |
| } | |
| return Super::onAddedClean(context); | |
| } | |
| void TextFollowPathModifier::update(ComponentDirt value) | |
| { | |
| auto* tgt = target(); | |
| std::vector<Path*> paths; | |
| if (tgt != nullptr && tgt->is<Shape>()) | |
| { | |
| auto shape = tgt->as<Shape>(); | |
| for (auto path : shape->paths()) | |
| { | |
| paths.push_back(path); | |
| } | |
| } | |
| else if (tgt != nullptr && tgt->is<Path>()) | |
| { | |
| paths.push_back(tgt->as<Path>()); | |
| } | |
| m_worldPath.rewind(); | |
| for (auto path : paths) | |
| { | |
| m_worldPath.addPath(path->rawPath(), &path->pathTransform()); | |
| } | |
| } | |
| void TextFollowPathModifier::radialChanged() { modifierShapeDirty(); } | |
| void TextFollowPathModifier::orientChanged() { modifierShapeDirty(); } | |
| void TextFollowPathModifier::startChanged() { modifierShapeDirty(); } | |
| void TextFollowPathModifier::endChanged() { modifierShapeDirty(); } | |
| void TextFollowPathModifier::offsetChanged() { modifierShapeDirty(); } | |
| void TextFollowPathModifier::strengthChanged() { modifierShapeDirty(); } | |
| void TextFollowPathModifier::modifierShapeDirty() | |
| { | |
| Text* text = textComponent(); | |
| if (text != nullptr) | |
| { | |
| text->modifierShapeDirty(); | |
| } | |
| } | |
| void TextFollowPathModifier::reset(const Mat2D* inverseText) | |
| { | |
| if (target() == nullptr) | |
| { | |
| m_pathMeasure = PathMeasure(); | |
| return; | |
| } | |
| m_localPath.rewind(); | |
| m_localPath.addPath(m_worldPath, inverseText); | |
| m_pathMeasure = PathMeasure(&m_localPath, 0.1f); | |
| } | |
| TransformComponents TextFollowPathModifier::transformGlyph( | |
| const TransformComponents& cur, | |
| const TransformGlyphArg& arg) | |
| { | |
| float pathLength = m_pathMeasure.length(); | |
| if (pathLength == 0) | |
| { | |
| return cur; | |
| } | |
| const auto& paragraphLines = arg.paragraphLines; | |
| int lineIndexInParagraph = arg.lineIndexInParagraph; | |
| Vec2D positionOnPath = arg.originPosition + arg.offset; | |
| // endPct >= startPct | |
| float startPct = math::clamp(std::min(start(), end()), 0.0, 1.0); | |
| float endPct = math::clamp(std::max(start(), end()), 0.0, 1.0); | |
| bool canWrap = m_localPath.isClosed() && (endPct - startPct) == 1.0; | |
| float validLength = (endPct - startPct) * pathLength; | |
| // 0%-100% | |
| auto offsetPct = std::fmod(std::fmod(offset(), 1.0f) + 1.0f, 1.0f); | |
| // 0%-200% | |
| startPct += offsetPct; | |
| endPct += offsetPct; | |
| Vec2D position, tangent; | |
| // Render along the tangent if overflowing on either ends | |
| if ((!canWrap && positionOnPath.x < 0) || startPct == endPct) | |
| { | |
| auto result = m_pathMeasure.atPercentage(startPct); | |
| tangent = result.tan; | |
| tangent = tangent.normalized(); | |
| Vec2D zeroPosition = result.pos; | |
| float extra = -positionOnPath.x; | |
| position = Vec2D(zeroPosition.x - tangent.x * extra, | |
| zeroPosition.y - tangent.y * extra); | |
| } | |
| else if (!canWrap && positionOnPath.x > validLength) | |
| { | |
| auto result = m_pathMeasure.atPercentage(endPct); | |
| tangent = result.tan; | |
| tangent = tangent.normalized(); | |
| Vec2D termPosition = result.pos; | |
| float extra = positionOnPath.x - validLength; | |
| position = Vec2D(termPosition.x + tangent.x * extra, | |
| termPosition.y + tangent.y * extra); | |
| } | |
| else | |
| { | |
| // Closed path, or in the range of the path. | |
| // Use the percentage API to wrap around | |
| auto result = m_pathMeasure.atPercentage(startPct + | |
| positionOnPath.x / pathLength); | |
| position = result.pos; | |
| tangent = result.tan; | |
| tangent = tangent.normalized(); | |
| } | |
| // Find out the baseline to sit on | |
| float lastBaseline = 0; | |
| float curBaseline = 0; | |
| auto validLine = [&](int i) -> bool { | |
| return i >= 0 && i < paragraphLines.size(); | |
| }; | |
| int lastLineIndex = lineIndexInParagraph - 1; | |
| if (validLine(lastLineIndex)) | |
| { | |
| lastBaseline = paragraphLines[lastLineIndex].baseline; | |
| } | |
| if (validLine(lineIndexInParagraph)) | |
| { | |
| curBaseline = paragraphLines[lineIndexInParagraph].baseline; | |
| } | |
| // We assume the glyph would have been rendered on the | |
| // current line's baseline. | |
| Vec2D translation; | |
| if (radial()) | |
| { | |
| float verticalSpacing = positionOnPath.y - curBaseline; | |
| Vec2D perpendicular(-tangent.y, tangent.x); | |
| translation = Vec2D(position.x + verticalSpacing * perpendicular.x, | |
| position.y + verticalSpacing * perpendicular.y); | |
| } | |
| else | |
| { | |
| translation = | |
| Vec2D(position.x, | |
| positionOnPath.y - curBaseline + position.y + lastBaseline); | |
| } | |
| float rotation = orient() ? std::atan2(tangent.y, tangent.x) : 0; | |
| TransformComponents tc; | |
| float t = math::clamp(strength(), 0.0, 1.0); | |
| float ti = 1.0f - t; | |
| tc.x(translation.x * t + cur.x() * ti); | |
| tc.y(translation.y * t + cur.y() * ti); | |
| tc.rotation(rotation * t + cur.rotation() * ti); | |
| return tc; | |
| } |