User Guide

Note: The images in this guide can usually be generated by prefixing the corresponding code example with:

import algoraphics as ag
w, h = 400, 400

and appending this:

c = ag.Canvas(w, h)
c.add(x)
c.png("test.png")

Components

Canvas

The algoraphics library uses the Cartesian coordinate system. This means that while SVG and other computer graphics systems often define the top of the canvas as y=0 with y increasing downward, here y=0 is the bottom of the canvas with y increasing upward. It also means that while the coordinate units are pixels, negative and non-integer values are allowed.

The Canvas object holds a list of objects to be drawn. It has a width, height, and background color (or None for transparent background). It has methods to add one or more objects and clear the canvas (for convenience, new clears the canvas and then adds the objects). The SVG representation of a filled canvas can be retrieved with get_svg, saved to file with svg, or rendered to PNG with png. Likewise, animated graphics can be saved as a GIF with gif.

Shapes

Primitive visible objects are represented with a few Shape subclasses:

Object

Attributes

Circle

c (point), r (float)

Group

members (list), clip (list)

Line

points (list)

Polygon

points (list)

Spline

points (list), smoothing (float), circular (bool)

Shapes can be stored in nested lists without affecting their rendering. The purpose of groups, on the other hand, is to apply things like clipping and shadows to its members. Convenience functions like rectangle exist to define one of these shapes in other ways.

Styles

SVG attributes are used to style shapes and are stored as a dictionary in each shape’s style attribute. Use the set_style function to set one style for one or more shapes, or set_styles to set separate copies of a style to each shape in the list, e.g. to sample from a Color object defined by random parameters.

Parameters

Many algoraphics functions accept abstract parameters that specify a distribution from which to randomly sample.

Algoraphics represents location and other properties declaratively. For example, the end of a tree tranch would be defined relative to the position of its start, and the position of its start would be defined with the same position object as the end of its parent branch.

This makes it simple to animate compound objects in a natural way. In the tree described above, when one branch sways, all of its child branches sway with it.

Parameters can also be defined with a distribution. This makes it easy to incorporate subtle or not-so-subtle randomness into the graphics. It also allows basic functions to be used in multiple ways to create different patterns.

For example, a simple command to draw 100 circles can produce this:

w, h = 400, 200
x = 100 * ag.Circle(
    c=(ag.Uniform(10, w - 10), ag.Uniform(10, h - 10)),
    r=ag.Uniform(1, 10),
    fill=ag.Color(hue=ag.Uniform(0.4, 0.6), sat=0.9, li=0.5),
)
_images/param1.png

or this:

w, h = 400, 200
x = [
    ag.Circle(
        c=(ag.Param([100, 300]), 100),
        r=r,
        fill=ag.Color(hue=0.8, sat=0.9, li=ag.Uniform(0, 1)),
    )
    for r in range(100, 0, -1)
]
_images/param2.png

or this:

w, h = 400, 200
x = 100 * ag.Circle(c=(ag.Uniform(0, 400), ag.Uniform(0, h)), r=ag.Uniform(5, 30))
for circle in x:
    color = ag.Color(hue=circle.c.state(0)[0] / 500, sat=0.9, li=0.5)
    ag.set_style(circle, "fill", color)
_images/param3.png

Parameters can be defined relative to other parameters:

p2y = ag.Delta(start=170, delta=-0.25)
x.append([ag.line((i * 4, 170), (i * 4, p2y)) for i in range(100)])

p2y = ag.Delta(start=100, min=70, max=130, delta=ag.Uniform(-5, 5))
x.append([ag.line((i * 4, 100), (i * 4, p2y)) for i in range(100)])

p2y = ag.Delta(
    start=30,
    min=0,
    max=60,
    delta=ag.Delta(start=0, min=-2, max=2, delta=ag.Uniform(-2, 2)),
)
x.append([ag.line((i * 4, 30), (i * 4, p2y)) for i in range(100)])

ag.set_style(x, 'stroke-width', 2)
_images/param4.png

By adding random values to another parameter, you get random-walk-type behavior (middle row of lines above). This chaining doesn’t have to apply directly to the shapes: you can use it to update a parameter representing the change in another parameter, resulting in second-order dynamics (bottom row of lines above).

A parameter can also be defined as a list, from which it will choose the value randomly, or with an arbitrary function, which will be called with no arguments to generate the value.

Location Parameters

While Param and its subclasses are used for one-dimensional parameters, two-dimensional, location-based parameters are handled with Point objects:

