Having fun with fonts doesn’t always mean obsessing over kerning and ligatures. Sometimes, writing text is not even the point!
You don’t believe it? Type something in here.
Teranoptia is a cool font that lets you build small creatures by mapping each letter (and a few other characters) to a piece of a creature like a head, a tail, a leg, a wing and so on. By typing words you can create strings of creatures.
Here is the glyphset:
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Ẋ
Ẋ
Y
Y
Z
Z
Ź
Ź
Ž
Ž
Ż
Ż
a
a
b
b
ḅ
ḅ
c
c
d
d
e
e
f
f
g
g
h
h
i
i
j
j
k
k
l
l
m
m
n
n
o
o
p
p
q
q
r
r
s
s
t
t
u
u
v
v
w
w
x
x
y
y
z
z
ź
ź
ž
ž
ż
ż
,
,
*
*
(
(
)
)
{
{
}
}
[
[
]
]
‐
‐
“
“
”
”
‘
‘
’
’
«
«
»
»
‹
‹
›
›
$
$
€
€
vTN
to more complex, multi-line designs:F] [Z
Let’s play with it a bit and see how we can put together a few “correct” looking creatures.
Mirroring animals Link to heading
To begin with, let’s start with a simple function: animal mirroring. The glyphset includes a mirrored version of each non-symmetric glyph, but the mapping is rather arbitrary, so we are going to need a map.
Here are the pairs:
By Ev Hs Kp Nm Ri Ve Za Żź Az Cx Fu Ir Lo Ol Sh Wd Źż vE Dw Gt Jq Mn Pk Qj Tg Uf Xc Ẋḅ Yb Žž bY cX () [] {}
Animal mirror Link to heading
ST»K*abd
const mirrorPairs = {"B": "y", "y": "B", "E": "v", "v": "E", "H": "s", "s": "H", "K": "p", "p": "K", "N": "m", "m": "N", "R": "i", "i": "R", "V": "e", "e": "V", "Z": "a", "a": "Z", "Ż": "ź", "ź": "Ż", "A": "z", "z": "A", "C": "x", "x": "C", "F": "u", "u": "F", "I": "r", "r": "I", "L": "o", "o": "L", "O": "l", "l": "O", "S": "h", "h": "S", "W": "d", "d": "W", "Ź": "ż", "ż": "Ź", "v": "E", "E": "v", "D": "w", "w": "D", "G": "t", "t": "G", "J": "q", "q": "J", "M": "n", "n": "M", "P": "k", "k": "P", "Q": "j", "j": "Q", "T": "g", "g": "T", "U": "f", "f": "U", "X": "c", "c": "X", "Ẋ": "ḅ", "ḅ": "Ẋ", "Y": "b", "b": "Y", "Ž": "ž", "ž": "Ž", "b": "Y", "Y": "b", "c": "X", "X": "c", "(": ")", ")": "(", "[": "]", "]": "[", "{": "}", "}": "{"};
function mirrorAnimal(original){
var mirror = '';
for (i = original.length-1; i >= 0; i--){
newChar = mirrorPairs[original.charAt(i)];
if (newChar){
mirror += newChar;
} else {
mirror += original.charAt(i)
}
}
return mirror;
}
Random animal generation Link to heading
While it’s fun to build complicated animals this way, you’ll notice something: it’s pretty hard to make them come out right by simply typing something. Most of the time you need quite careful planning. In addition there’s almost no meaningful (English) word that corresponds to a well-defined creature. Very often the characters don’t match, creating a sequence of “chopped” creatures.
For example, “Hello” becomes:
Hello
This is a problem if we want to make a parametric or random creature generator, because most of the random strings won’t look good.
Naive random generator Link to heading
n]Zgameź)‐
const validChars = "ABCDEFGHIJKLMNOPQRSTUVWXẊYZŹŽŻabḅcdefghijklmnopqrstuvwxyzźžż,*(){}[]‐“”«»$"; // ‘’‹›€ excluded because they're mostly vertical
function randomFrom(list){
return list[Math.floor(Math.random() * list.length)];
}
function generateNaive(value){
var newAnimal = '';
for (var i = 0; i < value; i++) {
newAnimal += randomFrom(validChars);
}
return newAnimal;
}
Can we do better than this?
Generating “good” animals Link to heading
There are many ways to define “good” or “well-formed” creatures. One of the first rules we can introduce is that we don’t want chopped body parts to float alone.
Translating it into a rule we can implement: a character that is “open” on the right must be followed by a character that is open on the left, and a character that is not open on the right must be followed by another character that is not open on the left.
For example, A may be followed by B to make AB, but A cannot be followed by C to make AC.
In the same way, Z may be followed by A to make ZA, but Z cannot be followed by ż to make Zż.
This way we will get rid of all those “chopped” monsters that make up most of the randomly generated string.
To summarize, the rules we have to implement are:
- Any character that is open on the right must be followed by another character that is open on the left.
- Any character that is closed on the right must be followed by another character that is closed on the left.
- The first character must not be open on the left.
- The last character must not be open on the right.
Non-chopped animals generator Link to heading
suSHebQ«EIl
const charsOpenOnTheRightOnly = "yvspmieaźACFILOSWŹ({[";
const charsOpenOnTheLeftOnly = "BEHKNRVZŻzxurolhdż)]}";
const charsOpenOnBothSides = "DGJMPQTUXẊYŽbcwtqnkjgfcḅbžYX«»";
const charsOpenOnNoSides = ",*-“”";
const charsOpenOnTheRight = charsOpenOnTheRightOnly + charsOpenOnBothSides;
const charsOpenOnTheLeft = charsOpenOnTheLeftOnly + charsOpenOnBothSides;
const validInitialChars = charsOpenOnTheRightOnly + charsOpenOnNoSides;
function generateNoChop(value){
var newAnimal = '' + randomFrom(validInitialChars);
for (var i = 0; i < value-1; i++) {
if (charsOpenOnTheRight.indexOf(newAnimal[i]) > -1){
newAnimal += randomFrom(charsOpenOnTheLeft);
} else if (charsOpenOnTheLeftOnly.indexOf(newAnimal[i]) > -1){
newAnimal += randomFrom(charsOpenOnTheRightOnly);
} else if (charsOpenOnNoSides.indexOf(newAnimal[i]) > -1){
newAnimal += randomFrom(validInitialChars);
}
}
// Final character
if (charsOpenOnTheRight.indexOf(newAnimal[i]) > -1){
newAnimal += randomFrom(charsOpenOnTheLeftOnly);
} else {
newAnimal += randomFrom(charsOpenOnNoSides);
}
return newAnimal;
}
The resulting animals are already quite better!
There are still a few things we may want to fix. For example, some animals end up being just a pair of heads (such as sN); others instead have their bodies oriented in the wrong direction (like IgV).
Let’s try to get rid of those too.
The trick here is to separate the characters into two groups: elements that are “facing left”, elements that are “facing right”, and symmetric ones. At this point, it’s convenient to call them “heads”, “bodies” and “tails” to make the code more understandable, like the following:
-
Right heads: BEHKNRVZŻ
-
Left heads: yvspmieaź
-
Right tails: ACFILOSWŹv
-
Left tails: zxurolhdżE
-
Right bodies: DGJMPQTUẊŽ
-
Left bodies: wtqnkjgfḅž
-
Entering hole: )]}
-
Exiting hole: ([{
-
Bounce & symmetric bodies: «»$bcXY
-
Singletons: ,*-
Let’s put this all together!
Oriented animals generator Link to heading
suSHebQ«EIl
const rightAnimalHeads = "BEHKNRVZŻ";
const leftAnimalHeads = "yvspmieaź";
const rightAnimalTails = "ACFILOSWŹv";
const leftAnimalTails = "zxurolhdżE";
const rightAnimalBodies = "DGJMPQTUẊŽ";
const leftAnimalBodies = "wtqnkjgfḅž";
const singletons = ",*‐";
const exitingHole = "([{";
const enteringHole = ")]}";
const bounce = "«»$bcXY";
const validStarts = leftAnimalHeads + rightAnimalTails + exitingHole;
const validSuccessors = {
[exitingHole + bounce]: rightAnimalHeads + rightAnimalBodies + leftAnimalBodies + leftAnimalTails + enteringHole + bounce,
[enteringHole]: rightAnimalTails + leftAnimalHeads + exitingHole + singletons,
[rightAnimalHeads + leftAnimalTails + singletons]: rightAnimalTails + leftAnimalHeads + exitingHole + singletons,
[leftAnimalHeads]: leftAnimalBodies + leftAnimalBodies + leftAnimalBodies + leftAnimalTails + enteringHole + bounce,
[rightAnimalTails]: rightAnimalBodies + rightAnimalBodies + rightAnimalBodies + rightAnimalHeads + enteringHole + bounce,
[rightAnimalBodies]: rightAnimalBodies + rightAnimalBodies + rightAnimalBodies + rightAnimalHeads + enteringHole + bounce,
[leftAnimalBodies]: leftAnimalBodies + leftAnimalBodies + leftAnimalBodies + leftAnimalTails + enteringHole + bounce,
};
const validEnds = {
[exitingHole + bounce]: leftAnimalTails + rightAnimalHeads + enteringHole,
[rightAnimalHeads + leftAnimalTails + enteringHole]: singletons,
[leftAnimalHeads]: leftAnimalTails + enteringHole,
[rightAnimalTails]: rightAnimalHeads + enteringHole,
[rightAnimalBodies]: rightAnimalHeads,
[leftAnimalBodies]: leftAnimalTails,
};
function generateOriented(value){
var newAnimal = '' + randomFrom(validStarts);
for (var i = 0; i < value-1; i++) {
last_char = newAnimal[i-1];
for (const [predecessor, successor] of Object.entries(validSuccessors)) {
if (predecessor.indexOf(last_char) > -1){
newAnimal += randomFrom(successor);
break;
}
}
}
last_char = newAnimal[i-1];
for (const [predecessor, successor] of Object.entries(validEnds)) {
if (predecessor.indexOf(last_char) > -1){
newAnimal += randomFrom(successor);
break;
}
}
return newAnimal;
}
A regular grammar Link to heading
Let’s move up a level now.
What we’ve defined up to this point is a set of rules that, given a string, determine what characters are allowed next. This is called a formal grammar in Computer Science.
A grammar is defined primarily by:
- an alphabet of symbols (our Teranoptia font).
- a set of starting characters: all the characters that can be used at the start of the string (such as a or *).
- a set of terminating character: all the characters that can be used to terminate the string (such as d or -).
- a set of production rules: the rules needed to generate valid strings in that grammar.
In our case, we’re looking for a grammar that defines “well formed” animals. For example, our production rules might look like this:
- S (the start of the string) → a (a)
- a (a) → ad (ad)
- a (a) → ab (ab)
- b (b) → bb (bb)
- b (b) → bd (bd)
- d (d) → E (the end of the string)
- , (,) → E (the end of the string)
and so on. Each combination would have its own rule.
There are three main types of grammars according to Chomsky’s hierarchy:
- Regular grammars: in all rules, the left-hand side is only a single nonterminal symbol and right-hand side may be the empty string, or a single terminal symbol, or a single terminal symbol followed by a nonterminal symbol, but nothing else.
- Context-free grammars: in all rules, the left-hand side of each production rule consists of only a single nonterminal symbol, while the right-hand side may contain any number of terminal and non-terminal symbols.
- Context-sensitive grammars: rules can contain many terminal and non-terminal characters on both sides.
In our case, all the production rules look very much like the examples we defined above: one character on the left-hand side, at most two on the right-hand side. This means we’re dealing with a regular grammar. And this is good news, because it means that this language can be encoded into a regular expression.
Building the regex Link to heading
Regular expressions are a very powerful tool, one that needs to be used with care. They’re best used for string validation: given an arbitrary string, they are going to check whether it respects the grammar, i.e. whether the string it could have been generated by applying the rules above.
Having a regex for our Teranoptia animals will allow us to search for valid animals in long lists of stroings, for example an English dictionary. Such a search would have been prohibitively expensive without a regular expression: using one, while still quite costly, is orders of magnitude more efficient.
In order to build this complex regex, let’s start with a very limited example: a regex that matches left-facing snakes.
^(a(b|c|X|Y)*d)+$
This regex is fairly straightforward: the string must start with a (a), can contain any number of b (b), c (c), X (X) and Y (Y), and must end with d (d). While we’re at it, let’s add a + to the end, meaning that this pattern can repeat multiple times: the string will simply contain many snakes.
Left-facing snakes regex Link to heading
Valid
What would it take to extend it to snakes that face either side? Luckily, snake bodies are symmetrical, so we can take advantage of that and write:
^((a|W)(b|c|X|Y)*(d|Z))+$
Naive snakes Link to heading
Valid
That looks super-promising until we realize that there’s a problem: this “snake” aZ also matches the regex. To generate well-formed animals we need to keep heads and tails separate. In the regex, it would look like:
^(
(a)(b|c|X|Y)*(d) |
(W)(b|c|X|Y)*(Z)
)+$
Correct snakes Link to heading
Valid
Once here, building the rest of the regex is simply matter of adding the correct characters to each group. We’re gonna trade some extra characters for an easier structure by duplicating the symmetric characters when needed.
^(
// Left-facing animals
(
y|v|s|p|m|i|e|a|ź|(|[|{ // Left heads & exiting holes
)(
w|t|q|n|k|j|g|f|ḅ|ž|X|Y|b|c|$|«|» // Left & symmetric bodies
)*(
z|x|u|r|o|l|h|d|ż|E|)|]|} // Left tails & entering holes
) |
// Right facing animals
(
A|C|F|I|L|O|S|W|Ź|v|(|[|{ // right tails & exiting holes
)(
D|G|J|M|P|Q|T|U|Ẋ|Ž|b|c|X|Y|$|«|» // right & symmetric bodies
)*(
B|E|H|K|N|R|V|Z|Ż|)|]|} // right heads & entering holes
) |
// Singletons
(,|-|*)
)+$
Well-formed animals regex Link to heading
Valid
If you play with the above regex, you’ll notice a slight discrepancy with what our well-formed animal generator creates. The generator can create “double-headed” monsters where a symmetric body part is inserted, like a«Z. However, the regex does not allow it. Extending it to account for these scenarios would make it even more unreadable, so this is left as an exercise for the reader.
Searching for “monstrous” words Link to heading
Let’s put the regex to use! There must be some English words that match the regex, right?
Google helpfully compiled a text file with the most frequent 10.000 English words by frequency. Let’s load it up and match every line with our brand-new regex. Unfortunately Teranoptia is case-sensitive and uses quite a few odd letters and special characters, so it’s unlikely we’re going to find many interesting creatures. Still worth an attempt.
Monster search Link to heading
Go ahead and put your own vocabulary file to see if your language contains more animals!
Conclusion Link to heading
In this post I’ve just put together a few exercises for fun, but these tools can be great for teaching purposes: the output is very easy to validate visually, and the grammar involved, while not trivial, is not as complex as natural language or as dry as numerical sequences. If you need something to keep your students engaged, this might be a simple trick to help them visualize the concepts better.
On my side, I think I’m going to use these neat little monsters as weird fleurons :)
Download Teranoptia at this link: https://www.tunera.xyz/fonts/teranoptia/