62
\$\begingroup\$

So I think we've all probably seen this xkcd comic:

http://imgs.xkcd.com/comics/self_description.png:

This might either be too general or too difficult, I'm not sure. But the challenge is to create a program in any language that creates a window that has at least 2 colors and displays in English words what percentage of the screen is each color.

ex. The simplest solution would be a white background with black letters that read "Percentage of this image that is black: [x]%. Percentage of this image that is white: [y]%"

You can go as crazy or as simple as you want; plain text is a valid solution but if you make interesting images like in the xkcd comic that's even better! The winner will be the most fun and creative solution that gets the most votes. So go forth and make something fun and worthy of xkcd! :)

So, what do you think? Sound like a fun challenge? :)

Please include a screenshot of your program running in your answer :)

\$\endgroup\$
11
  • 6
    \$\begingroup\$ A "this program has 64 A's, 4 B's, ... and 34 double quotes in the source code" program would be more interesting :-) \$\endgroup\$ Commented Jul 11, 2013 at 17:46
  • 2
    \$\begingroup\$ OK... what are the objective winning criteria? How do you determine if any specific output is valid? Is it sufficient that it is true and it describes a property of itself numerically? \$\endgroup\$ Commented Jul 11, 2013 at 17:48
  • \$\begingroup\$ @JanDvorak Oh, that's a good one! The alphabet program is actually what made me think of this originally, but I didn't consider adding the source code element to it! You should post that as a question :) Yes, it is sufficient that it is true and describes itself. Hmm, you're right though, I didn't think about how I would prove that the final results were correct. I'll need a way to count all the pixels of each color in a result image, I suppose. I'll go investigate that now. (Sorry my first question had problems... I tried but I'm new at this! Thank you :)) \$\endgroup\$
    – WendiKidd
    Commented Jul 11, 2013 at 19:14
  • \$\begingroup\$ if truthiness and self-reference are the sufficient criteria, here's my golfscript contestant: "/.*/" (read: [the source code] doesn't contain a newline) \$\endgroup\$ Commented Jul 11, 2013 at 19:23
  • \$\begingroup\$ @JanDvorak Hmm, I tried your code here and the output was the same as the code except without the quotes. Maybe I'm not explaining this right, sorry. There must be at least 2 colors generated, and in some form of an English sentence the output must generate true words that describe what percentage of the screen each of the colors occupies. Maybe this was a silly idea. I thought it would be fun but it might not work in practice :) \$\endgroup\$
    – WendiKidd
    Commented Jul 11, 2013 at 19:34

9 Answers 9

39
\$\begingroup\$

JavaScript with HTML

I tried to reproduce the original comic more precisely. A screenshot is taken using the html2canvas library. The numbers are calculated repeatedly, so you can resize the window or even add something to page in real time.

Try it online: http://copy.sh/xkcd-688.html

Here's a screenshot:

enter image description here

<html contenteditable>
<script src=http://html2canvas.hertzen.com/build/html2canvas.js></script>
<script>
onload = function() {
    setInterval(k, 750);
    k();
}
function k() {
    html2canvas(document.body, { onrendered: t });
}
function t(c) {
    z.getContext("2d").drawImage(c, 0, 0, 300, 150);
    c = c.getContext("2d").getImageData(0, 0, c.width, c.height).data;

    for(i = y = 0; i < c.length;) 
        y += c[i++];

    y /= c.length * 255;

    x.textContent = (y * 100).toFixed(6) + "% of this website is white";

    q = g.getContext("2d");

    q.fillStyle = "#eee";
    q.beginPath();
    q.moveTo(75, 75);
    q.arc(75,75,75,0,7,false);
    q.lineTo(75,75);
    q.fill();

    q.fillStyle = "#000";
    q.beginPath();
    q.moveTo(75, 75);
    q.arc(75,75,75,0,6.28319*(1-y),false);
    q.lineTo(75,75);
    q.fill();
}
</script>
<center>
<h2 id=x></h2>
<hr>
<table><tr>
<td>Fraction of<br>this website<br>which is white _/
<td><canvas width=150 id=g></canvas>
<td>&nbsp; Fraction of<br>- this website<br>&nbsp; which is black
</table>
<hr>
0
<canvas style="border-width: 0 0 1px 1px; border-style: solid" id=z></canvas>
<h4>Location of coloured pixels in this website</h4>
\$\endgroup\$
4
  • \$\begingroup\$ Nice!! Love the similarities to the xkcd comic, and the fact that I can change the text. Neat! : D \$\endgroup\$
    – WendiKidd
    Commented Jul 12, 2013 at 14:46
  • 1
    \$\begingroup\$ impressive work o.O \$\endgroup\$
    – izabera
    Commented Mar 4, 2014 at 21:44
  • \$\begingroup\$ Nifty... but I think it has to stabilize to be a "solution". Haven't thought through it entirely--but as there isn't necessarily a solution for arbitrary precision when drawing from a limited set of digit glyphs, you'll have to back off precision if it can't be solved at the higher precision you're trying. I imagine that using a monospace font that you pre-compute the black/white pixels will be necessary as well. \$\endgroup\$ Commented Jun 15, 2014 at 17:48
  • 1
    \$\begingroup\$ You are using 3 colors, so where are the percentages for grey? ;) \$\endgroup\$
    – M L
    Commented Mar 13, 2016 at 5:58