place = ag.Place(
    ref=(200, 0),
    direction=ag.Normal(90, 30),
    distance=ag.Exponential(mean=50, stdev=100, sigma=3),
)
x = [ag.circle(p, 2, fill="purple") for p in place.values(10000)]
_images/param7.png

As with single-dimension parameters, Points can be defined relative to others:

x = []
center = (0, 200)
direc = 0
deltadirec = 0
for i in range(300):
    x.append(ag.Circle(center, r=ag.Uniform(2, 4)))
    deltadirec = ag.Clip(deltadirec + ag.Uniform(-5, 5), -10, 10)
    direc = direc + deltadirec
    center = ag.Move(center, direction=direc, distance=ag.Uniform(8, 12))
_images/param8.png

Colors

Colors are represented as objects of the Color class. They are generally defined in the HSL (hue, saturation, lightness) color space. These can be supplied as Param objects:

outline = ag.Circle(c=(200, 200), r=150)
color = ag.Color(
    hue=ag.Uniform(min=0.6, max=0.8), sat=0.7, li=ag.Uniform(min=0.5, max=0.7)
)
x = ex.fill_spots(outline)
ag.set_styles(x, "fill", color)
_images/fill3.png

Shape color attributes like fill and stroke can be set with a string, which will be used as-is in the SVG file. This will work for hex codes, named colors, etc.

Animation

The Dynamic parameter type allows parameters to update at each frame of an animation. For each Dynamic parameter, you define its initial state with a parameter, and then the amount added or multiplied by the previous value to update over time. Every other parameter defined relative to that one is updated too. This allows structures to move in natural ways:

x = []
center = (0, 200)
direc = 0
deltadirec = 0
for i in range(100):
    deltar = ag.Dynamic(0, ag.Uniform(-0.2, 0.2, static=False), min=-0.2, max=0.2)
    r = ag.Dynamic(ag.Uniform(2, 4), delta=deltar, min=2, max=4)
    x.append(ag.Circle(center, r))
    # deltadirec = ag.Clip(deltadirec + ag.Uniform(-5, 5), -10, 10)
    deldeldir = ag.Dynamic(
        ag.Uniform(-5, 5), delta=ag.Uniform(-0.2, 0.2, static=False), min=-5, max=5
    )
    deltadirec = ag.Clip(deltadirec + deldeldir, -10, 10)
    direc = direc + deltadirec
    deltadist = ag.Dynamic(
        ag.Uniform(-2, 2), delta=ag.Uniform(-0.5, 0.5, static=False), min=-2, max=2
    )
    dist = ag.Dynamic(ag.Uniform(8, 12), delta=deltadist, min=8, max=12)
    center = ag.Move(center, direction=direc, distance=dist)
c = ag.Canvas(400, 400)
c.add(x)
c.gif("png/param9.gif", fps=12, seconds=4)
_images/param9.gif

SVG Representation

Shapes are converted to SVG for export. Each type of shape corresponds to a SVG object type or a specific form of one.

algoraphics

SVG

circle

circle

group

g

line

polyline

polygon

polygon

spline

path made of bezier curves

SVG-rendered effects like shadows applied to objects become references to SVG filters, which are defined at the beginning of the SVG file.

By default, the SVG code is optimized using svgo, but this can be skipped for more readable SVG code, e.g. for debugging.

Extras

You can create your own package of reusable functions built upon algoraphics. The extras subpackage is an example of this containing structures, fill functions, and more.

These code snippets are preceded by:

import algoraphics.extras as ex

Images

Images can be used as templates for use with patterns or textures. The simplest strategy is to sample colors from the image to color shapes at corresponding locations:

image = ag.open_image("test_images.jpg")
ag.resize_image(image, 800, None)
w, h = image.size
x = ag.tile_canvas(w, h, shape='polygon', tile_size=100)
ag.fill_shapes_from_image(x, image)
_images/images1.png

Images can also be segmented into regions that correspond to detected color boundaries with some smoothing, but are constrained to not be too large:

image = ag.open_image("test_images.jpg")
ag.resize_image(image, 800, None)
w, h = image.size
x = ag.image_regions(image, smoothness=3)
for outline in x:
    color = ag.region_color(outline, image)
    ag.set_style(outline, 'fill', color)
ag.add_paper_texture(x)
_images/images2.png

Fill functions can be applied and passed representative colors:

