Line joints and caps

Every Line2D joint, cap and gradient side by side.

▶ Run in browser

Tags: 2d

A Line2D is more than a run of segments. Where it bends it leaves a notch, and joint_mode decides what fills it; where it ends it stops flat, and begin_cap_mode / end_cap_mode decide whether it rounds off; and gradient colours it along its own length instead of flat.

How it works

Four rows, each the same zigzag drawn with a different setting so the difference is visible rather than described:

  1. joint_mode is "sharp" (a miter), "bevel" (a flat corner) or "round" (an arc). A miter that would shoot a spike off a near-reversal falls back to a bevel on its own.

  2. begin_cap_mode and end_cap_mode are "none" or "round", and each end is set independently.

  3. gradient is a list of (t, colour) stops. t runs from 0 at the first point to 1 at the last, measured along the line, and the gradient replaces colour rather than tinting it.

  4. A width of 1.0 or less is a hairline: it rides the one-pixel line pipeline whatever the camera is doing, so it has no width for a joint or a cap to fill and neither is drawn.

The alpha on the top row is deliberately low: a joint fills the outside of a bend only, so a translucent line does not darken along its own corners.

The grid divides the window into cells and scales each zigzag down to fit its cell, so it follows a resize and still fits a small window.

Source

  1"""Line joints and caps: Every Line2D joint, cap and gradient side by side.
  2
  3A `Line2D` is more than a run of segments. Where it bends it leaves a notch,
  4and `joint_mode` decides what fills it; where it ends it stops flat, and
  5`begin_cap_mode` / `end_cap_mode` decide whether it rounds off; and `gradient`
  6colours it along its own length instead of flat.
  7
  8## How it works
  9
 10Four rows, each the same zigzag drawn with a different setting so the
 11difference is visible rather than described:
 12
 131. `joint_mode` is `"sharp"` (a miter), `"bevel"` (a flat corner) or `"round"`
 14   (an arc). A miter that would shoot a spike off a near-reversal falls back to
 15   a bevel on its own.
 162. `begin_cap_mode` and `end_cap_mode` are `"none"` or `"round"`, and each end
 17   is set independently.
 183. `gradient` is a list of `(t, colour)` stops. `t` runs from 0 at the first
 19   point to 1 at the last, measured along the line, and the gradient replaces
 20   `colour` rather than tinting it.
 214. A `width` of 1.0 or less is a hairline: it rides the one-pixel line pipeline
 22   whatever the camera is doing, so it has no width for a joint or a cap to
 23   fill and neither is drawn.
 24
 25The alpha on the top row is deliberately low: a joint fills the outside of a
 26bend only, so a translucent line does not darken along its own corners.
 27
 28The grid divides the window into cells and scales each zigzag down to fit its
 29cell, so it follows a resize and still fits a small window.
 30"""
 31
 32from simvx.core import Line2D, Node2D
 33from simvx.graphics import App
 34
 35#: A right angle, an acute bend and a near-reversal, so every joint mode has
 36#: something to show.
 37ZIGZAG = [(0, 60), (60, 60), (60, 0), (140, 70), (200, 10), (150, 14)]
 38
 39#: The zigzag's extent at scale 1 with room for a 14-pixel stroke, and the band
 40#: above it that holds a row's title.
 41ZIGZAG_SIZE = (220.0, 90.0)
 42TITLE_BAND = 36.0
 43MARGIN = 40.0
 44
 45RED = (1.0, 0.2, 0.3, 1.0)
 46BLUE = (0.25, 0.4, 1.0, 1.0)
 47YELLOW = (1.0, 0.85, 0.2, 1.0)
 48GREEN = (0.2, 0.9, 0.5, 1.0)
 49CYAN = (0.3, 0.85, 1.0, 0.45)
 50AMBER = (1.0, 0.72, 0.25, 1.0)
 51WHITE = (0.9, 0.9, 0.95, 1.0)
 52TITLE_COLOUR = (0.75, 0.78, 0.85, 1.0)
 53
 54
 55def zigzag(width=14.0, joint_mode="round", **settings) -> Line2D:
 56    """The shared zigzag, 14 pixels wide with round joints unless told otherwise."""
 57    return Line2D(points=ZIGZAG, width=width, joint_mode=joint_mode, **settings)
 58
 59
 60class LineGallery(Node2D):
 61    dynamic = True  # the row titles move with the window
 62
 63    def on_ready(self):
 64        caps = (("none", "none"), ("round", "round"), ("none", "round"))
 65        gradients = (
 66            [(0.0, RED), (1.0, BLUE)],
 67            [(0.0, YELLOW), (0.5, YELLOW), (0.5, GREEN), (1.0, GREEN)],
 68            [(0.0, RED), (0.33, YELLOW), (0.66, GREEN), (1.0, BLUE)],
 69        )
 70        self.rows = [
 71            (
 72                "joint_mode: sharp, bevel, round",
 73                [zigzag(colour=CYAN, joint_mode=mode) for mode in ("sharp", "bevel", "round")],
 74            ),
 75            (
 76                "begin_cap_mode / end_cap_mode: none, both, end only",
 77                [zigzag(colour=AMBER, begin_cap_mode=begin, end_cap_mode=end) for begin, end in caps],
 78            ),
 79            (
 80                "gradient: a ramp, a hard stop at the midpoint, four stops",
 81                [zigzag(gradient=gradient) for gradient in gradients],
 82            ),
 83            (
 84                "width 1.0 or less is a hairline: no joint, no cap",
 85                [
 86                    zigzag(width=width, colour=WHITE, begin_cap_mode="round", end_cap_mode="round")
 87                    for width in (1.0, 3.0, 14.0)
 88                ],
 89            ),
 90        ]
 91        for _, lines in self.rows:
 92            for line in lines:
 93                self.add_child(line)
 94
 95    def cell_size(self) -> tuple[float, float]:
 96        width, height = self.viewport_size
 97        return (width - 2 * MARGIN) / 3, (height - MARGIN) / len(self.rows)
 98
 99    def row_top(self, row: int) -> float:
100        return MARGIN / 2 + row * self.cell_size()[1]
101
102    def on_update(self, dt):
103        cell_w, cell_h = self.cell_size()
104        scale = min(1.0, cell_w / ZIGZAG_SIZE[0], (cell_h - TITLE_BAND) / ZIGZAG_SIZE[1])
105        inset = 10.0 * scale  # half the stroke, so the round caps stay inside the cell
106        for row, (_, lines) in enumerate(self.rows):
107            for column, line in enumerate(lines):
108                line.scale = (scale, scale)
109                line.position = (MARGIN + column * cell_w + inset, self.row_top(row) + TITLE_BAND + inset)
110
111    def on_draw(self, renderer):
112        for row, (title, _) in enumerate(self.rows):
113            renderer.text(title, (MARGIN, self.row_top(row) + 4), fill=TITLE_COLOUR)
114
115
116if __name__ == "__main__":
117    App(title="Line joints and caps", width=900, height=620).run(LineGallery())