Take me home...

Unholy marriage of IOCCC and demoscene

Look closely. The characters that make the "sushio4" text are not random, as they are in fact a C code. But not just any code! This is the exact code that does the fire effect (with one exception explained later).

Why?

Because it's cool.

Well, once when thinking about writing my Personal HomePage I was wondering - what can I put at the top of the page to make it unique? Probably a picture of a potato wearing a sombrero hat would do the trick, but it's not quite what I was looking for. I was looking for something that:

And then I thought about one of my favourite pieces of code. The 2012 IOCCC ASCII fluid dynamics program by Yusuke Endoh. I'll give you a minute to appreciate its beauty. Being inspired by this masterpiece I made it with fire instead of water. And slightly less impressive. Definitely not ioccc worthy, but cool nonetheless.

How?

Just like any other obfuscated code, it started with unobfuscated code. I even considered adding more effects but then the code would be too long. Here's the unobfuscated code:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define FRAME_DELAY 125000

#define TEXT_Y 5
#define TEXT_X 10

#define WIDTH 80
#define HEIGHT 20
// 11 for ANSI color, 1 for character
#define CELL_SIZE 12 

// + 1 for \n or \0
#define LINESIZE (WIDTH * CELL_SIZE + 1)
#define BUFSIZE (LINESIZE * HEIGHT)

char screen_in[BUFSIZE] = {0};
char screen_out[BUFSIZE] = {0};

#define C(X) "\033[38;5;"#X"m"

// shader things
char* CS[8] = {
    C(231),
    C(196),
    C(202),
    C(208),
    C(214),
    C(220),
    C(226),
    C(231)
};

char fc[8] = " '~*h%$@";
unsigned char heat_map[WIDTH * HEIGHT] = {0};
unsigned char cool_chance = 5;
unsigned char cool_source = 0;
unsigned char change_source = 1;
unsigned char show_text = 0;

void init_buffs(FILE* f) {
    for(int y = 0; y < HEIGHT; y++) {
        char line[82];
        if(y < TEXT_Y || fgets(line, 82, f) == NULL) {
            line[0] = '\0';
        }
        int line_i = 0;

        for(int x = 0; x < WIDTH; x++) {
            memcpy(screen_in + y * LINESIZE + x * CELL_SIZE, CS[0], 11);
            memcpy(screen_out + y * LINESIZE + x * CELL_SIZE, CS[0], 11);
            if(x < TEXT_X || line[line_i] == '\0' || line[line_i] == '\n') {
                screen_in[y * LINESIZE + x * CELL_SIZE + 11] = ' ';
            }
            else {
                screen_in[y * LINESIZE + x * CELL_SIZE + 11] = line[line_i];
                line_i++;
            }
            screen_out[y * LINESIZE + x * CELL_SIZE + 11] = ' ';
        }
        if(y == HEIGHT - 1) break;
        screen_in[(y + 1) * LINESIZE - 1] = '\n';
        screen_in[(y + 1) * LINESIZE - 1] = '\n';
    }
}

// for every pixel
void fragment_shader(int x, int y) {
    // fire source
    unsigned char heat = 7;
    if(y == HEIGHT - 1) {
        unsigned char prev = heat_map[y * WIDTH + x];
        if(change_source) {
            if(cool_source) {
                heat = (rand() % 100) < 4 ? prev - 1 : prev;
            }
        }
        else {
            heat = prev;
        }
    }
    // regular fire particle
    else {
        signed char off = rand() & 3;
        if(x == WIDTH - 2) off = -off;

        heat = 2 * heat_map[(y + 1) * WIDTH + x + off];
        heat += (x < WIDTH - 1) ? heat_map[(y + 1) * WIDTH + x + 1 + off] : 0;
        heat += (x > 0) ? heat_map[(y + 1) * WIDTH + x - 1 + off] : 0;
        heat /= 4;
        if((rand() % 100) < cool_chance) heat--;
    }
    if(heat > 100) heat = 0;
    if(heat > 7) heat = 7;
    heat_map[y * WIDTH + x] = heat;
    // actual drawing
    if(show_text && screen_in[y * LINESIZE + x * CELL_SIZE + 11] != ' ' && heat < 2)
        return;

    memcpy(screen_out + y * LINESIZE + x * CELL_SIZE, CS[heat], 11);
    if(heat || !show_text)
        screen_out[y * LINESIZE + x * CELL_SIZE + 11] = fc[heat];
}

// once at the beginning
void init_shader(void) {
    for(int x = 0; x < WIDTH; x++)
        heat_map[(HEIGHT - 1) * WIDTH + x] = 7;
}

void gen_frame(int frame) {
    memcpy(screen_out, screen_in, BUFSIZE);
    for(int x = 0; x < WIDTH; x++) {
        for(int y = 0; y < HEIGHT; y++) {
            fragment_shader(x, y);
        }
    }
}

int main(int argc, char** argv) {
    if(argc != 2) return 1;
    FILE* f = fopen(argv[1], "r");
    init_buffs(f);
    fclose(f);

    srand(time(NULL));
    init_shader();

    puts("\033[1;1H\033[2J");
    long long frame = 0;
    while(1) {
        if(frame == 48) {
            cool_source = 1;
        }
        if(frame == 128) {
            change_source = 0;
        }
        if(frame == 16) {
            show_text = 1;
        }
        gen_frame(frame);
        puts("\033[H");
        puts(screen_out);
        // return 0;
        usleep(FRAME_DELAY);
        frame++;
    }
    return 0;
}

Too much to C? Alright, let me dumb it down a bit. At the beginning it reads a file from argv[1] and saves it to one buffer. Then in every frame it copies this buffer to the second one and draws fire over it.