37
\$\begingroup\$

Elm

Haven't seen anyone use this loophole yet: demo

import Color exposing (hsl)
import Graphics.Element exposing (..)
import Mouse
import Text
import Window

msg a = centered <| Text.color a (Text.fromString "half the screen is this color")

type Pos = Upper | Lower

screen (w,h) (x,y) = 
  let (dx,dy) = (toFloat x - toFloat w / 2, toFloat h / 2 - toFloat y)
      ang = hsl (atan2 dy dx) 0.7 0.5
      ang' = hsl (atan2 dx dy) 0.7 0.5
      box c = case c of
        Upper -> container w (h // 2) middle (msg ang) |> color ang'
        Lower -> container w (h // 2) middle (msg ang') |> color ang
  in  flow down [box Upper, box Lower]

main = Signal.map2 screen Window.dimensions Mouse.position

enter image description here

\$\endgroup\$
10
  • 3
    \$\begingroup\$ Great loophole! \$\endgroup\$
    – Timtech
    Commented Mar 21, 2014 at 15:05
  • \$\begingroup\$ I love this!!! At least for now, you get the checkmark for sheer clever points. Love it! \$\endgroup\$
    – WendiKidd
    Commented Mar 22, 2014 at 4:03
  • 15
    \$\begingroup\$ The best part is, I'm still not sure which sentence is talking about which color. \$\endgroup\$
    – Brilliand
    Commented Jun 9, 2014 at 19:08
  • 3
    \$\begingroup\$ The view source is put there by share-elm.com and is not part of the compiled JS/HTML. \$\endgroup\$ Commented Jun 18, 2014 at 14:56
  • 1
    \$\begingroup\$ @ML That depends on the scope of the word "this". JavaScript programmers understand... \$\endgroup\$ Commented Mar 14, 2016 at 13:59
26
\$\begingroup\$

Processing, 222 characters

https://i.sstatic.net/tcj1E.png

I've always wanted to make my own version of that comic strip! The simplest (only?) way I could think of doing this was trial and error - draw something, count, draw again...

This program settles for an accurate percentage after a few seconds. It's not very pretty, but it's interactive; you can resize the window and it will start to recalculate.

Added some newlines for readability:

float s,S,n;
int i;
void draw(){
frame.setResizable(true);
background(255);
fill(s=i=0);
text(String.format("%.2f%% of this is white",S/++n*100),10,10);
loadPixels();
while(i<width*height)if(pixels[i++]==-1)s++;
S+=s/height/width;
}

It only shows percentage of white pixels; Because of antialiasing of the text, non-white pixels are not necessarily black. The longer it is running the more time it will need to update itself on a resize.

Edit:

So, it's a code-challenge; I sort of golfed it anyways. Maybe I could add some sort of graphs later, but the general principle would remain the same. The interactiveness is the neat part I think.

\$\endgroup\$
5
  • \$\begingroup\$ Very nice!! I think you get extra credit for the interactivity; I had fun resizing the window! Very cool :) And you're my first ever response! I didn't know if anyone would want to play, so thanks. You've made my day. : D +1! (I'm curious though, why does it slow down as time goes on and it gets closer to reaching the correct percentage? I'm just curious as to what's happening, I've never seen this language before. I'm seeing a lot of new stuff poking around this site!) \$\endgroup\$
    – WendiKidd
    Commented Jul 11, 2013 at 22:27
  • \$\begingroup\$ headdesk Except I accidentally forgot to click the +1. Now +1...haha. Sorry! \$\endgroup\$
    – WendiKidd
    Commented Jul 11, 2013 at 22:46
  • 1
    \$\begingroup\$ You could add another function that allows users to draw on it with the mouse, for added interactivity. \$\endgroup\$ Commented Jul 12, 2013 at 12:53
  • 7
    \$\begingroup\$ Holy box shadow, Batman \$\endgroup\$
    – Bojangles
    Commented Jul 13, 2013 at 6:33
  • \$\begingroup\$ If you want to golf, you can use background(-1) instead of background(255) \$\endgroup\$
    – user41805
    Commented Apr 23, 2017 at 15:45
