xkcd challenge: "Percentage of the screen that is [x] color"

57

5

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 :)

WendiKidd

Posted 2013-07-11T17:28:00.183

Reputation: 711

6A "this program has 64 A's, 4 B's, ... and 34 double quotes in the source code" program would be more interesting :-) – John Dvorak – 2013-07-11T17:46:15.880

2OK... 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? – John Dvorak – 2013-07-11T17:48:00.497

@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 :)) – WendiKidd – 2013-07-11T19:14:46.760

if truthiness and self-reference are the sufficient criteria, here's my golfscript contestant: "/.*/" (read: [the source code] doesn't contain a newline) – John Dvorak – 2013-07-11T19:23:27.007

@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 :)

– WendiKidd – 2013-07-11T19:34:00.857

Maybe if you convert this to a popularity contest, you'll attract some more interesting answers. Code golf doesn't really promote creativity as far as output is concerned. – John Dvorak – 2013-07-11T19:35:58.983

@JanDvorak Okay, will do! That's what I was hoping for anyway but I thought the idea of the site was to encourage the shortest code. Changing now! – WendiKidd – 2013-07-11T19:38:03.300

popularity contest = votes decide for the winner. You can still specify some secondary criteria for the voters. code golf = the shortest code that satisfies the specified (objective) criteria ("working code") wins no matter how uncreative or unpopular it may be. – John Dvorak – 2013-07-11T19:40:56.097

there's a [tag:popularity-contest] tag that I was suggesting – John Dvorak – 2013-07-11T19:42:14.290

What kind of assumptions do we get to make about the screen? Does the code take up the entire screen? Do we get to pick the font? – Ry- – 2013-07-11T19:43:43.467

@minitech The screen can have anything you want on it (pictures, words, as many colors as you want (though more colors makes it harder)). There doesn't have to be any code on the screen at all, you can write code that creates a web page for all I care ;) It's all up to you, be as creative as you want! The screen, whatever form it takes, just has to be accurately self-descriptive of the colors, in English words that make sense :) For example, if you made a program that pixel-perfectly reproduced that xkcd strip, you'd have met the criteria. – WendiKidd – 2013-07-11T20:02:03.533

Answers

35

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

hoosierEE

Posted 2013-07-11T17:28:00.183

Reputation: 760

What about when the page height is an odd number of pixels? – FreeAsInBeer – 2015-08-03T14:58:19.860

Ah, that's a quick fix - "about half the screen is this color" – hoosierEE – 2015-08-03T15:17:24.507

The letters are antialiased, so the image is not using only 2 colors ;) – M L – 2016-03-13T05:54:39.923

1@ML That depends on the scope of the word "this". JavaScript programmers understand... – hoosierEE – 2016-03-14T13:59:24.927

demo link is dead – sergiol – 2017-04-22T10:42:46.523

3Great loophole! – Timtech – 2014-03-21T15:05:59.070

I love this!!! At least for now, you get the checkmark for sheer clever points. Love it! – WendiKidd – 2014-03-22T04:03:47.660

13The best part is, I'm still not sure which sentence is talking about which color. – Brilliand – 2014-06-09T19:08:14.720

It would be correct, if view source wasn't there. – daviewales – 2014-06-18T09:23:59.257

3The view source is put there by share-elm.com and is not part of the compiled JS/HTML. – hoosierEE – 2014-06-18T14:56:40.410

37

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>

copy

Posted 2013-07-11T17:28:00.183

Reputation: 6 466

You are using 3 colors, so where are the percentages for grey? ;) – M L – 2016-03-13T05:58:33.960

Nice!! Love the similarities to the xkcd comic, and the fact that I can change the text. Neat! : D – WendiKidd – 2013-07-12T14:46:39.647

1impressive work o.O – izabera – 2014-03-04T21:44:43.367

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. – HostileFork says dont trust SE – 2014-06-15T17:48:09.333

26

Processing, 222 characters

http://i.imgur.com/eQzMGzk.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.

daniero

Posted 2013-07-11T17:28:00.183

Reputation: 17 193

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!) – WendiKidd – 2013-07-11T22:27:28.000

headdesk Except I accidentally forgot to click the +1. Now +1...haha. Sorry! – WendiKidd – 2013-07-11T22:46:24.627

1You could add another function that allows users to draw on it with the mouse, for added interactivity. – AJMansfield – 2013-07-12T12:53:19.633

7Holy box shadow, Batman – Bojangles – 2013-07-13T06:33:30.447

If you want to golf, you can use background(-1) instead of background(255) – user41805 – 2017-04-23T15:45:34.577

20

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.

Kiran Price

Posted 2013-07-11T17:28:00.183

Reputation: 301

2A late entry, but one of the best. – primo – 2014-06-14T04:10:10.080

14

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!

breadbox

Posted 2013-07-11T17:28:00.183

Reputation: 6 893

You're mostly using the float versions of math-library functions, but then shouldn't fabs be fabsf? – luser droog – 2013-07-22T02:08:01.513

Technically, perhaps, but fabs() is more portable. – breadbox – 2013-07-22T02:57:18.513

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. :) – luser droog – 2013-07-22T03:16:47.000

10

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.

breadbox

Posted 2013-07-11T17:28:00.183

Reputation: 6 893

1Very nice solution. Could you possibly put in some screenshots of the program running, following off of @daniero's post? :) – Alex Brooks – 2013-07-12T14:36:15.120

+1, very nice! Thanks for adding the screenshot :) And the interview link is interesting, thanks! – WendiKidd – 2013-07-12T21:24:08.477

9

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

Jack M

Posted 2013-07-11T17:28:00.183

Reputation: 221

Maybe do a 250 x 400 image so you can get it to 3 decimal places and display more text in the meantime. – Joe Z. – 2014-03-21T18:31:22.907

Very nice solution, some brute force math can always solve this kind of problems! – CCP – 2014-03-22T16:59:06.830

6

QBasic

Because nostalgia.

And because I don't really know any image libraries is 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

DLosc

Posted 2013-07-11T17:28:00.183

Reputation: 21 213

3

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

user19214

Posted 2013-07-11T17:28:00.183

Reputation: