A DSL for rendering magic circles and runes

Let’s make magic circles/runes!

Turn this:

rune do
    scale 0.9 do 
        circle
        polygon 7
        star 14, 3
        star 7, 2
        children 7, scale: 1/8r, offset: 1 do |i|
            circle
            invert do
                text (0x2641 + i).chr Encoding::UTF_8
            end
        end
    end
    scale 0.15 do
        translate x: -2 do circle; moon 0.45 end
        circle
        translate x: 2 do circle; moon 0.55 end
    end
end

Into this:

All through the magic of RUBY!

Dir["./components/*.rb"].sort.each { |file| require file }

r = eval(ARGF.read)
puts r.to_xml
$NON_SCALING_ELEMENTS = %w[circle path polygon line]

class String
    def squish
        gsub!(/\A[[:space:]]+/, '')
        gsub!(/[[:space:]]+\z/, '')
        gsub!(/[[:space:]]+/, ' ')
        self
    end
end

class Node
    def initialize(name, children=nil, **attributes)
        @name = name
        @attributes = attributes
        @children = children || []

        @attributes['vector-effect'] = 'non-scaling-stroke' if $NON_SCALING_ELEMENTS.include?(@name) or $NON_SCALING_ELEMENTS.include?(@name.to_s)
    end

    def run(*args, **kwargs, &block)
        self.instance_exec(*args, **kwargs, &block)
        self
    end

    def <<(child)
        @children.push(child)
        self
    end

    def to_s
        parts = []
        parts.append @name
        parts.append @attributes unless @attributes.empty?
        parts.append "[" + @children.map{ |n| n.to_s }.join(", ") + "]" unless @children.empty?
        return "Node<#{parts.join(" ")}>"
    end

    def to_xml(depth: 0)
        xml = %(#{'  ' * depth}<#{@name})
        xml += " " + @attributes.map { |k, v| "#{k}=\"#{v}\"" }.join(" ") unless @attributes.empty?
        if @children.empty?
            xml += " />\n"
        else
            xml += ">\n"
            @children.map do |child| 
                if child.is_a? Node
                    xml += child.to_xml(depth: depth + 1) 
                else
                    xml += "#{'  ' * (depth + 1)}#{child.to_s}\n"
                end
            end
            xml += "#{'  ' * depth}</#{@name}>\n"
        end
        return xml
    end
end

def rune(&block)
    Node.new(:svg, xmlns: "http://www.w3.org/2000/svg", viewBox: "-100 -100 200 200") <<
        Node.new(:g, transform: "rotate(180)", stroke: "black", fill: "white").run(&block)
end
Node.class_eval do
    def children(ls, scale: 1, offset: 0, &block)
        ls = *(0..ls-1) if ls.is_a? Integer

        group = Node.new(:g)
        
        ls.each_with_index do |el, i|
            group << Node.new(:g, transform: %(
                rotate(#{i*360.0/ls.length})
                translate(0 #{100*offset.to_f})
                scale(#{scale.to_f})
            ).squish).run(el, &block)
        end

        @children.push(group)
        self
    end
end
Node.class_eval do
    def circle
        @children.push(Node.new(:circle, cx: 0, cy: 0, r: 100))
        self
    end

    def dividedCircle(divisions, width: 1)
        circle
        style width: 1 do
            children divisions do line min: 1-width end 
        end
        scale 1-width do circle end
    end

    def arc(min: 0, max: 360, width: 0.1)
        x1, y1, x2, y2, flag = calculateArc(2 * Math::PI * min / 360, 2 * Math::PI * max / 360)
    
        @children.push(
            Node.new(:g, fill: "none") << 
            Node.new(:path, d: %(
                M #{x1} #{y1}
                A 100 100, 0, #{flag}, 0, #{x2} #{y2}
            ).squish)
        )

        self
    end
    
    def moon(phase)
        x1, y1, x2, y2, f1 = calculateArc(Math::PI * phase, -Math::PI * phase)
        flip = phase < 0.5 ? 1 : 0

        rotate 90 do 
            @children.push(
                Node.new(:path, d: %(
                    M #{x1} #{y1}
                    A 100 100, 0, #{flip}, #{1-flip}, #{x2} #{y2}
                    A 100 100, 0, #{1-flip}, #{flip}, #{x1} #{y1}
                ).squish)
            )
        end

        self
    end 

    private

    def calculateArc(min, max)
        # https://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle
    
        # Rotate so 0 is the top
        min += Math::PI / 2
        max += Math::PI / 2
    
        startX = (100 * Math.cos(max)).round(4)
        startY = (100 * Math.sin(max)).round(4)
        endX = (100 * Math.cos(min)).round(4)
        endY = (100 * Math.sin(min)).round(4)
        largeArcFlag = max - min <= 180 ? "0" : "1"
    
        return startX, startY, endX, endY, largeArcFlag
    end    
end
Node.class_eval do
    def double(width, &block)
        run(&block)
        scale(1-width) { run(&block) }
        self
    end

    def invert(&block)
        rotate 180, &block
    end

    def transform(**kwargs, &block)
        ls = []
        ls.append("rotate(#{kwargs[:rotate].to_f})") if kwargs.include? :rotate
        ls.append("scale(#{kwargs[:scale].to_f})") if kwargs.include? :scale 
        ls.append("translate(#{100*(kwargs[:translateX] || 0).to_f},#{100*(kwargs[:translateY] || 0).to_f})") if kwargs.include?(:translateX) || kwargs.include?(:translateY)
        ls.append("skewX(#{kwargs[:skewX].to_f})") if kwargs.include? :skewX
        ls.append("skewY(#{kwargs[:skewY].to_f})") if kwargs.include? :skewY

        @children.push(Node.new(:g, transform: ls.join(" ")).run(&block))
        self
    end

    def rotate(degrees, &block)
        transform(rotate: degrees, &block)
    end

    def scale(scale, &block)
        transform(scale: scale, &block)
    end

    def translate(x: nil, y: nil, &block)
        transform(translateX: x, translateY: y, &block)
    end

    def skew(x: nil, y: nil, &block)
        transform(skewX: x, skewY: y, &block)
    end

    def style(width: nil, color: nil, fill: nil, &block)
        as = {}
        as[:"stroke-width"] = width if width
        as[:stroke] = color if color
        as[:fill] = fill if fill

        @children.push(Node.new(:g, **as).run(&block))
        self
    end
end
Node.class_eval do
    def line(min: 0, max: 1)
        @children.push(Node.new(
            :line,
            x1: 0,
            y1: 100 * min.to_f,
            x2: 0,
            y2: 100 * max.to_f
        ))
        self
    end
end
Node.class_eval do
    def triangle
        polygon 3
    end

    def polygon(points)
        star points, 1
    end

    def star(points, skip)
        pstring = points.times.map do |i|
            r = 100
            theta = Math::PI/2 + 2 * Math::PI / points * (i * skip % points)
             x = r * Math.cos(theta)
            y = r * Math.sin(theta)
            %(#{x},#{y})
        end

        @children.push(Node.new(:polygon, points: pstring.join(" ")))
        self
    end
end
Node.class_eval do
    def text(text, scale: 1)
        @children.push(Node.new(
            :text,
            stroke: "none",
            fill: "black",
            "font-size": "#{100*scale.to_f}px",
            "text-anchor": "middle",
            "dominant-baseline": "central"
        ) << text)
        self
    end

    def circleText(text, scale: 1)
        @children.push(
            Node.new(:text,
                stroke: "none",
                fill: "black",
                "font-size": "#{10*scale.to_f}px"
            ) <<
            (Node.new(:textPath, "path": %(
                M -100, 0
                a 100,100 0 1,0 200,0
                a 100,100 0 1,0 -200,0                
            ).squish) << text)
        )
        self
    end

    def randomString(set, length)
        Array.new(length) { randomSymbol(set) }.join(" ")
    end

    def randomSymbol(set)
        {
            :greeklower => [*"α".."ω"],
            :greekupper => [*"Α".."Ω"],
            :greek => [*"α".."ω", *"Α".."Ω"],
            :cyrilliclower => [*"а".."я"],
            :cyrillicupper => [*"А".."Я"],
            :cyrillic => [*"а".."я", *"А".."Я"],
            :mongolian => [*"ᠠ".."ᡯ"],
            :hebrew => [*"א".."ת"],
            :hiragana => [*"ぁ".."ゖ"],
            
            :egyptian => [*"𓀀".."𓐮"],
            :linearb => [*"𐂀".."𐃺"],

            :astrological => ["☉", "☽", *"♁".."♇"],
            :runic => [*"ᚠ".."ᛪ"],
            :chess => [*"♔".."♟"],
            :alchemy => [*"🜁".."🝳"],
            :arrows => [*"←".."⇿"],
            :dingbat => [*"✁".."➿"],
            :math => [*"∀".."⋿"],


            #:zodiac => [*"♈︎".."♓︎"], # fix emoji issues
        }[set].sample
    end
end

My goal here was to design a series of functions that each automatically generate parts of an SVG as they’re going, all scaled to a 100.0 unit radius circle (chosen arbitrarily) centered as 0,0. That way, you can do transformations/scaling/etc and it will always work. You can take a large, complicated rune and make it a small subset of another and it will just work™.

How?

The core of this code revolves around Ruby’s block syntax. If you aren’t familiar (well, really either way), blocks are a neat little way of passing a bit of code to a function without going so far as to sending around first class functions everywhere (you can do this, but it’s not as elegant). So every time you see do/end or {...}, you can think of that as an inline function call being passed to another function which can do with it what it wants. For example, double is defined as:

Node.class_eval do
    def double(width, &block)
        run(&block)
        scale(1-width) { run(&block) }
        self
    end
end

It takes in the width as a normal parameter and (in this case) explicitly takes a block. It actually uses another function that I defined (scale) and calls run on the &block twice. Now… that’s a lot of words, most of which is my custom functionality. Let’s take a look at the whole Node class (in components/_core.rb), which has a few very powerful functions:

First, how do we create a new Node explicitly:

class Node
    def initialize(name, children=nil, **attributes)
        @name = name
        @attributes = attributes
        @children = children || []

        @attributes['vector-effect'] = 'non-scaling-stroke' if $NON_SCALING_ELEMENTS.include?(@name) or $NON_SCALING_ELEMENTS.include?(@name.to_s)
    end
end

Most of the time, you won’t explicitly add the children (nodes contained by this node), but you can always set attributes. This is way of assigning arbitrary xml attributes to the generated svg. For example:

irb(main):008:0> Node.new(:g, width: 5).to_s
=> "Node<g {:width=>5}>"

irb(main):006:0> Node.new(:g, width: 5).to_xml
=> "<g width=\"5\" />\n"

Speaking of which, I have functions that can render Node to either a more explicit string or directly to_xml:

class Node
    def to_s
        parts = []
        parts.append @name
        parts.append @attributes unless @attributes.empty?
        parts.append "[" + @children.map{ |n| n.to_s }.join(", ") + "]" unless @children.empty?
        return "Node<#{parts.join(" ")}>"
    end

    def to_xml(depth: 0)
        xml = %(#{'  ' * depth}<#{@name})
        xml += " " + @attributes.map { |k, v| "#{k}=\"#{v}\"" }.join(" ") unless @attributes.empty?
        if @children.empty?
            xml += " />\n"
        else
            xml += ">\n"
            @children.map do |child| 
                if child.is_a? Node
                    xml += child.to_xml(depth: depth + 1) 
                else
                    xml += "#{'  ' * (depth + 1)}#{child.to_s}\n"
                end
            end
            xml += "#{'  ' * depth}</#{@name}>\n"
        end
        return xml
    end
end

to_xml is a recursive function that always expects children to be an array of either nodes or text elements. Other things are possible, but will just get to_sed, so strange things can happen. Essentially, it’s generating ‘nice’ XML for this limited subset of elements. First, the @attributes map, then (if no children) a closing tag or recursively do the children one level deeper. I like that it properly indents and newlines the output. It makes debugging easier.

Finally, still in Node, we have two more useful functions:

class Node
    def run(*args, **kwargs, &block)
        self.instance_exec(*args, **kwargs, &block)
        self
    end

    def <<(child)
        @children.push(child)
        self
    end
end

run we saw before. It looks bizarre, but all it really means is that you’re going to run the given &block in the context of the current Node object. This allows you to do things like access the @children attribute within the block, which is exactly how the basic elements work:

Node.class_eval do
    def circle
        @children.push(Node.new(:circle, cx: 0, cy: 0, r: 100))
        self
    end
end

Create a new svg circle element as a Node with a few default attributes and add it to the Node who is calling this function’s @children attribute. It’s always centered at the current origin and always 100 units in radius. Anyone using this DSL should never have to worry about explitic radii, instead making everything relative.

Originally, I didn’t use the @children attribute and instead had the elements implicitly being returned. But that only works for the last element. In a previous version of this work (in Racket), you could return lists of elements and automatically collect them, but that doesn’t work as well in Ruby. But this version works and the end user’s view is the same–even if implementation is a bit more complicated.

The last function (back in Node) is the << ‘add child’ function. It allows you to more easily nest nodes when writing helper functions. For example, the children constructor:

Node.class_eval do
    def children(ls, scale: 1, offset: 0, &block)
        ls = *(0..ls-1) if ls.is_a? Integer

        group = Node.new(:g)
        
        ls.each_with_index do |el, i|
            group << Node.new(:g, transform: %(
                rotate(#{i*360.0/ls.length})
                translate(0 #{100*offset.to_f})
                scale(#{scale.to_f})
            ).squish).run(el, &block)
        end

        @children.push(group)
        self
    end
end

This is one of the core functions of a ‘magic circle’ generator. You can either give it a number of children or a list of elements and it will evenly space them around a circle, applying a scale or offset (from the center) for each. So if you want a series of 14 scale circles in a circle themselves:

irb(main):033:2* puts(rune do
irb(main):034:3*   children(4, scale: 1/4r, offset: 3/4r) do
irb(main):035:3*     circle
irb(main):036:2*   end
irb(main):037:0> end.to_xml)
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-100 -100 200 200">
  <g transform="rotate(180)" stroke="black" fill="white">
    <g>
      <g transform="rotate(0.0) translate(0 75.0) scale(0.25)">
        <circle cx="0" cy="0" r="100" vector-effect="non-scaling-stroke" />
      </g>
      <g transform="rotate(90.0) translate(0 75.0) scale(0.25)">
        <circle cx="0" cy="0" r="100" vector-effect="non-scaling-stroke" />
      </g>
      <g transform="rotate(180.0) translate(0 75.0) scale(0.25)">
        <circle cx="0" cy="0" r="100" vector-effect="non-scaling-stroke" />
      </g>
      <g transform="rotate(270.0) translate(0 75.0) scale(0.25)">
        <circle cx="0" cy="0" r="100" vector-effect="non-scaling-stroke" />
      </g>
    </g>
  </g>
</svg>

Each one is rotated a different degree automatically, then translated out 34 of the default radius (100), and scaled to 14. The 1/4r is a Ruby way of specifying rational constants rather than 1/4 which is integer division and equals 0. That took a bit to discover. :)

And that’s just about it. I’ve already gone through and done a number of basic functions that can be used to create some pretty awesome examples.

A few more examples:

# https://cdnb.artstation.com/p/assets/covers/images/015/151/525/medium/taylor-richardson-magiccircle.jpg?1547239370
rune do
    style width:4 do
        dividedCircle 26, width: 1/6r
        triangle

        invert do
            children %w[𓀇 𓁻 𓆣], scale:0.21, offset:5/7r do |s| # ♌ ♊ ♉
                double 1/10r do circle end
                invert do 
                    text s
                end
            end
        end
        children 3, scale:1/3r, offset:11/12r do
            double 1/5r do arc min: 115, max: 245, width: 0.2 end
        end

        scale 0.5 do
            circle
            translate x: 0 do 
                dividedCircle 60, width: 2/3r
            end
            style width:2 do
                scale 1/6r do
                    circle
                    star 10, 3
                    scale 1/3r do
                        invert do triangle end
                    end
                end
            end
            rotate 200 do
                style(fill: "black") { moon 0.45 }
            end
        end
    end
end

𓀇 𓁻 𓆣

rune do
    scale 0.9 do 
        circle
        polygon 7
        star 14, 3
        star 7, 2
        children 7, scale: 1/8r, offset: 1 do |i|
            circle
            invert do
                text (0x2641 + i).chr Encoding::UTF_8
            end
        end
    end
    scale 0.15 do
        translate x: -2 do circle; moon 0.45 end
        circle
        translate x: 2 do circle; moon 0.55 end
    end
end

rune do 
    double(0.05) { circle }
    scale(0.95) { [*2..5].each {|i| star 12,i } }
    scale 0.99 do
        circleText randomString(:runic, 400), scale: 0.4
    end

    scale 3/4r do 
        double(0.05) { circle }
        scale 0.99 do
            circleText randomString(:runic, 400), scale: 0.4
        end

        style fill: "none" do 
            triangle
            invert { triangle }
        end

        invert do 
            children 6, scale: 1/6r, offset:1 do 
                double(0.1) do
                    circle
                    text randomSymbol(:astrological)
                end
            end
        end
    end

    scale 1/4r do
        double(0.1) { circle }
        scale 0.9 do 
            polygon 8
            star 8, 3
            children 8, scale: 1/6r, offset:1 do 
                double(0.1) do
                    circle
                    text randomSymbol(:greekupper)
                end
            end
        end

        scale(0.1) do
            skew y: 20 do 
                arc min: 0, max: 90
                arc min: 180, max: 270
            end
        end
    end
end

ᚿ ᛏ ᛍ ᚸ ᚠ ᛘ ᚵ ᚠ ᛝ ᚵ ᛦ ᚫ ᛦ ᛅ ᚲ ᚲ ᚡ ᚮ ᛝ ᚪ ᛉ ᛓ ᛍ ᚴ ᛁ ᛛ ᛩ ᛛ ᛓ ᛂ ᛋ ᛒ ᛞ ᛏ ᛁ ᛟ ᛌ ᛞ ᚪ ᛒ ᚢ ᚾ ᚷ ᛢ ᚧ ᛖ ᚥ ᛋ ᛡ ᛁ ᛧ ᛙ ᚯ ᛣ ᛅ ᚠ ᛄ ᛥ ᛤ ᛝ ᛔ ᚭ ᛙ ᛡ ᚯ ᚻ ᛜ ᚧ ᚭ ᛀ ᚧ ᚾ ᛅ ᛢ ᚬ ᚳ ᛦ ᛖ ᚲ ᛏ ᛒ ᛚ ᛩ ᛒ ᛂ ᛟ ᛂ ᛥ ᚧ ᛍ ᛑ ᚯ ᚫ ᚴ ᛠ ᛟ ᛣ ᛜ ᚭ ᚻ ᚿ ᛓ ᚿ ᚢ ᚸ ᛂ ᛐ ᛓ ᚬ ᛁ ᛏ ᛖ ᚼ ᛒ ᛁ ᛗ ᛜ ᛝ ᛩ ᛨ ᛤ ᚿ ᛀ ᛚ ᛓ ᛜ ᛓ ᛖ ᛜ ᛟ ᚿ ᛥ ᛨ ᚶ ᚤ ᚾ ᛣ ᛆ ᚻ ᚢ ᛖ ᛜ ᛓ ᛔ ᛍ ᚾ ᛁ ᚮ ᚽ ᛄ ᛄ ᛛ ᛨ ᛩ ᛒ ᛛ ᛅ ᛝ ᚻ ᛡ ᛀ ᛔ ᚽ ᚻ ᛐ ᚻ ᚨ ᛖ ᛤ ᚵ ᚥ ᚩ ᚹ ᚭ ᛆ ᚣ ᛕ ᛣ ᛇ ᚴ ᛙ ᛤ ᚲ ᚩ ᛋ ᛄ ᛠ ᚷ ᚳ ᚽ ᛊ ᛃ ᛊ ᚡ ᛞ ᛘ ᛙ ᛑ ᚼ ᛗ ᚱ ᛥ ᛚ ᛋ ᛐ ᚪ ᚩ ᚮ ᛑ ᛖ ᚨ ᛍ ᚽ ᛝ ᚭ ᛉ ᛠ ᛁ ᛙ ᚲ ᛝ ᛋ ᚾ ᛩ ᚵ ᚡ ᛕ ᚡ ᛐ ᛑ ᛞ ᛐ ᚷ ᛤ ᛈ ᛋ ᛞ ᚾ ᛃ ᛑ ᚫ ᚮ ᚵ ᚶ ᛥ ᛅ ᛧ ᚲ ᚷ ᛣ ᛊ ᚡ ᚨ ᚳ ᚬ ᚡ ᛋ ᚱ ᛀ ᚾ ᚬ ᚦ ᚩ ᛨ ᚱ ᛤ ᛐ ᚴ ᚠ ᛋ ᛌ ᛤ ᚧ ᚤ ᚵ ᚨ ᛄ ᛩ ᛄ ᚶ ᚵ ᚤ ᛓ ᚱ ᛉ ᛀ ᛥ ᛐ ᚾ ᚲ ᛃ ᚥ ᚻ ᚱ ᚪ ᛍ ᛝ ᚬ ᛦ ᛣ ᛌ ᚵ ᛙ ᛍ ᛤ ᚽ ᛎ ᚫ ᚭ ᚾ ᛇ ᚤ ᛓ ᛈ ᚸ ᚣ ᛜ ᛕ ᚤ ᚩ ᛑ ᛑ ᚰ ᛑ ᚿ ᛄ ᛤ ᚹ ᚿ ᚤ ᛥ ᛡ ᚥ ᛓ ᛉ ᚵ ᚻ ᚱ ᛗ ᛟ ᛁ ᛘ ᛔ ᛟ ᛄ ᛊ ᛚ ᛈ ᚥ ᛜ ᚼ ᛪ ᚢ ᛌ ᚥ ᚴ ᚰ ᚬ ᛈ ᛔ ᚷ ᛔ ᛃ ᛄ ᛨ ᚲ ᛗ ᛁ ᛐ ᛃ ᛛ ᛙ ᛖ ᛅ ᚹ ᛑ ᛧ ᚸ ᚧ ᛨ ᚳ ᛙ ᚷ ᛉ ᛨ ᛊ ᚣ ᛔ ᚧ ᚰ ᚮ ᚦ ᛝ ᛋ ᛄ ᛒ ᛆ ᚱ ᛂ ᛏ ᛈ ᛁ ᛋ ᛪ ᛙ ᛘ ᚴ ᛝ ᚪ ᚺ ᛞ ᚿ ᚽ ᚿ ᚤ ᚧ ᚹ ᛍ ᛂ ᛨ ᛛ ᚸ ᚿ ᚪ ᛌ ᛆ ᛐ ᚬ ᛢ ᛧ ᚲ ᛝ ᛤ ᛖ ᛖ ᛧ ᛋ ᚩ ᛈ ᛈ ᚫ ᛊ ᛒ ᚤ ᚪ ᛍ ᛏ ᚾ ᛄ ᚣ ᛄ ᛍ ᚠ ᛔ ᛡ ᚧ ᚢ ᛇ ᚦ ᛔ ᛥ ᛈ ᚰ ᛥ ᛁ ᚡ ᚶ ᚦ ᛧ ᚡ ᛦ ᛋ ᛄ ᛌ ᚿ ᚻ ᛙ ᛒ ᛪ ᚸ ᛦ ᚱ ᛔ ᛉ ᛔ ᛞ ᚵ ᛖ ᛎ ᚺ ᚡ ᛕ ᛤ ᚡ ᚰ ᛆ ᚤ ᚷ ᛜ ᛨ ᛚ ᚥ ᛐ ᚺ ᛨ ᚸ ᚪ ᚮ ᚺ ᛃ ᚾ ᚨ ᛏ ᛥ ᛊ ᛐ ᛂ ᚷ ᛡ ᛅ ᚨ ᛛ ᛧ ᚣ ᚾ ᛄ ᛊ ᛜ ᛛ ᛗ ᚺ ᛌ ᛉ ᛝ ᚲ ᚭ ᚷ ᛉ ᚴ ᚪ ᛗ ᛚ ᛊ ᚽ ᚢ ᛚ ᚯ ᚡ ᚯ ᚱ ᛢ ᚶ ᛋ ᚮ ᛞ ᚦ ᚸ ᚭ ᚫ ᚳ ᛄ ᛗ ᛛ ᚴ ᛔ ᚦ ᚻ ᚺ ᛜ ᛨ ᚣ ᛃ ᛇ ᚵ ᚬ ᚢ ᛌ ᛑ ᛌ ᚭ ᚸ ᚤ ᚻ ᛖ ᛃ ᚿ ᛡ ᛒ ᛚ ᚽ ᚾ ᚭ ᚬ ᛚ ᚭ ᛗ ᚮ ᚨ ᛈ ᚦ ᛣ ᛤ ᛟ ᛇ ᚩ ᛉ ᚿ ᛋ ᛅ ᛄ ᛏ ᛡ ᛖ ᛁ ᚪ ᚩ ᚦ ᚤ ᛝ ᚫ ᛃ ᛖ ᛋ ᚭ ᚥ ᛄ ᚠ ᚲ ᛎ ᛇ ᛘ ᛨ ᚳ ᛃ ᛠ ᛦ ᛊ ᛇ ᛒ ᛅ ᛆ ᚶ ᛍ ᛞ ᛉ ᛪ ᛌ ᛤ ᚧ ᛤ ᛩ ᚢ ᛤ ᚷ ᚳ ᛖ ᛘ ᛄ ᛑ ᚭ ᛍ ᛁ ᛦ ᛉ ᛄ ᛤ ᛇ ᛃ ᛢ ᚴ ᛍ ᛥ ᚿ ᛣ ᛥ ᛝ ᚧ ᛤ ᚾ ᚻ ᛪ ᛒ ᛍ ᚲ ᚿ ᚳ ᛋ ᛋ ᛅ ᛠ ᚸ ᚶ ᚡ ᚳ ᛙ ᛨ ᛝ ᚽ ᛈ ᚪ ᚫ ᚥ ᛦ ᛐ ᛝ ᚳ ᛑ ᚪ ᛩ ᛞ ᛁ ᚾ ᚩ ᛄ ᚧ ᚺ ᛎ ᛧ ᚪ ᚦ ᚱ ᛗ ᚯ ᚾ ᛪ ᛄ ᚫ ᚢ ᛩ ᛊ ᚡ ᛁ ᛦ ᚧ ᛂ ᛃ ᚨ ᚮ ᛥ ᛊ ᛣ ᛡ ᚣ ᚤ ᛈ ᚻ ᚫ ᚡ ᚲ ᚤ ᛌ ᛓ ᚥ ᛡ ᛆ ᛞ ᛞ ᛗ ᚼ ᛁ ᛪ ᚾ ᚣ ᚢ ᛄ ᚺ ᛅ ᛓ ᛢ ᚽ ᛕ ᚥ ᚧ ᚶ ᚳ ᛖ ᚹ ᛧ ᛛ ᛅ ᛟ ᚩ ᛢ ᛠ ᚤ ᛥ ᛀ ᚪ ᛎ ᛙ ᛢ ᛢ ᚪ ᛥ ᛅ ᛪ ᚡ ᚦ ᛤ ᚴ Ρ Θ Γ Σ Ο Β Ω Μ Κ Θ Ε Θ Δ Μ Υ Β

It’s wonderful. :D

Next up:

  • Add rules to make ‘runes’, such as Vegvisir:

  • Add a system to automatically generate runes that follow certain rules

Onward!