21
\$\begingroup\$

Great challenge. Here's my solution. I tried to get as close as possible to the original comic, I even used the xkcd font.

It's a WPF application, but I used System.Drawing to do the drawing parts because I'm lazy.

Basic concept: In WPF, windows are Visuals, which means they can be rendered. I render the entire Window instance onto a bitmap, count up the black and total black or white (ignoring the grays in the font smoothing and stuff) and also count these up for each 3rd of the image (for each panel). Then I do it again on a timer. It reaches equilibrium within a second or two.

Download:

MEGA Always check files you download for viruses, etc, etc.

You'll need to install the font above to your system if you want to see it, otherwise it's the WPF default one.

XAML:

<Window
 x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="xkcd: 688" Height="300" Width="1000" WindowStyle="ToolWindow">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.3*"/>
            <ColumnDefinition Width="0.3*"/>
            <ColumnDefinition Width="0.3*"/>
        </Grid.ColumnDefinitions>

        <Border BorderBrush="Black" x:Name="bFirstPanel" BorderThickness="3" Padding="10px" Margin="0 0 10px 0">
            <Grid>
                <Label FontSize="18" FontFamily="xkcd" VerticalAlignment="Top">Fraction of this window that is white</Label>
                <Label FontSize="18" FontFamily="xkcd" VerticalAlignment="Bottom">Fraction of this window that is black</Label>
                <Image x:Name="imgFirstPanel"></Image>
            </Grid>
        </Border>
        <Border Grid.Column="1" x:Name="bSecondPanel" BorderBrush="Black" BorderThickness="3" Padding="10px" Margin="10px 0">
            <Grid>
                <TextBlock FontSize="18" FontFamily="xkcd" VerticalAlignment="Top" HorizontalAlignment="Left">Amount of <LineBreak></LineBreak>black ink <LineBreak></LineBreak>by panel:</TextBlock>
                <Image x:Name="imgSecondPanel"></Image>
            </Grid>
        </Border>
        <Border Grid.Column="2" x:Name="bThirdPanel" BorderBrush="Black" BorderThickness="3" Padding="10px" Margin="10px 0 0 0">
            <Grid>
                <TextBlock FontSize="18" FontFamily="xkcd" VerticalAlignment="Top" HorizontalAlignment="Left">Location of <LineBreak></LineBreak>black ink <LineBreak></LineBreak>in this window:</TextBlock>
                <Image x:Name="imgThirdPanel"></Image>
            </Grid>
        </Border>

    </Grid>
</Window>

Code:

using System;
using System.Drawing;
using System.Timers;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Brushes = System.Drawing.Brushes;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        private Timer mainTimer = new Timer();
        public MainWindow()
        {
            InitializeComponent();

            Loaded += (o1,e1) =>
                          {
                              mainTimer = new Timer(1000/10);
                              mainTimer.Elapsed += (o, e) => {
                                  try
                                  {
                                      Dispatcher.Invoke(Refresh);
                                  } catch(Exception ex)
                                  {
                                      // Nope
                                  }
                              };
                              mainTimer.Start();
                          };
        }

        private void Refresh()
        {
            var actualh = this.RenderSize.Height;
            var actualw = this.RenderSize.Width;

            var renderTarget = new RenderTargetBitmap((int) actualw, (int) actualh, 96, 96, PixelFormats.Pbgra32);
            var sourceBrush = new VisualBrush(this);

            var visual = new DrawingVisual();
            var context = visual.RenderOpen();

            // Render the window onto the target bitmap
            using (context)
            {
                context.DrawRectangle(sourceBrush, null, new Rect(0,0, actualw, actualh));
            }
            renderTarget.Render(visual);

            // Create an array with all of the pixel data
            var stride = (int) actualw*4;
            var data = new byte[stride * (int)actualh];
            renderTarget.CopyPixels(data, stride, 0);

            var blackness = 0f;
            var total = 0f;

            var blacknessFirstPanel = 0f;
            var blacknessSecondPanel = 0f;
            var blacknessThirdPanel = 0f;
            var totalFirstPanel = 0f;
            var totalSecondPanel = 0f;
            var totalThirdPanel = 0f;

            // Count all of the things
            for (var i = 0; i < data.Length; i += 4)
            {
                var b = data[i];
                var g = data[i + 1];
                var r = data[i + 2];

                if (r == 0 && r == g && g == b)
                {
                    blackness += 1;
                    total += 1;

                    var x = i%(actualw*4) / 4;

                    if(x < actualw / 3f)
                    {
                        blacknessFirstPanel += 1;
                        totalFirstPanel += 1;
                    } else if (x < actualw * (2f / 3f))
                    {
                        blacknessSecondPanel += 1;
                        totalSecondPanel += 1;
                    }
                    else if (x < actualw)
                    {
                        blacknessThirdPanel += 1;
                        totalThirdPanel += 1;
                    }
                } else if (r == 255 && r == g && g == b)
                {
                    total += 1;

                    var x = i % (actualw * 4) / 4;

                    if (x < actualw / 3f)
                    {
                        totalFirstPanel += 1;
                    }
                    else if (x < actualw * (2f / 3f))
                    {
                        totalSecondPanel += 1;
                    }
                    else if (x < actualw)
                    {
                        totalThirdPanel += 1;
                    }
                }
            }

            var black = blackness/total;

            Redraw(black, blacknessFirstPanel, blacknessSecondPanel, blacknessThirdPanel, blackness, renderTarget);
        }

        private void Redraw(double black, double firstpanel, double secondpanel, double thirdpanel, double totalpanels, ImageSource window)
        {
            DrawPieChart(black);
            DrawBarChart(firstpanel, secondpanel, thirdpanel, totalpanels);
            DrawImage(window);
        }

        void DrawPieChart(double black)
        {
            var w = (float)bFirstPanel.ActualWidth;
            var h = (float)bFirstPanel.ActualHeight;
            var padding = 0.1f;

            var b = new Bitmap((int)w, (int)h);
            var g = Graphics.FromImage(b);

            var px = padding*w;
            var py = padding*h;

            var pw = w - (2*px);
            var ph = h - (2*py);

            g.DrawEllipse(Pens.Black, px,py,pw,ph);

            g.FillPie(Brushes.Black, px, py, pw, ph, 120, (float)black * 360);

            g.DrawLine(Pens.Black, 30f, h * 0.1f, w / 2 + w * 0.1f, h / 2 - h * 0.1f);
            g.DrawLine(Pens.Black, 30f, h - h * 0.1f, w / 2 - w * 0.2f, h / 2 + h * 0.2f);

            imgFirstPanel.Source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(b.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromWidthAndHeight(b.Width, b.Height));
        }

        void DrawBarChart(double b1, double b2, double b3, double btotal)
        {
            var w = (float)bFirstPanel.ActualWidth;
            var h = (float)bFirstPanel.ActualHeight;
            var padding = 0.1f;

            var b = new Bitmap((int)w, (int)h);
            var g = Graphics.FromImage(b);

            var px = padding * w;
            var py = padding * h;

            var pw = w - (2 * px);
            var ph = h - (2 * py);

            g.DrawLine(Pens.Black, px, py, px, ph+py);
            g.DrawLine(Pens.Black, px, py + ph, px+pw, py+ph);

            var fdrawbar = new Action<int, double>((number, value) =>
                {
                    var height = ph*(float) value/(float) btotal;
                    var width = pw/3f - 4f;

                    var x = px + (pw/3f)*(number-1);
                    var y = py + (ph - height);

                    g.FillRectangle(Brushes.Black, x, y, width, height);
                });

            fdrawbar(1, b1);
            fdrawbar(2, b2);
            fdrawbar(3, b3);

            imgSecondPanel.Source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(b.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromWidthAndHeight(b.Width, b.Height));
        }

        void DrawImage(ImageSource window)
        {
            imgThirdPanel.Source = window;
        }
    }
}