image = ag.open_image("test_images.jpg")
ag.resize_image(image, 800, None)
w, h = image.size
x = ag.image_regions(image, smoothness=3)
for i, outline in enumerate(x):
    color = ag.region_color(outline, image)
    maze = ag.Maze_Style_Pipes(rel_thickness=0.6)
    rot = color.value()[0] * 90
    x[i] = ag.fill_maze(outline, spacing=5, style=maze, rotation=rot)
    ag.set_style(x[i]["members"], "fill", color)
    ag.region_background(x[i], ag.contrasting_lightness(color, light_diff=0.2))
_images/images3.png

Text

Text can be created and stylized. Characters are generated as nested lists of points (one list per continuous pen stroke) along their form:

color = ag.Color(hue=ag.Uniform(0, 0.15), sat=0.8, li=0.5)

points = ex.text_points("ABCDEFG", 50, pt_spacing=0.5, char_spacing=0.15)
ag.jitter_points(points, 2)
size = ag.Exponential(2.2, stdev=1).values(len(points))
x1 = [ag.circle(c=p, r=size[i], fill=color) for i, p in enumerate(points)]
ag.reposition(x1, (w / 2, h - 50), "center", "top")
c.new(ag.shuffled(x1))

points = ex.text_points("HIJKLM", 50, pt_spacing=0.5, char_spacing=0.15)
ag.jitter_points(points, 2)
size = ag.Exponential(2.2, stdev=1).values(len(points))
x2 = [ag.circle(c=p, r=size[i], fill=color) for i, p in enumerate(points)]
ag.reposition(x2, (w / 2, h - 150), "center", "top")
c.add(ag.shuffled(x2))

points = ex.text_points("0123456789", 50, pt_spacing=0.5, char_spacing=0.15)
ag.jitter_points(points, 2)
size = ag.Exponential(2.2, stdev=1).values(len(points))
x3 = [ag.circle(c=p, r=size[i], fill=color) for i, p in enumerate(points)]
ag.reposition(x3, (w / 2, h - 250), "center", "top")
c.add(ag.shuffled(x3))

x = []
_images/text1.png

These points can then be manipulated in many ways:

points = ex.text_points("NOPQRST", 40, pt_spacing=0.3, char_spacing=0.15)
ag.jitter_points(points, 8)
size = ag.Exponential(2.2, stdev=1).values(len(points))
x1a = [ag.circle(c=p, r=size[i], fill="black") for i, p in enumerate(points)]

points = ex.text_points("NOPQRST", 40, pt_spacing=1, char_spacing=0.15)
ag.jitter_points(points, 2)
size = ag.Exponential(1.5, stdev=0.5).values(len(points))
x1b = [ag.circle(c=p, r=size[i], fill="white") for i, p in enumerate(points)]

ag.reposition([x1a, x1b], (w / 2, h - 50), "center", "top")
c.new(x1a, x1b)

points = ex.text_points("UVWXYZ", 40, pt_spacing=0.3, char_spacing=0.15)
ag.jitter_points(points, 8)
size = ag.Exponential(2.2, stdev=1).values(len(points))
x2a = [ag.circle(c=p, r=size[i], fill="black") for i, p in enumerate(points)]

points = ex.text_points("UVWXYZ", 40, pt_spacing=1, char_spacing=0.15)
ag.jitter_points(points, 2)
size = ag.Exponential(1.5, stdev=0.5).values(len(points))
x2b = [ag.circle(c=p, r=size[i], fill="white") for i, p in enumerate(points)]

ag.reposition([x2a, x2b], (w / 2, h - 150), "center", "top")
c.add(x2a, x2b)

points = ex.text_points(".,!?:;'\"/", 40, pt_spacing=0.3, char_spacing=0.15)
ag.jitter_points(points, 8)
size = ag.Exponential(2.2, stdev=1).values(len(points))
x3a = [ag.circle(c=p, r=size[i], fill="black") for i, p in enumerate(points)]

points = ex.text_points(".,!?:;'\"/", 40, pt_spacing=1, char_spacing=0.15)
ag.jitter_points(points, 2)
size = ag.Exponential(1.5, stdev=0.5).values(len(points))
x3b = [ag.circle(c=p, r=size[i], fill="white") for i, p in enumerate(points)]

ag.reposition([x3a, x3b], (w / 2, h - 250), "center", "top")
c.add(x3a, x3b)
_images/text2.png

Currently only the characters displayed in these examples are provided, though additional ones can be added on request:

