An in-depth look at a sigil generator using the Witches Circle pattern. Developed in Python and served as a Flask application through uWSGI. This is an extension and redevlopment of an earlier app that I originally hosted as a programming toy, but I’ve gone through and updated the generator code to create some far more æsthetic results, as well as using a new library for generating the result as an SVG
How does a computer factor into magick? Traditionally speaking, it wouldn’t even be a question. However, traditionally speaking also means that you should only ever be using beeswax candles, which are at a premium compared to paraffin wax. Of course, they may have something to do with the relative novelty of paraffin, being a product of the petroleum industry and far more recent a development1 when compared to the bee2. When you consider the fact that paraffin wasn’t even a candle option until the Victorian magickal renaissance, it makes for little wonder why texts would prescribe for a beeswax candle— They weren’t being snobs about materials, it would’ve just been vastly preferred over a tallow candle and been far more ubiquitous. Realistically, it was the period equivalent of popping out to a Yankee Candle and buying the first tea light that fits your needs.
Consider in that context, then, the application of other modern conveniences. For instance, why collect a library of reference books when you can easily pop online and look up any piece of information you could desire? Why sit and flip through notes when you can keep a personal wiki3 that lets you leverage search tools and regular expressions to find any thought you have ever recorded? In short: why not let a computer do some of the grunt work for you?
Using Computers to Generate Sigils
By my own loose interpretation of how and why magick works4: as long as the act of writing, debugging, testing, and deploying the code represents a significant investment of time, energy, focus, and Will, then it can also be considered to represent a deliberate ritual setting and scenario. Considering my personal belief that a ritual is only useful in the way that it “sets the mood” for a magickal event, which encourages the appropriate direction of Willful Energy, this seems to track. Taken, as well, within the occult framework of Chaos Magick5, why not give it a try?
The Plan
But, how can we get a meaningful result from a program? In Chaos Magick’s toolbox, frequently the first sledgehammer found is that old chestnut: Sigil Magick. The methods may vary from practitioner to practitioner, but the general gist is:
- Create a statement of intent:
I WANT TO DEMONSTRATE MAGICK
- Rephrase the statement as an immediate affirmation:
IT IS MY WILL THAT I DEMONSTRATE MAGICK
- Remove all vowels6:
T S MY WLL THT DMNSTRT MGCK
- Remove all duplicate letters:
TSMYWLHDNRGCK
- Mash those letters up into a sigil, let your inner vision of what “magick is supposed to look like” guide you. This is the artistic portion, and (for me) is the part where you really breathe that life and energy and intent into the creation. This is what plants the intent firmly into your subconscious and smuggles it past the “psychic censor”
- “Fire the sigil” in some manner. Maybe stare at it and masturbate, or light it on fire, or scream it into the void: Whichever way you want. This is not an instruction in magick, just an example.
Being a list of steps and instructions, and the overall iterative nature in the act of sigilizing the intent, it seems a prime candidate for automation. But: artistic vision isn’t really great for computers yet7, so an appropriate rules-based schema should be used. I considered a few different approaches, such as jittering and rotating letters, but none of them felt right until I remembered the Magick Square/“Kamea” methods of generating the sigils and seals of various planetary intelligences, and it was while researching the rules for generating a Magick Square that I stumbled upon the “Witches Circle” pattern.
The “Witches Circle” or “Witches Wheel”
A much simpler method of enciphering text, the wheel is constructed as follows:
- The 26 letters of the Latin alphabet are written in alphabetically in 3 concentric rings, moving clockwise with each letter
- The outer ring has the letters a through m
- The middle ring has the letters n through u
- The innermost ring has the letters v through z
To create a sigil representing some word, start at the first letter of the word and leave a mark of some sort to indicate beginning the word. Find the next letter of the word, and draw a line connecting your mark to that letter. Repeat this process until you get to the last letter, and then leave some mark indicating the end. Any which way you decide to interpret these rules is up to you, but I personally prefer the æsthetics that come from curving the lines to follow the circular pattern combined with sharp straight lines in or out to transition between the rings.
For instance, the word “Zone” would be constructed by: - Drawing a mark at the letter “Z” (innermost ring) - Locating the letter “O” on the wheel, draw a segment of a circle following the inner ring of letters until aligned with the letter “O” (middle ring) - Draw a straight line outwards to meet the letter - Locate letter “N” also in the middle ring, and draw a segment of circle following the middle ring until reaching the letter “N” - Locating the letter “E” on the wheel, draw a segment of a circle following the middle ring of letters until aligned with the letter “E” (outer ring) - Draw a straight line outwards to meet the letter - End the segment with a marker
This would produce a sigil that looks something like this:
Sigil Generator
To accomplish this generation, I’m using the drawSvg library. This lets me take a few shortcuts in defining arcs and curves, while still allowing me to manually hack together a few things like the jumps to new rings and connecting arcs within the same letter ring.
import grimoire as g
import drawSvg as draw
import string, argparse, copy
There is also a custom module of mine, Grimoire, which contains some utility functions, such as: Removing duplicate letters, removing vowels, and cleaning the input so it only contains lowercase ascii characters.
The raw code for the new sigil generator consists of 3 main parts
Mapping the Wheel
Firstly, we need to generate the weel letters and assign them both an angle and a radius to represent where they live around the wheel as well as which ring they live in. The radii are defined arbitrarily with the middle ring living at \(r = 1\) and the two other rings living at \(\pm \, 0.5\) relative to there. The angles, expressed in euler degrees, are then calculated by dividing the circle into however many slices are needed for that ring, and arbitrarily rotated to align the letter “A” with the vertical axis.
These coordiantes are added as a tuple value in a dictionary with the key being the lowercase letter, and the resulting dictionary of coordinates is returned.
## -- Generate Polar Coordinates for each letter -- ##
def DoWheel():
= {}
coords = string.ascii_lowercase[0:13]
outer = string.ascii_lowercase[13:21]
mid = string.ascii_lowercase[21:26]
inner
# Generate Outer Ring
for i in range(len(outer)):
# Divide the circle into angles for each letter
= i * (360/len(outer))
angle # Move CW for each letter placement
= 360 - angle
angle # Rotate to align A with +Y axis
+= 90
angle if angle > 360:
-= 360
angle = (angle, 1.5)
coords[outer[i]]
# Generate Middle Ring - Same as above
for i in range(len(mid)):
= i * (360/len(mid))
angle = 360 - angle
angle += 90
angle if angle > 360:
-= 360
angle = (angle, 1)
coords[mid[i]]
# Generate Inner Ring - Same as above
for i in range(len(inner)):
= i * (360/len(inner))
angle = 360 - angle
angle += 90
angle if angle > 360:
-= 360
angle = (angle, 0.5)
coords[inner[i]]
# Return coordinate dictionary
return coords
Generating the Sigil
The easy stuff
The bulk of the generation happens in this function. In order to cut
down on screenspace and annoyance in retyping the same variable names
over and over, I’m using the argparse
module to pass the
query parameters in.
## -- Generate the Sigil SVG -- ##
def Generate(args):
= fill_color = '#8A8785'
stroke_color = args.line_width
line_width = args.radius
radius
# Clean the text
= args.text
q = g.RemoveNonAscii(q)
instring
if args.remove_vowels:
= g.RemoveVowels(instring)
instring if args.remove_duplicates:
= g.RemoveDuplicates(instring) instring
This first part is pretty self-explanatory: setting some basic
parameters from the arguments, like the line width and radius, as well
as parsing through the text. As explained above, the first step is to
clean and normalize the input string, which is handled by the
RemoveNonAscii
method in the Grimoire
library.
This is then optionally passed through two other methods to remove the
vowels and the duplicate letters from the input.
I’ve chosen to make these optional here because the sigil wheel has the vowels available to choose from, and duplicate letters can sometimes generate more interesting layered curves. But, of course, it all only matters how I feel like it looks so— I’m really at my own discretion here. It does also mean, however, that certain long phrases can be shortened down— reducing the visual clutter of the resulting sigil.
Mapping and creating some markers
# Create the SVG object
= draw.Drawing(args.size, args.size, origin='center')
d
# Map the wheel
= DoWheel()
mapping
## --- Marker Objects -- ##
# Arrow
= draw.Marker(-0.3, -0.5, 1, 0.5, scale=4, orient='auto')
m_arrow -0.3, -0.5, 0, 0, -0.3, 0.5, 0.9, 0, fill=fill_color, close=True))
m_arrow.append(draw.Lines(# --- Bar
= draw.Marker(0, -0.5, 1, 0.5, scale=4, orient='auto')
m_bar -0.25, -0.5, -0.25, 0.5, 0.25, 0.5, 0.25, -0.5, fill=fill_color, close=True))
m_bar.append(draw.Lines(# --- Circle
= draw.Marker(-1, -1, 1, 1, scale=2, orient='auto')
m_circle 0, 0, .75, fill=fill_color, stroke_width=0.25, stroke=stroke_color)) m_circle.append(draw.Circle(
This next section of code is just a bit of housekeeping and setting up for the generation. We create a drawSvg object to construct the sigil within, and we generate the mapping coordinate dictionary as described above. We also set up some markers that might be useful later: An arrow, a bar, and a circle (just to keep it interesting)
Generating the curve segments
## --- Generate and space the arcs --- ##
= []
arcs # Generate
for i in range(len(instring) - 1):
= mapping[instring[i]]
start_a, start_r = mapping[instring[i + 1]]
end_a, end_r = {'id':i, 'start_a':start_a, 'end_a':end_a, 'radius':start_r}
arc
arcs.append(arc)
# Add last letter
= mapping[instring[-2]]
start_a, start_r = mapping[instring[-1]]
end_a, end_r = {'id':len(arcs), 'start_a':end_a, 'end_a':start_a, 'radius':end_r}
arc
arcs.append(arc)
# Space
= 0
last_r = []
spaced_arcs for arc in sorted(arcs, key=lambda a: a['radius'])[::-1]:
if arc['radius'] != last_r:
= arc['radius']
last_r = t = len([a for a in arcs if a['radius'] == last_r])
x = last_r + 0.25
max_r = last_r - 0.25
min_r else:
-= 1
t
# Calculate an even spread of radii inside the concentric ring of letters
= max_r - t * (max_r - min_r) / (x + 1)
nR = copy.deepcopy(arc)
newArc 'radius'] = nR
newArc[
spaced_arcs.append(newArc)
# Re-order the spaced arcs by ID. Could be done by deep copying the origin list for spacing
= sorted(spaced_arcs, key=lambda a: a['id']) spaced_arcs
The real meat and potatoes of the generation algorithm: creating a
list of arcs, complete with their start and end angles and relative
radii. It’s a pretty simple matter, just a workhorse loop. We iterate
through each letter of the string, looking at what letter we begin at
and what letter we end at8, get the radius and angle of both
points, and create a new object with an ID representing its position in
the string9, the start angle, end angle, and the
radius. This arc will always have a radius defined as matching the
origin letter. However, this will also mean that we need to run the
mapping and generation one extra time, because of the fact that we’re
only incremementing until len - 1
The next thing that this code block does is space apart arcs on the
same ring. This is important because I wanted to see the distinct
separation in the curve sements as they traverse wheel. It also just
looks cooler, and the rule of cool must always apply. For this, sort the
arcs by their radius, and then iterate from largest to smallest with the
clever python slicing [::-1]
and generate an even
distribution of numbers within the range defined as \(min, max = radius \pm \, 0.25\) using the
clever calculation: \(newRadius = max - t
\times (max - min) / (x + 1)\) for \(t\) being the ordinal location in the ring,
and \(x\) being the number of values to
generate.
These new arc segments are then added to a list. This could have been
made simpler with some clever use of the copy
library, but
since I still need the original radii later10,
I opted to hold that list harmless in all permutations. Finally, we
reorder the new list using their IDs to get the segments back in their
initial order.
Banging together the final SVG
## --- SVG Path Construction --- ##
# Create a parent path object : Will receive the concatenated 'd' parameter
= draw.Path()
s
# Seed last-radius for CW/CCW and Arc generation
= arcs[0]['radius']
last_r
# CW or CCW?
= abs(spaced_arcs[0]['start_a'] - spaced_arcs[0]['end_a']) <= 180
_cw
# Iterate through all paths and generate 'd' parameter
for i, arc in enumerate(spaced_arcs):
# Path object to generate 'd' parameter. Quick and dirty so I don't have to calculate curves
= draw.Path()
p = arc['start_a']
start_a = arc['radius']
_radius = arc['end_a']
end_a
# Determine 'Same Line' by looking at original arcs list for un-spaced radii
= True if arcs[i]['radius'] == last_r else False
same_line
# Switch CW and CCW if sequential same-ring letters
if same_line:
= not _cw
_cw
# Sweep flag for 'A' command
= 0 if _cw else 1
sweep
# Generate the arc
0, 0, radius * _radius, start_a, end_a, cw = _cw)
p.arc(
if i == 0:
# If we're at the start, append the first arc with no funny connections
'd'])
s.append(p.args[elif i == len(spaced_arcs) - 1:
= p.args['d'][1:]
svg_d if not same_line:
= svg_d.split(' ')[0]
coord f' L{coord}')
s.append(else:
# Otherwise generate either a jump line or a sweep to connect to the next arc
# Trim the 'M' move-to command to get the raw coords
= p.args['d'][1:]
svg_d if same_line:
# Generate a sweep if the next arc is in the same leter ring
# Calculate the sweep ellipse radius
= abs(spaced_arcs[i - 1]['radius'] - _radius)
sweep_r # Grab the coordinates from the extracted parameter
= svg_d.split(' ')[0]
coord # append the 'A' curve-to command
f' A{sweep_r},{sweep_r} 0 0,{sweep} {coord}')
s.append(# Attatch the sweep
'd'])
s.append(p.args[else:
# Generate a jump line to a distant ring and update the last radius for same-line checking
= arcs[i]['radius']
last_r f' L{svg_d}') s.append(
If the generation was the engine in this car, then this would be the transmission. Meaning, at least to a not-so-mechanically-inclined software developer like myself, if the first part breaks: I can fix it. If this part broke, I’d rather just throw it out and get a brand new algorithm. There is a lot of funkiness here that involved digging into the SVG standard, so I’m going to make my best effort attempt at explaining what I’m doing here.
Firstly, I’m creating a drawSvg Path
object, because
having one makes it easy to generate curve segments, and I can pull path
data from it later. This is really just a quick hack to avoid doing
trigonometry to generate the curve properties11.
I’m also doing some artistically flavored parsing: There is no rhyme or
reason to why I’m assigning whatever I am to the _cw
variable, other than what I wound up with looks a lot cooler. However,
flipping this variable really just makes the spiral begin in the
opposite direction.
For each generated arc, we then step through and create a path that
is defined to move through its designated start/end points as calculated
above. If the path is in the same ring segment, as determined by the
same_line
variable, the _cw
variable reverses,
which means that a line between multiple letters on the same ring will
double back on itself to reach its destination. This is both
æsthetically pleasing, and allows for a little ease in decoding the
output12.
- The code then decides whether it should create an arc along an elipse to join the current segment (if it’s on the same line) or if it should generate a line jump to another ring, and then either inserts the A (curve-to) or L (line-to) SVG instruction, which is then appended to the path data for the current path. This is done by stripping the M (move-to) tag from the next arc in the list and calculating based on the absolute coordinates that follow.
Rendering the path
## --- Render a path for color fill under hard lines --- ##
if args.do_fill:
= copy.deepcopy(s)
s_fill 'fill'] = '#721e1e'
s_fill.args['fill-opacity'] = '53%'
s_fill.args[
d.append(s_fill)
## --- Draw Boundary Rings --- ##
if args.draw_bounds:
0, 0, radius * 1.75, fill='none', stroke='#111111', stroke_width=3.5))
d.append(draw.Circle(0, 0, radius * 1.25, fill='none', stroke='#111111', stroke_width=3.5))
d.append(draw.Circle(0, 0, radius * 0.75, fill='#111111', stroke='#111111', stroke_width=3.5))
d.append(draw.Circle(
## --- Draw the Sigil --- ##
'fill'] = 'none'
s.args['marker-start'] = m_bar
s.args['marker-end'] = m_arrow
s.args['stroke'] = stroke_color
s.args['stroke-width'] = line_width
s.args[
d.append(s)
# Return the drawing object for output
return d
Finally, the path is added to the Drawing object we created in the first section. If the optional parameters to generate a “fill” or a “boundary rings” visual layer were set, then these elements are constructed. The fill is rather simply just telling the renderer to do a fill on the arbitrary path created by our lines. I did it by accident, honestly, and just sort of liked the visual interest it gives the sigil. This is added first so the “boundary rings” can be drawn over it.
The “boundary rings” are simple concentric circles marking the innermost and outermost bounds of each letter ring. As described above, these can be seen as each letter ring’s relative radial offset \(r\) being expanded to \(r \pm \, 0.25\) units. The center is filled with the same color, creating a dark circle over either the background color or the fill color, if the “fill” parameter was toggled.
Finally, the sigil is drawn on top of the other layers, and the object is returned to its caller for processing. This is also where the start and end markers are added to the path. I decided to go with the “bar” for a traditional feeling, while the flair of the “arrow” invokes the spirit of the energy moving through the spiral.
Returning the Image
The resulting SVG is then returned as a data uri with the following
utility method. This data uri is then parsed in the flask application
using the datauri
module and returned, allowing for a
dynamic endpoint that can generate an image on the fly and directly
insert it into a page. I (will) use this to generate the sigilized
versions of the article titles on each page.
## --- Render for Web --- ##
def RenderWebSigil(args):
= Generate(args)
sigil 0, 0)
sigil.setRenderSize(return sigil.asDataUri()
The render size is set to 0 so I can specify the height and width in code, either HTML or here in my markdown editor. From here, it is up to the site and browser to render the resulting image, and the sigil is considered to be out of my hands.
Lessons Learned
Overall, this was a fun and informative project to work on. I had
originally had the idea to generate sigils a few years ago when working
on some small applications for use in my cyberoccult experiments, but
they were very pixelated and locked in to a single size or format. This
was mostly a limitation of using Pillow
and
aggdraw
modules in python, but I had always had the plan to
turn the generator into something that could be used for SVG.
This desire mostly came from a place of really really not liking how I had to generate rasterized curves in those libraries. They always looked too jagged. I also had some issues getting the spiral to line up correctly or offset itself on the same radius, which made for some interesting but unsatisfying images. I’ll see if I can dig one up for this article some time.
Overall, I’m really pleased with these results. The path feels simultaneously labyrinthine and sci-fi. Like a polyamorous love child between a crop circle, a generic cyberpunk greeble, and a stone circle. There’s something soothing about following the path as it winds through the letters, making sure that it stops at all the places it needs to, and retracing the original phrase in my head. Almost meditative in my ability to contemplate the resulting image, which I feel has a great benefit to its occult significance and utility.
I’m starting to think of ways I can use this new python module to generate other types of sigils, too. A rework of my kamea sigil generator may be upcoming, including the ability to choose the planetary square being used for the generation, which could conceptually lead to the creation of any number of new “angelic seals” for uses in talismanic magick or just as symbols of power. But, of course, that remains to be seen.
First created by Carl Reichenbach in 1830.↩︎
Estimated to have first evolved somewhere around 130 million years ago.↩︎
Or a personal website.↩︎
Or otherwise, at least appears to work.↩︎
Wherein all rules can be considered suggestions, as long as breaking them doesn’t betray personal belief for the sake of conveninence.↩︎
I’m not really sure the reason why. But I also don’t count ‘Y’ as a vowel in my personal sigilization practice. For me, it’s just too convenient of a shape to remove.↩︎
This might actually be an interesting experiment to try out, and would coincide with my current personal projects in Machine Learning and GAN art. At the very least, it could be fun to run the statements of intent through a CLIP guided neural net to see what it creates. These could then, potentially, be used as a visual sigil while also smuggling the magickal intent into a digital subconsciousness while the NN iterates on the same thought like a digital mantra↩︎
Hence the rogue
- 1
in therange(len(instring) - 1)
… Gotta love those magic constants (magick constants?)↩︎Used later to reorder the list↩︎
For calculating ring jump maneuvers, which sounds sci-fi as hell↩︎
Actually, it was just written before I dug into the SVG standards and definitions, so it might be easier than I think to hand-craft a curve with a defined radius.↩︎
- ↩︎