The code isn't cleaned up, but it should be somewhat readable, sorry.

\$\endgroup\$
1
  • 3
    \$\begingroup\$ A late entry, but one of the best. \$\endgroup\$
    – primo
    Commented Jun 14, 2014 at 4:10
14
\$\begingroup\$

C (with SDL and SDL_ttf): Grayscale solution

Here's a solution that takes advantage of the pie chart form to capture the complete spectrum of grayscale pixel colors, clocking in at just under 100 lines.

#include <stdio.h>
#include <string.h>
#include <math.h>
#include "SDL.h"
#include "SDL_ttf.h"

int main(void)
{
    SDL_Surface *screen, *buffer, *caption;
    SDL_Color pal[256];
    SDL_Rect rect;
    SDL_Event event;
    TTF_Font *font;
    int levels[256], plev[256];
    Uint8 *p;
    float g;
    int cr, redraw, hoffset, h, n, v, w, x, y;

    SDL_Init(SDL_INIT_VIDEO);
    TTF_Init();
    screen = SDL_SetVideoMode(640, 480, 0, SDL_ANYFORMAT | SDL_RESIZABLE);
    font = TTF_OpenFont(FONTPATH, 24);
    buffer = 0;
    for (;;) {
        if (!buffer) {
            buffer = SDL_CreateRGBSurface(SDL_SWSURFACE, screen->w, screen->h,
                                          8, 0, 0, 0, 0);
            for (n = 0 ; n < 256 ; ++n)
                pal[n].r = pal[n].g = pal[n].b = n;
            SDL_SetColors(buffer, pal, 0, 256);
        }
        memcpy(plev, levels, sizeof levels);
        memset(levels, 0, sizeof levels);
        SDL_LockSurface(buffer);
        p = buffer->pixels;
        for (h = 0 ; h < buffer->h ; ++h) {
            for (w = 0 ; w < buffer->w ; ++w)
                ++levels[p[w]];
            p += buffer->pitch;
        }
        for (n = 1 ; n < 256 ; ++n)
            levels[n] += levels[n - 1];
        redraw = memcmp(levels, plev, sizeof levels);
        if (redraw) {
            SDL_UnlockSurface(buffer);
            SDL_FillRect(buffer, NULL, 255);
            caption = TTF_RenderText_Shaded(font,
                        "Distribution of pixel color in this image",
                        pal[0], pal[255]);
            rect.x = (buffer->w - caption->w) / 2;
            rect.y = 4;
            hoffset = caption->h + 4;
            SDL_BlitSurface(caption, NULL, buffer, &rect);
            SDL_FreeSurface(caption);
            SDL_LockSurface(buffer);
            cr = buffer->h - hoffset;
            cr = (cr < buffer->w ? cr : buffer->w) / 2 - 4;
            p = buffer->pixels;
            for (h = 0 ; h < buffer->h ; ++h) {
                y = h - (screen->h + hoffset) / 2;
                for (w = 0 ; w < buffer->w ; ++w) {
                    x = w - buffer->w / 2;
                    g = sqrtf(x * x + y * y);
                    if (g < cr - 1) {
                        g = atanf((float)y / (x + g));
                        v = levels[255] * (g / M_PI + 0.5);
                        for (n = 0 ; n < 255 && levels[n] < v ; ++n) ;
                        p[w] = n;
                    } else if (g < cr + 1) {
                        p[w] = (int)(128.0 * fabs(g - cr));
                    }
                }
                p += buffer->pitch;
            }
        }
        SDL_UnlockSurface(buffer);
        SDL_BlitSurface(buffer, NULL, screen, NULL);
        SDL_UpdateRect(screen, 0, 0, 0, 0);
        if (redraw ? SDL_PollEvent(&event) : SDL_WaitEvent(&event)) {
            if (event.type == SDL_QUIT)
                break;
            if (event.type == SDL_VIDEORESIZE) {
                SDL_SetVideoMode(event.resize.w, event.resize.h, 0,
                                 SDL_ANYFORMAT | SDL_RESIZABLE);
                SDL_FreeSurface(buffer);
                buffer = 0;
            }
        }
    }
    SDL_Quit();
    TTF_Quit();
    return 0;
}