The fire itself is based on a heat map, which is just a buffer for the whole screen with heat values from 0 to 7. Before each frame, there's fragment_shader() function that executes for each pixel (character) and it updates its heat value based on the ones beneath it with a chance of cooling down. Kinda like cellular automata (ever heard of Conway's Game Of Life?). The bottom row is referred to as the source, because these heat values do not change at first and all the flames rise from here.

Colours are achieved via ANSI escape sequences and each pixel can have its own colour. The 8-bit ones (256 colours) to be exact. That's why you see CELL_SIZE equal to more than 1. Oh, and clearing the screen is also done with these.

Handy source

And then at the end there are some timed events. First, there's a delay to showing input file contents that waits for the flames to rise. It makes the text being revealed by the flames. After some time the source begins to cool down and the cooling stops moreless when the flames are low enough to see the text clearly. Simple!

Obfuscation

Not much to say here, just basic code golfing stuff.

Basically learn how to code well and then do the exact opposite. Besides DRY. Obey it at all costs as long as it can shave even one byte of your code. Maybe KISS too. Complicated code can be longer than simple code. Alright, don't do the exact opposite, just cherry pick to achieve your desired effect. Also, learn about some quirks of language and compiler, they might come in handy.

After golfing I opened up gimp and tried to write "sushio4" using the amount of pixels as close to non-whitespace characters of my code as I could. Then it's just a matter of indentation. Here's the complete obfuscated code:

               c t[B],T[B       ],*    r[]       ={C(231),C       (196   ),C       (202      ),C(208),C       (214   ),C
              (220),C(226      ),C    (231      )},*Q=" '"       "~"    "*"       "h"       "%$@",l[L]       ;m[    B];
             q=0              ;d=    1;z       =0;              h,p    ,o;       x,y       =0,    f=0;      main   (A, 
            V)c              **V    ;{D       *F=              N(V    [1]       ,"r"      );i    (;y       <H;    y++
           ){I              (y<    Y||       !G(              l,W    +2,       F))       *l=    0;i       (x=    o=0
          ;x<              W;x    ++)       {m[              H*W    -W+       x]=       7;p    =y*       L+x    *K;
         M(t              +p,    *r,       j);              M(T    +p,       *r,       j);    h=32      ;T[    p+j
        ]=h;I(x>X&&      l[o    ]>j       )h=l[o++];       t[p+j]=h;}       I(y       >H-    2)k       ;t[y*L+L-1
       ]=10;}i(;;       f++    ){I       (f>48)q=1;       I(f>128)d=       0;I       (f>    32)       z=1;M(T,t,
             B);       i(x    =0;              x<W       ;x     ++)i      (y=       H-1    ;y;              y--
            ){h       =7;    I(y              >H-       2){    p=m       [y*       W+x    ];I              (!d
           )h=       p;E    I(q              )h=       R%    25<1       &&p       ?p-    1:p              ;}E
          {p=       y*W    +W+              x+(       R&3    )-1       ;h=       (m[    p]+              m[p
         +1]       )/2    ;I(              R%50      <1&&   h)h       --;       }m[    y*W              +x]
 =h;p=y*L+x       *K;I(z&&t[       p+j]>j&&h<       2)e    ;M(       T+p       ,r[h],j);I              (h||
!z)T[p+j]=       Q[h+0];};        ;P("\033["       "H"    );P       (T)       ;U(125000)              ;}}

I lied, that's not the complete code. We also need a bit of shell code to compile this atrocity.

#!/bin/bash
gcc demo_golf.c -o demo_golf \
    -Wno-implicit-int -Wno-implicit-function-declaration -Wno-builtin-declaration-mismatch \
    -include stdio.h \
    -D Y=10 -D X=20 -D W=160 -D H=40 -D K=12 \
    -D L='(W * K + 1)' -D B='(L * H)' \
    -D 'C(ARG)'='"\033[38;5;"#ARG"m"' \
    -D M=memcpy -D P=puts -D U='usleep' -D R='rand()' \
    -D G=fgets -D N=fopen -D c=char -D e=continue -D k=break \
    -D i=for -D I=if -D E=else -D D=FILE \
    -D j=11

Maybe you consider the sheer amount of definitions here cheating, maybe you don't, I don't care. All I care about is it looks good.

Done! ... Or is it?

Well, we have this pretty effect in linux terminal, but I wanted it on my homepage and I can't just execute this code in someone's browser. ... or can I?

There's this cool thing called webassembly that allows you to compile real programming languages for browsers.

Sadly, just doing it didn't work. Whole UI on this website was frozen, I couldn't even right-click properly. The issue was in... JavaScript of course! This single threaded piece of... technology. I had to make a few changes, like using some libraries to set up main loop with delay instead of just sleeping in a while loop.

Another problem, how to display it? I tried some js terminals but the text couldn't be shrunk as much as I needed. HTML Canvas? With ANSI codes support? Yeah, I'd have to write more code for that to work, no, thanks. Except that's exactly what I did out of frustration with existing solutions. It's really only useful for this exact purpose, although technically it supports basic colours, 8-bit colours and clearing screen just fine. I even made a lookup table for colours I use for performance. Yeah, open dev tools, roast this rubbish, I'm not a JS dev.

Yet another problem, how to load a file? My program takes filename as an argument and has to open it. Is there a way to simulate it in browser environment? I've been told so. Tried it, didn't believe. Hardcoded it as text in my already edited version of the code.

All these changes were made to non-obfuscated version of the code of course.

Conclusion

I spent hours of hard work on this few second moment for you to watch when you enter my page. This was the motivation for me to even start writing this. I'm happy with the result and I hope you like it.

Return