Shape Functions

class pyonfx.shape.ShapeElement(command: str, coordinates: list[Point])[source]

Represents a single drawing command with its associated coordinates.

command: str

The drawing command (one of “m”, “n”, “l”, “p”, “b”, “s”, “c”).

coordinates: list[Point]

List of (x, y) coordinate pairs for this command.

classmethod from_ass_drawing_cmd(command: str, *args: str) list[ShapeElement][source]

Parses a drawing command and its arguments from an ASS drawing string.

Since some commands can be implicit, this method can return more than one element.

Parameters:
  • command (str) – The drawing command (one of “m”, “n”, “l”, “p”, “b”, “s”, “c”).

  • *args (str) – The arguments for the command.

Returns:

class pyonfx.shape.Shape(drawing_cmds: str = '', elements: list[ShapeElement] = [])[source]

High-level wrapper around ASS drawing commands.

A Shape instance stores and manipulates the vector outlines that you would normally place in a {\p} override tag.

Internally the outline is represented as a list of pyonfx.shape.ShapeElement objects exposed through elements. The textual ASS representation returned by the read-only drawing_cmds property is generated on-the-fly from that list, so it can never fall out of sync with the actual geometry.

The class provides a rich tool-set to work with shapes: bounding-box calculation, geometric transformations, curve flattening, segmentations and more. Most methods mutate the instance and return self so they can be chained.

Shape also implements __iter__(), therefore you can simply write:

>>> for element in shape:
>>>     ...

The iterator yields the underlying ShapeElement objects in the same order they appear in the ASS drawing string. Every explicit command (m, n, l, p, b, s, c) is returned one-to-one. In addition, implicit continuations after a command - for example extra coordinate pairs that follow an l or b - are split so that each segment becomes its own ShapeElement:

>>> shape = Shape("m 0 0 l 10 0 10 10")
>>> list(shape)
[ShapeElement('m', [Point(0, 0)]),
 ShapeElement('l', [Point(10, 0)]),
 ShapeElement('l', [Point(10, 10)])]
elements: list[ShapeElement]

The shape’s elements as a list of ShapeElement objects.

property drawing_cmds: str

The shape’s drawing commands in ASS format as a string.

to_multipolygon(tolerance: float = 1.0) MultiPolygon[source]

Converts shape to a Shapely MultiPolygon with proper shell-hole relationships.

Polygons don’t have curves, so Shape.flatten() is automatically called with the given tolerance.

Parameters:

tolerance (float) – Angle in degree to define a curve as flat (increasing it will boost performance during reproduction, but lower accuracy)

Returns:

A MultiPolygon where each polygon represents a compound with outer shell and holes.

classmethod from_multipolygon(multipolygon: MultiPolygon, min_point_spacing: float = 0.5) Shape[source]

Creates a Shape from a Shapely MultiPolygon.

Parameters:
  • multipolygon (MultiPolygon) – The MultiPolygon to convert.

  • min_point_spacing (float) – Per-axis spacing threshold - a vertex is kept only if both |Δx| and |Δy| from the previous vertex are ≥ this value (increasing it will boost performance during reproduction, but lower accuracy).

Returns:

A new Shape instance representing the MultiPolygon.

bounding(exact: bool = False) tuple[float, float, float, float][source]

Calculates shape bounding box.

Tips: Using this you can get more precise information about a shape (width, height, position).

Parameters:

exact (bool) – Whether the calculation of the bounding box should be exact, which is more precise for Bézier curves.

Returns:

A tuple (x0, y0, x1, y1) containing coordinates of the bounding box.

Examples

print( "Left-top: %d %d\nRight-bottom: %d %d" % ( Shape("m 10 5 l 25 5 25 42 10 42").bounding() ) )
print( Shape("m 313 312 b 254 287 482 38 277 212 l 436 269 b 378 388 461 671 260 481").bounding() )
print( Shape("m 313 312 b 254 287 482 38 277 212 l 436 269 b 378 388 461 671 260 481").bounding(exact=True) )
>>> Left-top: 10 5
>>> Right-bottom: 25 42
>>> (254.0, 38.0, 482.0, 671.0)
>>> (260.0, 150.67823683425252, 436.0, 544.871772934194)
boolean(other: Shape, op: Literal['union', 'intersection', 'difference', 'xor'], *, tolerance: float = 1.0, min_point_spacing: float = 0.5) Shape[source]

