User Guide

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

import algoraphics as ag
w = 400
h = 400

and appending this:

ag.write_SVG(x, w, h, 'test.svg')
ag.to_PNG('test.svg')

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.

Shapes

Primitive visible objects are represented as dictionaries. Their type attribute defines what is drawn and which other attributes will be accessed (though additional attributes can be supplied and will be ignored).

Object

Attributes

circle

c (point), r (float)

group

members (list), clip (list or dict)

raster

image (PIL Image), w (float), h (float), format (str)

line

p1 (point), p2 (point)

polygon

points (list)

polyline

points (list)

spline

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

text

text (str), x (float), y (float), align (str), font_size (float)

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 a collection of shapes.

Convenience functions like circle exist that simply return a dictionary. Functions also exist to define shapes in alternative ways for convenience. For example, the rectangle function accepts a starting point, width, and height, or just a set of x and y bounds, and returns a polygon defined by its four corners.

Styles

SVG attributes are used to style shapes and are stored as a dictionary in each shape’s style attribute.

Parameters

Many algoraphics functions accept abstract parameters that specify a distribution to randomly sample from. 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
center = (ag.Uniform(10, w - 10), ag.Uniform(10, h - 10))
radius = ag.Uniform(1, 10)
color = ag.Color(hue=ag.Uniform(0.4, 0.6), sat=0.9, li=0.5)
x = [ag.circle(center, radius) for i in range(100)]
ag.set_style(x, 'fill', color)
_images/param1.png

or this:

w, h = 400, 200
center = (ag.Param([100, 300]), 100)
radius = ag.Param(100, delta=-1)
color = ag.Color(hue=0.8, sat=0.9, li=ag.Uniform(0, 1))
x = [ag.circle(center, radius) for i in range(100)]
ag.set_style(x, 'fill', color)
_images/param2.png

or this:

w, h = 400, 200
center = (ag.Param(0, delta=4), ag.Uniform(0, h))
radius = ag.Uniform(5, 30)
color = ag.Color(hue=ag.Param(0, delta=0.005), sat=0.9, li=0.5)
x = [ag.circle(center, radius) for i in range(100)]
ag.set_style(x, 'fill', color)
_images/param3.png

Parameter classes for random distributions like Uniform, Normal, and Exponential are memoryless. A parameter can instead have a delta attribute, whose value is added to the last value to get the next one each time the parameter value is accessed:

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

