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.
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.857Maybe 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