Return the boolean combination between self and other.

The two shapes are converted to Shapely MultiPolygon objects (curves are automatically flattened with the given tolerance just like in to_multipolygon()). The requested boolean operation is performed and the resulting geometry is converted back to a Shape.

Parameters:
  • other – The other shape to combine with self.

  • op – One of union, intersection, difference or xor (symmetric difference).

  • tolerance – Angle in degrees used when flattening Bézier curves (see flatten()).

  • min_point_spacing – Per-axis spacing threshold passed to from_multipolygon().

Returns:

A new shape representing the result of the boolean operation.

map(fun: Callable[[float, float], tuple[float, float]] | Callable[[float, float, str], tuple[float, float]]) Shape[source]

Sends every point of a shape through given transformation function to change them.

Tips: Working with outline points can be used to deform the whole shape and make f.e. a wobble effect.

Parameters:

fun (function) – A function with two (or optionally three) parameters. It will define how each coordinate will be changed. The first two parameters represent the x and y coordinates of each point. The third optional it represents the type of each point (move, line, bezier…).

Returns:

A pointer to the current object.

Examples

original = Shape("m 0 0 l 20 0 20 10 0 10")
print ( original.map(lambda x, y: (x+10, y+5) ) )
>>> m 10 5 l 30 5 30 15 10 15
move(x: float, y: float) Shape[source]

Moves shape coordinates in given direction.

This function is a high level function, it just uses Shape.map, which is more advanced.
Parameters:
  • x (int or float) – Displacement along the x-axis.

  • y (int or float) – Displacement along the y-axis.

Returns:

A pointer to the current object.

Examples

print( Shape("m 0 0 l 30 0 30 20 0 20").move(-5, 10) )
>>> m -5 10 l 25 10 25 30 -5 30
align(an: int = 5, anchor: int | None = None) Shape[source]

Moves the outline so that a chosen pivot inside the shape coincides with the point that will be used for \pos when the line is rendered with a given {\an..} tag.

If no argument for anchor is passed, it will automatically center the shape.
Parameters:
  • an (int) – Alignment of the subtitle line ({\an1}{\an9}).

  • anchor (int, optional) – Pivot inside the shape - uses the same keypad convention. Defaults to an.

Returns:

A pointer to the current object.

Examples

print( Shape("m 10 10 l 30 10 30 20 10 20").align() )
>>> m 0 0 l 20 0 20 10 0 10
scale(fscx: float = 100, fscy: float = 100, origin: tuple[float, float] = (0.0, 0.0)) Shape[source]

Scales shape coordinates horizontally and vertically, similar to ASS fscx and fscy tags.

Parameters:
  • fscx (int or float) – Horizontal scale factor as percentage (100 = normal, 200 = double width, 50 = half width).

  • fscy (int or float) – Vertical scale factor as percentage (100 = normal, 200 = double height, 50 = half height).

  • origin (tuple[float, float], optional) – The pivot point around which the scaling is applied.

Returns:

A pointer to the current object.

Examples

# Double the width, keep height the same
print( Shape("m 0 50 l 0 0 50 0 50 50").scale(fscx=200) )

# Scale to half size
print( Shape("m 0 50 l 0 0 50 0 50 50").scale(fscx=50, fscy=50) )
>>> m 0 50 l 0 0 100 0 100 50
>>> m 0 25 l 0 0 25 0 25 25
rotate(*, frx: float = 0.0, fry: float = 0.0, frz: float = 0.0, origin: tuple[float, float] = (0.0, 0.0)) Shape[source]

Rotates the shape mimicking the behaviour of frx, fry and frz tags.

