blob: d39ad111f2954faf3fb097dbf9787700da0b7588 [file] [edit]
#include "rive/shapes/paint/paint_outset.hpp"
#include "rive/math/math_types.hpp"
#include "rive/shapes/paint/feather.hpp"
#include "rive/shapes/paint/shape_paint.hpp"
#include "rive/shapes/paint/stroke.hpp"
#include "rive/shapes/shape_paint_container.hpp"
#include "rive/transform_space.hpp"
#include <algorithm>
#include <cmath>
namespace rive
{
float paintBoundsOutset(
const std::optional<StrokeParams>& stroke,
float feather)
{
float outset = 0.0f;
if (stroke.has_value())
{
// A centered stroke straddles the path, reaching half its thickness.
// The renderer draws an outside stroke as a centered stroke twice as
// thick, clipped to the outside, so it reaches the full thickness (and
// its miter twice as far). An inside one is clipped to the path and
// reaches nothing past it; half is a safe over-estimate.
outset = stroke->thickness *
(stroke->position == StrokePosition::outside ? 1.0f : .5f);
if (stroke->join == StrokeJoin::miter)
{
// Miter joins may be longer than the stroke radius.
outset *= kMiterLimit;
}
else if (stroke->cap == StrokeCap::square)
{
// The diagonal of a square cap is longer than the stroke radius.
outset *= math::SQRT2;
}
}
if (feather != 0.0f)
{
outset += featherRadiusFromFeather(feather);
}
return outset;
}
ShapePaintOutset shapePaintOutset(const ShapePaint* paint)
{
ShapePaintOutset out;
// shouldDraw(), not isVisible(): a paint whose mutator is fully transparent
// reaches nowhere, and Stroke::isVisible() additionally folds in
// thickness > 0 so a zero-width stroke costs nothing here.
if (paint == nullptr || !paint->shouldDraw())
{
return out;
}
std::optional<StrokeParams> stroke;
if (paint->is<Stroke>())
{
const Stroke* s = paint->as<Stroke>();
// Mirrors Stroke::applyTo, which is what the renderer actually sees,
// position included: an outside stroke reaches twice as far.
StrokeParams params;
params.thickness = s->thickness();
params.join = static_cast<StrokeJoin>(s->join());
params.cap = static_cast<StrokeCap>(s->cap());
params.position = s->strokePosition();
stroke = params;
// Stroke::pickPath: the local path when the transform affects the
// stroke, the already-world path otherwise.
out.pathIsLocal = s->transformAffectsStroke();
}
float feather = 0.0f;
if (const Feather* f = paint->feather())
{
// inner() only ever moves the blur inward, so treating every feather as
// outward keeps this an over-estimate -- the safe direction, since the
// cost of over-estimating is a few wasted texels and the cost of
// under-estimating is a visible crop.
feather = f->strength();
out.featherOffset = {f->offsetX(), f->offsetY()};
out.featherOffsetInWorld = static_cast<TransformSpace>(f->spaceValue()) ==
TransformSpace::world;
}
out.outset = paintBoundsOutset(stroke, feather);
out.trustworthy = !paint->hasEffects();
return out;
}
PaintReach shapePaintsWorldReach(
const ShapePaintContainer* container,
const Mat2D& world)
{
PaintReach reach;
if (container == nullptr)
{
return reach;
}
const float worldScale = world.findMaxScale();
for (const ShapePaint* paint : container->shapePaints())
{
const ShapePaintOutset po = shapePaintOutset(paint);
reach.trustworthy = reach.trustworthy && po.trustworthy;
// The outset is expressed in the space of the path the paint actually
// draws, which pickPath decides. A local-path outset has to be pushed
// through the shape's scale to mean anything against a world box; a
// world-path one is already in world units.
float distance = po.pathIsLocal ? po.outset * worldScale : po.outset;
// A feather's offset is not symmetric, but padding per axis would have
// to account for rotation redistributing an x offset into y. Its length
// covers every orientation.
Vec2D offset = po.featherOffset;
if (!po.featherOffsetInWorld)
{
// A displacement, not a position: linear part only.
offset = Vec2D::transformDir(offset, world);
}
distance += offset.length();
if (std::isfinite(distance))
{
reach.worldOutset = std::max(reach.worldOutset, distance);
}
}
return reach;
}
} // namespace rive