Simply trying every character and considering their entire bitmap, and keeping the character that reduces the distance to the target gives better results, at the cost of more CPU.
This is a well known problem because early computers with monitors used to only be able to display characters.
At some point we were able to define custom character bitmap, but not enough custom characters to cover the entire screen, so the problem became more complex. Which new character do you create to reproduce an image optimally?
And separately we could choose the foreground/background color of individual characters, which opened up more possibilities.
For example, limiting output to a small set of characters gives it a more uniform look which may be nicer. Then also there’s the “retro” effect of using certain characters over others.
And in the extreme that could totally change things. Maybe you want to reject ASCII and instead use the Unicode block that has every 2x3 and 2x4 braille pattern.
I'd probably arrive at a very different solution if coming at this from a "you've got infinite compute resources, maximize quality" angle.
It's not just monitors. My first exposure to ASCII art were posters that were printed on a Teletype, in the mid 1970's. The files had attributions to RTTY operators, which made me believe they were done by hand. Of course a Teletype had no concept of pixels.
Bravo, beautiful article! The rest of this blog is at this same level of depth, worth a sub: https://alexharri.com/blog
The code for this post is all in PR #15 if you want to take a look.
Surely you mean 16 milliseconds ?
However, the ASCII output quality is nondiverse despite using the same technique, so will need to do significantly more testing and this likely won't be released soon.
How do you arrive at that? It's presented like it's a natural conclusion, but if I was trying to adjust contrast... I don't see the connection.
> Consider how an exponent affects values between 0 and 1. Numbers close to experience a strong pull towards while larger numbers experience less pull. For example 0.1^2=0.01, a 90% reduction, while 0.9^2=0.81, only a reduction of 10%.
That's exactly the reason why it works, it's even nicely visualized below. If you've dealt with similar problems before you might know this in the back of your head. Eg you may have had a problem where you wanted to measure distance from 0 but wanted to remove the sign. You may have tried absolute value and squaring, and noticed that the latter has the additional effect described above.
It's a bit like a math undergrad wondering about a proof 'I understand the argument, but how on earth do you come up with this?'. The answer is to keep doing similar problems and at some point you've developed an arsenal of tricks.
It reminds me of how chafa uses an 8x8 bitmap for each glyph: https://github.com/hpjansson/chafa/blob/master/chafa/interna...
There's a lot of nitty gritty concerns I haven't dug into: how to make it fast, how to handle colorspaces, or like the author mentions, how to exaggerate contrast for certain scenes. But I think 99% of the time, it will be hard to beat chafa. Such a good library.
EDIT - a gallery of (Unicode-heavy) examples, in case you haven't seen chafa yet: https://hpjansson.org/chafa/gallery/
and damn that article is so cool, what a rabbithole.
Here's a copy-paste snippet where you can try chafa-ascii-fying images in your own terminal, if you have uvx:
uvx --with chafa-py python -c '
from chafa import *
from chafa.loader import Loader
import sys
img = Loader(sys.argv[1])
config = CanvasConfig()
config.calc_canvas_geometry(img.width,img.height,0.5,True,False)
symbol_map = SymbolMap()
symbol_map.add_by_tags(SymbolTags.CHAFA_SYMBOL_TAG_ASCII)
config.set_symbol_map(symbol_map)
config.canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_FGBG
canvas = Canvas(config)
canvas.draw_all_pixels(img.pixel_type,img.get_pixels(),img.width,img.height,img.rowstride)
print(canvas.print().decode())
' \
myimage.jpg
But results are not as good as the OP's work. https://wonger.dev/assets/chafa-ascii-examples.png So I'll revise my claim that chafa is great for unicodey colorful environments, but hand-tailored ascii-only work like the OP is worth the effort.More seriously, using colors (not trivial probably, as it adds another dimension), and some select Unicode characters, this could produce really fancy renderings in consoles!
It probably has a different looking result, though.
GitHub: https://github.com/symisc/ascii_art/blob/master/README.md Docs: https://pixlab.io/art
I feel confident stating that - unless fed something comprehensive like this post as input, and perhaps not even then - an LLM could not do something novel and complex like this, and will not be able to for some time, if ever. I’d love to read about someone proving me wrong on that.
Everyone seems now familiar with hallucinations. When a model's knowledge is lacking and it is fine tuned to give an answer. A simplistic calculation says that if an accurate answer gets you 100%, then an answer gets you 50% and being accurate gets you 50%. Hallucinations are trying to get partial credit for bullshit. Teaching a model that a wrong answer is worse than no answer is the obvious solution, turning that lesson into training methods is harder.
That's a bit of a digression but I think it helps explain the difference to why I think a model would find writing an article like this.
Models have difficulty in understanding what is important. The degree to which they do achieve this is amazing, but it is still trained on data that heavily biases their conclusions to the mainstream thinking. In that respect I'm not even sure if it is a fundamental lack in what they could do. It seems to be that they are implicitly made to think of problems as "it's one of those, I'll do what people do when faced with one of those"
There are even hints in fiction that this is what we were going to do. There is a fairly common sci-fi trope of an AI giving a thorough and reasoned analysis of a problem only to be cut off by a human wanting the simple and obvious answer. If not done carefully RLHF becomes the embodiment of this trope in action.
This gives a result that makes the most people immediately happy, without regard for what is best long term, or indeed what is actually needed. Asimov explored the notion of robots lying so as to not hurt feelings. Much of the point of the robot books was to express the notion that what we want AI to be is more complicated than it appears at first glance.
Of course it likely still needs a skilled pair of eyes and a steady hand to keep it on track or keep things performant, but it's an iterative process. I've already built my own ASCII rendering engines in the past, and have recently built one with a coding model, and there was no friction.
But that's key here.
"A hammer and a chisel can build a 6ft wooden sculpture by themselves just fine .. as long as guided by a skilled pair of eyes and steady hands"
I think there's a small problem with intermediate values in this code snippet:
const maxValue = Math.max(...samplingVector)
samplingVector = samplingVector.map((value) => {
value = x / maxValue; // Normalize
value = Math.pow(x, exponent);
value = x * maxValue; // Denormalize
return value;
})
Replace x by value. let maxValue = value;
for (const externalIndex of AFFECTING_EXTERNAL_INDICES[i]) {
maxValue = Math.max(value, externalSamplingVector[externalIndex]);
}> It may seem odd or arbitrary to use circles instead of just splitting the cell into two rectangles, but using circles will give us more flexibility later on.
I still don’t really understand why the inner part of the rectangle can’t just be split in a 2x3 grid. Did I miss the explanation?
A grid can have unwanted aliasing effects. It all depends on the kinds of images you're working with.
Not to take away from this truly amazing write-up (wow), but there's at least one generator that uses shape:
https://meatfighter.com/ascii-silhouettify/
See particularly the image right above where it says "Note how the algorithm selects the largest characters that fit within the outlines of each colored region."
There's also a description at the bottom of how its algorithm works, if anyone wants to compare.
My go version: https://github.com/BigJk/imeji
Edit: nvm, confused by the libraries purpose. Thought it was primarily character based rendering focused based on the subject under discussion.
[1] https://en.wikipedia.org/wiki/List_of_8-bit_computer_hardwar...
Note: If you happen to know how to do multi-color dithering with some of these that would actually make significant improvements on some of these old picture hardware tests.
But maybe I didn't understand your real problem yet
However, for many the result is that the color choices are akin to a posterization filter in photoshop, where the nearest color is simply chosen. Often, there's actually the freedom 'available' to define a character set and choose at least a background / foreground color, with some kind of dithering pattern.
Sometimes the character set that can be defined is limited, so it has to be chosen carefully. Yet there's improvement from a 'large blobs of color' poster result to a smooth dither tone change.
The problem with the quantization result, is it just snaps to the 'nearest'. So even for relatively large areas of slowly gradiating color, if you only have one 'nearby' color, everything inbetween just snaps to that single color choice. You might have red, with slowly increasing green / yellow, yet it will always just snap to solid red.
This example from the Vic-20 kind of shows that issue. Large areas where it posterizes severely.
https://upload.wikimedia.org/wikipedia/commons/3/32/Screen_c...
Dithering suggested is something like this (greyscale example) except with choosable foreground / background (maybe 3-4, although less frequently)
https://araesmojo-eng.github.io/images/GreyScale_Dithering.p...
This example from the Vic-20 game Tutankarman shows that kind of approach. Varying amounts of dither and color used in dithing give the impression of changing skin tones.
https://www.neilhuggett.com/vic20/tutankarman03.png
They're both the Vic-20
Taking into account the shape of different ASCII characters is brilliant, though!
The resulting ASCII looks dithered, with sequences like e.g. :-:-:-:-:. I'd guess that it's an intentional effect since a flat surface would naturally repeat the same character, right? Where does the dithering come from?
Lovely article, and the dynamic examples are :chefs-kiss:
Acerola worked a bit on this in 2024[1], using edge detection to layer correctly oriented |/-\ over the usual brightness-only pass. I think either technique has cases where one looks better than the other.
I am however am struck with the from an outsider POV highly niche specific terminology used in the title.
"ASCII rendering".
Yes, I know what ASCII is. I understand text rendering in sometimes painful detail. This was something else.
Yes, it's a niche and niches have their own terminologies that may or may not make sense in a broader context.
HN guidelines says "Otherwise please use the original title, unless it is misleading or linkbait; don't editorialize."
I'm not sure what is the best course of action here - perhaps nothing. I keep bumping into this issue all the time at HN, though. Basically the titles very often don't include the context/niche.
Since you are just interested in the ranking, not the actual distance, you could also consider skipping the sqrt. This gives the same ranking, but will be a little faster.
Or, the GameBoy Advance https://github.com/GValiente/butano
I wonder how the latest and greatest Wonderswan is doing in terms of price.
Yes, but part of the joy is the anticipation of playing on a real device at the end.
> Try with C64 and VICE and join us at https://csdb.dk/
Thanks for the invitation! I used a C64 as my only computer in the late 1990s long past its prime, because my mother got a really good deal on a whole set with printer and disk drives and plenty of disks with software (mostly games, from magazines). However, I was still a bit annoyed by the limitations of the system. I guess, if I had had a forth disk, I might have felt different.
In any case, for personal reasons I don't want to explore the C64 more.
But I never had a GameBoy Advance nor a Wonderswan.
Or the Super Nintendo Entertainment System https://github.com/alekmaul/pvsneslib
Or the Gameboy / GBC, Sega Master System, Gamegear, Nintendo Entertainment System https://github.com/gbdk-2020/gbdk-2020
Or the TurboGrafx-16 / PC Engine, Nintendo Entertainment System (alt), Commodore 64, Vic-20, Atari 2600, Atari 7800, Apple II/IIe, or Pet 2001 https://github.com/cc65/cc65
Or the ZX Spectrum, TRS-80, Apple II (alt), Gameboy (alt), Sega Master System (alt), and Game Gear (alt) https://github.com/z88dk/z88dk
Or the Fairchild Channel F https://channelf.se/veswiki/index.php?title=Main_Page
Note: Some are slightly pre-1999 (all these, I have at least successfully made a "Hello World" with)
----------------
If they're really wanting 1999, that's the 5th to 6th generation console range with Sega Saturn, PlayStation, Nintendo 64, and Dreamcast. (on these, only recommendations, no successful compiled software)
Playstation is really challenging and remains so even in 2026. Lots of Modchip and disk swap issues on real hardware. Possibilities: https://www.psx.dev/getting-started and https://github.com/Lameguy64/PSn00bSDK
N64 is less horrible, and there's quite a few resources: https://github.com/DragonMinded/libdragon and https://github.com/command-tab/awesome-n64-development
Sega Saturn is still pretty difficult. However, there is: https://github.com/yaul-org/libyaul?tab=readme-ov-file and https://github.com/ReyeMe/SaturnRingLib plus the old development kits from the 90's are still around https://techdocs.exodusemulator.com/Console/SegaSaturn/Softw...
Dreamcast is similar to the Saturn situation, yet strangely, a little better. There's https://github.com/dreamsdk/dreamsdk/releases and https://github.com/KallistiOS/KallistiOS along with the official SDKs that are still around https://www.sega-dreamcast-info-games-preservation.com/en/re...
So it was a known thing...
This is a trick I reach for all the time: it’s cheaper to compare squared distances than completing the Euclidean calculation. For example, to determine whether to stop calculating lerp: x*x+y*y <= epsilon.
Squared euclidean distance of normalized vectors is an affine transform of their cosine similarity (the cosine of the angle between them).
EuclideanDistance(x, y) = sqrt(dot(x - y, x - y)) = sqrt(dot(x, x) - 2dot(x, y) + dot(y, y)) = sqrt(2 - 2dot(x, y))Reminds me of this underrated library which uses braille alphabet to draw lines. Behold:
https://github.com/tammoippen/plotille
It's a really nice plotting tool for the terminal. For me it increases the utility of LLMs.
Supports color output, contrast enhancement, custom charsets. MIT licensed.
Non-ascii, I tried various subsets of Unicode. There’s the geometric shape area, CJK, dingbats, lots of others
Different fonts - there are lots of different monospace fonts. I even tried non-monospaced fonts tho still drawn in grid
ANSI color style https://16colo.rs/
My results weren’t nearly as good as the ones in this article but just suggesting more ways of exploration
https://greggman.github.io/doodles/textme10.html
Note: options are buried in the menu. Best to pick a scene other than the default
Wait...wh...why?!? Of all the things, actual pictures of the planet Saturn are readily available in the public domain. Why poison the internet with fake images of it?
Are we sure the planets are real?
> Wait...wh...why?!?
It has just begun. Wait until nobody bothers using Wikipedia, websites, or even one day forums.
This is going to eat everything.
And when it's immediate to say something like, "I need a high contrast image of Saturn of dimensions X by Y, focus on Saturn, oblique angle" -- that's going to be magic.
We'll look at the internet and Google like we look at going to the library and grabbing an encyclopedia off the shelves.
The use of calculators didn't kill ingenuity, nor did the switch to the internet. Despite teachers protesting both.
Humans will always use the lowest friction thing, and we will never stop reaching for the stars.
But it's not happened yet
Anecdotally, I'm seeing a lot of "it looks like AI" comments on photos and videos now. That's the new "is it Photoshop?"
I'd hold off on judgment until we get population studies on this.
I haven't presented a measurement, just an expectation.
Why on earth would we ban it?
"Eschew flamebait. Avoid generic tangents."
The display mode is actually a hacked up 80x25 text mode. So in that specific narrow case, you have a display mode where text characters very much function as pixels.
Are you planning to release this as a library or a tool, or should we just take the relevant MIT licensed code from your website [4]?
[0] https://aleyan.com/projects/ascii-side-of-the-moon
[1] https://news.ycombinator.com/item?id=46421045
[2] https://en.wikipedia.org/wiki/Lunar_mare
No plans to build a library right now, but who knows. Feel free to grab what you need from the website's code!
If I were to build a library, I'd probably convert the shaders from WebGL 2 to WebGL 1 for better browser compatibility. Would also need to figure out a good API for the library.
One thing that a library would need to deal with is that the shape vector depends on the font family, so the user of the library would need to precompute the shape vectors with the input font family. The sampling circles, internal and external, would likely need to be positioned differently for different font families. It's not obvious to me how a user of the library would go about that. There'd probably need to be some tool for that (I have a script to generate the shape vectors with a hardcoded link to a font in the website repository).
Lucas Pope did a really nice write up on how he developed his dithering system for Return of The Obra Dinn. Recommended if you also enjoyed this blog post.
https://forums.tigsource.com/index.php?topic=40832.msg136374...
However, there might still be room for competition, heh. I always wanted to do this on the _entirety_ of Unicode to try getting the most possible resolution out of the image.
I found myself thinking, “I wonder if some of this could be used to playback video on old 8-bit machines?” But they’re so underpowered…
Using only ASCII felt more in the "spirit" of the post and reduced scope (which is always good)
nolen: "unicode braille characters are 2x4 rectangles of dots that can be individually set. That's 8x the pixels you normally get in the terminal! anyway here's a proof of concept terminal SVG renderer using unicode braille", https://x.com/itseieio/status/2011101813647556902
ashfn: "@itseieio You can use 'persistence of vision' to individually address each of the 8 dots with their own color if you want, there's some messy code of an example here", https://x.com/ashfncom/status/2011135962970218736
I'm hoping people who harness ASCII for stuff like this consider using Code Page 437, or similar. Extended ASCII sets comprising Foreign Chars are for staid business machines, and sort of familiar but out of place accented chars have a bit of a distracting quality.
437 and so on taps the nostalgia for BBS Art, DOS, TUIs scene NFOs, 8 bit micros.... Everything pre Code Page 1252, in other words. Whilst it was a pragmatic decision for MS, it's also true that marketing needs demanded all text interfaces disappeared because they looked old. Text graphics, doubly so. That design space was now reserved for functional icons. A bit of creativity went from (home) computing right there and then. Stuffing it all into a separate font ensured it died.
But, that stuff is genuinely cool to a lot of people in a way VIM, (for example) has never been and nor will it ever. This is a case of Form Over Function. Foreign chars are not as friendly or fun as hearts, building blocks, smileys, musical notes, etc.
(I've previously tried pre-transforming on the image side to do color contrast enhancement, but without success: I take the Sobel filter of an image, and use it to identify regions where I boost contrast. However, since this is a step preceding "rasterization", the results don't align well with character grids.)
in general, ascii rendering is when ascii character codes are converted to pixels. if you wish to render other pixels onto a screen using characters, they are not ascii characters, they are roman or latin character glyphs, no ascii involved. that is all.
- His breaking up images into grids was a poor-man's convolution. Render each letter. Render the image. Dot product.
- His "contrast" setting didn't really work. It was meant to emulate a sharpen filter. Convolve with a kernel appropriate for letter size. He operated over the wrong dimensions (intensity, rather than X-Y)
- Dithering should be done with something like Floyd-Steinberg: You spill over errors to adjacent pixels.
Most of these problems have solutions, and in some cases, optimal ones. They were reinvented, perhaps cleverly, but not as well as those standard solutions.
Bonus:
- Handle above as a global optimization problem. Possible with 2026-era CPUs (and even more-so, GPUs).
- Unicode :)
I am actually really curious how performant this is and whether something like this would be able to contribute beyond just demo displays. It's obviously beautiful and a marvel of work, but it seems like there should be a way to use it for more.
Also, I did find myself wondering about the inevitable Doom engine
Really nice job!
BTW, aalib was using character shape back in the 90s. This is very cool but there is prior art!
Thanks for erasing all the content once the page loads, saved me the time I would have spent reading the article.
There really needs to be a name for error handling that is worse than the initial error.
Was there something wrong with using an actual image of saturn? NASA lets you use their images for stuff if you want https://www.nasa.gov/nasa-brand-center/images-and-media/, and if you're worried that might change down the line, you could just add a little attribution thing for NASA
Maybe it's just me, but I'd prefer a real image rather than something generated by the plagiarism machine that almost certainly took in that exact image as part of its training data
A similar technique could probably be used here.
nathaah3•2w ago