[skottie] Initial text editing experiment
Introduce an optional GlyphDecorator callback, executed when rendering
each individual glyph.
Start experimenting with a sample editor implementation for Viewer
(selection-only so far).
Bug: b/259298707
Change-Id: I5737c91f4fc56d8877b7919009d889865aac1135
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/606516
Reviewed-by: Jorge Betancourt <jmbetancourt@google.com>
Commit-Queue: Florin Malita <fmalita@google.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index 1fea0b8..1c3bc74 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -3044,6 +3044,8 @@
"tools/viewer/SkSLSlide.h",
"tools/viewer/SkottieSlide.cpp",
"tools/viewer/SkottieSlide.h",
+ "tools/viewer/SkottieTextEditor.cpp",
+ "tools/viewer/SkottieTextEditor.h",
"tools/viewer/Slide.h",
"tools/viewer/SlideDir.cpp",
"tools/viewer/SlideDir.h",
diff --git a/modules/skottie/include/SkottieProperty.h b/modules/skottie/include/SkottieProperty.h
index b9ccbb4..625c7d6 100644
--- a/modules/skottie/include/SkottieProperty.h
+++ b/modules/skottie/include/SkottieProperty.h
@@ -9,16 +9,19 @@
#define SkottieProperty_DEFINED
#include "include/core/SkColor.h"
+#include "include/core/SkMatrix.h"
#include "include/core/SkPaint.h"
#include "include/core/SkPoint.h"
+#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkTypeface.h"
#include "include/utils/SkTextUtils.h"
#include "modules/skottie/src/text/SkottieShaper.h"
#include <functional>
+#include <vector>
-class SkMatrix;
+class SkCanvas;
namespace sksg {
@@ -37,6 +40,19 @@
kStrokeFill,
};
+// EXPERIMENTAL
+// Optional callback invoked when drawing text layers.
+// Allows clients to render custom text decorations.
+class GlyphDecorator : public SkRefCnt {
+public:
+ struct GlyphInfo {
+ SkRect fBounds; // visual glyph bounds
+ SkMatrix fMatrix; // glyph matrix
+ };
+
+ virtual void onDecorate(SkCanvas*, const GlyphInfo[], size_t size) = 0;
+};
+
struct TextPropertyValue {
sk_sp<SkTypeface> fTypeface;
SkString fText;
@@ -61,6 +77,7 @@
SkPaint::Join fStrokeJoin = SkPaint::Join::kMiter_Join;
bool fHasFill = false,
fHasStroke = false;
+ sk_sp<GlyphDecorator> fDecorator;
bool operator==(const TextPropertyValue& other) const;
bool operator!=(const TextPropertyValue& other) const;
diff --git a/modules/skottie/src/SkottieProperty.cpp b/modules/skottie/src/SkottieProperty.cpp
index 2dddc9a..db1d19e 100644
--- a/modules/skottie/src/SkottieProperty.cpp
+++ b/modules/skottie/src/SkottieProperty.cpp
@@ -35,7 +35,8 @@
&& fPaintOrder == other.fPaintOrder
&& fStrokeJoin == other.fStrokeJoin
&& fHasFill == other.fHasFill
- && fHasStroke == other.fHasStroke;
+ && fHasStroke == other.fHasStroke
+ && fDecorator == other.fDecorator;
}
bool TextPropertyValue::operator!=(const TextPropertyValue& other) const {
diff --git a/modules/skottie/src/SkottieTest.cpp b/modules/skottie/src/SkottieTest.cpp
index ed30dc3..4018b00 100644
--- a/modules/skottie/src/SkottieTest.cpp
+++ b/modules/skottie/src/SkottieTest.cpp
@@ -331,7 +331,8 @@
TextPaintOrder::kFillStroke,
SkPaint::Join::kDefault_Join,
false,
- false
+ false,
+ nullptr
}));
}
diff --git a/modules/skottie/src/text/TextAdapter.cpp b/modules/skottie/src/text/TextAdapter.cpp
index b422888..29b15a2 100644
--- a/modules/skottie/src/text/TextAdapter.cpp
+++ b/modules/skottie/src/text/TextAdapter.cpp
@@ -22,19 +22,25 @@
#include "modules/sksg/include/SkSGPath.h"
#include "modules/sksg/include/SkSGRect.h"
#include "modules/sksg/include/SkSGRenderEffect.h"
+#include "modules/sksg/include/SkSGRenderNode.h"
#include "modules/sksg/include/SkSGTransform.h"
+#include "modules/sksg/src/SkSGTransformPriv.h"
// Enable for text layout debugging.
#define SHOW_LAYOUT_BOXES 0
namespace skottie::internal {
+namespace {
+
class GlyphTextNode final : public sksg::GeometryNode {
public:
- explicit GlyphTextNode(skottie::Shaper::ShapedGlyphs&& glyphs) : fGlyphs(std::move(glyphs)) {}
+ explicit GlyphTextNode(Shaper::ShapedGlyphs&& glyphs) : fGlyphs(std::move(glyphs)) {}
~GlyphTextNode() override = default;
+ const Shaper::ShapedGlyphs* glyphs() const { return &fGlyphs; }
+
protected:
SkRect onRevalidate(sksg::InvalidationController*, const SkMatrix&) override {
return fGlyphs.computeBounds(Shaper::ShapedGlyphs::BoundsType::kConservative);
@@ -58,7 +64,7 @@
}
private:
- const skottie::Shaper::ShapedGlyphs fGlyphs;
+ const Shaper::ShapedGlyphs fGlyphs;
};
static float align_factor(SkTextUtils::Align a) {
@@ -71,6 +77,67 @@
SkUNREACHABLE;
}
+} // namespace
+
+class TextAdapter::GlyphDecoratorNode final : public sksg::Group {
+public:
+ GlyphDecoratorNode(sk_sp<GlyphDecorator> decorator)
+ : fDecorator(std::move(decorator))
+ {}
+
+ ~GlyphDecoratorNode() override = default;
+
+ void updateFragmentData(const std::vector<TextAdapter::FragmentRec>& recs) {
+ fFragCount = recs.size();
+
+ SkASSERT(!fFragInfo);
+ fFragInfo = std::make_unique<FragmentInfo[]>(recs.size());
+
+ for (size_t i = 0; i < recs.size(); ++i) {
+ const auto& rec = recs[i];
+ fFragInfo[i] = {rec.fOrigin, rec.fGlyphs, rec.fMatrixNode};
+ }
+
+ SkASSERT(!fDecoratorInfo);
+ fDecoratorInfo = std::make_unique<GlyphDecorator::GlyphInfo[]>(recs.size());
+ }
+
+ SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override {
+ const auto child_bounds = INHERITED::onRevalidate(ic, ctm);
+
+ for (size_t i = 0; i < fFragCount; ++i) {
+ fDecoratorInfo[i].fBounds =
+ fFragInfo[i].fGlyphs->computeBounds(Shaper::ShapedGlyphs::BoundsType::kTight);
+ fDecoratorInfo[i].fMatrix = sksg::TransformPriv::As<SkMatrix>(fFragInfo[i].fMatrixNode);
+ }
+
+ return child_bounds;
+ }
+
+ void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
+ auto local_ctx = ScopedRenderContext(canvas, ctx).setIsolation(this->bounds(),
+ canvas->getTotalMatrix(),
+ true);
+ this->INHERITED::onRender(canvas, local_ctx);
+
+ fDecorator->onDecorate(canvas, fDecoratorInfo.get(), fFragCount);
+ }
+
+private:
+ struct FragmentInfo {
+ SkPoint fOrigin;
+ const Shaper::ShapedGlyphs* fGlyphs;
+ sk_sp<sksg::Matrix<SkM44>> fMatrixNode;
+ };
+
+ const sk_sp<GlyphDecorator> fDecorator;
+ std::unique_ptr<FragmentInfo[]> fFragInfo;
+ std::unique_ptr<GlyphDecorator::GlyphInfo[]> fDecoratorInfo;
+ size_t fFragCount;
+
+ using INHERITED = Group;
+};
+
// Text path semantics
//
// * glyphs are positioned on the path based on their horizontal/x anchor point, interpreted as
@@ -365,7 +432,7 @@
return draws;
}
-void TextAdapter::addFragment(Shaper::Fragment& frag) {
+void TextAdapter::addFragment(Shaper::Fragment& frag, sksg::Group* container) {
// For a given shaped fragment, build a corresponding SG fragment:
//
// [TransformEffect] -> [Transform]
@@ -390,9 +457,11 @@
// Note: comp glyph IDs are still present in the list, but they don't draw anything
// (using empty path in SkCustomTypeface).
auto text_node = sk_make_sp<GlyphTextNode>(std::move(frag.fGlyphs));
+ rec.fGlyphs = text_node->glyphs();
draws.reserve(draws.size() +
- static_cast<size_t>(fText->fHasFill) + static_cast<size_t>(fText->fHasStroke));
+ static_cast<size_t>(fText->fHasFill) +
+ static_cast<size_t>(fText->fHasStroke));
SkASSERT(fText->fHasFill || fText->fHasStroke);
@@ -446,7 +515,7 @@
draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur);
}
- fRoot->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
+ container->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
fFragments.push_back(std::move(rec));
}
@@ -522,7 +591,8 @@
// - when animating
// - when positioning on a path
// - when clamping the number or lines (for accurate line count)
- if (!fAnimators.empty() || fPathInfo || fText->fMaxLines) {
+ // - when a text decorator is present
+ if (!fAnimators.empty() || fPathInfo || fText->fMaxLines || fText->fDecorator) {
flags |= Shaper::Flags::kFragmentGlyphs;
}
@@ -614,10 +684,24 @@
}
}
+ // Depending on whether a GlyphDecorator is present, we either add the glyph render nodes
+ // directly to the root group, or to an intermediate GlyphDecoratorNode container.
+ sksg::Group* container = fRoot.get();
+ sk_sp<GlyphDecoratorNode> decorator_node;
+ if (fText->fDecorator) {
+ decorator_node = sk_make_sp<GlyphDecoratorNode>(fText->fDecorator);
+ container = decorator_node.get();
+ }
+
// N.B. addFragment moves shaped glyph data out of the fragment, so only the fragment
// metrics are valid after this block.
- for (auto& frag : shape_result.fFragments) {
- this->addFragment(frag);
+ for (size_t i = 0; i < shape_result.fFragments.size(); ++i) {
+ this->addFragment(shape_result.fFragments[i], container);
+ }
+
+ if (decorator_node) {
+ decorator_node->updateFragmentData(fFragments);
+ fRoot->addChild(std::move(decorator_node));
}
if (!fAnimators.empty() || fPathInfo) {
diff --git a/modules/skottie/src/text/TextAdapter.h b/modules/skottie/src/text/TextAdapter.h
index 8ab28e9..5a53ffd 100644
--- a/modules/skottie/src/text/TextAdapter.h
+++ b/modules/skottie/src/text/TextAdapter.h
@@ -47,6 +47,8 @@
void onSync() override;
private:
+ class GlyphDecoratorNode;
+
enum class AnchorPointGrouping : uint8_t {
kCharacter,
kWord,
@@ -62,6 +64,7 @@
struct FragmentRec {
SkPoint fOrigin; // fragment position
+ const Shaper::ShapedGlyphs* fGlyphs = nullptr;
sk_sp<sksg::Matrix<SkM44>> fMatrixNode;
sk_sp<sksg::Color> fFillColorNode,
fStrokeColorNode;
@@ -72,7 +75,7 @@
};
void reshape();
- void addFragment(Shaper::Fragment&);
+ void addFragment(Shaper::Fragment&, sksg::Group* container);
void buildDomainMaps(const Shaper::Result&);
std::vector<sk_sp<sksg::RenderNode>> buildGlyphCompNodes(Shaper::Fragment&) const;
diff --git a/tools/viewer/SkottieSlide.cpp b/tools/viewer/SkottieSlide.cpp
index e15bc65..d37ebcd 100644
--- a/tools/viewer/SkottieSlide.cpp
+++ b/tools/viewer/SkottieSlide.cpp
@@ -25,6 +25,7 @@
#include "src/utils/SkOSPath.h"
#include "tools/Resources.h"
#include "tools/timer/TimeUtils.h"
+#include "tools/viewer/SkottieTextEditor.h"
#include <cmath>
#include <vector>
@@ -192,6 +193,28 @@
{ "Nested Skotties", ParticleMarker::MakeSkottie },
};
+class TextTracker final : public skottie::PropertyObserver {
+public:
+ explicit TextTracker(sk_sp<PropertyObserver> delegate) : fDelegate(std::move(delegate)) {}
+
+ std::vector<std::unique_ptr<skottie::TextPropertyHandle>>& props() {
+ return fTextProps;
+ }
+
+private:
+ void onTextProperty(const char node_name[],
+ const LazyHandle<skottie::TextPropertyHandle>& lh) override {
+ fTextProps.push_back(lh());
+
+ if (fDelegate) {
+ fDelegate->onTextProperty(node_name, lh);
+ }
+ }
+
+ const sk_sp<PropertyObserver> fDelegate;
+ std::vector<std::unique_ptr<skottie::TextPropertyHandle>> fTextProps;
+};
+
} // namespace
class SkottieSlide::TransformTracker : public skottie::PropertyObserver {
@@ -481,6 +504,8 @@
kInterceptPrefix);
fTransformTracker = sk_make_sp<TransformTracker>();
+ auto text_tracker = sk_make_sp<TextTracker>(fTransformTracker);
+
if (!fSlotManagerWrapper) {
fSlotManagerWrapper = std::make_unique<SlotManagerWrapper>(resource_provider, this);
}
@@ -494,7 +519,7 @@
.setPropertyObserver(fSlotManagerWrapper->getPropertyObserver());
} else {
builder.setResourceProvider(std::move(resource_provider))
- .setPropertyObserver(fTransformTracker);
+ .setPropertyObserver(text_tracker);
}
fAnimation = builder.makeFromFile(fPath.c_str());
fAnimationStats = builder.getStats();
@@ -508,6 +533,12 @@
fAnimation->size().width(),
fAnimation->size().height());
logger->report();
+
+ // Create an editor for the first text layer only.
+ // TODO: editors for all layers?
+ if (!text_tracker->props().empty()) {
+ fTextEditor = sk_make_sp<SkottieTextEditor>(std::move(text_tracker->props()[0]));
+ }
} else {
SkDebugf("failed to load Bodymovin animation: %s\n", fPath.c_str());
}
@@ -628,12 +659,19 @@
case 'M':
fShowSlotManager = !fShowSlotManager;
return true;
+ case 'E':
+ fTextEditor->toggleEnabled();
+ return true;
}
return INHERITED::onChar(c);
}
-bool SkottieSlide::onMouse(SkScalar x, SkScalar y, skui::InputState state, skui::ModifierKey) {
+bool SkottieSlide::onMouse(SkScalar x, SkScalar y, skui::InputState state, skui::ModifierKey mod) {
+ if (fTextEditor->onMouseInput(x, y, state, mod)) {
+ return true;
+ }
+
switch (state) {
case skui::InputState::kUp:
fShowAnimationInval = !fShowAnimationInval;
diff --git a/tools/viewer/SkottieSlide.h b/tools/viewer/SkottieSlide.h
index 8115da5..7464dc1 100644
--- a/tools/viewer/SkottieSlide.h
+++ b/tools/viewer/SkottieSlide.h
@@ -16,6 +16,8 @@
#include <vector>
+class SkottieTextEditor;
+
namespace sksg { class Scene; }
class SkottieSlide : public Slide {
@@ -50,6 +52,7 @@
sksg::InvalidationController fInvalController;
sk_sp<TransformTracker> fTransformTracker;
std::unique_ptr<SlotManagerWrapper>fSlotManagerWrapper;
+ sk_sp<SkottieTextEditor> fTextEditor;
std::vector<float> fFrameTimes;
SkSize fWinSize = SkSize::MakeEmpty();
double fTimeBase = 0,
diff --git a/tools/viewer/SkottieTextEditor.cpp b/tools/viewer/SkottieTextEditor.cpp
new file mode 100644
index 0000000..67eb1ce
--- /dev/null
+++ b/tools/viewer/SkottieTextEditor.cpp
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2022 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "tools/viewer/SkottieTextEditor.h"
+
+#include "include/core/SkCanvas.h"
+#include "include/core/SkM44.h"
+
+SkottieTextEditor::SkottieTextEditor(std::unique_ptr<skottie::TextPropertyHandle>&& prop)
+ : fTextProp(std::move(prop))
+{}
+
+SkottieTextEditor::~SkottieTextEditor() = default;
+
+void SkottieTextEditor::toggleEnabled() {
+ fEnabled = !fEnabled;
+
+ auto txt = fTextProp->get();
+ txt.fDecorator = fEnabled ? sk_ref_sp(this) : nullptr;
+ fTextProp->set(txt);
+}
+
+
+size_t SkottieTextEditor::closestGlyph(const SkPoint& pt) const {
+ float min_distance = std::numeric_limits<float>::max();
+ size_t min_index = 0;
+
+ for (size_t i = 0; i < fGlyphData.size(); ++i) {
+ const auto dist = (fGlyphData[i].fDevBounds.center() - pt).length();
+ if (dist < min_distance) {
+ min_distance = dist;
+ min_index = i;
+ }
+ }
+
+ return min_index;
+}
+
+void SkottieTextEditor::onDecorate(SkCanvas* canvas, const GlyphInfo glyphs[], size_t size) {
+ // Selection can be inverted.
+ const auto sel_start = std::min(std::get<0>(fSelection), std::get<1>(fSelection)),
+ sel_end = std::max(std::get<0>(fSelection), std::get<1>(fSelection));
+
+ fGlyphData.clear();
+
+ for (size_t i = 0; i < size; ++i) {
+ const auto& ginfo = glyphs[i];
+
+ SkAutoCanvasRestore acr(canvas, true);
+ canvas->concat(ginfo.fMatrix);
+
+ // Stash some glyph info, for later use.
+ fGlyphData.push_back({canvas->getLocalToDevice().asM33().mapRect(ginfo.fBounds)});
+
+ if (i < sel_start || i >= sel_end) {
+ continue;
+ }
+
+ static constexpr SkColor4f kSelectionColor{0, 0, 1, 0.4f};
+ canvas->drawRect(ginfo.fBounds, SkPaint(kSelectionColor));
+ }
+}
+
+bool SkottieTextEditor::onMouseInput(SkScalar x, SkScalar y, skui::InputState state,
+ skui::ModifierKey) {
+ if (!fEnabled || fGlyphData.empty()) {
+ return false;
+ }
+
+ switch (state) {
+ case skui::InputState::kDown: {
+ fMouseDown = true;
+
+ const auto closest = this->closestGlyph({x, y});
+ fSelection = {closest, closest};
+ } break;
+ case skui::InputState::kUp:
+ fMouseDown = false;
+ break;
+ case skui::InputState::kMove:
+ if (fMouseDown) {
+ const auto closest = this->closestGlyph({x, y});
+ std::get<1>(fSelection) = closest < std::get<0>(fSelection)
+ ? closest
+ : closest + 1;
+ }
+ break;
+ default:
+ break;
+ }
+
+ return true;
+}
diff --git a/tools/viewer/SkottieTextEditor.h b/tools/viewer/SkottieTextEditor.h
new file mode 100644
index 0000000..dd7cdef
--- /dev/null
+++ b/tools/viewer/SkottieTextEditor.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkottieTextEditor_DEFINED
+#define SkottieTextEditor_DEFINED
+
+#include "modules/skottie/include/SkottieProperty.h"
+#include "tools/skui/InputState.h"
+#include "tools/skui/ModifierKey.h"
+
+// A sample WYSIWYG text editor built using the GlyphDecorator API.
+class SkottieTextEditor final : public skottie::GlyphDecorator {
+public:
+ explicit SkottieTextEditor(std::unique_ptr<skottie::TextPropertyHandle>&&);
+ ~SkottieTextEditor() override;
+
+ void toggleEnabled();
+
+ void onDecorate(SkCanvas*, const GlyphInfo[], size_t) override;
+
+ bool onMouseInput(SkScalar x, SkScalar y, skui::InputState state, skui::ModifierKey);
+
+private:
+ struct GlyphData {
+ SkRect fDevBounds; // Glyph bounds mapped to device space.
+ };
+
+ size_t closestGlyph(const SkPoint& pt) const;
+
+ const std::unique_ptr<skottie::TextPropertyHandle> fTextProp;
+
+ std::vector<GlyphData> fGlyphData;
+ std::tuple<size_t, size_t> fSelection = {0, std::numeric_limits<size_t>::max()};
+ bool fEnabled = false;
+ bool fMouseDown = false;
+};
+
+#endif // SkottieTextEditor_DEFINED