From 16d0ad06b46841c78cce816406574314fab95c13 Mon Sep 17 00:00:00 2001 From: Florin Malita Date: Fri, 19 Jan 2018 15:07:29 -0500 Subject: [skottie,sksg] Improved shape group semantics * paints also apply to preceding nested geometries * path effects also apply to preceding nested geometries TBR= Change-Id: Ic72f8d032fb5823f506ff688630b786a23219f20 Reviewed-on: https://skia-review.googlesource.com/97222 Reviewed-by: Florin Malita Commit-Queue: Florin Malita --- experimental/skottie/Skottie.cpp | 174 +++++++++++++-------- experimental/sksg/SkSGNode.h | 13 +- experimental/sksg/effects/SkSGTransform.h | 8 +- .../sksg/geometry/SkSGGeometryTransform.cpp | 49 ++++++ experimental/sksg/geometry/SkSGGeometryTransform.h | 56 +++++++ experimental/sksg/geometry/SkSGMerge.cpp | 1 - experimental/sksg/geometry/SkSGTrimEffect.cpp | 2 - 7 files changed, 223 insertions(+), 80 deletions(-) create mode 100644 experimental/sksg/geometry/SkSGGeometryTransform.cpp create mode 100644 experimental/sksg/geometry/SkSGGeometryTransform.h (limited to 'experimental') diff --git a/experimental/skottie/Skottie.cpp b/experimental/skottie/Skottie.cpp index f8c5ea021c..696f619f83 100644 --- a/experimental/skottie/Skottie.cpp +++ b/experimental/skottie/Skottie.cpp @@ -21,6 +21,7 @@ #include "SkPoint.h" #include "SkSGColor.h" #include "SkSGDraw.h" +#include "SkSGGeometryTransform.h" #include "SkSGGradient.h" #include "SkSGGroup.h" #include "SkSGImage.h" @@ -169,15 +170,8 @@ sk_sp AttachOpacity(const Json::Value& jtransform, AttachConte return opacityNode; } -sk_sp AttachShape(const Json::Value&, AttachContext* ctx); sk_sp AttachComposition(const Json::Value&, AttachContext* ctx); -sk_sp AttachShapeGroup(const Json::Value& jgroup, AttachContext* ctx) { - SkASSERT(jgroup.isObject()); - - return AttachShape(jgroup["it"], ctx); -} - sk_sp AttachPathGeometry(const Json::Value& jpath, AttachContext* ctx) { SkASSERT(jpath.isObject()); @@ -502,11 +496,6 @@ static constexpr PaintAttacherT gPaintAttachers[] = { AttachGradientStroke, }; -using GroupAttacherT = sk_sp (*)(const Json::Value&, AttachContext*); -static constexpr GroupAttacherT gGroupAttachers[] = { - AttachShapeGroup, -}; - using GeometryEffectAttacherT = std::vector> (*)(const Json::Value&, AttachContext*, @@ -535,7 +524,7 @@ const ShapeInfo* FindShapeInfo(const Json::Value& shape) { { "el", ShapeType::kGeometry , 2 }, // ellipse -> AttachEllipseGeometry { "fl", ShapeType::kPaint , 0 }, // fill -> AttachColorFill { "gf", ShapeType::kPaint , 2 }, // gfill -> AttachGradientFill - { "gr", ShapeType::kGroup , 0 }, // group -> AttachShapeGroup + { "gr", ShapeType::kGroup , 0 }, // group -> Inline handler { "gs", ShapeType::kPaint , 3 }, // gstroke -> AttachGradientStroke { "mm", ShapeType::kGeometryEffect, 0 }, // merge -> AttachMergeGeometryEffect { "rc", ShapeType::kGeometry , 1 }, // rrect -> AttachRRectGeometry @@ -543,7 +532,7 @@ const ShapeInfo* FindShapeInfo(const Json::Value& shape) { { "sr", ShapeType::kGeometry , 3 }, // polystar -> AttachPolyStarGeometry { "st", ShapeType::kPaint , 1 }, // stroke -> AttachColorStroke { "tm", ShapeType::kGeometryEffect, 1 }, // trim -> AttachTrimGeometryEffect - { "tr", ShapeType::kTransform , 0 }, // transform -> In-place handler + { "tr", ShapeType::kTransform , 0 }, // transform -> Inline handler }; if (!shape.isObject()) @@ -565,87 +554,136 @@ const ShapeInfo* FindShapeInfo(const Json::Value& shape) { return static_cast(info); } -sk_sp AttachShape(const Json::Value& shapeArray, AttachContext* ctx) { - if (!shapeArray.isArray()) +struct GeometryEffectRec { + const Json::Value& fJson; + GeometryEffectAttacherT fAttach; +}; + +sk_sp AttachShape(const Json::Value& jshape, AttachContext* ctx, + std::vector>* geometryStack, + std::vector* geometryEffectStack) { + if (!jshape.isArray()) return nullptr; - // (https://helpx.adobe.com/after-effects/using/overview-shape-layers-paths-vector.html#groups_and_render_order_for_shapes_and_shape_attributes) - // - // Render order for shapes within a shape layer - // - // The rules for rendering a shape layer are similar to the rules for rendering a composition - // that contains nested compositions: - // - // * Within a group, the shape at the bottom of the Timeline panel stacking order is rendered - // first. - // - // * All path operations within a group are performed before paint operations. This means, - // for example, that the stroke follows the distortions in the path made by the Wiggle Paths - // path operation. Path operations within a group are performed from top to bottom. - // - // * Paint operations within a group are performed from the bottom to the top in the Timeline - // panel stacking order. This means, for example, that a stroke is rendered on top of - // (in front of) a stroke that appears after it in the Timeline panel. - // - sk_sp shape_group = sksg::Group::Make(); - sk_sp xformed_group = shape_group; + SkDEBUGCODE(const auto initialGeometryEffects = geometryEffectStack->size();) - std::vector> geos; - std::vector> draws; + sk_sp shape_group = sksg::Group::Make(); + sk_sp shape_wrapper = shape_group; + sk_sp shape_matrix; - for (const auto& s : shapeArray) { + struct ShapeRec { + const Json::Value& fJson; + const ShapeInfo& fInfo; + }; + + // First pass (bottom->top): + // + // * pick up the group transform and opacity + // * push local geometry effects onto the stack + // * store recs for next pass + // + std::vector recs; + for (Json::ArrayIndex i = 0; i < jshape.size(); ++i) { + const auto& s = jshape[jshape.size() - 1 - i]; const auto* info = FindShapeInfo(s); if (!info) { LogFail(s.isObject() ? s["ty"] : s, "Unknown shape"); continue; } + recs.push_back({ s, *info }); + switch (info->fShapeType) { + case ShapeType::kTransform: + if ((shape_matrix = AttachMatrix(s, ctx, nullptr))) { + shape_wrapper = sksg::Transform::Make(std::move(shape_wrapper), shape_matrix); + } + shape_wrapper = AttachOpacity(s, ctx, std::move(shape_wrapper)); + break; + case ShapeType::kGeometryEffect: + SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers)); + geometryEffectStack->push_back( + { s, gGeometryEffectAttachers[info->fAttacherIndex] }); + break; + default: + break; + } + } + + // Second pass (top -> bottom, after 2x reverse): + // + // * track local geometry + // * emit local paints + // + std::vector> geos; + std::vector> draws; + for (auto rec = recs.rbegin(); rec != recs.rend(); ++rec) { + switch (rec->fInfo.fShapeType) { case ShapeType::kGeometry: { - SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryAttachers)); - if (auto geo = gGeometryAttachers[info->fAttacherIndex](s, ctx)) { + SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryAttachers)); + if (auto geo = gGeometryAttachers[rec->fInfo.fAttacherIndex](rec->fJson, ctx)) { geos.push_back(std::move(geo)); } } break; case ShapeType::kGeometryEffect: { - SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers)); - geos = gGeometryEffectAttachers[info->fAttacherIndex](s, ctx, std::move(geos)); - } break; - case ShapeType::kPaint: { - SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gPaintAttachers)); - if (auto paint = gPaintAttachers[info->fAttacherIndex](s, ctx)) { - for (const auto& geo : geos) { - draws.push_back(sksg::Draw::Make(geo, paint)); - } - } + // Apply the current effect and pop from the stack. + SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers)); + geos = gGeometryEffectAttachers[rec->fInfo.fAttacherIndex](rec->fJson, + ctx, + std::move(geos)); + + SkASSERT(geometryEffectStack->back().fJson == rec->fJson); + SkASSERT(geometryEffectStack->back().fAttach == + gGeometryEffectAttachers[rec->fInfo.fAttacherIndex]); + geometryEffectStack->pop_back(); } break; case ShapeType::kGroup: { - SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGroupAttachers)); - if (auto group = gGroupAttachers[info->fAttacherIndex](s, ctx)) { - draws.push_back(std::move(group)); + if (auto subgroup = AttachShape(rec->fJson["it"], ctx, &geos, geometryEffectStack)) { + draws.push_back(std::move(subgroup)); } } break; - case ShapeType::kTransform: { - // TODO: BM appears to transform the geometry, not the draw op itself. - if (auto matrix = AttachMatrix(s, ctx, nullptr)) { - xformed_group = sksg::Transform::Make(std::move(xformed_group), - std::move(matrix)); + case ShapeType::kPaint: { + SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gPaintAttachers)); + auto paint = gPaintAttachers[rec->fInfo.fAttacherIndex](rec->fJson, ctx); + if (!paint || geos.empty()) + break; + + auto drawGeos = geos; + + // Apply all pending effects from the stack. + for (auto it = geometryEffectStack->rbegin(); it != geometryEffectStack->rend(); ++it) { + drawGeos = it->fAttach(it->fJson, ctx, std::move(drawGeos)); } - xformed_group = AttachOpacity(s, ctx, std::move(xformed_group)); + + // If we still have multiple geos, reduce using 'merge'. + auto geo = drawGeos.size() > 1 + ? sksg::Merge::Make(std::move(drawGeos), sksg::Merge::Mode::kMerge) + : drawGeos[0]; + + SkASSERT(geo); + draws.push_back(sksg::Draw::Make(std::move(geo), std::move(paint))); } break; + default: + break; } } - if (draws.empty()) { - return nullptr; + // By now we should have popped all local geometry effects. + SkASSERT(geometryEffectStack->size() == initialGeometryEffects); + + // Push transformed local geometries to parent list, for subsequent paints. + for (const auto& geo : geos) { + geometryStack->push_back(shape_matrix + ? sksg::GeometryTransform::Make(std::move(geo), shape_matrix) + : std::move(geo)); } - for (auto draw = draws.rbegin(); draw != draws.rend(); ++draw) { - shape_group->addChild(std::move(*draw)); + // Emit local draws reversed (bottom->top, per spec). + for (auto it = draws.rbegin(); it != draws.rend(); ++it) { + shape_group->addChild(std::move(*it)); } - LOG("** Attached shape: %zd draws.\n", draws.size()); - return xformed_group; + return draws.empty() ? nullptr : shape_wrapper; } sk_sp AttachCompLayer(const Json::Value& layer, AttachContext* ctx) { @@ -739,7 +777,9 @@ sk_sp AttachShapeLayer(const Json::Value& layer, AttachContext LOG("** Attaching shape layer ind: %d\n", ParseInt(layer["ind"], 0)); - return AttachShape(layer["shapes"], ctx); + std::vector> geometryStack; + std::vector geometryEffectStack; + return AttachShape(layer["shapes"], ctx, &geometryStack, &geometryEffectStack); } sk_sp AttachTextLayer(const Json::Value& layer, AttachContext*) { diff --git a/experimental/sksg/SkSGNode.h b/experimental/sksg/SkSGNode.h index 37139339f2..5518984e51 100644 --- a/experimental/sksg/SkSGNode.h +++ b/experimental/sksg/SkSGNode.h @@ -70,6 +70,7 @@ private: // TODO: too friendly, find another way. friend class Draw; friend class EffectNode; + friend class GeometryTransform; friend class Group; friend class MaskEffect; friend class Matrix; @@ -95,12 +96,12 @@ private: }; // Helper for defining attribute getters/setters in subclasses. -#define SG_ATTRIBUTE(attr_name, attr_type, attr_container) \ - attr_type get##attr_name() const { return attr_container; } \ - void set##attr_name(attr_type v) { \ - if (attr_container == v) return; \ - attr_container = v; \ - this->invalidate(); \ +#define SG_ATTRIBUTE(attr_name, attr_type, attr_container) \ + const attr_type& get##attr_name() const { return attr_container; } \ + void set##attr_name(const attr_type& v) { \ + if (attr_container == v) return; \ + attr_container = v; \ + this->invalidate(); \ } } // namespace sksg diff --git a/experimental/sksg/effects/SkSGTransform.h b/experimental/sksg/effects/SkSGTransform.h index 0694117a2a..6b7fbc010b 100644 --- a/experimental/sksg/effects/SkSGTransform.h +++ b/experimental/sksg/effects/SkSGTransform.h @@ -47,7 +47,7 @@ private: /** * Concrete Effect node, binding a Matrix to a RenderNode. */ -class Transform : public EffectNode { +class Transform final : public EffectNode { public: static sk_sp Make(sk_sp child, sk_sp matrix) { return child && matrix @@ -64,14 +64,14 @@ public: const sk_sp& getMatrix() const { return fMatrix; } protected: - Transform(sk_sp, sk_sp); - void onRender(SkCanvas*) const override; SkRect onRevalidate(InvalidationController*, const SkMatrix&) override; private: - sk_sp fMatrix; + Transform(sk_sp, sk_sp); + + const sk_sp fMatrix; typedef EffectNode INHERITED; }; diff --git a/experimental/sksg/geometry/SkSGGeometryTransform.cpp b/experimental/sksg/geometry/SkSGGeometryTransform.cpp new file mode 100644 index 0000000000..14c37d98b9 --- /dev/null +++ b/experimental/sksg/geometry/SkSGGeometryTransform.cpp @@ -0,0 +1,49 @@ +/* + * 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 "SkSGGeometryTransform.h" + +#include "SkCanvas.h" + +namespace sksg { + +GeometryTransform::GeometryTransform(sk_sp child, sk_sp matrix) + : fChild(std::move(child)) + , fMatrix(std::move(matrix)) { + fChild->addInvalReceiver(this); + fMatrix->addInvalReceiver(this); +} + +GeometryTransform::~GeometryTransform() { + fChild->removeInvalReceiver(this); + fMatrix->removeInvalReceiver(this); +} + +SkRect GeometryTransform::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) { + SkASSERT(this->hasInval()); + + // We don't care about matrix reval results. + fMatrix->revalidate(ic, ctm); + const auto& m = fMatrix->getMatrix(); + + auto bounds = fChild->revalidate(ic, ctm); + fTransformed = fChild->asPath(); + fTransformed.transform(m); + + m.mapRect(&bounds); + return bounds; +} + +SkPath GeometryTransform::onAsPath() const { + return fTransformed; +} + +void GeometryTransform::onDraw(SkCanvas* canvas, const SkPaint& paint) const { + canvas->drawPath(fTransformed, paint); +} + +} // namespace sksg diff --git a/experimental/sksg/geometry/SkSGGeometryTransform.h b/experimental/sksg/geometry/SkSGGeometryTransform.h new file mode 100644 index 0000000000..31a3371408 --- /dev/null +++ b/experimental/sksg/geometry/SkSGGeometryTransform.h @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkSGGeometryTransform_DEFINED +#define SkSGGeometryTransform_DEFINED + +#include "SkSGGeometryNode.h" + +#include "SkPath.h" +#include "SkSGTransform.h" + +class SkMatrix; + +namespace sksg { + +/** + * Concrete Effect node, binding a Matrix to a GeometryNode. + */ +class GeometryTransform final : public GeometryNode { +public: + static sk_sp Make(sk_sp child, sk_sp matrix) { + return child && matrix + ? sk_sp(new GeometryTransform(std::move(child), std::move(matrix))) + : nullptr; + } + + static sk_sp Make(sk_sp child, const SkMatrix& m) { + return Make(std::move(child), Matrix::Make(m)); + } + + ~GeometryTransform() override; + + const sk_sp& getMatrix() const { return fMatrix; } + +protected: + SkRect onRevalidate(InvalidationController*, const SkMatrix&) override; + SkPath onAsPath() const override; + void onDraw(SkCanvas*, const SkPaint&) const override; + +private: + GeometryTransform(sk_sp, sk_sp); + + const sk_sp fChild; + const sk_sp fMatrix; + SkPath fTransformed; + + typedef GeometryNode INHERITED; +}; + +} + +#endif // SkSGGeometryTransform_DEFINED diff --git a/experimental/sksg/geometry/SkSGMerge.cpp b/experimental/sksg/geometry/SkSGMerge.cpp index f75945d85e..9c22e75a81 100644 --- a/experimental/sksg/geometry/SkSGMerge.cpp +++ b/experimental/sksg/geometry/SkSGMerge.cpp @@ -27,7 +27,6 @@ Merge::~Merge() { } void Merge::onDraw(SkCanvas* canvas, const SkPaint& paint) const { - SkASSERT(!this->hasInval()); canvas->drawPath(fMerged, paint); } diff --git a/experimental/sksg/geometry/SkSGTrimEffect.cpp b/experimental/sksg/geometry/SkSGTrimEffect.cpp index 4c30389877..af0b640a4c 100644 --- a/experimental/sksg/geometry/SkSGTrimEffect.cpp +++ b/experimental/sksg/geometry/SkSGTrimEffect.cpp @@ -26,8 +26,6 @@ TrimEffect::~TrimEffect() { // This is a quick hack to get something on the screen. What we really want here is to apply // the geometry transformation and cache the result on revalidation. Or an SkTrimPathEffect. void TrimEffect::onDraw(SkCanvas* canvas, const SkPaint& paint) const { - SkASSERT(!this->hasInval()); - SkASSERT(!paint.getPathEffect()); const auto path = fChild->asPath(); -- cgit v1.2.3