pts = ex.text_points("abcdefg", height=50, pt_spacing=1/4, char_spacing=0.15)
dists = ag.Uniform(0, 10).values(len(pts))
points = [ag.endpoint(p, ag.Uniform(0, 360).value(), dists[i]) for i, p in enumerate (pts)]
radii = 0.5 * np.sqrt(10 - np.array(dists))
x1 = [ag.circle(c=p, r=radii[i]) for i, p in enumerate(points)]
ag.reposition(x1, (w / 2, h - 100), "center", "top")
ag.set_style(x1, "fill", "green")

pts = ex.text_points("hijklm", height=50, pt_spacing=1/4, char_spacing=0.15)
dists = ag.Uniform(0, 10).values(len(pts))
points = [ag.endpoint(p, ag.Uniform(0, 360).value(), dists[i]) for i, p in enumerate(pts)]
radii = 0.5 * np.sqrt(10 - np.array(dists))
x2 = [ag.circle(c=p, r=radii[i]) for i, p in enumerate(points)]
ag.reposition(x2, (w / 2, h - 250), "center", "top")
ag.set_style(x2, "fill", "green")

c.new(x1, x2)
_images/text3.png

Since generated points are grouped by continuous pen strokes, points within each list can be joined:

strokes = ex.text_points("nopqrst", 60, pt_spacing=1,
                         char_spacing=0.2, grouping='strokes')
for stroke in strokes:
    ag.jitter_points(stroke, 10)
x1 = [ag.spline(points=stroke) for stroke in strokes]
ag.reposition(x1, (w / 2, h - 100), "center", "top")

strokes = ex.text_points("uvwxyz", 60, pt_spacing=1,
                         char_spacing=0.2, grouping='strokes')
for stroke in strokes:
    ag.jitter_points(stroke, 10)
x2 = [ag.spline(points=stroke) for stroke in strokes]
ag.reposition(x2, (w / 2, h - 250), "center", "top")

c.new(x1, x2)
_images/text4.png

Filaments

Filaments made of quadrilateral segments can be generated:

dirs = [ag.Param(d, delta=ag.Uniform(min=-20, max=20))
        for d in range(360)[::10]]
width = ag.Uniform(min=8, max=12)
length = ag.Uniform(min=8, max=12)
x = [ag.filament(start=(w / 2., h / 2.), direction=d, width=width,
                 seg_length=length, n_segments=20) for d in dirs]
ag.set_style(x, 'fill', ag.Color(hsl=(ag.Uniform(min=0, max=0.15), 1, 0.5)))
_images/structures1.png

The direction parameter’s delta or ratio attribute allows the filament to move in different directions. Nested deltas produce smooth curves:

direc = ag.Param(90, delta=ag.Param(0, min=-20, max=20,
                                    delta=ag.Uniform(min=-3, max=3)))
x = [ag.filament(start=(z, -10), direction=direc, width=8,
                 seg_length=10, n_segments=50) for z in range(w)[::30]]
ag.set_style(x, 'fill',
             ag.Color(hsl=(0.33, 1, ag.Uniform(min=0.15, max=0.35))))
_images/structures2.png

A tentacle is a convenience wrapper for a filament with steadily decreasing segment width and length to come to a point at a specified total length:

dirs = [ag.Param(d, delta=ag.Param(0, min=-20, max=20,
                                   delta=ag.Uniform(min=-30, max=30)))
        for d in range(360)[::10]]
x = [ag.tentacle(start=(w/2, h/2), length=225, direction=d, width=15,
                 seg_length=10) for d in dirs]

ag.set_style(x, 'fill', ag.Color(hsl=(ag.Uniform(min=0.6, max=0.75), 1, 0.5)))
_images/structures3.png

Blow paint

Blow painting effects (i.e., droplets of paint blown outward from an object) can be created for 0D, 1D, and 2D forms:

pts1 = [(50, 50), (50, 100), (100, 70), (150, 130), (200, 60)]
x1 = ag.blow_paint_area(pts1)

pts2 = [(250, 50), (350, 50), (300, 200)]
x2 = ag.blow_paint_area(pts2, spacing=20, length=20, len_dev=0.4, width=8)
ag.set_style(x2, 'fill', 'orange')

pts3 = [(50, 300), (100, 350), (200, 250), (300, 300)]
y = ag.blow_paint_line(pts3, line_width=8, spacing=15, length=30,
                       len_dev=0.4, width=6)
