Skip to contents

shape_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. For shape_strokes(), a list() 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. Default 100L.

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().

Value

A drawable.

For shape_strokes(), a sketch.

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