As with my previous solution, the path to the font file needs to be either hardcoded in the source or added to the build command, e.g.:

gcc -Wall -o xkcdgolf `sdl-config --cflags`
    -DFONTPATH=`fc-match --format='"%{file}"' :bold`
    xkcdgolf.c -lSDL_ttf `sdl-config --libs` -lm

The output of the program looks like this:

Pie chart showing full grayscale pixel color distribution

This one is fun to watch, because all the math slows down the redraws to where you can see the program zero in on the stable solution. The first estimate is wildly off (since the surface starts out all-black), and then shrinks down to the final size after about a dozen or so iterations.

The code works by taking a population count of each pixel color in the current image. If this population count doesn't match the last one, then it redraws the image. The code iterates over every pixel, but it transforms the x,y coordinates into polar coordinates, computing first the radius (using the center of the image as the origin). If the radius is within the pie chart area, it then computes the theta. The theta is easily scaled to the population counts, which determines the pixel color. On the other hand, if the radius is right on the border of the pie chart, then an anti-aliased value is computed to draw the circle around the outside of the chart. Polar coordinates make everything easy!

\$\endgroup\$
3
  • \$\begingroup\$ You're mostly using the float versions of math-library functions, but then shouldn't fabs be fabsf? \$\endgroup\$ Commented Jul 22, 2013 at 2:08
  • \$\begingroup\$ Technically, perhaps, but fabs() is more portable. \$\endgroup\$
    – breadbox
    Commented Jul 22, 2013 at 2:57
  • \$\begingroup\$ True, I've had trouble with that one not being defined in headers even when present in the library. Also there's less performance to be gained than with the transcendentals. :) \$\endgroup\$ Commented Jul 22, 2013 at 3:16
10
\$\begingroup\$

C (with SDL and SDL_ttf)

Here's a very simple implementation, in about 60 lines of C code:

#include <stdio.h>
#include "SDL.h"
#include "SDL_ttf.h"

int main(void)
{
    char buf[64];
    SDL_Surface *screen, *text;
    SDL_Rect rect;
    SDL_Color black;
    SDL_Event event;
    TTF_Font *font;
    Uint32 blackval, *p;
    int size, b, prevb, h, i;

    SDL_Init(SDL_INIT_VIDEO);
    TTF_Init();
    screen = SDL_SetVideoMode(640, 480, 32, SDL_ANYFORMAT | SDL_RESIZABLE);
    font = TTF_OpenFont(FONTPATH, 32);
    black.r = black.g = black.b = 0;
    blackval = SDL_MapRGB(screen->format, 0, 0, 0);

    b = -1;
    for (;;) {
        prevb = b;
        b = 0;
        SDL_LockSurface(screen);
        p = screen->pixels;
        for (h = screen->h ; h ; --h) {
            for (i = 0 ; i < screen->w ; ++i)
                b += p[i] == blackval;
            p = (Uint32*)((Uint8*)p + screen->pitch);
        }
        SDL_UnlockSurface(screen);
        size = screen->w * screen->h;
        SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format, 255, 255, 255));
        sprintf(buf, "This image is %.2f%% black pixels", (100.0 * b) / size);
        text = TTF_RenderText_Solid(font, buf, black);
        rect.x = (screen->w - text->w) / 2;
        rect.y = screen->h / 2 - text->h;
        SDL_BlitSurface(text, NULL, screen, &rect);
        SDL_FreeSurface(text);
        sprintf(buf, "and %.2f%% white pixels.", (100.0 * (size - b)) / size);
        text = TTF_RenderText_Solid(font, buf, black);
        rect.x = (screen->w - text->w) / 2;
        rect.y = screen->h / 2;
        SDL_BlitSurface(text, NULL, screen, &rect);
        SDL_FreeSurface(text);
        SDL_UpdateRect(screen, 0, 0, 0, 0);
        if (b == prevb ? SDL_WaitEvent(&event) : SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT)
                break;
            if (event.type == SDL_VIDEORESIZE)
                SDL_SetVideoMode(event.resize.w, event.resize.h, 32,
                                 SDL_ANYFORMAT | SDL_RESIZABLE);
        }
    }

    TTF_Quit();
    SDL_Quit();
    return 0;
}

