blob: 97ef3f80902a29b9c78ac7b326b1a032741b79b0 [file]
/*
* Copyright 2026 Rive
*/
// Inside/center/outside strokes on the shapes the circle-only stroke_position
// GMs don't reach. Each cell draws the stroke, then the path's fill region in
// translucent purple, then the path itself as a thin red line.
//
// Pinned behavior, all intended:
// - Inside/outside close open paths, so the closing segment is stroked too.
// - Joins only exist on the outer side of a turn: inside strokes show them
// at concave corners, outside strokes at convex ones.
// - Which side is "inside" follows the stroke path's own fill rule, so holes
// and contours wound in opposite directions resolve as that rule says.
#include "gm.hpp"
#include "gmutils.hpp"
#include "rive/renderer.hpp"
using namespace rivegm;
using namespace rive;
namespace
{
constexpr float kCell = 160.0f;
constexpr float kThickness = 18.0f;
Path openZigzag()
{
return PathBuilder()
.moveTo(24, 50)
.lineTo(60, 120)
.lineTo(96, 50)
.lineTo(136, 120)
.detach();
}
// Four convex corners and one concave one, at the notch.
Path notchedSquare()
{
return PathBuilder()
.moveTo(30, 30)
.lineTo(130, 30)
.lineTo(130, 130)
.lineTo(80, 80)
.lineTo(30, 130)
.close()
.detach();
}
// Both squares wound the same way, so only even-odd sees a hole.
Path squareWithHole(FillRule fillRule)
{
Path path = PathBuilder()
.moveTo(30, 30)
.lineTo(130, 30)
.lineTo(130, 130)
.lineTo(30, 130)
.close()
.moveTo(60, 60)
.lineTo(100, 60)
.lineTo(100, 100)
.lineTo(60, 100)
.close()
.detach();
path->fillRule(fillRule);
return path;
}
// A clockwise square beside a counter-clockwise one, as two pen-drawn paths
// in one shape can be.
Path oppositeSquares()
{
return PathBuilder()
.moveTo(20, 40)
.lineTo(90, 40)
.lineTo(90, 110)
.lineTo(20, 110)
.close()
.moveTo(110, 70)
.lineTo(110, 120)
.lineTo(140, 120)
.lineTo(140, 70)
.close()
.detach();
}
void drawBackground(Renderer* renderer, float width, float height)
{
Paint bg;
bg->color(0xff000000);
renderer->drawPath(PathBuilder::Rect({0, 0, width, height}), bg);
}
// [strokePath] is what gets stroked; [fillPath] only shows the region the
// stroke is measured against, and is usually the same path.
void drawCell(Renderer* renderer,
const Path& strokePath,
const Path& fillPath,
StrokeParams params,
float feather)
{
Paint stroke;
stroke->color(0xFFEFEFEF);
stroke->stroke(params);
stroke->feather(feather);
renderer->drawPath(strokePath, stroke);
Paint fill;
fill->color(0x5f881177);
renderer->drawPath(fillPath, fill);
Paint line;
line->color(0xFFFF3030);
line->stroke({.thickness = 1.5f});
renderer->drawPath(strokePath, line);
}
// Rows: shape x join. Columns: inside, center, outside.
void drawShapesByJoin(Renderer* renderer, float feather)
{
drawBackground(renderer, 3 * kCell, 9 * kCell);
Path shapes[] = {openZigzag(),
notchedSquare(),
squareWithHole(FillRule::evenOdd)};
StrokeJoin joins[] = {StrokeJoin::miter,
StrokeJoin::round,
StrokeJoin::bevel};
for (int s = 0; s < 3; ++s)
{
for (int j = 0; j < 3; ++j)
{
for (int p = 0; p < 3; ++p)
{
renderer->save();
renderer->translate(p * kCell, (s * 3 + j) * kCell);
drawCell(renderer,
shapes[s],
shapes[s],
{.thickness = kThickness,
.join = joins[j],
.position = StrokePosition(p)},
feather);
renderer->restore();
}
}
}
}
} // namespace
DEF_SIMPLE_GM(stroke_position_shapes, 480, 1440, renderer)
{
drawShapesByJoin(renderer, 0.0f);
}
// Feathered inside/outside strokes take a different branch in the renderer:
// clip by the path, then draw the thickened stroke.
DEF_SIMPLE_GM(stroke_position_shapes_feathered, 480, 1440, renderer)
{
drawShapesByJoin(renderer, 6.0f);
}
// What the editor sends: stroke paths are always non-zero, whatever the Fill's
// rule. Row 1 is a same-direction hole that an even-odd Fill shows, but the
// non-zero stroke path counts as inside. Row 2 is two opposite-wound squares.
// Columns: inside, center, outside.
DEF_SIMPLE_GM(stroke_position_winding, 480, 320, renderer)
{
drawBackground(renderer, 3 * kCell, 2 * kCell);
Path hole = squareWithHole(FillRule::nonZero);
Path holeFill = squareWithHole(FillRule::evenOdd);
Path pair = oppositeSquares();
for (int p = 0; p < 3; ++p)
{
StrokeParams params = {.thickness = 12.0f,
.position = StrokePosition(p)};
renderer->save();
renderer->translate(p * kCell, 0);
drawCell(renderer, hole, holeFill, params, 0.0f);
renderer->translate(0, kCell);
drawCell(renderer, pair, pair, params, 0.0f);
renderer->restore();
}
}