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
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:
Zone Sigil
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 gimport drawSvg as drawimport 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 = {} outer = string.ascii_lowercase[0:13] mid = string.ascii_lowercase[13:21] inner = string.ascii_lowercase[21:26]# Generate Outer Ringfor i inrange(len(outer)):# Divide the circle into angles for each letter angle = i * (360/len(outer))# Move CW for each letter placement angle =360- angle# Rotate to align A with +Y axis angle +=90if angle >360: angle -=360 coords[outer[i]] = (angle, 1.5)# Generate Middle Ring - Same as abovefor i inrange(len(mid)): angle = i * (360/len(mid)) angle =360- angle angle +=90if angle >360: angle -=360 coords[mid[i]] = (angle, 1)# Generate Inner Ring - Same as abovefor i inrange(len(inner)): angle = i * (360/len(inner)) angle =360- angle angle +=90if angle >360: angle -=360 coords[inner[i]] = (angle, 0.5)# Return coordinate dictionaryreturn 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.
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.
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 = []# Generatefor i inrange(len(instring) -1): start_a, start_r = mapping[instring[i]] end_a, end_r = mapping[instring[i +1]] arc = {'id':i, 'start_a':start_a, 'end_a':end_a, 'radius':start_r} arcs.append(arc)# Add last letter start_a, start_r = mapping[instring[-2]] end_a, end_r = mapping[instring[-1]] arc = {'id':len(arcs), 'start_a':end_a, 'end_a':start_a, 'radius':end_r} arcs.append(arc)# Space last_r =0 spaced_arcs = []for arc insorted(arcs, key=lambda a: a['radius'])[::-1]:if arc['radius'] != last_r: last_r = arc['radius'] x = t =len([a for a in arcs if a['radius'] == last_r]) max_r = last_r +0.25 min_r = last_r -0.25else: t -=1# Calculate an even spread of radii inside the concentric ring of letters nR = max_r - t * (max_r - min_r) / (x +1) newArc = copy.deepcopy(arc) newArc['radius'] = nR spaced_arcs.append(newArc)# Re-order the spaced arcs by ID. Could be done by deep copying the origin list for spacing spaced_arcs =sorted(spaced_arcs, key=lambda a: a['id'])
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 s = draw.Path()# Seed last-radius for CW/CCW and Arc generation last_r = arcs[0]['radius']# CW or CCW? _cw =abs(spaced_arcs[0]['start_a'] - spaced_arcs[0]['end_a']) <=180# Iterate through all paths and generate 'd' parameterfor i, arc inenumerate(spaced_arcs):# Path object to generate 'd' parameter. Quick and dirty so I don't have to calculate curves p = draw.Path() start_a = arc['start_a'] _radius = arc['radius'] end_a = arc['end_a']# Determine 'Same Line' by looking at original arcs list for un-spaced radii same_line =Trueif arcs[i]['radius'] == last_r elseFalse# Switch CW and CCW if sequential same-ring lettersif same_line: _cw =not _cw# Sweep flag for 'A' command sweep =0if _cw else1# Generate the arc p.arc(0, 0, radius * _radius, start_a, end_a, cw = _cw)if i ==0:# If we're at the start, append the first arc with no funny connections s.append(p.args['d'])elif i ==len(spaced_arcs) -1: svg_d = p.args['d'][1:]ifnot same_line: coord = svg_d.split(' ')[0] s.append(f' L{coord}')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 svg_d = p.args['d'][1:]if same_line:# Generate a sweep if the next arc is in the same leter ring# Calculate the sweep ellipse radius sweep_r =abs(spaced_arcs[i -1]['radius'] - _radius)# Grab the coordinates from the extracted parameter coord = svg_d.split(' ')[0]# append the 'A' curve-to command s.append(f' A{sweep_r},{sweep_r} 0 0,{sweep}{coord}')# Attatch the sweep s.append(p.args['d'])else:# Generate a jump line to a distant ring and update the last radius for same-line checking last_r = arcs[i]['radius'] s.append(f' L{svg_d}')
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: s_fill = copy.deepcopy(s) s_fill.args['fill'] ='#721e1e' s_fill.args['fill-opacity'] ='53%' d.append(s_fill)## --- Draw Boundary Rings --- ##if args.draw_bounds: d.append(draw.Circle(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))## --- Draw the Sigil --- ## s.args['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 d.append(s)# Return the drawing object for outputreturn 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): sigil = Generate(args) sigil.setRenderSize(0, 0)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.
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
the range(len(instring) - 1)… Gotta love those magic
constants (magick constants?)↩︎
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.↩︎