Beefy Boxes and Bandwidth Generously Provided by pair Networks
Welcome to the Monastery

Fun with terminal color

by cavac (Parson)
on Feb 09, 2024 at 23:10 UTC ( [id://11157635] : CUFP . print w/replies, xml ) Need Help??

Yesterday i thought i might fix some bugs in my imagecat routine (printing images to the terminal). But somehow i, uhm, improved things a little too far. If you're now thinking "Oh no, he did it again!", i'm afraid, yes, i did.

This time, i made a 3 minute animated demo with sound, running at (some) FPS in a Linux terminal.

You can find a download of the current version as a tarball or you could clone my public mercurial repo if you prefer that. (Edit: Link to mercurial has changed/updated)

The demo needs quite a few CPAN modules, i'm afraid, including the somewhat hard-to-install SDL bindings. (For SDL, you might have to change a checksum in the Alien::SDL Build.PL and ignore some test errors in SDL as well.) I'm using SDL for the sound output.

Also, your Terminal needs to support full RGB colors /see the Imagecat - show color images in a terminal thread) and have a size of at least 270x60 in size (characters, not pixels).

If you want to avoid the hassle of getting it to work on your system, you can watch the YouTube video instead.

PerlMonks XP is useless? Not anymore: XPD - Do more with your PerlMonks XP

Replies are listed 'Best First'.
Re: Fun with terminal color
by NERDVANA (Deacon) on Feb 10, 2024 at 05:00 UTC
    That... is significantly prettier than TTY Quake. Wow. What is the size of a single frame though? How bulky are those TTY codes for true color? And how many terminals support that?

      TTY Quake uses AALib, which is from 1997. There's a a demo called "bb" to show off its features, as well as Text::AAlib perl bindings.

      I have no idea how many Terminals support full RGB color, but it seems more and more do these days. I was able to double the vertical resolution by printing a Unicode Upper Half Block character and setting both it's foreground and background color.

      The escape codes are quite bulky, though. To increase FPS, i remember the last colors used and only send color changes when needed. Nevertheless, screens with lots of color changes result in a much lower FPS that ones with a uniform background and only a few, big colored areas. To make a colored "doublepixel", this is essentially how it looks:

      my $halfblock = chr(9600); utf8::encode($halfblock); # Needs to be UTF8-encoded ... print "\e[38;2;255;255;255m"; # White foreground print "\e[48;2;0;0;0m"; # Black background print $halfblock; ...

      In reality, i internally hold an array of pixels (basically, my graphics memory), then render the whole screen into a big string, THEN output the string all at once. This avoids flickering the cursor all over the screen and increases the framerate as well.

      sub render($self) { my $lastfgcolor = ''; my $lastbgcolor = ''; my $out = '' . $self->{home}; my ($r, $g, $b); # Color vars for(my $y = 0; $y < $self->{rows}; $y+=2) { for(my $x = 0; $x < $self->{cols}; $x++) { # Foreground color ($r,$g,$b) = @{$self->{img}->[$x + ($y * $self->{cols})]}; my $newfgcolor = "\e[38;2;" . join(';', $r, $g, $b) . "m"; if($newfgcolor ne $lastfgcolor) { $lastfgcolor = $newfgcolor; $out .= $newfgcolor; } # Background color my $lowy = $y + 1; if($lowy == $self->{rows}) { # End of image. need a black half-line ($r, $g, $b) = (0, 0, 0); } else { ($r,$g,$b) = @{$self->{img}->[$x + ($lowy * $self->{co +ls})]}; } my $newbgcolor = "\e[48;2;" . join(';', $r, $g, $b) . "m"; if($newbgcolor ne $lastbgcolor) { $lastbgcolor = $newbgcolor; $out .= $newbgcolor; } $out .= $halfblock; #print utf8::encode("\N{FULL BLOCK}"); } ($r, $g, $b) = (0, 0, 0); $lastfgcolor = "\e[38;2;" . join(';', 255, 255, 255) . "m"; $lastbgcolor = "\e[48;2;" . join(';', 0, 0, 0) . "m"; $out .= $lastfgcolor . $lastbgcolor . "\n"; } my $now = time; my $fps = 0; if($self->{lastrender}) { $fps = (int((1 / ($now - $self->{lastrender})) * 100)/100); } $self->{lastrender} = $now; # Status line my ($pre, $post) = split/\./o, $fps; if(!defined($post)) { $post = '00'; } while(length($pre) < 4) { $pre = ' ' . $pre; } while(length($post) < 2) { $post .= '0'; } $fps = 'FPS: ' . $pre . '.' . $post; $fps .= ' ' x (18 - length($fps)); $out .= $fps; $out .= $self->{statustext}; if($self->{info} ne '') { $out .= $self->{info}; $out .= ' ' x (220 - length($self->{info})); } elsif($self->{progress} == -1) { $out .= ' ' x 220; } elsif($self->{info} ne '') { $out .= $self->{info}; $out .= ' ' x (220 - length($self->{info})); } else { $out .= $fullblock; my $full = int(218 * $self->{progress}); $out .= $darkshade x $full; $out .= $lightshade x (218-$full); $out .= $fullblock; } print $out; my $key = ReadKey(-1); if(defined($key)) { $key = ord($key); #croak("\n\n\n ** $key **\n"); if($key == 32) { return 1; } elsif($key == 27 || $key == 113) { return -1; } } return 0; }

      My code is far from optimized, because i keep tinkering with it and optimization would make that much harder. If i reorganize how the data is layed out in memory, i could (potentially) rewrite the output function in C for better performance. But i'm currently not sure if one of the bottlenecks isn't simply the STDOUT output pipe to the Terminal. But potentially, with optimized code on a modern computer (mine is 12 years old), you could get 100s of frames per second. (Edit: Flattened image structure, pushed to mercurial. Still have to look into doing something with Inline::C)

      There's also the possibility to go pure black&white, but increase the resolution to 4 pixels per character with the clever use of a combination of Unicode block characters. This would work quite well for line graphics and pencil sketches.

      Another possible optimization would be to basically diff the current and previous image and only update the areas that have changed by jumping the cursor directly to that area. And example where that would be quite effective is the scroller in my demo. Frame-by-Frame, only about roughly 30% on average (probably less) of the screen lines change, and even those might have long stretches of unchanged characters. Basically, what you would do is to paint an interframe instead of a keyframe, to borrow from mpeg terminology. (Edit: Implemented the line-by-line buffering and pushed it to mercurial. It gives a somewhat higher framerate when not much is changing on screen. The "partial rows" update doesn't seem worth the hassle at the moment)

      Additional links:

      PerlMonks XP is useless? Not anymore: XPD - Do more with your PerlMonks XP

      I can now answer the question What is the size of a single frame though?:

      When the whole screen updates, i have to push (depending on the image) roughly somewhere between 400kB and 600kB to the Terminal.

      PerlMonks XP is useless? Not anymore: XPD - Do more with your PerlMonks XP
Re: Fun with terminal color
by talexb (Chancellor) on Feb 10, 2024 at 14:20 UTC

    Oh, wow. ANSI escape codes -- this takes me back to the mid-80's, when I discovered if I added the ANSI.SYS device driver to my CONFIG.SYS, I could use escape sequences to do Clever Windowing Stuff on the video screen. I graduated from there to using BIOS calls to do the same thing, but much more portably. (Because, strangely enough, some users didn't want to update their CONFIG.SYS.)

    Alex / talexb / Toronto

    Thanks PJ. We owe you so much. Groklaw -- RIP -- 2003 to 2013.