To compile this, you need to define FONTPATH to point to a .ttf file of the font to use:

gcc -Wall -o xkcdgolf `sdl-config --cflags`
    -DFONTPATH='"/usr/share/fonts/truetype/freefont/FreeSansBold.ttf"'
    xkcdgolf.c -lSDL_ttf `sdl-config --libs`

On most modern Linux machines you can use the fc-match utility to look up font locations, so the compile command becomes:

gcc -Wall -o xkcdgolf `sdl-config --cflags`
    -DFONTPATH=`fc-match --format='"%{file}"' :bold`
    xkcdgolf.c -lSDL_ttf `sdl-config --libs`

(Of course you can replace the requested font with your personal favorite.)

The code specifically requests no anti-aliasing, so that the window contains only black and white pixels.

Finally, I was inspired by @daniero's elegant solution to permit window resizing. You'll see that sometimes the program oscillates between counts, stuck in an orbit around an attractor it can never reach. When that happens, just resize the window a bit until it stops.

And, per request, here's what it looks like when I run it on my system:

This image is 3.36% black pixels and 96.64% white pixels.

Finally, I feel that I should point out, in case anyone here hasn't already seen it, that the MAA published an interview with Randall Munroe in which he discusses the making of cartoon #688 in some detail.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Very nice solution. Could you possibly put in some screenshots of the program running, following off of @daniero's post? :) \$\endgroup\$ Commented Jul 12, 2013 at 14:36
  • \$\begingroup\$ +1, very nice! Thanks for adding the screenshot :) And the interview link is interesting, thanks! \$\endgroup\$
    – WendiKidd
    Commented Jul 12, 2013 at 21:24
9
\$\begingroup\$

enter image description here

The image is 100x100 and the numbers are exact, and I do mean exact - I chose a 10000 pixel image so that the percentages could be expressed with two decimal places. The method was a bit of math, a bit of guessing, and some number crunching in Python.

Seeing as I knew in advance that the percentages could be expressed in 4 digits, I counted how many black pixels were in each of the digits 0 through 9, in 8 pixel high Arial, which is what the text is written in. I wrote a quick function weight which tells you how many pixels are needed to write a given number, left padded with zeros to have 4 digits:

def weight(x):
    total = 4 * px[0]
    while x > 0:
       total = total - px[0] + px[x % 10]
       x = x / 10
    return total

px is an array mapping digits to number of required pixels. If B is the number of black pixels, and W is the number of white pixels, we have B + W = 10000, and we need:

B = 423 + weight(B) + weight(W)
W = 9577 - weight(B) - weight(W)

Where did the constants come from? 423 is the "initial" number of black pixels, the number of black pixels in the text without the numbers. 9577 is the number of initial white pixels. I had to adjust the amount of initial black pixels several times before I managed to get constants such that the above system even has a solution. This was done by guessing and crossing my fingers.

The above system is horribly non-linear, so obviously you can forget about solving it symbolically, but what you can do is just loop through every value of B, set W = 10000 - B, and check the equations explicitly.

