The Matoran Alphabet is the alphabet used by many of the characters in the Bionicle universe.

Matoran Alphabet

Your challenge is to create a program or function that takes a string as input and creates an image of the input written in the Matoran alphabet.

The input will only consist of uppercase letters (A-Z), numbers (0-9), and spaces. You may instead take input in lowercase.


As you can probably notice, the alphabet comprises glyphs made up of circles and straight lines.

For the sake of this challenge, let's define each glyph as a circle of radius 4 units (8 x 8), with some internal designs. The internal designs of letters can be any of:

  • Vertical Line at position 2, 4, or 6 (either full length or half)
  • Horizontal Line equivalent to above
  • Circle of radius 1, centered at [2, 2], [2, 6], [6, 2], or [6, 6]
  • Any of the above rotated by 45 degrees about the centre

Note that for some glyphs (notably D, H, and K) only some of the drawn parts have the rotation applied.

Number glyphs work slightly differently; they always have a circle of radius 1 in the centre. For numbers 0 - 5, the glyph has evenly spaced spokes between the circles, starting from the top. For numbers greater than 5, the glyph has an additional circle of radius 2 in the centre, and the number of spokes is 6 less than the number.

Here is a diagram of the glyphs in spec, any glyphs with 45 degree rotations have an orange grid.

Matoran Alphabet

For drawing the glyphs, any line stroke in the range (0, 1) can be used. Glyphs can be kerned (space between adjacent glyphs) any amount from 0 - 4 units. Spaces should be 8 units wide (in addition to kerning).

This is a code-golf challenge, so the shortest code in bytes in each language wins.

  • \$\begingroup\$ I will actually note that (something I did not quite notice while this was in the sandbox) that your description for the construction of these glyphs is not technically enough to explain the construction of the D, H, and K glyphs, however this may be corrected simply by specifying that the 45 degree rotation may be performed at a single arbitrary point in the construction of the glyph, such that some parts of the glyph are drawn before the rotation, and some after the rotation \$\endgroup\$
    – des54321
    Apr 19, 2022 at 3:19
  • \$\begingroup\$ I did try to explain that the rotation can be applied to any subset of parts of the glyph, but I will clarify. \$\endgroup\$ Apr 19, 2022 at 3:30

7 Answers 7


Java 10, 1291 1271 1265 bytes

import java.awt.*;a->new Frame(){Graphics G;void o(int x,int y,int s){G.drawOval(x,y,s,s);}void l(int...c){G.drawLine(c[0],c[1],c[2],c[3]);}public void paint(Graphics g){G=g;int x=10,A=20,B=40,C,D=60,E=70,F=80,G=100,H=120,I,J;for(var c:a){C=x+B;I=x+67;J=x+30;if(c>32){o(x,B,F);if(c<58){o(J,E,A);if(c>53){o(x+A,D,B);if(c>56){l(C,B,C,D);l(x+22,90,x+7,G);l(x+58,90,x+73,G);}else{if(c>54)l(C,B,C,D);if(c>55)l(C,G,C,H);}}else{if(c>52){l(C,B,C,E);l(x+2,69,x+31,77);l(x+78,69,x+49,77);l(x+34,87,x+17,112);l(x+46,87,x+63,112);}else if(c==51){l(C,B,C,E);l(x+32,85,x+7,G);l(x+48,85,x+73,G);}else{if(c>48)l(C,B,C,E);if(c>49)l(C,90,C,H);if(c>51){l(x,F,J,F);l(x+50,F,x+F,F);}}}}else{var q=c+"";if("BHUV".contains(q))o(J,B,A);if(c>66&c<69|c==87)o(x+D,E,A);if(c<67|c==72)o(J,G,A);if(c==87)o(x,E,A);if(c>81&c<84|c==F)o(x+50,50,A);if(c==76|c==81)o(x+50,90,A);if(c==74|c==83)o(x+10,90,A);if("DGIJLPR".contains(q))l(C,B,C,H);if(c==71|c==82|c==84)l(C,F,x+F,F);if(c==84)l(C,F,C,H);if(c==75|c==84)l(x,F,C,F);if("KVWXYZ".contains(q))l(C,F,I,52);if("KNVWXY".contains(q))l(C,F,I,108);if("VWXZ".contains(q))l(x+13,108,I,52);if("NVWXY".contains(q))l(x+13,52,I,108);if(c>68&c<71){l(x+5,D,x+75,D);if(c<70)l(x+5,G,x+75,G);}if(c==72|c==77){l(x+A,45,x+A,115);l(x+D,45,x+D,115);}}}x+=F;}}{show();}}

Input as a character-array. Output with a unit being 10 pixels without kerning.

Some example I/O screenshots:


enter image description here


enter image description here


enter image description here