Parameters:
  • frx – Rotation angles in degrees around, respectively, the X, Y and Z axes.

  • fry – Rotation angles in degrees around, respectively, the X, Y and Z axes.

  • frz – Rotation angles in degrees around, respectively, the X, Y and Z axes.

  • origin – Pivot around which the rotation is applied.

Returns:

A pointer to the current object.

shear(*, fax: float = 0.0, fay: float = 0.0, origin: tuple[float, float] = (0.0, 0.0)) Shape[source]

Applies a shear (aka slant/skew) transformation to the shape, mimicking the fax and fay tags.

Parameters:
  • fax – Horizontal shear factor. Positive values slant the top of the shape to the right, negative to the left.

  • fay – Vertical shear factor. Positive values slant the right side of the shape downwards, negative upwards.

  • origin – Pivot around which the shear is applied.

Returns:

A pointer to the current object.

flatten(tolerance: float = 1.0) Shape[source]

Splits shape’s bezier curves into lines.

This is a low level function. Instead, you should use split() which already calls this function.
Parameters:

tolerance (float) – Angle in degree to define a curve as flat (increasing it will boost performance during reproduction, but lower accuracy)

Returns:

A pointer to the current object.

Returns:

The shape as a string, with bezier curves converted to lines.

split(max_len: float = 16, tolerance: float = 1.0) Shape[source]

Splits shape bezier curves into lines and splits lines into shorter segments with maximum given length.

Tips: You can call this before using :func:`map` to work with more outline points for smoother deforming.

Parameters:
  • max_len (int or float) – The max length that you want all the lines to be.

  • tolerance (float) – Angle in degree to define a bezier curve as flat (increasing it will boost performance during reproduction, but lower accuracy).

Returns:

A pointer to the current object.

Examples

print( Shape("m -100.5 0 l 100 0 b 100 100 -100 100 -100.5 0 c").split() )
>>> m -100.5 0 l -100 0 -90 0 -80 0 -70 0 -60 0 -50 0 -40 0 -30 0 -20 0 -10 0 0 0 10 0 20 0 30 0 40 0 50 0 60 0 70 0 80 0 90 0 100 0 l 99.964 2.325 99.855 4.614 99.676 6.866 99.426 9.082 99.108 11.261 98.723 13.403 98.271 15.509 97.754 17.578 97.173 19.611 96.528 21.606 95.822 23.566 95.056 25.488 94.23 27.374 93.345 29.224 92.403 31.036 91.405 32.812 90.352 34.552 89.246 36.255 88.086 37.921 86.876 39.551 85.614 41.144 84.304 42.7 82.945 44.22 81.54 45.703 80.088 47.15 78.592 48.56 77.053 49.933 75.471 51.27 73.848 52.57 72.184 53.833 70.482 55.06 68.742 56.25 66.965 57.404 65.153 58.521 63.307 59.601 61.427 60.645 59.515 61.652 57.572 62.622 55.599 63.556 53.598 64.453 51.569 65.314 49.514 66.138 47.433 66.925 45.329 67.676 43.201 68.39 41.052 69.067 38.882 69.708 36.692 70.312 34.484 70.88 32.259 71.411 27.762 72.363 23.209 73.169 18.61 73.828 13.975 74.341 9.311 74.707 4.629 74.927 -0.062 75 -4.755 74.927 -9.438 74.707 -14.103 74.341 -18.741 73.828 -23.343 73.169 -27.9 72.363 -32.402 71.411 -34.63 70.88 -36.841 70.312 -39.033 69.708 -41.207 69.067 -43.359 68.39 -45.49 67.676 -47.599 66.925 -49.683 66.138 -51.743 65.314 -53.776 64.453 -55.782 63.556 -57.759 62.622 -59.707 61.652 -61.624 60.645 -63.509 59.601 -65.361 58.521 -67.178 57.404 -68.961 56.25 -70.707 55.06 -72.415 53.833 -74.085 52.57 -75.714 51.27 -77.303 49.933 -78.85 48.56 -80.353 47.15 -81.811 45.703 -83.224 44.22 -84.59 42.7 -85.909 41.144 -87.178 39.551 -88.397 37.921 -89.564 36.255 -90.68 34.552 -91.741 32.812 -92.748 31.036 -93.699 29.224 -94.593 27.374 -95.428 25.488 -96.205 23.566 -96.92 21.606 -97.575 19.611 -98.166 17.578 -98.693 15.509 -99.156 13.403 -99.552 11.261 -99.881 9.082 -100.141 6.866 -100.332 4.614 -100.452 2.325 -100.5 0
buffer(dist_xy: float, dist_y: float | None = None, *, kind: Literal['fill', 'border'] = 'border', join: Literal['round', 'bevel', 'mitre'] = 'round') Shape[source]

