73e6a66146
Change-Id: Ibfa2c0efa3e4addf21aa400e3d323675ac0185ef Reviewed-on: https://skia-review.googlesource.com/c/skia/+/551886 Reviewed-by: John Stiles <johnstiles@google.com> Commit-Queue: Herb Derby <herb@google.com>
352 lines
14 KiB
C++
352 lines
14 KiB
C++
/*
|
|
* Copyright 2019 Google LLC
|
|
*
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
* found in the LICENSE file.
|
|
*/
|
|
|
|
#include "samplecode/Sample.h"
|
|
|
|
#include "include/core/SkCanvas.h"
|
|
#include "include/core/SkColor.h"
|
|
#include "include/core/SkColorFilter.h"
|
|
#include "include/core/SkFont.h"
|
|
#include "include/core/SkImage.h"
|
|
#include "include/core/SkImageFilter.h"
|
|
#include "include/core/SkImageInfo.h"
|
|
#include "include/core/SkPaint.h"
|
|
#include "include/core/SkPoint.h"
|
|
#include "include/core/SkRect.h"
|
|
#include "include/core/SkSurface.h"
|
|
|
|
#include "include/effects/SkDashPathEffect.h"
|
|
#include "include/effects/SkGradientShader.h"
|
|
#include "include/effects/SkImageFilters.h"
|
|
|
|
#include "src/core/SkImageFilter_Base.h"
|
|
#include "src/core/SkSpecialImage.h"
|
|
|
|
#include "tools/ToolUtils.h"
|
|
|
|
namespace {
|
|
|
|
struct FilterNode {
|
|
// Pointer to the actual filter in the DAG, so it still contains its input filters and
|
|
// may be used as an input in an earlier node. Null when this represents the "source" input
|
|
sk_sp<SkImageFilter> fFilter;
|
|
|
|
// FilterNodes wrapping each of fFilter's inputs. Leaf node when fInputNodes is empty.
|
|
SkTArray<FilterNode> fInputNodes;
|
|
|
|
// Distance from root filter
|
|
int fDepth;
|
|
|
|
// The source content rect (this is the same for all nodes, but is stored here for convenience)
|
|
skif::ParameterSpace<SkRect> fContent;
|
|
// The mapping for the filter dag (same for all nodes, but stored here for convenience)
|
|
skif::Mapping fMapping;
|
|
|
|
// Cached reverse bounds using device-space clip bounds (e.g. no local bounds hint passed to
|
|
// saveLayer). This represents the layer calculated in SkCanvas for the filtering.
|
|
skif::LayerSpace<SkIRect> fUnhintedLayerBounds;
|
|
|
|
// Cached input bounds using the local draw bounds (e.g. saveLayer with a bounds rect, or
|
|
// an auto-layer for a draw with image filter). This represents the layer bounds up to this
|
|
// point of the DAG.
|
|
skif::LayerSpace<SkIRect> fHintedLayerBounds;
|
|
|
|
// Cached output bounds based on local draw bounds. This represents the output up to this
|
|
// point of the DAG.
|
|
skif::LayerSpace<SkIRect> fOutputBounds;
|
|
|
|
FilterNode(const SkImageFilter* filter,
|
|
const skif::Mapping& mapping,
|
|
const skif::ParameterSpace<SkRect>& content,
|
|
int depth)
|
|
: fFilter(sk_ref_sp(filter))
|
|
, fDepth(depth)
|
|
, fContent(content)
|
|
, fMapping(mapping) {
|
|
this->computeInputBounds();
|
|
this->computeOutputBounds();
|
|
if (fFilter) {
|
|
fInputNodes.reserve_back(fFilter->countInputs());
|
|
for (int i = 0; i < fFilter->countInputs(); ++i) {
|
|
fInputNodes.emplace_back(fFilter->getInput(i), mapping, content, depth + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
private:
|
|
void computeOutputBounds() {
|
|
if (fFilter) {
|
|
// For visualization purposes, we want the output bounds in layer space, before it's
|
|
// been transformed to device space. To achieve that, we mock a new mapping with the
|
|
// identity matrix transform.
|
|
skif::Mapping layerOnly{fMapping.layerMatrix()};
|
|
skif::DeviceSpace<SkIRect> pseudoDeviceBounds =
|
|
as_IFB(fFilter)->getOutputBounds(layerOnly, fContent);
|
|
// Since layerOnly's device matrix is I, this is effectively a cast to layer space
|
|
fOutputBounds = layerOnly.deviceToLayer(pseudoDeviceBounds);
|
|
} else {
|
|
fOutputBounds = fMapping.paramToLayer(fContent).roundOut();
|
|
}
|
|
|
|
// Fill in children
|
|
for (int i = 0; i < fInputNodes.count(); ++i) {
|
|
fInputNodes[i].computeOutputBounds();
|
|
}
|
|
}
|
|
|
|
void computeInputBounds() {
|
|
// As a proxy for what the base device had, use the content rect mapped to device space
|
|
// (e.g. clipRect() was called with the same coords prior to the draw).
|
|
skif::DeviceSpace<SkIRect> targetOutput(fMapping.totalMatrix()
|
|
.mapRect(SkRect(fContent))
|
|
.roundOut());
|
|
|
|
if (fFilter) {
|
|
fHintedLayerBounds = as_IFB(fFilter)->getInputBounds(fMapping, targetOutput, &fContent);
|
|
fUnhintedLayerBounds = as_IFB(fFilter)->getInputBounds(fMapping, targetOutput, nullptr);
|
|
} else {
|
|
fHintedLayerBounds = fMapping.paramToLayer(fContent).roundOut();
|
|
fUnhintedLayerBounds = fMapping.deviceToLayer(targetOutput);
|
|
}
|
|
}
|
|
};
|
|
|
|
} // anonymous namespace
|
|
|
|
static FilterNode build_dag(const SkMatrix& ctm, const SkRect& rect,
|
|
const SkImageFilter* rootFilter) {
|
|
// Emulate SkCanvas::internalSaveLayer's decomposition of the CTM.
|
|
skif::ParameterSpace<SkRect> content(rect);
|
|
skif::ParameterSpace<SkPoint> center({rect.centerX(), rect.centerY()});
|
|
skif::Mapping mapping;
|
|
SkAssertResult(mapping.decomposeCTM(ctm, rootFilter, center));
|
|
return FilterNode(rootFilter, mapping, content, 0);
|
|
}
|
|
|
|
static void draw_node(SkCanvas* canvas, const FilterNode& node) {
|
|
canvas->clear(SK_ColorTRANSPARENT);
|
|
|
|
SkPaint filterPaint;
|
|
filterPaint.setImageFilter(node.fFilter);
|
|
|
|
SkRect content = SkRect(node.fContent);
|
|
SkPaint paint;
|
|
static const SkColor kColors[2] = {SK_ColorGREEN, SK_ColorWHITE};
|
|
SkPoint points[2] = { {content.fLeft + 15.f, content.fTop + 15.f},
|
|
{content.fRight - 15.f, content.fBottom - 15.f} };
|
|
paint.setShader(SkGradientShader::MakeLinear(points, kColors, nullptr, std::size(kColors),
|
|
SkTileMode::kRepeat));
|
|
|
|
SkPaint line;
|
|
line.setStrokeWidth(0.f);
|
|
line.setStyle(SkPaint::kStroke_Style);
|
|
|
|
canvas->save();
|
|
canvas->concat(node.fMapping.layerToDevice());
|
|
canvas->save();
|
|
canvas->concat(node.fMapping.layerMatrix());
|
|
|
|
canvas->saveLayer(&content, &filterPaint);
|
|
canvas->drawRect(content, paint);
|
|
canvas->restore(); // Completes the image filter
|
|
|
|
// Draw content-rect bounds
|
|
line.setColor(SK_ColorBLACK);
|
|
canvas->drawRect(content, line);
|
|
|
|
// Bounding boxes have all been mapped by the layer matrix from local to layer space, so undo
|
|
// the layer matrix, leaving just the device matrix.
|
|
canvas->restore();
|
|
|
|
// The hinted bounds of the layer saved for the filtering
|
|
line.setColor(SK_ColorRED);
|
|
canvas->drawRect(SkRect::Make(SkIRect(node.fHintedLayerBounds)).makeOutset(3.f, 3.f), line);
|
|
// The bounds of the layer if there was no local content hint
|
|
line.setColor(SK_ColorGREEN);
|
|
canvas->drawRect(SkRect::Make(SkIRect(node.fUnhintedLayerBounds)).makeOutset(2.f, 2.f), line);
|
|
|
|
// The output bounds in layer space
|
|
line.setColor(SK_ColorBLUE);
|
|
canvas->drawRect(SkRect::Make(SkIRect(node.fOutputBounds)).makeOutset(1.f, 1.f), line);
|
|
// Device-space bounding box of the output bounds (e.g. what legacy DAG manipulation via
|
|
// MatrixTransform would produce).
|
|
static const SkScalar kDashParams[] = {6.f, 12.f};
|
|
line.setPathEffect(SkDashPathEffect::Make(kDashParams, 2, 0.f));
|
|
SkRect devOutputBounds = SkRect::Make(SkIRect(node.fMapping.layerToDevice(node.fOutputBounds)));
|
|
canvas->restore(); // undoes device matrix
|
|
canvas->drawRect(devOutputBounds, line);
|
|
}
|
|
|
|
static constexpr float kLineHeight = 16.f;
|
|
static constexpr float kLineInset = 8.f;
|
|
|
|
static float print_matrix(SkCanvas* canvas, const char* prefix, const SkMatrix& matrix,
|
|
float x, float y, const SkFont& font, const SkPaint& paint) {
|
|
canvas->drawString(prefix, x, y, font, paint);
|
|
y += kLineHeight;
|
|
for (int i = 0; i < 3; ++i) {
|
|
SkString row;
|
|
row.appendf("[%.2f %.2f %.2f]",
|
|
matrix.get(i * 3), matrix.get(i * 3 + 1), matrix.get(i * 3 + 2));
|
|
canvas->drawString(row, x, y, font, paint);
|
|
y += kLineHeight;
|
|
}
|
|
return y;
|
|
}
|
|
|
|
static float print_size(SkCanvas* canvas, const char* prefix, const SkIRect& rect,
|
|
float x, float y, const SkFont& font, const SkPaint& paint) {
|
|
canvas->drawString(prefix, x, y, font, paint);
|
|
y += kLineHeight;
|
|
SkString sz;
|
|
sz.appendf("%d x %d", rect.width(), rect.height());
|
|
canvas->drawString(sz, x, y, font, paint);
|
|
return y + kLineHeight;
|
|
}
|
|
|
|
static float print_info(SkCanvas* canvas, const FilterNode& node) {
|
|
SkFont font(nullptr, 12);
|
|
SkPaint text;
|
|
text.setAntiAlias(true);
|
|
|
|
float y = kLineHeight;
|
|
if (node.fFilter) {
|
|
canvas->drawString(node.fFilter->getTypeName(), kLineInset, y, font, text);
|
|
y += kLineHeight;
|
|
if (node.fDepth == 0) {
|
|
// The mapping is the same for all nodes, so only print at the root
|
|
y = print_matrix(canvas, "Param->Layer", node.fMapping.layerMatrix(),
|
|
kLineInset, y, font, text);
|
|
y = print_matrix(canvas,
|
|
"Layer->Device",
|
|
node.fMapping.layerToDevice(),
|
|
kLineInset,
|
|
y,
|
|
font,
|
|
text);
|
|
}
|
|
|
|
y = print_size(canvas, "Layer Size", SkIRect(node.fUnhintedLayerBounds),
|
|
kLineInset, y, font, text);
|
|
y = print_size(canvas, "Layer Size (hinted)", SkIRect(node.fHintedLayerBounds),
|
|
kLineInset, y, font, text);
|
|
} else {
|
|
canvas->drawString("Source Input", kLineInset, y, font, text);
|
|
y += kLineHeight;
|
|
}
|
|
|
|
return y;
|
|
}
|
|
|
|
// Returns bottom edge in pixels that the subtree reached in canvas
|
|
static float draw_dag(SkCanvas* canvas, SkSurface* nodeSurface, const FilterNode& node) {
|
|
// First capture the results of the node, into nodeSurface
|
|
draw_node(nodeSurface->getCanvas(), node);
|
|
sk_sp<SkImage> nodeResults = nodeSurface->makeImageSnapshot();
|
|
|
|
// Fill in background of the filter node with a checkerboard
|
|
canvas->save();
|
|
canvas->clipRect(SkRect::MakeWH(nodeResults->width(), nodeResults->height()));
|
|
ToolUtils::draw_checkerboard(canvas, SK_ColorGRAY, SK_ColorLTGRAY, 10);
|
|
canvas->restore();
|
|
|
|
// Display filtered results in current canvas' location (assumed CTM is set for this node)
|
|
canvas->drawImage(nodeResults, 0, 0);
|
|
|
|
SkPaint line;
|
|
line.setAntiAlias(true);
|
|
line.setStyle(SkPaint::kStroke_Style);
|
|
line.setStrokeWidth(3.f);
|
|
|
|
// Text info
|
|
canvas->save();
|
|
canvas->translate(0, nodeResults->height());
|
|
float textHeight = print_info(canvas, node);
|
|
canvas->restore();
|
|
|
|
// Border around filtered results + text info
|
|
canvas->drawRect(SkRect::MakeWH(nodeResults->width(), nodeResults->height() + textHeight),
|
|
line);
|
|
|
|
static const float kPad = 20.f;
|
|
float x = nodeResults->width() + kPad;
|
|
float y = 0;
|
|
for (int i = 0; i < node.fInputNodes.count(); ++i) {
|
|
// Line connecting this node to its child
|
|
canvas->drawLine(nodeResults->width(), 0.5f * nodeResults->height(), // right of node
|
|
x, y + 0.5f * nodeResults->height(), line); // left of child
|
|
canvas->save();
|
|
canvas->translate(x, y);
|
|
y = draw_dag(canvas, nodeSurface, node.fInputNodes[i]);
|
|
canvas->restore();
|
|
}
|
|
return std::max(y, nodeResults->height() + textHeight + kPad);
|
|
}
|
|
|
|
static void draw_dag(SkCanvas* canvas, sk_sp<SkImageFilter> filter,
|
|
const SkRect& rect, const SkISize& surfaceSize) {
|
|
// Get the current CTM, which includes all the viewer's UI modifications, which we want to
|
|
// pass into our mock canvases for each DAG node.
|
|
SkMatrix ctm = canvas->getTotalMatrix();
|
|
|
|
canvas->save();
|
|
// Reset the matrix so that the DAG layout and instructional text is fixed to the window.
|
|
canvas->resetMatrix();
|
|
|
|
// Process the image filter DAG to display intermediate results later on, which will apply the
|
|
// provided CTM during draw_node calls.
|
|
FilterNode dag = build_dag(ctm, rect, filter.get());
|
|
|
|
sk_sp<SkSurface> nodeSurface =
|
|
canvas->makeSurface(canvas->imageInfo().makeDimensions(surfaceSize));
|
|
draw_dag(canvas, nodeSurface.get(), dag);
|
|
|
|
canvas->restore();
|
|
}
|
|
|
|
class ImageFilterDAGSample : public Sample {
|
|
public:
|
|
ImageFilterDAGSample() {}
|
|
|
|
void onDrawContent(SkCanvas* canvas) override {
|
|
static const SkRect kFilterRect = SkRect::MakeXYWH(20.f, 20.f, 60.f, 60.f);
|
|
static const SkISize kFilterSurfaceSize = SkISize::Make(
|
|
2 * (kFilterRect.fRight + kFilterRect.fLeft),
|
|
2 * (kFilterRect.fBottom + kFilterRect.fTop));
|
|
|
|
// Somewhat clunky, but we want to use the viewer calculated CTM in the mini surfaces used
|
|
// per DAG node. The rotation matrix viewer calculates is based on the sample size so trick
|
|
// it into calculating the right matrix for us w/ 1 frame latency.
|
|
this->setSize(kFilterSurfaceSize.width(), kFilterSurfaceSize.height());
|
|
|
|
// Make a large DAG
|
|
// /--- Color Filter <---- Blur <--- Offset
|
|
// Merge <
|
|
// \--- Blur <--- Drop Shadow
|
|
sk_sp<SkImageFilter> drop2 = SkImageFilters::DropShadow(
|
|
10.f, 5.f, 3.f, 3.f, SK_ColorBLACK, nullptr);
|
|
sk_sp<SkImageFilter> blur1 = SkImageFilters::Blur(2.f, 2.f, std::move(drop2));
|
|
|
|
sk_sp<SkImageFilter> offset3 = SkImageFilters::Offset(-5.f, -5.f, nullptr);
|
|
sk_sp<SkImageFilter> blur2 = SkImageFilters::Blur(4.f, 4.f, std::move(offset3));
|
|
sk_sp<SkImageFilter> cf1 = SkImageFilters::ColorFilter(
|
|
SkColorFilters::Blend(SK_ColorGRAY, SkBlendMode::kModulate), std::move(blur2));
|
|
|
|
sk_sp<SkImageFilter> merge0 = SkImageFilters::Merge(std::move(blur1), std::move(cf1));
|
|
|
|
draw_dag(canvas, std::move(merge0), kFilterRect, kFilterSurfaceSize);
|
|
}
|
|
|
|
SkString name() override { return SkString("ImageFilterDAG"); }
|
|
|
|
private:
|
|
|
|
using INHERITED = Sample;
|
|
};
|
|
|
|
DEF_SAMPLE(return new ImageFilterDAGSample();)
|