ag.set_style(y, 'fill', 'red')

z = ag.blow_paint_spot((350, 350), length=20)
ag.set_style(z, 'stroke', 'blue')
_images/structures4.png

Trees

Trees with randomly bifurcating branches can be generated:

x = [ag.tree((200, 200), direction=d,
             branch_length=ag.Uniform(min=8, max=20),
             theta=ag.Uniform(min=15, max=20),
             p=ag.Param(1, delta=-0.08))
     for d in range(360)[::20]]
ag.set_style(x, 'stroke', ag.Color(hue=ag.Normal(0.12, stdev=0.05),
                                   sat=ag.Uniform(0.4, 0.7),
                                   li=0.3))
_images/structures5.png

Fills

These functions fill a region with structures and patterns.

Tiling

These functions divide a region’s area into tiles.

Random polygonal (i.e. Voronoi) tiles can be generated:

outline = ag.circle(c=(200, 200), r=150)
colors = ag.Color(hue=ag.Uniform(min=0, max=0.15), sat=0.8, li=0.5)
x = ag.tile_region(outline, shape='polygon', tile_size=500)
ag.set_style(x['members'], 'fill', colors)
_images/tiling1.png

Random triangular (i.e. Delaunay) tiles can be generated:

outline = ag.circle(c=(200, 200), r=150)
colors = ag.Color(hue=ag.Uniform(min=0, max=0.15), sat=0.8, li=0.5)
x = ag.tile_region(outline, shape='triangle', tile_size=500)
ag.set_style(x['members'], 'fill', colors)
_images/tiling2.png

The edges between polygonal or triangular tiles can be created instead:

outline = ag.circle(c=(200, 200), r=150)
colors = ag.Color(hue=ag.Uniform(min=0.6, max=0.8), sat=0.7,
                  li=ag.Uniform(min=0.5, max=0.7))
x = ag.tile_region(outline, shape='polygon', edges=True, tile_size=1000)
ag.set_style(x['members'], 'stroke', colors)
ag.set_style(x['members'], 'stroke-width', 2)
_images/tiling3.png

Nested equilateral triangles can be created, with the level of nesting random but specifiable:

outline = ag.circle(c=(200, 200), r=150)
color = ag.Color(hue=ag.Uniform(min=0, max=0.15), sat=0.8, li=0.5)
x = ag.fill_nested_triangles(outline, min_level=2, max_level=5, color=color)
_images/tiling5.png

Mazes

These patterns resemble mazes, but are actually random spanning trees:

outline = ag.rectangle(bounds=(0, 0, w, h))
x = ag.fill_maze(outline, spacing=20,
                 style=ag.Maze_Style_Straight(rel_thickness=0.2))
ag.set_style(x['members'], 'fill', 'blue')
_images/mazes1.png

The maze style is defined by an instance of a subclass of Maze_Style:

outline = ag.rectangle(bounds=(0, 0, w, h))
x = ag.fill_maze(outline, spacing=20,
                 style=ag.Maze_Style_Jagged(min_w=0.2, max_w=0.8))
ag.set_style(x['members'], 'fill', 'blue')
_images/mazes2.png

Each style defines the appearance of five maze components that each occupy one grid cell: tip, turn, straight, T, and cross. Each grid cell contains a rotation and/or reflection of one of these components:

outline = ag.rectangle(bounds=(0, 0, w, h))
x = ag.fill_maze(outline, spacing=20,
                 style=ag.Maze_Style_Pipes(rel_thickness=0.6))
ag.set_style(x['members'], 'fill', 'blue')
_images/mazes3.png

The grid can be rotated:

outline = ag.rectangle(bounds=(0, 0, w, h))
x = ag.fill_maze(outline, spacing=20,
                 style=ag.Maze_Style_Round(rel_thickness=0.3),
                 rotation=45)
ag.set_style(x['members'], 'fill', 'blue')
_images/mazes4.png

Custom styles can be used by creating a new subclass of Maze_Style.

Doodles

Small arbitrary objects, a.k.a. doodles, can be tiled to fill a region, creating a wrapping-paper-type pattern. The ‘footprint’, or shape of grid cells occupied, for each doodle is used to place different doodles in random orientations to fill a grid:

def doodle1_fun():
    d = ag.circle(c=(0.5, 0.5), r=0.45)
    ag.set_style(d, 'fill', 'green')
    return d