Return a buffered version of the shape.

A buffer is the set of points whose distance from the original geometryis <= to dist. You could use this to create a shape representing the border you usually get with {\bord}, or to expand/contract the shape.

Parameters:
  • dist_xy (float) – Horizontal buffer distance. Positive values “expand” the shape, negative values “contract” it.

  • dist_y (float | None, optional) – Vertical buffer distance. If None the same value as dist_xy is used. The sign must match that of dist_xy.

  • kind ({"fill", "border"}, optional) – “fill” ⇒ return the filled buffered geometry, “border” ⇒ return only the ring between the original shape and the buffered geometry (external or internal border).

  • join ({"round", "bevel", "mitre"}, optional) – Corner-join style.

morph(target: Shape, t: float, max_len: float = 16.0, tolerance: float = 1.0, min_point_spacing: float = 0.5, w_dist: float = 0.55, w_area: float = 0.35, w_overlap: float = 0.1, cost_threshold: float = 2.5, ensure_shell_pairs: bool = True) Shape[source]

Interpolates the current shape towards target, returning a new Shape that represents the intermediate state at fraction t.

Parameters:
  • target (Shape) – Destination shape.

  • t (float) – Interpolation factor (0 ≤ t ≤ 1).

  • max_len (int or float) – The max length that you want all the lines to be.

  • tolerance (float) – Angle in degree to define a bezier curve as flat (increasing it will boost performance during reproduction, but lower accuracy)

  • min_point_spacing (float) – Per-axis spacing threshold - a vertex is kept only if both |Δx| and |Δy| from the previous vertex are ≥ this value (increasing it will boost performance during reproduction, but lower accuracy).

  • w_dist (float, optional) – Weight for the centroid-distance term (higher values make proximity more important).

  • w_area (float, optional) – Weight for the relative area-difference term (higher values make size similarity more important).

  • w_overlap (float, optional) – Weight for the overlap / IoU term that penalises pairs with little spatial intersection.

  • cost_threshold (float, optional) – Maximum acceptable cost for a pairing. Pairs whose cost is above this threshold are treated as unmatched and will grow/shrink to the closest centroid.

  • ensure_shell_pairs (bool, optional) – If True shell rings that would otherwise remain unmatched will be force-paired with the shell that yields the minimum cost. This guarantees that every visible contour morphs into something, at the price of allowing the same shell to be reused multiple times.

Returns:

A new Shape instance representing the morph at t.

Note

Shapes are first decomposed into compounds (outer shells with holes). Then, individual loops are matched based on: - Centroid distance (preferring loops with closer centers); - Area similarity (preferring loops of similar size); - Overlap (preferring loops that share space); - Shell/hole role (avoiding matching shells with holes).

The matched loops are interpolated. The unmatched ones are either shrunk or grown.

static morph_multi(src_shapes: dict[str, Shape], tgt_shapes: dict[str, Shape], t: float, *, max_len: float = 16.0, tolerance: float = 1.0, min_point_spacing: float = 0.5, w_dist: float = 0.55, w_area: float = 0.35, w_overlap: float = 0.1, cost_threshold: float = 2.5, ensure_shell_pairs: bool = True) dict[tuple[str | None, str | None], Shape][source]

Interpolates multiple shapes at once and returns a dictionary mapping (src_id, tgt_id) tuples to their interpolated shapes.

