[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 <fmalita@chromium.org>
Commit-Queue: Florin Malita <fmalita@chromium.org>
This commit is contained in:
Florin Malita 2018-01-19 15:07:29 -05:00 committed by Skia Commit-Bot
parent a99b393995
commit 16d0ad06b4
8 changed files with 224 additions and 80 deletions

View File

@ -1388,6 +1388,7 @@ if (skia_enable_tools) {
"experimental/sksg/effects/SkSGMaskEffect.cpp",
"experimental/sksg/effects/SkSGOpacityEffect.cpp",
"experimental/sksg/effects/SkSGTransform.cpp",
"experimental/sksg/geometry/SkSGGeometryTransform.cpp",
"experimental/sksg/geometry/SkSGMerge.cpp",
"experimental/sksg/geometry/SkSGPath.cpp",
"experimental/sksg/geometry/SkSGRect.cpp",

View File

@ -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<sksg::RenderNode> AttachOpacity(const Json::Value& jtransform, AttachConte
return opacityNode;
}
sk_sp<sksg::RenderNode> AttachShape(const Json::Value&, AttachContext* ctx);
sk_sp<sksg::RenderNode> AttachComposition(const Json::Value&, AttachContext* ctx);
sk_sp<sksg::RenderNode> AttachShapeGroup(const Json::Value& jgroup, AttachContext* ctx) {
SkASSERT(jgroup.isObject());
return AttachShape(jgroup["it"], ctx);
}
sk_sp<sksg::GeometryNode> AttachPathGeometry(const Json::Value& jpath, AttachContext* ctx) {
SkASSERT(jpath.isObject());
@ -502,11 +496,6 @@ static constexpr PaintAttacherT gPaintAttachers[] = {
AttachGradientStroke,
};
using GroupAttacherT = sk_sp<sksg::RenderNode> (*)(const Json::Value&, AttachContext*);
static constexpr GroupAttacherT gGroupAttachers[] = {
AttachShapeGroup,
};
using GeometryEffectAttacherT =
std::vector<sk_sp<sksg::GeometryNode>> (*)(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<const ShapeInfo*>(info);
}
sk_sp<sksg::RenderNode> AttachShape(const Json::Value& shapeArray, AttachContext* ctx) {
if (!shapeArray.isArray())
struct GeometryEffectRec {
const Json::Value& fJson;
GeometryEffectAttacherT fAttach;
};
sk_sp<sksg::RenderNode> AttachShape(const Json::Value& jshape, AttachContext* ctx,
std::vector<sk_sp<sksg::GeometryNode>>* geometryStack,
std::vector<GeometryEffectRec>* 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<sksg::Group> shape_group = sksg::Group::Make();
sk_sp<sksg::RenderNode> xformed_group = shape_group;
SkDEBUGCODE(const auto initialGeometryEffects = geometryEffectStack->size();)
std::vector<sk_sp<sksg::GeometryNode>> geos;
std::vector<sk_sp<sksg::RenderNode>> draws;
sk_sp<sksg::Group> shape_group = sksg::Group::Make();
sk_sp<sksg::RenderNode> shape_wrapper = shape_group;
sk_sp<sksg::Matrix> 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<ShapeRec> 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<sk_sp<sksg::GeometryNode>> geos;
std::vector<sk_sp<sksg::RenderNode >> 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<sksg::RenderNode> AttachCompLayer(const Json::Value& layer, AttachContext* ctx) {
@ -739,7 +777,9 @@ sk_sp<sksg::RenderNode> AttachShapeLayer(const Json::Value& layer, AttachContext
LOG("** Attaching shape layer ind: %d\n", ParseInt(layer["ind"], 0));
return AttachShape(layer["shapes"], ctx);
std::vector<sk_sp<sksg::GeometryNode>> geometryStack;
std::vector<GeometryEffectRec> geometryEffectStack;
return AttachShape(layer["shapes"], ctx, &geometryStack, &geometryEffectStack);
}
sk_sp<sksg::RenderNode> AttachTextLayer(const Json::Value& layer, AttachContext*) {

View File

@ -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

View File

@ -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<Transform> Make(sk_sp<RenderNode> child, sk_sp<Matrix> matrix) {
return child && matrix
@ -64,14 +64,14 @@ public:
const sk_sp<Matrix>& getMatrix() const { return fMatrix; }
protected:
Transform(sk_sp<RenderNode>, sk_sp<Matrix>);
void onRender(SkCanvas*) const override;
SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
private:
sk_sp<Matrix> fMatrix;
Transform(sk_sp<RenderNode>, sk_sp<Matrix>);
const sk_sp<Matrix> fMatrix;
typedef EffectNode INHERITED;
};

View File

@ -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<GeometryNode> child, sk_sp<Matrix> 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

View File

@ -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<GeometryTransform> Make(sk_sp<GeometryNode> child, sk_sp<Matrix> matrix) {
return child && matrix
? sk_sp<GeometryTransform>(new GeometryTransform(std::move(child), std::move(matrix)))
: nullptr;
}
static sk_sp<GeometryTransform> Make(sk_sp<GeometryNode> child, const SkMatrix& m) {
return Make(std::move(child), Matrix::Make(m));
}
~GeometryTransform() override;
const sk_sp<Matrix>& 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<GeometryNode>, sk_sp<Matrix>);
const sk_sp<GeometryNode> fChild;
const sk_sp<Matrix> fMatrix;
SkPath fTransformed;
typedef GeometryNode INHERITED;
};
}
#endif // SkSGGeometryTransform_DEFINED

View File

@ -27,7 +27,6 @@ Merge::~Merge() {
}
void Merge::onDraw(SkCanvas* canvas, const SkPaint& paint) const {
SkASSERT(!this->hasInval());
canvas->drawPath(fMerged, paint);
}

View File

@ -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();