p2y = ag.Param(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.Param(30, min=0, max=60,
               delta=ag.Param(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

The delta attribute can itself be a parameter, which can allow for shape attributes to be generated as a random walk (middle row of lines above).

If the delta parameter has its own delta attribute, second-order changes are produced (bottom row of lines above).

Parameters can have a ratio attribute instead of delta, which works the same way but multiplies, rather than adds, ratio to the last value.

A parameter can also be defined with a list of values, which will be uniformly randomly sampled:

w, h = 400, 200
center = (ag.Uniform(10, w - 10), ag.Uniform(10, h - 10))
radius = ag.Uniform(5, 15)
color = ag.Param(['blue', 'blue', 'blue', 'red'])
x = [ag.circle(center, radius) for i in range(100)]
ag.set_style(x, 'fill', color)
_images/param5.png

Finally, a parameter can be defined with an arbitrary function, which will be called with no arguments to generate values.

Note that once a shape is generated, its parameters are generally static.

Colors

Colors are represented as objects of the Color class. They are generally defined in the HSL (hue, saturation, lightness) color space. If these are supplied as Param objects, the color object represents a distribution from which colors will be sampled:

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 = ag.fill_spots(outline)
ag.set_style(x, 'fill', color)
_images/fill3.png

Color values can be defined and retrieved using other color specifications.

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.

Output

Shapes are written to an SVG file using the write_SVG function. Each type of shape corresponds to a SVG object type or a specific form of one.

algoraphics

SVG

circle

circle

group

g

raster

image

line

line

polygon

polygon

polyline

polyline

spline

path made of bezier curves

text

text

SVG-rendered effects like shadows and paper texture 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.

SVG files can then be converted to PNG files using the to_PNG function.

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)
    x[i] = ag.fill_maze_hue_rotate(outline, spacing=5, style=maze,
                                   color=color)
    ag.region_background(x[i], ag.contrasting_lightness(color, light_diff=0.2))
    ag.set_style(outline, 'fill', color)
ag.add_paper_texture(x)
_images/images3.png

Structures

Text

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

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

y = ag.splatter_text('ABCDEFG', height=50, spread=2, density=2,
                     min_size=1, max_size=3, color=color)
ag.reposition(y, (w / 2, h - 50), 'center', 'top')
x.append(y)

y = ag.splatter_text('HIJKLM', height=50, spread=2, density=2,
                     min_size=1, max_size=3, color=color)
ag.reposition(y, (w / 2, h - 150), 'center', 'top')
x.append(y)

y = ag.splatter_text('0123456789', height=50, spread=2, density=2,
                     min_size=1, max_size=3, color=color)
ag.reposition(y, (w / 2, h - 250), 'center', 'top')
x.append(y)
_images/text1.png

These points can then be manipulated in many ways:

x = []

y = ag.double_dots_text('NOPQRST', height=40)
ag.reposition(y, (w / 2, h - 50), 'center', 'top')
x.append(y)

y = ag.double_dots_text('UVWXYZ', height=40, top_color='#FF8888',
                        bottom_color='#555555')
ag.reposition(y, (w / 2, h - 150), 'center', 'top')
x.append(y)

y = ag.double_dots_text(".,!?:;'\"/", height=40, top_color='#FF8888',
                        bottom_color='#555555')
ag.reposition(y, (w / 2, h - 250), 'center', 'top')
x.append(y)
_images/text2.png

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

x = []

y = ag.hazy_text('abcdefg', height=50, spread=10, density=3,
                 min_size=0.5, max_size=2, color='green')
ag.reposition(y, (w / 2, h - 100), 'center', 'top')
x.append(y)

y = ag.hazy_text('hijklm', height=50, spread=10, density=3,
                 min_size=0.5, max_size=2, color='green')
ag.reposition(y, (w / 2, h - 250), 'center', 'top')
x.append(y)
_images/text3.png

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

x = []

y = ag.squiggle_text('nopqrst', height=60, spread=10, density=1)
ag.reposition(y, (w / 2, h - 100), 'center', 'top')
x.append(y)

y = ag.squiggle_text('uvwxyz', height=60, spread=10, density=1)
ag.reposition(y, (w / 2, h - 250), 'center', 'top')
x.append(y)
_images/text4.png

Actual SVG text can also be produced:

w, h = 400, 100
x = ag.caption("SVG text.", x=w-20, y=20)
_images/text5.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/grid1.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/grid2.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/grid3.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/grid4.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:

color = ag.Color(hsl=(ag.Uniform(min=0, max=0.15), 1, 0.5))
outline = ag.circle(c=(200, 200), r=100)
dir_delta = ag.Uniform(min=-20, max=20)
width = ag.Uniform(min=8, max=12)
length = ag.Uniform(min=8, max=12)
filfun = ag.filament_fill(direction_delta=dir_delta, width=width,
                          seg_length=length, color=color)
x = ag.fill_region(outline, filfun)
ag.add_shadows(x['members'])
_images/fill1.png

Effects

Shadows can be added to shapes or collections:

d = [dict(command='M', to=(50, 50)),
     dict(command='L', to=(50, 350)),
     dict(command='L', to=(350, 50)),
     dict(command='L', to=(50, 50)),
     dict(command='M', to=(70, 70)),
     dict(command='L', to=(320, 70)),
     dict(command='L', to=(70, 320)),
     dict(command='L', to=(70, 70))]
path = dict(type='path', d=d)
ag.set_style(path, 'fill', "#55CC55")

centers = [(300, 250), (250, 300)]
circles = [ag.circle(c=c, r=50) for c in centers]
ag.set_style(circles[0], 'fill', "#FFDDDD")
ag.set_style(circles[1], 'fill', "#DDDDFF")

x = [path, circles]
ag.add_shadows(x, stdev=20, darkness=0.5)
_images/textures1.png

Shapes or collections can be given a rough paper texture, and their edges can appear torn:

x = [ag.rectangle(start=(50, 50), w=300, h=300),
     ag.circle(c=(200, 200), r=150)]
ag.set_style(x[0], 'fill', 'green')
ag.set_style(x[1], 'fill', '#FFCCCC')
ag.add_paper_texture(x)
x = ag.tear_paper_rect(x, (60, 340, 60, 340))
_images/textures3.png