def doodle2_fun():
    d = [ag.circle(c=(0.5, 0.5), r=0.45),
         ag.circle(c=(1, 0.5), r=0.45),
         ag.circle(c=(1.5, 0.5), r=0.45)]
    ag.set_style(d, 'fill', 'red')
    return d

def doodle3_fun():
    d = [ag.rectangle(start=(0.2, 1.2), w=2.6, h=0.6),
         ag.rectangle(start=(1.2, 0.2), w=0.6, h=1.6)]
    ag.set_style(d, 'fill', 'blue')
    return d

doodle1 = ag.Doodle(doodle1_fun, footprint=[[True]])
doodle2 = ag.Doodle(doodle2_fun, footprint=[[True, True]])
doodle3 = ag.Doodle(doodle3_fun, footprint=[[True, True, True],
                                            [False, True, False]])
doodles = [doodle1, doodle2, doodle3]
outline = ag.circle(c=(200, 200), r=180)
x = ag.fill_wrapping_paper(outline, 30, doodles, rotate=True)
_images/fill2.png

Each doodle is defined by creating a Doodle object that specifies a generating function and footprint. This allows each doodle to vary in appearance as long as it roughly conforms to the footprint.

Other fills

Ripples can fill the canvas while avoiding specified points:

circ = ag.points_on_arc(center=(200, 200), radius=100, theta_start=0,
                        theta_end=360, spacing=10)
x = ag.ripple_canvas(w, h, spacing=10, existing_pts=circ)
_images/ripples1.png

They are generated by a Markov chain telling them when to follow a boundary on the left, on the right, or to change direction. The transition probabilities for the Markov chain can be specified to alter the appearance:

trans_probs = dict(S=dict(X=1),
                   R=dict(R=0.9, L=0.05, X=0.05),
                   L=dict(L=0.9, R=0.05, X=0.05),
                   X=dict(R=0.5, L=0.5))
circ = ag.points_on_arc(center=(200, 200), radius=100, theta_start=0,
                        theta_end=360, spacing=10)
x = ag.ripple_canvas(w, h, spacing=10, trans_probs=trans_probs,
                     existing_pts=circ)
_images/ripples2.png

A billowing texture is produced by generating a random spanning tree across a grid of pixels, and then moving through the tree and coloring them with a cyclical color gradient:

outline = ag.circle(c=(120, 120), r=100)
colors = [(0, 1, 0.3), (0.1, 1, 0.5), (0.2, 1, 0.5), (0.4, 1, 0.3)]
x = ag.billow_region(outline, colors, scale=200, gradient_mode='rgb')

outline = ag.circle(c=(280, 280), r=100)
colors = [(0, 1, 0.3), (0.6, 1, 0.3)]
y = ag.billow_region(outline, colors, scale=400, gradient_mode='hsv')
_images/textures2.png

A region can be filled with structures such as filaments using a generic function that generates random instances of the structure and places them until the region is filled:

def filament_fill(bounds):
    c = ((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2)
    r = ag.distance(c, (bounds[2], bounds[3]))
    start = ag.rand_point_on_circle(c, r)
    dir_start = ag.direction_to(start, c)
    filament = ag.filament(
        start=start,
        direction=ag.Delta(dir_start, delta=ag.Uniform(min=-20, max=20)),
        width=ag.Uniform(min=8, max=12),
        seg_length=ag.Uniform(min=8, max=12),
        n_segments=int(2.2 * r / 10),
    )
    color = ag.Color(hsl=(ag.Uniform(min=0, max=0.15), 1, 0.5))
    ag.set_style(filament, "fill", color)
    return filament


outline = ag.circle(c=(200, 200), r=100)
x = ag.fill_region(outline, filament_fill)
ag.add_shadows(x["members"])
_images/fill1.png

Effects

Shadows can be added to shapes or collections, and shapes can be given rough paper textures:

x = [
    ag.circle(c=(100, 150), r=50, stroke="#FFDDDD"),
    ag.circle(c=(150, 100), r=50, stroke="#DDDDFF"),
]
ag.set_style(x, "stroke-width", 10)
ag.add_shadows(x, stdev=20, darkness=0.5)

y = [[
    ag.circle(c=(300, 250), r=50, fill="#FFDDDD"),
    ag.circle(c=(250, 300), r=50, fill="#DDDDFF"),
]]
ag.add_paper_texture(y)

ag.add_shadows(y, stdev=20, darkness=0.5)
_images/textures1.png