/* * Copyright 2018 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "SkottieAdapter.h" #include "SkFont.h" #include "SkMatrix.h" #include "SkMatrix44.h" #include "SkPath.h" #include "SkRRect.h" #include "SkSGColor.h" #include "SkSGDraw.h" #include "SkSGGradient.h" #include "SkSGGroup.h" #include "SkSGPath.h" #include "SkSGRect.h" #include "SkSGRenderEffect.h" #include "SkSGText.h" #include "SkSGTransform.h" #include "SkSGTrimEffect.h" #include "SkShaper.h" #include "SkTextBlob.h" #include "SkTextUtils.h" #include "SkTo.h" #include "SkUTF.h" #include "SkottieValue.h" #include <cmath> #include <utility> namespace skottie { RRectAdapter::RRectAdapter(sk_sp<sksg::RRect> wrapped_node) : fRRectNode(std::move(wrapped_node)) {} RRectAdapter::~RRectAdapter() = default; void RRectAdapter::apply() { // BM "position" == "center position" auto rr = SkRRect::MakeRectXY(SkRect::MakeXYWH(fPosition.x() - fSize.width() / 2, fPosition.y() - fSize.height() / 2, fSize.width(), fSize.height()), fRadius.width(), fRadius.height()); fRRectNode->setRRect(rr); } TransformAdapter2D::TransformAdapter2D(sk_sp<sksg::Matrix<SkMatrix>> matrix) : fMatrixNode(std::move(matrix)) {} TransformAdapter2D::~TransformAdapter2D() = default; SkMatrix TransformAdapter2D::totalMatrix() const { SkMatrix t = SkMatrix::MakeTrans(-fAnchorPoint.x(), -fAnchorPoint.y()); t.postScale(fScale.x() / 100, fScale.y() / 100); // 100% based t.postRotate(fRotation); t.postTranslate(fPosition.x(), fPosition.y()); // TODO: skew return t; } void TransformAdapter2D::apply() { fMatrixNode->setMatrix(this->totalMatrix()); } TransformAdapter3D::Vec3::Vec3(const VectorValue& v) { fX = v.size() > 0 ? v[0] : 0; fY = v.size() > 1 ? v[1] : 0; fZ = v.size() > 2 ? v[2] : 0; } TransformAdapter3D::TransformAdapter3D(sk_sp<sksg::Matrix<SkMatrix44>> matrix) : fMatrixNode(std::move(matrix)) {} TransformAdapter3D::~TransformAdapter3D() = default; SkMatrix44 TransformAdapter3D::totalMatrix() const { SkMatrix44 t; t.setTranslate(-fAnchorPoint.fX, -fAnchorPoint.fY, -fAnchorPoint.fZ); t.postScale(fScale.fX / 100, fScale.fY / 100, fScale.fZ / 100); // TODO: SkMatrix44:postRotate()? SkMatrix44 r; r.setRotateDegreesAbout(1, 0, 0, fRotation.fX); t.postConcat(r); r.setRotateDegreesAbout(0, 1, 0, fRotation.fY); t.postConcat(r); r.setRotateDegreesAbout(0, 0, 1, fRotation.fZ); t.postConcat(r); t.postTranslate(fPosition.fX, fPosition.fY, fPosition.fZ); return t; } void TransformAdapter3D::apply() { fMatrixNode->setMatrix(this->totalMatrix()); } RepeaterAdapter::RepeaterAdapter(sk_sp<sksg::RenderNode> repeater_node, Composite composite) : fRepeaterNode(repeater_node) , fComposite(composite) , fRoot(sksg::Group::Make()) {} RepeaterAdapter::~RepeaterAdapter() = default; void RepeaterAdapter::apply() { static constexpr SkScalar kMaxCount = 512; const auto count = static_cast<size_t>(SkTPin(fCount, 0.0f, kMaxCount) + 0.5f); const auto& compute_transform = [this] (size_t index) { const auto t = fOffset + index; // Position, scale & rotation are "scaled" by index/offset. SkMatrix m = SkMatrix::MakeTrans(-fAnchorPoint.x(), -fAnchorPoint.y()); m.postScale(std::pow(fScale.x() * .01f, fOffset), std::pow(fScale.y() * .01f, fOffset)); m.postRotate(t * fRotation); m.postTranslate(t * fPosition.x() + fAnchorPoint.x(), t * fPosition.y() + fAnchorPoint.y()); return m; }; // TODO: start/end opacity support. // TODO: we can avoid rebuilding all the fragments in most cases. fRoot->clear(); for (size_t i = 0; i < count; ++i) { const auto insert_index = (fComposite == Composite::kAbove) ? i : count - i - 1; fRoot->addChild(sksg::TransformEffect::Make(fRepeaterNode, compute_transform(insert_index))); } } PolyStarAdapter::PolyStarAdapter(sk_sp<sksg::Path> wrapped_node, Type t) : fPathNode(std::move(wrapped_node)) , fType(t) {} PolyStarAdapter::~PolyStarAdapter() = default; void PolyStarAdapter::apply() { static constexpr int kMaxPointCount = 100000; const auto count = SkToUInt(SkTPin(SkScalarRoundToInt(fPointCount), 0, kMaxPointCount)); const auto arc = sk_ieee_float_divide(SK_ScalarPI * 2, count); const auto pt_on_circle = [](const SkPoint& c, SkScalar r, SkScalar a) { return SkPoint::Make(c.x() + r * std::cos(a), c.y() + r * std::sin(a)); }; // TODO: inner/outer "roundness"? SkPath poly; auto angle = SkDegreesToRadians(fRotation - 90); poly.moveTo(pt_on_circle(fPosition, fOuterRadius, angle)); poly.incReserve(fType == Type::kStar ? count * 2 : count); for (unsigned i = 0; i < count; ++i) { if (fType == Type::kStar) { poly.lineTo(pt_on_circle(fPosition, fInnerRadius, angle + arc * 0.5f)); } angle += arc; poly.lineTo(pt_on_circle(fPosition, fOuterRadius, angle)); } poly.close(); fPathNode->setPath(poly); } GradientAdapter::GradientAdapter(sk_sp<sksg::Gradient> grad, size_t stopCount) : fGradient(std::move(grad)) , fStopCount(stopCount) {} void GradientAdapter::apply() { this->onApply(); // |fColorStops| holds |fStopCount| x [ pos, r, g, g ] + ? x [ pos, alpha ] if (fColorStops.size() < fStopCount * 4 || ((fColorStops.size() - fStopCount * 4) % 2)) { // apply() may get called before the stops are set, so only log when we have some stops. if (!fColorStops.empty()) { SkDebugf("!! Invalid gradient stop array size: %zu\n", fColorStops.size()); } return; } std::vector<sksg::Gradient::ColorStop> stops; // TODO: merge/lerp opacity stops const auto csEnd = fColorStops.cbegin() + fStopCount * 4; for (auto cs = fColorStops.cbegin(); cs != csEnd; cs += 4) { const auto pos = cs[0]; const VectorValue rgb({ cs[1], cs[2], cs[3] }); stops.push_back({ pos, ValueTraits<VectorValue>::As<SkColor>(rgb) }); } fGradient->setColorStops(std::move(stops)); } LinearGradientAdapter::LinearGradientAdapter(sk_sp<sksg::LinearGradient> grad, size_t stopCount) : INHERITED(std::move(grad), stopCount) {} void LinearGradientAdapter::onApply() { auto* grad = static_cast<sksg::LinearGradient*>(fGradient.get()); grad->setStartPoint(this->startPoint()); grad->setEndPoint(this->endPoint()); } RadialGradientAdapter::RadialGradientAdapter(sk_sp<sksg::RadialGradient> grad, size_t stopCount) : INHERITED(std::move(grad), stopCount) {} void RadialGradientAdapter::onApply() { auto* grad = static_cast<sksg::RadialGradient*>(fGradient.get()); grad->setStartCenter(this->startPoint()); grad->setEndCenter(this->startPoint()); grad->setStartRadius(0); grad->setEndRadius(SkPoint::Distance(this->startPoint(), this->endPoint())); } TrimEffectAdapter::TrimEffectAdapter(sk_sp<sksg::TrimEffect> trimEffect) : fTrimEffect(std::move(trimEffect)) { SkASSERT(fTrimEffect); } TrimEffectAdapter::~TrimEffectAdapter() = default; void TrimEffectAdapter::apply() { // BM semantics: start/end are percentages, offset is "degrees" (?!). const auto start = fStart / 100, end = fEnd / 100, offset = fOffset / 360; auto startT = SkTMin(start, end) + offset, stopT = SkTMax(start, end) + offset; auto mode = SkTrimPathEffect::Mode::kNormal; if (stopT - startT < 1) { startT -= SkScalarFloorToScalar(startT); stopT -= SkScalarFloorToScalar(stopT); if (startT > stopT) { using std::swap; swap(startT, stopT); mode = SkTrimPathEffect::Mode::kInverted; } } else { startT = 0; stopT = 1; } fTrimEffect->setStart(startT); fTrimEffect->setStop(stopT); fTrimEffect->setMode(mode); } DropShadowEffectAdapter::DropShadowEffectAdapter(sk_sp<sksg::DropShadowImageFilter> dropShadow) : fDropShadow(std::move(dropShadow)) { SkASSERT(fDropShadow); } DropShadowEffectAdapter::~DropShadowEffectAdapter() = default; void DropShadowEffectAdapter::apply() { // fColor -> RGB, fOpacity -> A fDropShadow->setColor(SkColorSetA(fColor, SkTPin(SkScalarRoundToInt(fOpacity), 0, 255))); // The offset is specified in terms of a bearing angle + distance. SkScalar sinV, cosV; sinV = SkScalarSinCos(SkDegreesToRadians(90 - fDirection), &cosV); fDropShadow->setOffset(SkVector::Make(fDistance * cosV, -fDistance * sinV)); // Close enough to AE. static constexpr SkScalar kSoftnessToSigmaFactor = 0.3f; const auto sigma = fSoftness * kSoftnessToSigmaFactor; fDropShadow->setSigma(SkVector::Make(sigma, sigma)); fDropShadow->setMode(fShadowOnly ? sksg::DropShadowImageFilter::Mode::kShadowOnly : sksg::DropShadowImageFilter::Mode::kShadowAndForeground); } GaussianBlurEffectAdapter::GaussianBlurEffectAdapter(sk_sp<sksg::BlurImageFilter> blur) : fBlur(std::move(blur)) { SkASSERT(fBlur); } GaussianBlurEffectAdapter::~GaussianBlurEffectAdapter() = default; void GaussianBlurEffectAdapter::apply() { static constexpr SkVector kDimensionsMap[] = { { 1, 1 }, // 1 -> horizontal and vertical { 1, 0 }, // 2 -> horizontal { 0, 1 }, // 3 -> vertical }; const auto dim_index = SkTPin<size_t>(static_cast<size_t>(fDimensions), 1, SK_ARRAY_COUNT(kDimensionsMap)) - 1; // Close enough to AE. static constexpr SkScalar kBlurrinessToSigmaFactor = 0.3f; const auto sigma = fBlurriness * kBlurrinessToSigmaFactor; fBlur->setSigma({ sigma * kDimensionsMap[dim_index].x(), sigma * kDimensionsMap[dim_index].y() }); static constexpr SkBlurImageFilter::TileMode kRepeatEdgeMap[] = { SkBlurImageFilter::kClampToBlack_TileMode, // 0 -> repeat edge pixels: off SkBlurImageFilter:: kClamp_TileMode, // 1 -> repeat edge pixels: on }; const auto repeat_index = SkTPin<size_t>(static_cast<size_t>(fRepeatEdge), 0, SK_ARRAY_COUNT(kRepeatEdgeMap) - 1); fBlur->setTileMode(kRepeatEdgeMap[repeat_index]); } TextAdapter::TextAdapter(sk_sp<sksg::Group> root) : fRoot(std::move(root)) , fTextNode(sksg::TextBlob::Make()) , fFillColor(sksg::Color::Make(SK_ColorTRANSPARENT)) , fStrokeColor(sksg::Color::Make(SK_ColorTRANSPARENT)) , fFillNode(sksg::Draw::Make(fTextNode, fFillColor)) , fStrokeNode(sksg::Draw::Make(fTextNode, fStrokeColor)) , fHadFill(false) , fHadStroke(false) { // Build a SG fragment with the following general format: // // [Group] // [Draw] // [FillPaint] // [Text]* // [Draw] // [StrokePaint] // [Text]* // // * where the text node is shared fFillColor->setAntiAlias(true); fStrokeColor->setAntiAlias(true); fStrokeColor->setStyle(SkPaint::kStroke_Style); } TextAdapter::~TextAdapter() = default; sk_sp<SkTextBlob> TextAdapter::makeBlob() const { SkFont font(fText.fTypeface, fText.fTextSize); font.setHinting(kNo_SkFontHinting); font.setSubpixel(true); font.setEdging(SkFont::Edging::kAntiAlias); // Helper for interfacing with SkShaper: buffers shaper-fed runs and performs // per-line position adjustments (for external line breaking, horizontal alignment, etc). class BlobMaker final : public SkShaper::RunHandler { public: BlobMaker(SkTextUtils::Align align) : fAlignFactor(AlignFactor(align)) {} Buffer newRunBuffer(const RunInfo& info, const SkFont& font, int glyphCount, SkSpan<const char> utf8) override { fPendingLineAdvance += info.fAdvance; auto& run = fPendingLineRuns.emplace_back(font, info, glyphCount); return { run.fGlyphs .data(), run.fPositions.data(), nullptr, }; } void commitRun() override { } void commitLine() override { SkScalar line_spacing = 0; for (const auto& run : fPendingLineRuns) { const auto runSize = run.size(); const auto& blobBuffer = fBuilder.allocRunPos(run.fFont, SkToInt(runSize)); sk_careful_memcpy(blobBuffer.glyphs, run.fGlyphs.data(), runSize * sizeof(SkGlyphID)); // For each buffered line, perform the following position adjustments: // 1) horizontal alignment // 2) vertical advance (based on line number/offset) // 3) baseline/ascent adjustment const auto offset = SkVector::Make(fAlignFactor * fPendingLineAdvance.x(), fPendingLineVOffset + run.fInfo.fAscent); for (size_t i = 0; i < runSize; ++i) { blobBuffer.points()[i] = run.fPositions[SkToInt(i)] + offset; } line_spacing = SkTMax(line_spacing, run.fInfo.fDescent - run.fInfo.fAscent + run.fInfo.fLeading); } fPendingLineRuns.reset(); fPendingLineVOffset += line_spacing; fPendingLineAdvance = { 0, 0 }; } sk_sp<SkTextBlob> makeBlob() { return fBuilder.make(); } private: static float AlignFactor(SkTextUtils::Align align) { switch (align) { case SkTextUtils::kLeft_Align: return 0.0f; case SkTextUtils::kCenter_Align: return -0.5f; case SkTextUtils::kRight_Align: return -1.0f; } return 0.0f; // go home, msvc... } struct Run { SkFont fFont; SkShaper::RunHandler::RunInfo fInfo; SkSTArray<128, SkGlyphID, true> fGlyphs; SkSTArray<128, SkPoint , true> fPositions; Run(const SkFont& font, const SkShaper::RunHandler::RunInfo& info, int count) : fFont(font) , fInfo(info) , fGlyphs (count) , fPositions(count) { fGlyphs .push_back_n(count); fPositions.push_back_n(count); } size_t size() const { SkASSERT(fGlyphs.size() == fPositions.size()); return fGlyphs.size(); } }; const float fAlignFactor; SkTextBlobBuilder fBuilder; SkSTArray<2, Run, false> fPendingLineRuns; SkScalar fPendingLineVOffset = 0; SkVector fPendingLineAdvance = { 0, 0 }; }; BlobMaker blobMaker(fText.fAlign); const auto& push_line = [&](const char* start, const char* end) { std::unique_ptr<SkShaper> shaper = SkShaper::Make(); if (!shaper) { return; } shaper->shape(&blobMaker, font, start, SkToSizeT(end - start), true, { 0, 0 }, SK_ScalarMax); }; const auto& is_line_break = [](SkUnichar uch) { // TODO: other explicit breaks? return uch == '\r'; }; const char* ptr = fText.fText.c_str(); const char* line_start = ptr; const char* end = ptr + fText.fText.size(); while (ptr < end) { if (is_line_break(SkUTF::NextUTF8(&ptr, end))) { push_line(line_start, ptr - 1); line_start = ptr; } } push_line(line_start, ptr); return blobMaker.makeBlob(); } void TextAdapter::apply() { fTextNode->setBlob(this->makeBlob()); fFillColor->setColor(fText.fFillColor); fStrokeColor->setColor(fText.fStrokeColor); fStrokeColor->setStrokeWidth(fText.fStrokeWidth); // Turn the state transition into a tri-state value: // -1: detach node // 0: no change // 1: attach node const auto fill_change = SkToInt(fText.fHasFill) - SkToInt(fHadFill); const auto stroke_change = SkToInt(fText.fHasStroke) - SkToInt(fHadStroke); // Sync SG topology. if (fill_change || stroke_change) { // This is trickier than it should be because sksg::Group only allows adding children // in paint-order. if (stroke_change < 0 || (fHadStroke && fill_change > 0)) { fRoot->removeChild(fStrokeNode); } if (fill_change < 0) { fRoot->removeChild(fFillNode); } else if (fill_change > 0) { fRoot->addChild(fFillNode); } if (stroke_change > 0 || (fHadStroke && fill_change > 0)) { fRoot->addChild(fStrokeNode); } } // Track current state. fHadFill = fText.fHasFill; fHadStroke = fText.fHasStroke; } } // namespace skottie