Schrofer modular script code

  • Here's the cleaned-up code for generating a modular script that Jurriaan Schrofer designed. Schrofer made a number of versions of the script over the years—I'll be posting to Twitter and/or Fonts in Use a recap of that history. It's interesting to me because his revisions showed that whether he knew it or not he was pushing the design from less to more modular, reducing and simplifying the rules and making them more extensible.

    Genootschap voor Reclame annual report, 1970

    Study for cover of a book series, Le Saviore Geographique (1971)

    Cover of book from series Le Saviore Historique (1973)

    The most refined version of Schrofer's design uses a 3 x 3 grid of nodes. Each glyph is a path that uses 8 of the 12 possible node⟷node segments to connect all the nodes in the grid. One other apparent rule, that there be no enclosed counters, is enforced by the other rules and the choice of parameters (8 segments, all nodes).

    The code I wrote defines the grid nodes and a list of all unique segments. It then uses combinations() to generate a list of every combination of 8 segments. There are 495 candidate paths meeting this criterion.

    Then the code tests each candidate by making a dictionary, keyworded by node, of every segment in the path. It validates the path by checking, first, whether the path does in fact touch every node: if there are no values for any node in the dictionary, then the path fails.

    Second, it uses a rudimentary graph-checking function (similar to what map applications use to find and compare routes) against that dictionary to confirm that it's possible to use the segments in the candidate path to connect every node to every other node. If it can't find a path for every pairwise combination of nodes, then the path fails.

    There are 192 8-segment paths that meet both criteria, so 192 possible glyphs in the system that Schrofer defined. The code draws each path, in such a way that you can adjust the weight of the strokes as Schrofer did—at their most extreme thickness, almost reversing the positive/negative relationship of the strokes and spaces.

    from itertools import combinations
    # next steps: make the number of nodes and number of nodes that path must touch parametric and extensible, generate a master dictionary of nodes/segments, then generate and test paths from that dictionary
    canvas = 720
    off = canvas/4 # distance between the nodes of the glyph
    nodes = [
        (-off, off),
        (0, off),
        (off, off),
        (-off, 0),
        (0, 0),
        (off, 0),
        (-off, -off),
        (0, -off),
        (off, -off)]
    width = off/8 # thickness of the drawn path
    numSegs = 8 # how many segments long the path for each glyph must be
    segs = [
        (0, 1),
        (0, 3),
        (1, 2),
        (1, 4),
        (2, 5),
        (3, 4),
        (3, 6),
        (4, 5),
        (4, 7),
        (5, 8),
        (6, 7),
        (7, 8)]
    paths = []
    for subset in combinations(range(0, len(segs)), numSegs):
    # this function is a placeholder in case future methods could draw paths better
    def seg(start, stop): 
        line(start, stop)
    # is_empty function from <>
    def is_empty(any_structure):
        if any_structure:
            return False
            return True
    # find_path function adapted from <>
    def find_path(graph, start, end, path=[]):
        path = path + [start]
        if start == end:
            return path
        for node in graph[start]:
            if node not in path:
                newpath = find_path(graph, node, end, path)
                if newpath: return newpath
        return None
    def validate_path(path):
        # dictionary of all segments in path kw by node
        p = {'0': [], '1': [], '2': [], '3': [], '4': [], '5': [], '6': [], '7': [], '8': []}
        for n in range(len(path)): # this could use cleaning up
            for r in range(2):
                if r == 0:
                    k = str(segs[path[n]][r])
                    p['%s' %k].append(str(segs[path[n]][r+1]))
                    k = str(segs[path[n]][r])
                    p['%s' %k].append(str(segs[path[n]][r-1]))
        for key in p:
            if is_empty(p[key]) == True: # do path segs include all nodes
                return False
            for l in range(len(p)):
                if find_path(p, key, str(l)) == None: # are all nodes connected by same path
                    return False
        return True
    valPaths = []   
    for m in range(len(paths)):
        if validate_path(paths[m]) == True:
    for n in range(len(valPaths)):
        newPage(canvas, canvas)
        rect(0, 0, canvas, canvas)
        translate(canvas/2, canvas/2)
        for s in range(len(valPaths[n])):
            g = segs[valPaths[n][s]]
            seg(nodes[g[0]], nodes[g[1]])

    Here's the result:

Log in to reply