A tapered, pressure-modulated stroke along an arbitrary path
Source:R/shape_stroke.R
shape_stroke.Rdshape_stroke is a drawable polygon that follows an arbitrary open
path through (x, y) control points, offset into a ribbon whose width
tapers to zero at both ends and varies along its length according to a
noise_field – intended as a "pressure" curve, giving the outline an
ink/brush-stroke look rather than a constant-width line.
shape_strokes() is a vectorized version of shape_stroke(). Since
x/y are themselves numeric vectors of control points for a single
stroke, shape_strokes() takes them as a list() of numeric vectors
instead – one vector of control points per stroke.
Usage
shape_stroke(
x,
y,
width = 0.2,
n = 100L,
distortion = noise_field(),
trans = trans_identity(),
...
)
shape_strokes(
x,
y,
width = 0.2,
n = 100L,
distortion = noise_field(),
trans = trans_identity(),
...
)Arguments
- x, y
For
shape_stroke(), numeric vectors of control point coordinates, the same length, with at least two control points. Forshape_strokes(), alist()of such vectors instead – one vector of control points per stroke.- width
Maximum width. Must be non-negative. Default
0.2.- n
Number of points used along the resampled path. Must be at least
2. Default100L.- distortion
A noise_field controlling the width ("pressure") modulation. Default
noise_field().- trans
A trans object applied to the shape's computed points. Default
trans_identity()(no transform).- ...
Arguments passed to
style().
Details
The path is resampled to n evenly arc-length-spaced points (unlike
curve_line()'s exact control-point vertices). This generalizes
shape_ribbon() from a single straight segment to any path, at the
cost of computing a true per-point unit normal (via the internal
stroke_normals() helper) rather than shape_ribbon()/shape_twist()'s
shared single offset direction – necessary once the path can genuinely
curve, not just wander slightly off straight.
Unlike shape_ribbon()'s own taper (which peaks at 0.5, an
undocumented quirk of its sqrt(t * (1 - t)) formula), shape_stroke's
taper is normalized to peak at 1 at the path's midpoint, so width
is exactly the maximum rendered width.
Resampling only redistributes points evenly along the control
polyline's own straight segments – it does not smooth or curve-fit
sharp corners, matching curve_line()'s own no-smoothing convention.
A shape_stroke() built from only a few widely-spaced control points
will still have visibly angular corners; supply a denser x/y (e.g.
points already sampled from some smooth function) for a smoothly
curving stroke.
Every other argument may be a plain vector, recycled against x/y via
purrr::pmap()'s own vctrs-based rules (any length-1 element is
broadcast to the common length; mismatched lengths greater than 1
raise an error). A shared distortion noise_field is automatically
recycled across every stroke; pass a list() of several different
noise_fields instead to vary it per stroke. The result is a sketch
containing one shape_stroke() per list element/recycled row, rather
than a single drawable.
Examples
draw(shape_stroke(x = c(0, 1, 2, 3), y = c(0, 1, 0, 1), width = 0.3))
draw(shape_stroke(
x = c(0, 1, 2, 3), y = c(0, 1, 0, 1), width = 0.3,
distortion = noise_field(frequency = 3, seed = 7734)
))
# a few widely-spaced control points give angular corners, since
# resampling redistributes points but never smooths them
draw(shape_stroke(x = c(0, 1, 0), y = c(0, 2, 0), width = 0.2))
# denser input points (already sampled from a smooth function) give a
# smoothly curving stroke instead
t <- seq(0, 2 * pi, length.out = 200)
draw(shape_stroke(x = t, y = sin(t), width = 0.15, fill = fill_charcoal()))
# layer effect_tremor() on top for a hand-drawn ink look
draw(effect_tremor(
shape_stroke(
x = c(0, 1, 2, 3),
y = c(0, 1, 0, 1),
width = 0.2,
fill_alpha = 0.4
),
layers = 4L
))
draw(shape_strokes(
x = list(c(0, 1, 2, 3), c(0, 1, 2, 3)),
y = list(c(0, 1, 0, 1), c(1, 2, 1, 2)),
width = 0.3
))
# a shared distortion recycles across every stroke; width can vary too
draw(shape_strokes(
x = list(c(0, 1, 2), c(0, 1, 2)),
y = list(c(0, 1, 0), c(2, 3, 2)),
width = c(0.15, 0.35)
))