Triggered by Mike Duggan: https://twitter.com/mickduggan/status/1090577885067386880

```
# triggered by Mike Duggan:
# https://twitter.com/mickduggan/status/1090577885067386880
def arrow(center, size, rotation, barThickness, arrowhead):
with savedState():
translate(*center)
scale(size)
rotate(rotation)
points = [
(-0.5, barThickness/2),
(0.5 - arrowhead, barThickness/2),
(0.5 - arrowhead, 0.5),
(0.5, 0),
(0.5 - arrowhead, -0.5),
(0.5 - arrowhead, -barThickness/2),
(-0.5, -barThickness/2),
]
polygon(*points)
def arrowGrid(numArrows, arrowSize, barThickness, arrowhead, rotation):
for i in range(numArrows):
x = i * arrowSize
for j in range(numArrows):
y = j * arrowSize
arrow((x, y), arrowSize, rotation, barThickness, arrowhead)
def easeInOut(t):
assert 0 <= t <= 1
return (1 - cos(t * pi)) / 2
canvasSize = 400
numArrows = 4
arrowSize = canvasSize / numArrows
barThickness = 0.5
arrowhead = 0.5
rotation = 180
duration = 4
framesPerSecond = 15
numFrames = duration * framesPerSecond
for frame in range(numFrames):
t = 4 * frame / numFrames
quadrant = floor(t)
t = easeInOut(t % 1)
angle = ((quadrant + t) * 90) % 380
flip = quadrant % 2
angle = -angle
newPage(canvasSize, canvasSize)
frameDuration(1/framesPerSecond)
if flip:
fill(1)
rect(0, 0, canvasSize, canvasSize)
fill(0)
arrowGrid(numArrows + 1, arrowSize, barThickness, arrowhead, angle)
else:
fill(0)
rect(0, 0, canvasSize, canvasSize)
fill(1)
translate(-arrowSize/2, -arrowSize/2)
arrowGrid(numArrows + 2, arrowSize, 1 - barThickness, arrowhead, angle + 180)
saveImage("Arrows.gif")
```

]]>

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):
paths.append(subset)
print(len(paths))
# this function is a placeholder in case future methods could draw paths better
def seg(start, stop):
line(start, stop)
# is_empty function from <pythoncentral.io/how-to-check-if-a-list-tuple-or-dictionary-is-empty-in-python/>
def is_empty(any_structure):
if any_structure:
return False
else:
return True
# find_path function adapted from <python.org/doc/essays/graphs/>
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]))
else:
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:
valPaths.append(paths[m])
print(len(valPaths))
for n in range(len(valPaths)):
newPage(canvas, canvas)
frameDuration(1/3)
fill(1)
rect(0, 0, canvas, canvas)
translate(canvas/2, canvas/2)
fill(None)
stroke(0)
strokeWidth(width)
lineCap('square')
for s in range(len(valPaths[n])):
g = segs[valPaths[n][s]]
seg(nodes[g[0]], nodes[g[1]])
#saveImage('~/Desktop/schrofer_alpha_test.mp4')
saveImage('~/Desktop/schrofer_alpha_test.gif')
```

Here's the result:

```
# See https://twitter.com/bicvudesign/status/950508015131734017
def circle(x, y, diameter):
radius = diameter / 2
oval(x - radius, y - radius, diameter, diameter)
def element(x, y, diameter, numCircles, relativeStrokeWidth):
step = diameter / numCircles
strokeWidth(relativeStrokeWidth * step)
for i in range(numCircles):
circle(x, y, diameter)
diameter -= step
diameter = 250
numCircles = 6
relativeStrokeWidth = 0.2
numRows = 13
numColumns = 5
stroke(0.7)
fill(1)
for j in range(numRows + 4):
y = 0.5 * diameter + 0.25 * diameter * (numRows - j)
xOffset = 0.5 * diameter * (j % 2)
for i in range(numColumns):
x = diameter * i + xOffset
element(x, y, diameter, numCircles, relativeStrokeWidth)
```

]]>