How about graphics?
Start out simple with requiring them to do things like "make a bitmask that starts at bit 7 and ends at bit 12".
Use a single-bit image format. Display the results on the screen in black and white.
Now have 3 bitmasks, one for each of RGB. The library takes the 3 bitmasks and combines them to display it on the screen.
Now they have to find the mask for the combined colors -- the ones that have RG (yellow), GB (aqua), RB (fuchsia), and RGB (white) turned on.
Now we change the compositing rule. Things that have any two channels produce the third. Things that have one channel twice produce black.
You can keep doing more stuff. Shift one the channels 1, or X, pixels left or right. Stretch one of the channels by a factor of 2, 3, an integer. Stretch a channel by a rational factor.
Apply a texture; given a mask-channel, apply it to a set of other channels that represent a color texture you want to see in that mask-channel.
Punch through. Given a mask-channel, produce a hole in a set of other channels.
Horizontal squeeze by 2, 4, 8 16; given a scanline, count how many bits are in each run. Library displays the result (with anti-aliasing!)
Vertical squeeze. Shuffle two scanlines together so you can feed it to horizontal squeeze and get a vertical one.
Zoom out. Do a 4x vertical and 4x horizontal squeeze to get a 4x zoom out.
This can go even further; the occlusion based painting algorithm uses bit operations in place of the usual alpha calculations. (a 4x4 mask can be represented as a 16 bit value; instead of (both premultiplied color) A' = A0*(1-A1) + A1
and C' = C0*(1-A1) + C1
we get A' = A1|A0
and C' = C1 + BitCount(A0&~A1)*C0/16
, for example.)
But the point is, you can (with a library) turn their bit operation work into visible graphics effects, with the students not having to know anything about how the graphics part works.
If you write the library well, they can also get to play with fun and strange visual effects.
using mutable_scanline = std::span<uint32_t>;
using scanline = std::span<const uint32_t>;
ScanlineDrawing create_drawing();
// 1 bit per pixel
void ScanlineDrawing::draw_bw( scanline );
// 1 bit per channel, 3 bits per pixel
void ScanlineDrawing::draw_rgb3( scanline );
// values only go up to 2^(bits_per_pixel-1).
void ScanlineDrawing::draw_greyscale( scanline, int bits_per_pixel );
void ScanlineDrawing::draw_rgb_x( scanline, int bits_per_pixel );
void ScanlineDrawing::display();
there is our basic library.
We also get a test harness that provides you with scanlines to process.
Functions to write:
void fill( mutable_scanline, int start, int length );
// take 3 bw channels and make a single rgb3 one.
std::vector<uint32_t> make_rgb3( scanline, scanline, scanline );
// Overwrites [start, start+length) bits in dst with bits from src.
// all bits must be within dst's range, or no guarantees
void blit( scanline src, mutable_scanline dst, int start, int length );
// moves values amount right (or left if negative).
// Fills blanks with 0s.
void shift( mutable_scanline, int amount );
// moves values amount right (or left if negative). Stuff falling off
// edge shows up on other side
void rotate( mutable_scanline, int amount );
// scale the scanline down. Tail should be 0s
void scale_by_2( mutable_scanline );
// scale the scanline down. Tail should be 0s
void scale_by_4( mutable_scanline );
// scale the scanline down. Tail should be 0s
void scale_by_16( mutable_scanline );
// take any number of channels, and interlace them 1 bit at a time
std::vector<uint32_t> interlace( std::span<const scanline> );
// take any number of channels, and interlace them width bits at a time
std::vector<uint32_t> interlace( std::span<const scanline>, int width );
build some test harnesses to use the above and they'll be able to do most of what I described above.
Calls fill
(which students write)
void make_square( std::span<mutable_scanline>, int x, int y, int size );
Calls shift or rotate:
void do_pan( std::span<mutable_scanline>, int dx, int dy, bool bWrap=false );
Copies src multiple times over dst -- calls blit and shift.
void tile( std::span<scanline const> src, std::span<mutable_scanline> dst, int offsetx, int offsety );
Instead, calls blit and rotate:
void tile2( std::span<scanline const> src, std::span<mutable_scanline> dst, int offsetx, int offsety );
// zooms by a factor of 2 or 4. Uses interlace(scan, width)
and scale_by_4
or scale_by_16
.
void zoom_x2( std::span<mutable_scanline> )
void zoom_x4( std::span<mutable_scanline> )
Here we have relatively simple tasks in a real framework that does real things on the real screen.
You expose the new programmer to "real programs" they can read over, and mimic the style of.
You can have bonus assignments like "make a pretty drawing using a custom-written function".
You can convert real pictures into this format; the anti-aliasing zoom trick lets you get up to 4 bits of color in each channel, which is enough to have a real color picture show up. The bit-operations applied to 15 bits per pixel channels still works, you just have to pan by multiples of 15 horizontally. So you could even get a successful program to generate a little animation. ;)