>>> for b in range(10000 + 1):
...     if b == weight(b) + weight(10000 - b)+423: print b;
...
562
564
\$\endgroup\$
2
  • \$\begingroup\$ Maybe do a 250 x 400 image so you can get it to 3 decimal places and display more text in the meantime. \$\endgroup\$
    – Joe Z.
    Commented Mar 21, 2014 at 18:31
  • \$\begingroup\$ Very nice solution, some brute force math can always solve this kind of problems! \$\endgroup\$
    – CCP
    Commented Mar 22, 2014 at 16:59
9
\$\begingroup\$

QBasic

Because nostalgia.

And because I don't really know any image libraries in modern languages.

SCREEN 9

CONST screenWidth = 640
CONST screenHeight = 350
CONST totalPixels# = screenWidth * screenHeight

accuracy = 6

newWhite# = 0
newGreen# = 0
newBlack# = totalPixels#

DO
    CLS
    white# = newWhite#
    green# = newGreen#
    black# = newBlack#

    ' Change the precision of the percentages every once in a while
    ' This helps in finding values that converge
    IF RND < .1 THEN accuracy = INT(RND * 4) + 2
    format$ = "###." + LEFT$("######", accuracy) + "%"

    ' Display text
    LOCATE 1
    PRINT "Percentage of the screen which is white:";
    PRINT USING format$; pct(white#)
    LOCATE 4
    PRINT white#; "/"; totalPixels#; "pixels"
    LOCATE 7
    PRINT "Percentage of the screen which is black:";
    PRINT USING format$; pct(black#)
    LOCATE 10
    PRINT black#; "/"; totalPixels#; "pixels"
    LOCATE 13
    PRINT "Percentage of the screen which is green:";
    PRINT USING format$; pct(green#)
    LOCATE 16
    PRINT green#; "/"; totalPixels#; "pixels"

    ' Display bar graphs
    LINE (0, 16)-(pct(white#) / 100 * screenWidth, 36), 2, BF
    LINE (0, 100)-(pct(black#) / 100 * screenWidth, 120), 2, BF
    LINE (0, 184)-(pct(green#) / 100 * screenWidth, 204), 2, BF

    newBlack# = pixels#(0)
    newGreen# = pixels#(2)
    newWhite# = pixels#(15)
LOOP UNTIL black# = newBlack# AND white# = newWhite# AND green# = newGreen#

' Wait for user keypress before ending program: otherwise the "Press any
' key to continue" message would instantly make the results incorrect!
x$ = INPUT$(1)


FUNCTION pixels# (colr)
' Counts how many pixels of the given color are on the screen

pixels# = 0

FOR i = 0 TO screenWidth - 1
    FOR j = 0 TO screenHeight - 1
        IF POINT(i, j) = colr THEN pixels# = pixels# + 1
    NEXT j
NEXT i

END FUNCTION

FUNCTION pct (numPixels#)
' Returns percentage, given a number of pixels

pct = numPixels# / totalPixels# * 100

END FUNCTION

Pretty straightforward output-count-repeat method. The main "interesting" thing is that the program randomly tries different precisions for the percentages--I found that it didn't always converge otherwise.

And the output (tested on QB64):

QBasic metagraph

\$\endgroup\$
3
\$\begingroup\$

AWK

... with netpbm and other helpers

The 'x' file:

BEGIN {
        FS=""
        n++
        while(n!=m) {
                c="printf '%s\n' '"m"% black pixels'"
                c=c" '"100-m"% white pixels'"
                c=c" | pbmtext -space 1 -lspace 1 | pnmtoplainpnm | tee x.pbm"
                n=m
                delete P
                nr=0
                while(c|getline==1) if(++nr>2) for(i=1;i<=NF;i++) P[$i]++
                close(c)
                m=100*P[1]/(P[0]+P[1])
                print m"%"
        }
}

The run:

$ awk -f x
4.44242%
5.2424%
5.04953%
5.42649%
5.27746%
5.1635%
5.15473%
5.20733%
5.20733%

The picture is written as 'x.pbm', I converted it to png for uploading:

x.png

\$\endgroup\$

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