Skip to content

geomotif.motifs.stringart

String art: straight lines only, and a curve appears anyway.

Nails in a board, thread between them, and the thing you end up looking at is not any of the threads. The curve is their envelope -- the line every thread is tangent to -- and it is a real curve with a real equation that no thread ever lies along. A corner strung this way produces a parabola; a circle strung with the two times table produces a cardioid.

Four ways in, from the most specific to the most general:

  • :class:StringArtCorner -- two arms and a parabola between them, which is the one everybody has made.
  • :class:StringArtPolygon -- that corner, at every corner of a polygon.
  • StringArtCircle -- nails around a circle joined by a times table. This is :class:~geomotif.motifs.graphs.ModularMultiplication under the name a string artist would look for it under; the geometry is identical, so it is re-exported here rather than reimplemented.
  • :class:StringArtEnvelope -- point i on one curve to point rule(i) on another, for any curves and any rule. The other three are special cases.

Classes:

Name Description
StringArtCorner

Two arms, laced so the far end of one meets the near end of the other.

StringArtPolygon

A strung corner at every corner of a regular polygon.

StringArtEnvelope

The general engine: nail i on one curve, to nail rule(i) on another.

StringArtCorner dataclass

StringArtCorner(count: int = 20, corner: Point = (0.0, 0.0), arm_a: Point = (200.0, 0.0), arm_b: Point = (0.0, 200.0), *, merge: bool = False, show_nodes: bool = False)

Bases: SegmentMotif

Two arms, laced so the far end of one meets the near end of the other.

The first piece of string art anybody makes, and the reason the whole family is interesting: every thread is straight, and the curve they hug is a parabola -- specifically the one tangent to both arms. It is exactly tangent to each thread at one point and touches nothing else, which is what an envelope is.

Parameters:

Name Type Description Default
count int

Nails per arm, not counting the corner itself. More threads means a smoother-looking parabola and no change at all to the parabola.

20
corner (float, float)

Where the two arms meet.

(0.0, 0.0)
arm_a (float, float)

The far end of each arm. They need not be the same length or at a right angle -- an oblique corner still gives a parabola, just a skewed one.

(200.0, 0.0)
arm_b (float, float)

The far end of each arm. They need not be the same length or at a right angle -- an oblique corner still gives a parabola, just a skewed one.

(200.0, 0.0)

StringArtPolygon dataclass

StringArtPolygon(sides: int = 5, count: int = 16, radius: float = 130.0, rotation: float = pi / 2.0, center: Point = (0.0, 0.0), *, merge: bool = False, show_nodes: bool = False)

Bases: SegmentMotif

A strung corner at every corner of a regular polygon.

Each edge carries nails, and each corner's two edges are laced against each other, so the finished piece is a ring of parabolic arcs meeting at the vertices. Three sides gives the classic triangle; raise the count and it converges on a flower.

Parameters:

Name Type Description Default
sides int

Corners of the underlying polygon.

5
count int

Nails along each edge, not counting its endpoints.

16
radius float

Distance from the middle to each corner.

130.0
rotation float

Angle of the first corner, in radians. A quarter turn puts it at the top.

pi / 2.0
center (float, float)

Middle of the polygon.

(0.0, 0.0)

StringArtEnvelope dataclass

StringArtEnvelope(count: int = 144, rule: Callable[[int], int] = _doubled, curve: Callable[[float], Point] = _ring, partner: Callable[[float], Point] | None = _frame, *, merge: bool = False, show_nodes: bool = False)

Bases: SegmentMotif

The general engine: nail i on one curve, to nail rule(i) on another.

Every other motif in this module and the whole of :mod:geomotif.motifs.graphs is this with the curves and the rule filled in. Supply your own and see what the threads hug::

StringArtEnvelope(
    count=240,
    rule=lambda i: 3 * i,
    curve=lambda t: (150 * math.cos(math.tau * t), 150 * math.sin(math.tau * t)),
)

Leaving partner unset strings the curve against itself, which is the usual case; setting it laces two different curves together -- a circle to a square, an ellipse to a line. The default does the latter, because two different curves is the case that needs a general engine at all: one circle laced to itself already has a name, and it is :class:~geomotif.motifs.graphs.ModularMultiplication.

Parameters:

Name Type Description Default
count int

Nails on each curve.

144
rule callable

Maps a nail index to the index it is strung to. Taken modulo count, so it may return anything.

_doubled
curve callable

Maps a fraction of the way round, in [0, 1), to a point.

_ring
partner callable

A second curve for the far end of each thread. None strings curve against itself.

_frame
Notes

The design records the function objects in its metadata, so it round-trips within a session but not through a file. Anything that has to survive being written down wants a registered class of its own.