import java.awt.*;                   // Required import for Frame and Graphics
a->                                  // Method with char-array parameter and Frame return-type
  new Frame(){                       //  Create the Frame
    Graphics G;                      //   Put a Graphics instance on class-level
    void o(int x,int y,int s){       //   Create a shorter method `o` for drawOval
      G.drawOval(x,y,s,s);}          //    Draw a circle with width/height `s` with its top-left corner of a virtual surrounding square at position `x,y`
    void l(int...c){                 //   Create a shorter method `l` for drawLine
                                     //    Draw a line from x1,y1 to x2,y2
    public void paint(Graphics g){   //   Overwrite the Frame's paint method:
      G=g;                           //    Save its Graphics parameter in the class-level variable
      int x=10,                      //    Start the `x` position at 10 (for the left-side border of the Frame)
                                     //    Constants to save bytes
      for(var c:a){                  //    Loop over the characters `c`:
        C=x+B;I=x+67;J=x+30;         //     Set more constants that use variable `x`
        if(c>32){                    //     If `c` is NOT a space:
          o(x,B,F);                  //      Draw a big surrounding circle
          if(c<58){                  //      If `c` is a digit:
            o(J,E,A);                //       Draw the center circle
            if(c>53){                //       If `c` is larger than '5':
              o(x+A,D,B);            //        Draw the larger center circle
              if(c>56){              //        If `c` is '9':
                                     //         Draw its three small lines
              else{                  //        Else (it's '6'/'7'/'8'):
                if(c>54)             //         If it's '7' or '8':
                  l(C,B,C,D);        //          Draw the small top vertical line
                if(c>55)             //         If it's '8':
                  l(C,G,C,H);}}      //          Also draw the small bottom vertical line
            else{                    //       Else (`c` is '5' or smaller):
             if(c>52){               //        If `c` is '5':
                                     //         Draw its five small lines
             else if(c==51){         //        Else-if `c` is '3':
                                     //         Draw its three small lines
             else{                   //        Else (`c` is '0'/'1'/'2'/'4'):
               if(c>48)              //         If `c` is larger than '0':
                 l(C,B,C,E);         //          Draw the top medium vertical line
               if(c>49)              //         If `c` is larger than '1' (thus '2'/'4'):
                 l(C,90,C,H);        //          Draw the bottom medium vertical line
               if(c>51){             //         If `c` is '4':
                                     //          Draw its left/right lines
          else{                      //      Else (`c` is a letter):
            var q=c+"";              //       Transform the char to a String
            if("BHUV".contains(q))   //       If `c` is one of BHUV:
              o(J,B,A);              //        Draw the top small circle
            if(c>66&c<69|c==87)      //       If `c` is one of CDW:
              o(x+D,E,A);            //        Draw the right small circle
            if(c<67|c==72)           //       If `c` is one of ABH:
              o(J,G,A);              //        Draw the bottom small circle
            if(c==87)                //       If `c` is W:
              o(x,E,A);              //        Draw the left small circle
            if(c>81&c<84|c==F)       //       If `c` is one of PRS:
              o(x+50,50,A);          //        Draw the top-right small circle
            if(c==76|c==81)          //       If `c` is L or Q:
              o(x+50,90,A);          //        Draw the bottom-right small circle
            if(c==74|c==83)          //       If `c` is J or S:
              o(x+10,90,A);          //        Draw the bottom-left small circle
            if("DGIJLPR".contains(q))//       If `c` is one of DGIJLPR:
              l(C,B,C,H);            //        Draw the large vertical line
            if(c==71|c==82|c==84)    //       If `c` is one of GRT:
              l(C,F,x+F,F);          //        Draw the right horizontal line
            if(c==84)                //       If `c` is T:
              l(C,F,C,H);            //        Draw the bottom vertical line
            if(c==75|c==84)          //       If `c` is K or T:
              l(x,F,C,F);            //        Draw the left horizontal line
            if("KVWXYZ".contains(q)) //       If `c` is one of KVWXYZ:
              l(C,F,I,52);           //        Draw the top-right anti-diagonal
            if("KNVWXY".contains(q)) //       If `c` is one of KNVWXY:
              l(C,F,I,108);          //        Draw the bottom-right diagonal
            if("VWXZ".contains(q))   //       If `c` is one of VWXZ:
              l(x+13,108,I,52);      //        Draw the bottom-left anti-diagonal
            if("NVWXY".contains(q))  //       If `c` is one of NVWXY:
              l(x+13,52,I,108);      //        Draw the top-left diagonal
            if(c>68&c<71){           //       If `c` is E or F:
              l(x+5,D,x+75,D);       //        Draw the top horizontal line
              if(c<70)               //        If `c` is E:
                l(x+5,G,x+75,G);}    //         Draw the bottom horizontal line
            if(c==72|c==77){         //       If `c` is H or M:
                                     //        Draw the left/right vertical lines
        x+=F;}}                      //     Increase `x` by 80 (8 units)
    {                                //   And then in an initializer-block:
      show();}}                      //    Show the Frame
  • \$\begingroup\$ Very cool! I look forward to the explanation if it comes \$\endgroup\$ Apr 20, 2022 at 1:19
  • \$\begingroup\$ @MatthewJensen I've added an explanation. It's not too exciting tbh. (And I'm sure some mathematical formulas and magic numbers could save some bytes here and there in the if-statements, although I'm not too good at those.) \$\endgroup\$ Apr 20, 2022 at 7:39
  • 3
    \$\begingroup\$ how did you beat python with java? \$\endgroup\$
    – emanresu A
    Apr 20, 2022 at 11:24
  • 1
    \$\begingroup\$ @emanresuA You tell me, lol. ;) I had already posted my answer before the Python one. Although I'm sure both can still be golfed some more. Still, I'm indeed a bit surprised as well, since the Python answer uses rotates, which are usually a lot shorter in [graphical-output] challenges. I never use rotations in my Java AWT answers, because it usually would result in double the amount of bytes. \$\endgroup\$ Apr 20, 2022 at 11:56
  • 1
    \$\begingroup\$ actually reading through this answer more closely, this is a pretty darn good way of doing this TBH, I suppose my method of breaking the glyphs up into components and making a function that draws them by those components, and then using a dict to look things up, isnt quite as compact \$\endgroup\$
    – des54321
    Apr 20, 2022 at 18:46

Python 3 + pygame, 1365 1270 1261 bytes

Full program that takes a string from stdin, and displays the glyphs in a window.

Edit -95 bytes: Now shorter than Java!!! I've lost track of what all I golfed, but theres a hell of a lot of constants in here that replace repeated numbers and tuples, and various other stuff. Also note that the \x0cs on line 8 represent literal ASCII formfeeds (codepoint 0C)

Edit: @Steffan pointed out a few mistakes that were breaking code, fixed them.

Edit -9 bytes: Take this Java!! Big thanks to user in chat for helping me golf down the number section

from pygame import*
from math import*
def L(C,L,R,E):
 for i in C:c(s,A,{1:(v,w),2:(v,v),3:(w,v)}[i],8,1)
 for i in L:l(s,A,*_[i])
 if R:s=transform.rotate(s,R*45).subsurface(*b'\x0c\x0cKK')
 for i in E:l(s,A,*_[i])
 return s
def N(n):
 for i in range(a:=n%6):z=lambda r:(x+r*sin(b:=2*pi*i/a),x-r*cos(b));l(s,A,z(8+n//6*8),z(x))
 return s
for i in range(len(t)):s.blit(L(*{"A":([3],I,1,I),"B":((1,3),I,1,I),"C":([2],I,1,I),"D":([2],I,1,[1]),"E":(I,(5,7),0,I),"F":(I,[5],0,I),"G":(I,(1,9),0,I),"H":((1,3),I,1,(0,2)),"I":(I,[1],0,I),"J":([3],[1],0,I),"K":(I,(4,9),1,[8]),"L":([2],[1],0,I),"M":(I,(0,2),0,I),"N":(I,[1],1,I),"O":(I,I,0,I),"P":([2],[1],0,I),"Q":([2],I,0,I),"R":([1],(1,9),0,I),"S":((1,3),I,0,I),"T":(I,(4,6),0,I),"U":([1],I,1,I),"V":([1],(1,6),1,I),"W":((1,3),(1,6),-1,I),"X":(I,(1,6),1,I),"Y":(I,(1,9),1,I),"Z":(I,[6],1,I),}[t[i]])if t[i].isalpha()else N(int(t[i]))if" "!=t[i]else S(Y),(y*i,0))

Some sample outputs:

HELLO WORLD: Matoran glyphs reading "HELLO WORLD"

0123456789: Matoran glyphs reading "0123456789"


Ungolfed and (somewhat) explained version

from pygame import*
from math import*
def Letter(circles, lines, rot, extra): ## Function that contructs a letter glyph 
    S = Surface((64,64)) ## Surface to draw on
    draw.circle(S,W,(32,32),32,1) ## Exterior circle
    for i in circles: ## Draw the small circles
        draw.circle(S,W,{0:(16,16),1:(48,16),2:(48,48),3:(16,48)}[i],8,1) ## This dict maps 0,1,2,3 to the different circle positions, clockwise from top left. Circle 0 is not used in the golfed version.
    for i in lines: ## Draw the lines
        draw.line(S,W,*{0:((16,4),(16,60)),1:((32,0),(32,64)), ## This dict maps numbers to the coords for various line segments. 
                        2:((48,4),(48,60)),3:((32,0),(32,32)),4:((32,32),(32,64)), ## 0,1,2 are full length vertical lines, 4,5 are half length vertical lines
                        5:((4,16),(60,16)),6:((0,32),(64,32)),7:((4,48),(60,48)), ## 5,6,7 are full length horizontal lines, 8,9 are half length horizontal lines
                        8:((0,32),(32,32)),9:((32,32),(64,32))}[i]) ## 3 and 8 are not used in the golfed version
    if rot: ## rotate the glyph 45 degrees, if necessary
        S=transform.rotate(S,R*45).subsurface((12,12,75,75)) ## the R*45 is used in the golfed version to sometimes rotate the other direction, which allowed the removal of some circle/line keys from the dicts
    for i in extra: ## draw the extra line segments necessary, names the same as above for consistency
    return S ## return the surface
def Number(n): ## contructs a number glyph from its value
    S = Surface((64,64)) ## Contruct a surface to draw on
    draw.circle(S,W,(32,32),32,1) ## Exterior circle
    draw.circle(S,W,(32,32),8,1) ## Central circle
    draw.circle(S,W,(32,32),16*(n//6),1) ## Larger central circle, radius 0 if unneeded (radius 0 == nothing drawn)
    for i in range(n%6): ## draw the spokes
        a,r=(2*pi*i)/(n%6),8+n//6*8 ## current angle and the radius of the central circle
        draw.line(S,W, ## draw the line, with appropriate endpoints
    return S ## return the surface
T=input().upper() ## take input from stdin, .upper() to make ungolfed version more friendly
s=display.set_mode((64*len(T),64)) ## setup the display wide enough for the input text
for i in range(len(T)): ## loop through the characters
    s.blit( ## blit (copy) the glyph surface onto the display
           Letter(*{"A":((3,),(),1,()),"B":((1,3),(),1,()),"C":((2,),(),1,()), ## if its a letter, this (somewhat scary) dict maps 
                    "D":((2,),(),1,(1,)),"E":((),(5,7),0,()),"F":((),(5,),0,()), ## the letter to the correct args for Letter()
           if T[i].isalpha() ## only send the char to Letter() if it is a letter
           else Number(int(T[i])) ## otherwise send it to Number() as an int
           if T[i]!=" " ## but if its a space, don't send it to either function
           else Surface((64,64)), ## instead just make a new, empty surface
           (64*i,0)) ## place at the right position
display.flip() ## render all chars
  • 1
    \$\begingroup\$ A few ideas: all of the tuples containing only constant integers can probably be replaced by byte strings (for example (48,48) -> b'00'). In some cases \n can be replaced by a ; to save a byte. You can probably merge the last two dictionaries in L, as all shared keys point to the same values \$\endgroup\$
    – ovs
    Apr 20, 2022 at 8:06
  • \$\begingroup\$ Some minor things to golf: you can remove the space at .isalpha() else; 16*(n//6) can be n//6*16; (2*pi*i) can be without parenthesis; if t[i]!=" "else can be if" "!=t[i]else to get rid of the space. (And as mentioned in the comment above, on some lines you can change the newline+space to a ;.) \$\endgroup\$ Apr 20, 2022 at 12:05
  • \$\begingroup\$ @ovs those are some good thoughts, I actually have a somewhat more golfed version that assigns a lot of repeated tuples to constants, but thats a good shout about using bytestrings, that I dont think I wouldve thought of, and I cant believe I didnt think of merging the dicts for L (that will make the shared dict a tiny bit longer though, as IIRC line 8 is not in the bigger dict) \$\endgroup\$
    – des54321
    Apr 20, 2022 at 18:18
  • \$\begingroup\$ @KevinCruijssen good thoughts on cleaning up some whitespace, I think I figured a few of those out myself, but good thoughts about that third one in the space-checking \$\endgroup\$
    – des54321
    Apr 20, 2022 at 18:19
  • \$\begingroup\$ @Steffan oh thanks for catching those, problem 1 is an extra paren (should be "K":(I,(4,9),1,[8]), and the N problem was a find-and-replace I made that accidentally changed the dict keys too, adding an edit now which should fix everything (and lower bytecount to 1269 bytes) \$\endgroup\$
    – des54321
    Apr 23, 2022 at 0:54

BBC BASIC, 395 385 bytes

ORIGIN80*i,80m=ASCM."CBDFFDFRFATETsDFDF?UDxhxsI",c,1)*95+ASCM."!~R8Kg\SJ?bs9?dD.VS{bN|PEu",c,1)t=m MOD2*20n=8m /=2v=t*7/4w=40-t
IFc>47n=c MOD6+1E-9m=728+(n<1)
F.j=0TOa*n S.a:u=m MOD3m/=3y=COSj:x=SINj
N.F.k=5-c DIV27*2TO5j=k MOD5+1MOVE0,0PLOT155-j MOD2*10,0,40>>j/2N.N.

Download interpreter at http://www.bbcbasic.co.uk/bbcwin/bbcwin.html

Keyword abbreviations expand on loading into IDE.


For letters, each of the 8 positions from N through E, S and W to NW can be either empty space, a line or a small circle. Additionally, the lines within each letter are usually radial, but for certain letters EFHM they are at right angles. The information for each letter is stored as an 8 digit ternary number, which is multiplied by 2 and the final bit indicates whether lines are radial or at right angles. Encoding is as follows to minimise the range of codes (there is never a circle in the NW position)

  0 = circle              0 = radial
  1 = space               1 = at right angles to radial
  2 = line

For digits, the number of spokes is reduced to the ASCII code mod 6, and it is assumed that all spaces are lines with the constant 728. BBC BASIC FOR loops are always excecuted at least once, so where the ASCII code mod 6 is zero, the plotting of the first spoke is suppressed by subtracting 1 from the constant. (n<1) evaluates to TRUE which is represented in BBC BASIC by -1.

A radius of 40 was selected for each character. This is because of the well known approximations 40*SIN45=28 and 40*SIN60=35

Ungolfed code

  INPUT s$                                     :REM input from stdin
  FOR i=1 TO LEN(s$)                           :REM for each character in input
    c=ASC(MID$(s$,i,1))MOD64                   :REM convert to ASCII code MOD 64 so that "A"=1
    IF c=32NEXT                                :REM if it is a space do nothing
  ORIGIN 80*i,80                               :REM move graphics origin to the cente of the current letter
  t=m MOD2*20                                  :REM use the 2 magic strings to lookup the letter data m
  m/=2                                         :REM t indicates if lines are radial. Extract t and divide by 2
  v=t*7/4                                      :REM for nonradial lines, tangential offset of 20*7/4=35 is needed
  w=40-t                                       :REM radial offest of line endpoint is 40 for radial, 20 for nonradial
  n=8                                          :REM 8 spokes for letters
  IF c>47 n=c MOD 6+1E-9:m=728+(n<1)           :REM if character is a digit (c>47), there are c MOD6 spokes
  a=PI*2/n                                     :REM calculate angle between spokes (+1E-9 on previous line avoids division by 0)
  FOR j =0 TO a*n STEP a                         :REM iterate through spokes
    u=m MOD 3: m/=3                              :REM extract data u for current spoke then divide m by 3 for the next one
    y=COSj:x=SINj                                :REM calculate (x,y) vector for radial direction of current spoke
    IF u<1 CIRCLE x*28,y*28,10                   :REM if u=0 draw a circle instead of a spoke
    IF u>1 LINE x*t-y*v,y*t+x*v,x*w+y*v,y*w-x*v  :REM if u=2 draw a spoke starting at radius t=0 and ending at radius w=40
  NEXT                                           :REM if t=20 draw a nonradial line instead from -v to v passing through radius t=w=20
  FOR k=5-c DIV27*2TO5                         :REM iterate through circles: c>=54 k starts at 1; c>=27 k starts at 3, c<2 k starts at 5
    j=k MOD 5+1                                  :REM order of circles: 1+1=2, 2+1=3 (digits 6789); 3+1=4 4+1=5 (all digits); 0+1=1 (all symbols)
    MOVE 0,0:PLOT 155-j MOD2*10,0,40>>j/2        :REM clear the background for even number circles, draw the circle for odd number circles
  NEXT                                           :REM radius 40>>(2/2 or 3/2)=20, 40>>(4/2 or 5/2)=10, 40>>(1/2) = 40
  NEXT                                         :REM repeat for next character

enter image description here


HTML + JS + SVG, 577 575 571 552 545 544 522 511 497 507 503 bytes

On my browser (Firefox 100 on Linux), the 0 and 6 used to render correctly, because an invalid transform hid the SVG elements. A few months later, I'm adding 10 bytes, since I can't seem to reproduce that anymore.

t=>[...t,0].map(c=>document.body.innerHTML+=c?`<svg viewBox=-4,-4,8,8 width=50 fill=#fff stroke=#000 stroke-width=.1>`+(c>" "&&['D',..."Y Y] _ _` PT T J` RVY] ` Z` IKN X` RV c = `^ X J^` Z^ Lb ] ]ac [_ac ac Ic a".split` `[parseInt(c,36)-10]||(c%6?'()*+,':'')+(c>5?'BA':'A')].map(u=>`<path d=${`M57A5,57,177,5A5,5777,5784 M6,2H-6 M2,1939184v484`.replace(/./g,k=>[x=(y=u.charCodeAt())&7,3.4,',0',' M0,-4v','A1,1,0,0,0,2,'][k-5]||k).split` `[y>>3&7]} transform=rotate(${y<64?x*360/(c%6):45*x}) />`)):'<p>')

Or 493 bytes if you don't care about separating the outputs of different calls with <p>. (Replacing the page contents is longer.)

t=>[...t].map(c=>document.body.innerHTML+=`<svg viewBox=-4,-4,8,8 width=50 fill=#fff stroke=#000 stroke-width=.1>`+(c>" "&&['D',..."Y Y] _ _` PT T J` RVY] ` Z` IKN X` RV c = `^ X J^` Z^ Lb ] ]ac [_ac ac Ic a".split` `[parseInt(c,36)-10]||(c%6?'()*+,':'')+(c>5?'BA':'A')].map(u=>`<path d=${`M57A5,57,177,5A5,5777,5784 M6,2H-6 M2,1939184v484`.replace(/./g,k=>[x=(y=u.charCodeAt())&7,3.4,',0',' M0,-4v','A1,1,0,0,0,2,'][k-5]||k).split` `[y>>3&7]} transform=rotate(${y<64?x*360/(c%6):45*x}) />`)))

Or 482 bytes if you don't care about the colors at all:

t=>[...t].map(c=>document.body.innerHTML+=`<svg viewBox=-4,-4,8,8 width=50 stroke=red stroke-width=.1>`+(c>" "&&['D',..."Y Y] _ _` PT T J` RVY] ` Z` IKN X` RV c = `^ X J^` Z^ Lb ] ]ac [_ac ac Ic a".split` `[parseInt(c,36)-10]||(c%6?'()*+,':'')+(c>5?'BA':'A')].map(u=>`<path d=${`M57A5,57,177,5A5,5777,5784 M6,2H-6 M2,1939184v484`.replace(/./g,k=>[x=(y=u.charCodeAt())&7,3.4,',0',' M0,-4v','A1,1,0,0,0,2,'][k-5]||k).split` `[y>>3&7]} transform=rotate(${y<64?x*360/(c%6):45*x}) />`)))

Apparently, I had a very similar idea to @Matthew Jensen about SVG being much shorter than the other options. However, I took a pretty different approach.

I think this is finally getting pretty close to optimal with my current approach. If only I had a language with JS semantics but Python builtins, like int(), str.translate, oct, str[:], …

The output may be wrapped on small screens or if your Stack Snippet is not fullscreen. (You can reduce width=50 to fix this.)

renderMatoran =

t=>[...t,0].map(c=>document.body.innerHTML+=c?`<svg viewBox=-4,-4,8,8 width=50 fill=#fff stroke=#000 stroke-width=.1>`+(c>" "&&['D',..."Y Y] _ _` PT T J` RVY] ` Z` IKN X` RV c = `^ X J^` Z^ Lb ] ]ac [_ac ac Ic a".split` `[parseInt(c,36)-10]||(c%6?'()*+,':'')+(c>5?'BA':'A')].map(u=>`<path d=${`M57A5,57,177,5A5,5777,5784 M6,2H-6 M2,1939184v484`.replace(/./g,k=>[x=(y=u.charCodeAt())&7,3.4,',0',' M0,-4v','A1,1,0,0,0,2,'][k-5]||k).split` `[y>>3&7]} transform=rotate(${y<64?x*360/(c%6):45*x}) />`)):'<p>')

for (const text of ["ABCDEFGHIJ","KLMNOPQRST", "UVWXYZ", "0123456789", "CODE GOLF 101"]) {


t => 
  [...t, 0].map(c =>  /* for each char c (plus terminator) ... */
    document.body.innerHTML +=  /* the HTML parser will enjoy this... */
    c ?                         /* handle terminator */
      viewBox=-4,-4,8,8         /* we want nice and short coords */
      width=50                  /* specify some width so the browser */
                                /* puts our svgs next to each other */
      fill=#fff stroke=#000
      stroke-width=.1           /* much cheaper than big coords */
    >` + 
    (c > " " &&  /* yield false if space (gets dumped in the SVG, */
                 /* but it does not care about stray strings) */
    ['D',    /* everything has a big circle */
    ...      /* convert rest to array elements */
    "Y Y] _ _` PT T J` RVY] ` Z` IKN X` RV c = `^ X J^` Z^ Lb ] ]ac [_ac ac Ic a"
    .split` `[parseInt(c,36)-10] /* for letters: take corresponding item */
                                 /* from the data string */
    || `()*+,${c>5?'B':''}A`     /* for numbers: generate string with */
                                 /* 5 spokes and 1-2 circles */
    ].map(u =>  /* for each char u in the instructions... */
        d=${  /* svg path black magic */
          `M57A5,57,177,5A5,5777,5784 M6,2H-6 M2,1939184v484`
          .replace(/./g, k => /* map each character k to... */
              x = (y = u.charCodeAt()) & 7,  /* 5 -> instruction arg */
              3.4,                   /* 6 -> approx. 2*sqrt(3) */
              ',0',                          /* 7 */
              ' M0,-4v',                     /* 8 */
              'A1,1,0,0,0,2,'                /* 9 */
            ][k-5] || k                      /* everything else to itself */
          ).split` `[y>>3&7]  /* grab correct instruction */
          y<64 ? x*360/(c%6)  /* rotation for spokes */
          : 45*x              /* rotation for everything else */
    ))       /* just dump the array in the string, the parser will eat the */
             /* useless commas :) */
    : '<p>'  /* make sure each call goes on its own line */
  • \$\begingroup\$ You can save 2 bytes by changing both .innerHTML's to [H='innerHTML'] and [H] \$\endgroup\$ Apr 21, 2022 at 21:52
  • \$\begingroup\$ Great job on breaking 500 bytes! \$\endgroup\$ Apr 22, 2022 at 2:08
  • \$\begingroup\$ Nice work! However the shapes for 0 & 6 seem to be incorrect; they're the same as 1 & 7. \$\endgroup\$
    – Shaggy
    Sep 27, 2022 at 9:59
  • \$\begingroup\$ @Shaggy yup... they definitely worked on my laptop when I originally posted this, but now they're rendering "correctly" for some reason and I can't seem to find a browser version that did what I saw back then. \$\endgroup\$ Sep 27, 2022 at 12:06
  • \$\begingroup\$ I don't see anything in the challenge spec about the colours so you could get some of those bytes back by removing the fill and changing the stroke to red. \$\endgroup\$
    – Shaggy
    Sep 28, 2022 at 9:25

JavaScript + HTML, 582 bytes

-many, many bytes thanks to tsh!

Changed it to modify document.body to better match this other JS + HTML answer.

s=>document.body.innerHTML+=`<svg viewBox=5,-5,${[c,l]=['<circle r=cx=cy=','<line y2=y1=x2=x1='].map(n=>(...a)=>n[$](/=/g,_=>`=${a.pop()} `)+`transform=rotate(${R*45}) />`),s.length}0,10 stroke=red stroke-width=.2>`+s.toLowerCase()[$='replace'](/./g,x=>'<g transform=translate(10)>'+(x>' '&&c(R=0,4)+(1/x?[...Array(L=x%6)].map(_=>(R+=8/L,l(-4)))+[x>5&&c(2)]+c(1):'aC1bC1C5cC7dC7LeE2E6fE6gI6LhC1C5E4EiLjC2LkI2I5I7lC0LmE4EnL3pC6LqCrC6I6LsC2C6tL2IuC5vC5L1L3wC3C7L1L3xL1L3yL3I5zL1'.match(x+'([0-Z]+)')?.[1][$](/..?/g,([z,r])=>(R=r,{C:c(2,2,1),L:l(-4,4),I:l(4),E:l(2,2,-3.5,3.5)}[z])))))

s=>document.body.innerHTML+=`<svg viewBox=5,-5,${[c,l]=['<circle r=cx=cy=','<line y2=y1=x2=x1='].map(n=>(...a)=>n[$](/=/g,_=>`=${a.pop()} `)+`transform=rotate(${R*45}) />`),s.length}0,10 stroke=red stroke-width=.2>`+s.toLowerCase()[$='replace'](/./g,x=>'<g transform=translate(10)>'+(x>' '&&c(R=0,4)+(1/x?[...Array(L=x%6)].map(_=>(R+=8/L,l(-4)))+[x>5&&c(2)]+c(1):'aC1bC1C5cC7dC7LeE2E6fE6gI6LhC1C5E4EiLjC2LkI2I5I7lC0LmE4EnL3pC6LqCrC6I6LsC2C6tL2IuC5vC5L1L3wC3C7L1L3xL1L3yL3I5zL1'.match(x+'([0-Z]+)')?.[1][$](/..?/g,([z,r])=>(R=r,{C:c(2,2,1),L:l(-4,4),I:l(4),E:l(2,2,-3.5,3.5)}[z])))))


  'Hello World',
  'Matthew Jensen',
  'Matoran Alphabet',
].forEach(text => {
  //document.body.appendChild(document.createElement('div')).innerHTML = f(text);
svg {
  background-color: black;
  height: 64px;

Explained a bit

s => document.body.innerHTML += `<svg viewBox=5,-5,${ // add a new SVG element to doc body
  [c, l] = [ // define function to generate circle and line
    '<circle r=cx=cy=',
    '<line y2=y1=x2=x1='
  ].map(n => (...a) => // map each above string to function to assign args (reverse order)
    n[$ /* = 'replace' */ ](/=/g, _ => `=${a.pop()} `) +
    `transform=rotate(${R * 45}) />`), // apply 1/8 rotations by global R
}0,10 stroke=red stroke-width=.2>` +
  s.toLowerCase()[$ = 'replace'](/./g, x => // replace each char with its svg
    '<g transform=translate(10)>' + // begin new group (nested in previous char, idea from tsh!)
    (x > ' ' && c(R = 0, 4) + // if not a space, draw the large circle
      (1 / x ? // if char is number
        [...Array(L = x % 6)] // draw line segments
          .map(_ => (R += 8 / L, l(-4))) +
        [x > 5 && c(2)] + // draw r=2 circle if >5
        c(1) // draw r=1 circle
      : // otherwise char is letter
          .match(x + '([0-Z]+)')?.[1] // find code for letter
          [$ /* = 'replace' */ ](/..?/g, ([z, r]) => ( // for each instruction [shape, rotation]
            R = r, // set global rotation to r
            { // look up instruction
              C: c(2, 2, 1),
              L: l(-4, 4),
              I: l(4),
              E: l(2, 2, -3.5, 3.5)
  • \$\begingroup\$ save some bytes \$\endgroup\$
    – tsh
    Apr 21, 2022 at 2:40
  • \$\begingroup\$ This is not generating "SVG". You may claim it as "SVG in HTML" or simply "HTML". As it lacks quotes on attribute and becomes a invalid XML; xmlns is also required for SVG. \$\endgroup\$
    – tsh
    Apr 21, 2022 at 3:13
  • \$\begingroup\$ Fair point, I'll update. Nice byte saving too! \$\endgroup\$ Apr 21, 2022 at 3:46
  • \$\begingroup\$ 580bytes: s=>`<svg viewBox=5,-5,${[c,l]=['<circle r=cx=cy=','<line x1=y1=x2=y2='].map(n=>(...a)=>n[$](/=/g,_=>`=${a.shift()} `)+`transform=rotate(${R*45}) />`),s.length}0,10 stroke=red stroke-width=.2>`+s.toLowerCase()[$='replace'](/./g,x=>'<g transform=translate(10)>'+(x>' '&&c(4,R=0)+(1/x?[...Array(L=x%6)].map(_=>l(0,-4,0,0,R=i++*8/L),i=0)+[x>5&&c(2)]+c(1):'aC1bC1C5cC7dC7LeE2E6fE6gI6LhC1C5E4EiLjC2LkI2I5I7lC0LmE4EnL3pC6LqCrC6I6LsC2C6tL2IuC5vC5L1L3wC3C7L1L3xL1L3yL3I5zL1'.match(x+'([0-Z]+)')?.[1][$](/..?/g,z=>({C:c(1,2,2,R=z[1]),L:l(0,-4,0,4),I:l(0,0,0,4),E:l(2,-3.5,2,3.5)})[z[0]])))) \$\endgroup\$
    – tsh
    Apr 21, 2022 at 7:50

Perl 5 + -M5.10.0 -lF, 1017 bytes

sub d{unpack"B*",pop}sub a{$A[$_].="@_"eq''?0 x8:("@_"=~/.{17}/g)[$_]for 0..16}sub z{0 x pop}a{O,($x=d"........\$............`.0........0...."),A,$a=$x|z(194).($C=d"....D.\".."),U,$u=$x|z(24).$C,B,$b=$a|$u,C,$c=$x|z(114).$C,I,$i=$x|z(8).($l=d"..@. .............")|0 x($-=144).$l,D,$c|$i,F,$f=$x|z(70).($h=1x13),E,$f|z(206).$h,G,$g=$i|0 x$-.($H=1x9),M,$m=$x|z(38).($k=(1 .z 16)x13)|z(46).$k,H,$m|$b,J,$i|z(173).$C,K,$x|z(64).($y=d".."x6)|0 x$-.($Y=d".. ........ ")|z(135).$H,Q,$q=$x|z(181).$C,L,$i|$q,N,$n=$x|z(54).$Y|0 x$-.$Y,P,$p=$i|z(45).$C,R,$p|$g,S,$x|z(173).$C|z(45).$C,T,$g=$x|0 x$-.$H|z(135).$H|0 x$-.$l,Y,$z=$n|z(64).$y,X,$X=$z|0 x$-.$y,V,$X|$u,W,$X|z(104).$C|z(114).$C,Z,$x|z(64).$y|0 x$-.$y,0,$Z=$x|z(109).$C,1,$O=$Z|z(25).($L=d"..@. ...."),2,$T=$O|z(195).$L,3,$O|(z(174).d"...0"),4,$T|z(137).1111100000 x2,5,$O|(z(86).d"......`.0D......D..0."),6,$s=$x|(z(74).d".P....QF(..Q....P."),7,$S=$s|(z(25).($w=d"..@. ")),8,$S|z(229).$w,9,$S|z(180).(z(9).11)x2}->{$_}for@F;$-=y/ //c;say"P1 ",17*$-+8*(@F-$-)," 17 @A"

Try it online!


The main functionality of this answer utilises a lookup table to store information about each letter we translate, this lookup table is built up from previous letters and augmented using stringwise-OR operations.

We start with O which is just a circle. Using the PBM format, this is stored as:


From here, A (for example) is augmented by ORing the string:


with a specific number of leading 0s (194 for A), so as to place this at the right position.

The same technique is used to build the other letters and numbers, sometimes using the result of ORing two other letters to get the required output. The binary strings are stored using Perl's pack functionality and the data is output by repeatedly appending each required line from the letters to the list @A's indices 0..16.

All the glyphs are hand-made by eye, so please shout if this is not to your liking!


C (GCC), 871 bytes

#define Q(X,Y)(r=(X)*(X)+(Y)*(Y))
#define S(M)(y&M?-1:1)
#define C;for(y=1;y<16;y*=2)c|=x&y<<
#define A labs
int T[]={0,1,3,1,15,1,0,1,3,1,8192,12288,32768,32771,768,256,11,15360,3,131075,164,65539,3072,48,0,262147,65536,262155,393216,14,4096,4336,49392,240,176,192};main(i,v,j,k,x,r,y,n,c,w)char**v;{w=90*strlen(v[1]);printf("P5 %d %d 1 ",w,79);for(i=-40;i++<39;)for(j=-1;++j<w;putchar(c)){c=0;k=j%90-40;n=v[1][j/90];if(n-32){c|=Q(i,k)<1600&r>1400;x=T[n-48-7*(n>64)]C 0&&A(y&3?k:i)<2&k<40&(y&3?i:k)*S(5)>=0 C 4&&A(k)<28&A(i+k*S(3))<2&k*S(5)>=0 C 8&&A((y&3?i:k)-20*S(5))<2&A(i)<34&A(k)<34 C 12&&Q((y&3?i:k)-30*S(5),y&3?k:i)<99&r>63 C 16&&Q(i+20*S(3),k+20*S(5))<99&r>63;if(n<58){for(y=2;A(n-54)==3&&y--;)c|=A(1.73*i+k*S(y))<3&i>0&A(k)<34;for(y=2;n==53&&y--;)c|=A(3.08*i+k*S(y))<5&i<0&A(k)<39,c|=A(.73*i-k*S(y))<2&i>0&A(k)<23;if(n>53)c=(c|r<399)&r>323;c=(c|r<99)&r>63;}}}}

Attempt This Online!

Input is given via argv[1]. Outputs a PGM image.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.