This is a higher-level variant of morph() that works on two collections of shapes rather than a single pair. Rings from all sources are matched against rings from all destinations using the same cost function (centroid distance, area similarity, overlap), then each matched pair is interpolated at the requested point in time t.

Parameters:
  • src_shapes (dict[str, Shape]) – Dictionary id starting shape.

  • tgt_shapes (dict[str, Shape]) – Dictionary id ending shape.

  • t (float) – Interpolation factor (0 = src, 1 = dst).

  • max_len (int or float) – Maximum length of line segments after splitting.

  • tolerance (float) – Angle in degrees to consider a Bézier curve flat during flattening.

  • min_point_spacing (float) – Minimum per-axis spacing when converting back from polygons to shapes.

  • w_dist (float) – Weight of the centroid-distance term in the cost function.

  • w_area (float) – Weight of the relative area-difference term.

  • w_overlap (float) – Weight of the overlap / IoU penalty term.

  • cost_threshold (float) – Maximum acceptable pairing cost; above this value rings are treated as unmatched.

  • ensure_shell_pairs (bool) – Force every shell to morph into something even if the best match is above cost_threshold.

Returns:

A dictionary where keys are (src_id, tgt_id) tuples and values are the interpolated shapes. src_id is None if the geometry is appearing, tgt_id is None if the geometry is disappearing.

Return type:

dict[tuple[str | None, str | None], Shape]

Examples

start = {
    'A': Shape.star(5, 20, 40),
    'B': Shape.ellipse(50, 30).move(100, 0),
}
end = {
    'X': Shape.polygon(6, 45),
}
morphs = Shape.morph_multi(start, end, t=0.5)
for (src_id, tgt_id), shape in morphs.items():
    print(f"{src_id}{tgt_id}: {shape}")
static polygon(edges: int, side_length: float) Shape[source]

Returns a shape representing a regular n-sided polygon.

Parameters:
  • edges (int) – Number of sides.

  • side_length (float) – Length of each side.

Returns:

A shape representing the polygon.

static ellipse(w: float, h: float) Shape[source]

Returns a shape object of an ellipse with given width and height, centered around (0,0).

Tips: You could use that to create rounded stribes or arcs in combination with blurring for light effects.

Parameters:
  • w (int or float) – The width for the ellipse.

  • h (int or float) – The height for the ellipse.

Returns:

A shape object representing an ellipse.

static ring(out_r: float, in_r: float) Shape[source]

Returns a shape object of a ring with given inner and outer radius, centered around (0,0).

Tips: A ring with increasing inner radius, starting from 0, can look like an outfading point.

Parameters:
  • out_r (int or float) – The outer radius for the ring.

  • in_r (int or float) – The inner radius for the ring.

Returns:

A shape object representing a ring.

static heart(size: float, offset: float = 0) Shape[source]

Returns a shape object of a heart object with given size (width&height) and vertical offset of center point, centered around (0,0).

Tips: An offset=size*(2/3) results in a splitted heart.

Parameters:
  • size (int or float) – The width&height for the heart.

  • offset (int or float) – The vertical offset of center point.

Returns:

A shape object representing an heart.

static star(edges: int, inner_size: float, outer_size: float) Shape[source]

Returns a shape object of a star object with given number of outer edges and sizes, centered around (0,0).

Tips: Different numbers of edges and edge distances allow individual n-angles.

Parameters:
  • edges (int) – The number of edges of the star.

  • inner_size (int or float) – The inner edges distance from center.

  • outer_size (int or float) – The outer edges distance from center.

Returns:

A shape object as a string representing a star.

static glance(edges: int, inner_size: float, outer_size: float) Shape[source]

Returns a shape object of a glance object with given number of outer edges and sizes, centered around (0,0).

Tips: Glance is similar to Star, but with curves instead of inner edges between the outer edges.

Parameters:
  • edges (int) – The number of edges of the star.

  • inner_size (int or float) – The inner edges distance from center.

  • outer_size (int or float) – The control points for bezier curves between edges distance from center.

Returns:

A shape object as a string representing a glance.

PIXEL: str = 'm 0 1 l 0 0 1 0 1 1'

A string representing a pixel.