Tips & Tricks

Freehand Anything

While this library was designed for rendering the types of input points generated by the movement of the human hand, you can pass any set of points into the library’s functions. For example, here’s what you get when running Feather Icons through perfect_freehand.get_stroke().

_images/icons.png

Rendering

While perfect_freehand.get_stroke() returns an array of points representing the outline of a stroke, it’s up to you to decide how you will render these points.

The function below will turn the points returned by get_stroke() into SVG path data:

from itertools import chain, pairwise

def svg_path_from_stroke(stroke):
    if len(stroke) == 0:
        return ""

    d = ['M', f'{stroke[0][0]}', f'{stroke[0][1]}', 'Q']
    for ((x0, y0), (x1, y1)) in pairwise(chain(stroke, [stroke[0]])):
        d.extend([
            f'{(x0 + x1) / 2}',
            f'{(y0 + y1) / 2}',
            f'{x1}',
            f'{y1}'
        ])

    d.append('Z')
    return ' '.join(d)

To use this function, first run your input points through get_stroke(), then pass the result to svg_path_from_stroke. The path must be rendered in the SVG using fill-rule="nonzero".

Flattening

By default, the polygon’s paths include self-crossings. You may wish to remove these crossings and render a stroke as a “flattened” polygon. One example of how this can be done is using the object.buffer() method provided by shapely.

As an example, here’s a method for generating a flattened svg polygon, re-using the svg_path_from_stroke() function seen in the previous example:

from shapely.geometry import Polygon

def flat_svg_path_from_stroke(stroke):
    polygon = Polygon(stroke)

    # At this point, polygon.is_valid may be False due to self-
    # crossings. To fix this, use the "buffer" method with an
    # offset of 0.
    polygon = polygon.buffer(0)

    # The polygon will now consist of one exterior ring, and holes
    # will be represented by interior rings.
    d = [svg_path_from_stroke(polygon.exterior.coords)]
    for interior in polygon.interiors:
        d.append(svg_path_from_stroke(interior.coords))

    return ' '.join(d)

Since the SVG path now consists of multiple separate rings, in order for the holes to be rendered you must used fill-rule="evenodd".