Skip to content

Tutorial: 'Console Screen' Colour

Hapaxia edited this page Mar 27, 2017 · 4 revisions

Console Screen Tutorial

Colour


Selba Ward Tutorials


Starting Point

We're going to need a basic program to use as a starting point. The following program is our starting point and has a Console Screen already prepared:

#include <SFML/Graphics.hpp>
#include <SelbaWard/ConsoleScreen.hpp>

int main()
{
	sf::Texture csTexture;
	if (!csTexture.loadFromFile("PATH/Windows Console ASCII.png"))
		return EXIT_FAILURE;

	sf::RenderWindow window(sf::VideoMode(800, 600), "Console Screen tutorial - Colour", sf::Style::Default);

	Cs cs;
	cs.setTexture(csTexture);
	cs.setTextureTileSize({ 8, 12 });
	cs.setNumberOfTextureTilesPerRow(16);
	cs.setMode({ 40, 22 });
	cs.setSize(cs.getPerfectSize());
	cs.setScale({ 2.f, 2.f });
	cs.setOrigin(cs.getSize() / 2.f);
	cs.setPosition(sf::Vector2f(window.getSize() / 2u));

	cs << "Hello" << Cs::CursorCommand::Newline << "World!";
	cs.clear();
	cs << "Welcome to the new world!" << Cs::CursorCommand::Newline;

	while (window.isOpen())
	{
		sf::Event event;
		while (window.pollEvent(event))
		{
			if (event.type == sf::Event::Closed)
				window.close();
		}

		window.clear(sf::Color(128, 128, 128));
		window.draw(cs);
		window.display();
	}

	return EXIT_SUCCESS;
}

This starting point is the final program from the Basics tutorial so you can continue from there (our new output will follow the previous output). Note that the window's title has been altered here to match the current tutorial.

Foreground

The foreground colour can be considered the colour of the text, assuming that our texture is a text font.

The colour is multiplied by the texture so text fonts are usually best being white although, technically, they can be any colour or even multi-coloured.

To change the foreground colour, we simply stream a Console Screen Color into the Console Screen object. The Color can be constructed from a palette index (number of the colour in the stored palette):
cs << Cs::Color(12);

To see the change in colour, we can output some more text:
cs << "Hello";

Since we can chain everything that is streamed, we can put the colour change and the text into one chain. In fact, we can even chain multiple colours and texts as well as a new-line cursor command so we change the above two outputs to:
cs << Cs::Color(11) << "Hello " << Cs::Color(12) << "World" << Cs::Color(9) << "!" << Cs::CursorCommand::Newline;

If you run the program now, you will see that below the white "Welcome to the new world!" from the starting point, there is now a sentence saying "Hello world!" in different colours: hello in magenta, world in green and the exclamation mark in blue.

Background

The background colour can be considered the rest of the cell that isn't foreground.

The background of a cell is a solid colour that is drawn below the foreground texture. Any transparencies in the foreground will allow the background to show through.

To change the background colour, we can change the ColorType:
cs << Cs::ColorType::Background;

A streamed Cs::Color changes the colour of the current destination, which can be the foreground or the background. The destination defaults to foreground. The destination can be changed using Cs::ColorType.

Now, any Color streamed will change the background instead of the foreground:
cs << Cs::Color(14) << "Yellow" << Cs::Color(0) << " and " << Cs::Color(13) << "Cyan" << Cs::Color(0) << Cs::CursorCommand::Newline;

On running the program, you will notice that the words "yellow" and "cyan" both have the backgrounds of their cells in those colours whereas the rest of the text has a black background. Notice that we changed the background back to black at the end so that the next output won't still have a blue background.

Of course, we can change both colours in the same stream by using ColorType:
cs << Cs::Color(12) << "blue on green" << Cs::CursorCommand::Newline << Cs::Color(15) << Cs::ColorType::Foreground << Cs::Color(10) << "red on white" << Cs::CursorCommand::Newline;

Following through the chain, you should be able to work out what that line of code does: the destination colour is still background from the previous section so Cs::Color(12) changes the background to green and the foreground colour is still blue from the previous line, it outputs "blue on green" in blue text on a green background and moves to a new line, since the destination is still background, Cs::Color(15) changes the background to white, Cs::ColorType::Foreground changes the destination to foreground and then Cs::Color(10) changes the foreground to red, it outputs "red on white" in red text on a white background and finally moves to a new line.

Color Pair

Instead of specifying foreground and background colours separately, we can specify them together using Cs::ColorPair:
cs << Cs::ColorPair(0, 14) << "Black on yellow" << Cs::CursorCommand::Newline;

A ColorPair takes two parameters: the foreground Color and the background Color.

Although ColorPair actually takes two Colors, the parameters for ColorPair can be palette IDs and the Colors will be constructed automatically from those.

Destination Shortcuts

There are shortcuts that change the colour of a specified destination colour directly without the need to change the current destination using ColorType.

To change the foreground directly, we pass a Color (or palette index) to Cs::Fg and to change the background directly, we pass a Color (or palette index) to Cs::Bg:
cs << Cs::ColorPair(9, 0) << "blue " << Cs::Fg(8) << "dark grey " << Cs::Fg(10) << "red " << Cs::Bg(7) << "light grey background" << Cs::CursorCommand::Newline;

As you can see, you can easily mix ColorPairs, Fgs and Bgs within the stream as required.

Palette

Colours in Console Screen are stored in a palette and then any colour changes used for the output just changes which colour in the palette is used.

The default palette (Console Screen has multiple palettes available) is loaded automatically and has 16 colours i.e. palette ID 0 - 15. The first 8 (0-7) are darker versions of the last 8 (8-15). The colours are:

  • 0 - black
  • 1 - dark blue
  • 2 - dark red
  • 3 - dark magenta
  • 4 - dark green
  • 5 - dark cyan
  • 6 - dark yellow
  • 7 - dark white (medium gray)
  • 8 - gray (light black/dark gray)
  • 9 - blue
  • 10 - red
  • 11 - magenta
  • 12 - green
  • 13 - cyan
  • 14 - yellow
  • 15 - white

The above colours are the values used in the earlier code to change the colours.

However, Selba Ward provides enums to use clearer colour selection.

First, we need to include the palette enum header:
#include <SelbaWard/PaletteEnums.hpp>

Then, we can use the enums to specify palette IDs when specifying colours. For example, we will repeat the previous output line using the enums:
cs << Cs::ColorPair(sw::Palette::Default::Blue, sw::Palette::Default::Black) << "blue " << Cs::Fg(sw::Palette::Default::Gray) << "grey " << Cs::Fg(sw::Palette::Default::Red) << "red " << Cs::Bg(sw::Palette::Default::DarkWhite) << "light grey background" << Cs::CursorCommand::Newline;
Although it is a lot longer, it is obviously also a lot clearer.

The namespace for the palette enums are "sw::Palette::" + the name of the palette + "::" + the name of the colour. Each palette has its own enum so you must use the correct enum for its palette. If the palette is changed, the enum may no longer represent the colour to which it seems.

Commands

A Color can also, instead of storing a palette ID, store a ColorCommand.

These commands represent which colour to use based on its opposite colour.

"Opposite" means the ColorType not representing this colour i.e. foreground's opposite is background and background's opposite is foreground.

Let's compare. First we set up basic yellow-on-blue colours and print "A":
cs << Cs::ColorPair(sw::Palette::Default::Yellow, sw::Palette::Default::Blue) << "A";

One possible value of ColorCommand is "Opposite".
A Color set to ColorCommand::Opposite will use the colour of its opposite directly.

Although adding a ColorCommand directly to the stream is not possible, the ColorCommand can be used to construct a Color which can be streamed directly (even containing a command). Since the current ColorType is Foreground, we can change the foreground to ColorCommand::Opposite and print "B":
cs << Cs::Color(s::ColorCommand::Opposite) << "B";

Here, the background isn't changed and the foreground is changed to match the background. This results in both being identical. Using our text texture makes the text invisible.

Next, we change both colours using ColorPair. The foreground is changed back to yellow but the background is now set to Opposite. We then print "C":
cs << Cs::ColorPair(sw::Palette::Default::Yellow, Cs::ColorCommand::Opposite) << "C";

This time, Opposite instructs the background to match the foreground so, again, the text should be invisible but this time yellow.

Lastly, we change the foreground to Opposite and print "D". This means that both background and foreground are Opposite:
cs << Cs::Fg(Cs::ColorCommand::Opposite) << "D" << Cs::CursorCommand::Newline;

If foreground and background are both set to a colour command, they both get shown as 0 (black in the default palette).

Another possible value of ColorCommand is "Invert". A Color set to Invert will take its opposite colour (as above), invert it (inverts R, G and B components) and then find the closest matching colour in the palette to the resulting inverted colour.

We set the background to blue and the foreground to Invert and print "E":
cs << Cs::ColorPair(Cs::ColorCommand::Invert, sw::Palette::Default::Blue) << "E";

When updating the screen, the foreground colour of any cells with a Color matching ColorCommand::Invert takes the background colour, which is blue here, and inverts it, which would be yellow in this case. It then finds the closest match in the palette and that is, of course, yellow. The cell then becomes updated as if the foreground colour is the palette ID that is the closest match; in this case, the foreground shows as yellow.

Cells continue to contain the colour commands until changed. The resulting colour is only calculated during an update. This means that if its opposite colour is changed, the command may result in a differing colour. By default, updates are perfomed automatically whenever a change is applied.

We then changed the background to red, print "F" and add a new line cursor command:
cs << Cs::Bg(sw::Palette::Default::Red) << "F" << Cs::CursorCommand::Newline;

This shows that how the Invert command is dependant on the colour of its opposite.

We can see this more fully if we output each of the palette colours as backgrounds while leaving the foreground to Invert. We output the palette ID in the foreground colour:

for (unsigned int i{ 0u }; i < cs.getPaletteSize(); ++i)
	cs << Cs::Bg(i) << std::to_string(i);
cs << Cs::CursorCommand::Newline;

Note that we've added a newline too.

Looking at the palette output, you can see that number 7 seems invisible. This is because the background colour is mid-grey and inverting mid-grey, you get... mid-grey!

One other possible value for ColorCommand is "Contrast". A Color set to Contrast will take its opposite colour (as above), choose black or white depending on which is furthest from that colour and then find the closest matching colour to that chosen black or white (if they aren't stored in the palette.

Again, we can see it in action by outputting the palette as background and using Contrast for the foreground.

First, we set the foreground colour to contrast:
cs << Cs::Fg(Cs::ColorCommand::Contrast);

Then, we output the palette as before:

for (unsigned int i{ 0u }; i < cs.getPaletteSize(); ++i)
	cs << Cs::Bg(i) << std::to_string(i);
cs << Cs::CursorCommand::Newline;

Running the program now, you can see that using Contrast for the foreground forces it to be black when the background is a brighter colour and white when the background is a darker colour.

Whether a contrasted colour should be black or white is determined by the "relative luminance" of its opposite colour.

Clearing The Screen

When clearing the screen, the empty, clear cells have their colours set to the currently set foreground and background colours. This means that you if you have blue foreground colour and yellow background colour, the entire cleared screen will now have those colours.

You can specify a different pair of colours to use instead of the current ones by passing a ColorPair to the clear command. This would clear the screen with cells that have a white background and a black foreground:
cs.clear(Cs::ColorPair(sw::Palette::Default::Black, sw::Palette::Default::White));

You can also specify just a Color to use as the screen's cells' background colour; the current foreground colour is still used for the new foreground. This clears the screen with a background colour of cyan (note that the foreground is still red from the previous output so the foreground of the cleared screen's cells is red:
cs.clear(Cs::Color(sw::Palette::Default::Cyan));

In addition, you can specify a ColorCommand to use as the background for the clear. This uses the current foreground colour along with the specified ColorCommand to determine the background colour with which to clear the screen.

It is even possible to provide a ColorPair that contains a ColorCommand for either the foreground or the background and it will clear the screen with those colour commands (the commands are stored in the colour for that cell):
cs.clear(Cs::ColorPair(Cs::ColorCommand::Invert, sw::Palette::Default::Red));

This will clear with a red background and the foreground colour of the cells being set to the Invert command. This would display as cyan on red.

Note that, although it is possible to clear with both colours being commands, it is not recommended as it will show as colour zero on colour zero.

Before running the program, make sure that you remove (or comment out) any clear commands that you added during this tutorial. Then, you can run the program and see the result - multiple lines of text in different colours (foreground and backgrounds were changed):
Final

That concludes the tutorial on colour.


Selba Ward Tutorials


The Final Program

#include <SFML/Graphics.hpp>
#include <SelbaWard/ConsoleScreen.hpp>
#include <SelbaWard/PaletteEnums.hpp>

int main()
{
	sf::Texture csTexture;
	if (!csTexture.loadFromFile("PATH/Windows Console ASCII.png"))
		return EXIT_FAILURE;

	sf::RenderWindow window(sf::VideoMode(800, 600), "Console Screen tutorial - Colour");

	Cs cs;
	cs.setTexture(csTexture);
	cs.setTextureTileSize({ 8, 12 });
	cs.setNumberOfTextureTilesPerRow(16);
	cs.setMode({ 40, 22 });
	cs.setSize(cs.getPerfectSize());
	cs.setScale({ 2.f, 2.f });
	cs.setOrigin(cs.getSize() / 2.f);
	cs.setPosition(sf::Vector2f(window.getSize() / 2u));

	cs << "Hello" << Cs::CursorCommand::Newline << "World!";
	cs.clear();
	cs << "Welcome to the new world!" << Cs::CursorCommand::Newline;

	cs << Cs::Color(11) << "Hello " << Cs::Color(12) << "World" << Cs::Color(9) << "!" << Cs::CursorCommand::Newline;
	cs << Cs::ColorType::Background;
	cs << Cs::Color(14) << "Yellow" << Cs::Color(0) << " and " << Cs::Color(13) << "Cyan" << Cs::Color(0) << Cs::CursorCommand::Newline;
	cs << Cs::Color(12) << "blue on green" << Cs::CursorCommand::Newline << Cs::Color(15) << Cs::ColorType::Foreground << Cs::Color(10) << "red on white" << Cs::CursorCommand::Newline;
	cs << Cs::ColorPair(0, 14) << "Black on yellow" << Cs::CursorCommand::Newline;
	cs << Cs::ColorPair(9, 0) << "blue " << Cs::Fg(8) << "grey " << Cs::Fg(10) << "red " << Cs::Bg(7) << "light grey background" << Cs::CursorCommand::Newline;
	cs << Cs::ColorPair(sw::Palette::Default::Blue, sw::Palette::Default::Black) << "blue " << Cs::Fg(sw::Palette::Default::Gray) << "grey " << Cs::Fg(sw::Palette::Default::Red) << "red " << Cs::Bg(sw::Palette::Default::DarkWhite) << "light grey background" << Cs::CursorCommand::Newline;

	// ColorCommand::Opposite
	cs << Cs::ColorPair(sw::Palette::Default::Yellow, sw::Palette::Default::Blue) << "A";
	cs << Cs::Color(Cs::ColorCommand::Opposite) << "B";
	cs << Cs::ColorPair(sw::Palette::Default::Yellow, Cs::ColorCommand::Opposite) << "C";
	cs << Cs::Fg(Cs::ColorCommand::Opposite) << "D";

	// ColorCommand::Invert
	cs << Cs::ColorPair(Cs::ColorCommand::Invert, sw::Palette::Default::Blue) << "E";
	cs << Cs::Bg(sw::Palette::Default::Red) << "F" << Cs::CursorCommand::Newline;
	for (unsigned int i{ 0u }; i < cs.getPaletteSize(); ++i)
		cs << Cs::Bg(i) << std::to_string(i);
	cs << Cs::CursorCommand::Newline;

	// ColorCommand::Contrast
	cs << Cs::Fg(Cs::ColorCommand::Contrast);
	for (unsigned int i{ 0u }; i < cs.getPaletteSize(); ++i)
		cs << Cs::Bg(i) << std::to_string(i);
	cs << Cs::CursorCommand::Newline;
	
	// clear()
	//cs.clear(Cs::ColorPair(sw::Palette::Default::Black, sw::Palette::Default::White));
	//cs.clear(Cs::Color(sw::Palette::Default::Cyan));
	//cs.clear(Cs::ColorPair(Cs::ColorCommand::Invert, sw::Palette::Default::Red));

	while (window.isOpen())
	{
		sf::Event event;
		while (window.pollEvent(event))
		{
			if (event.type == sf::Event::Closed)
				window.close();
		}

		window.clear(sf::Color(128, 128, 128));
		window.draw(cs);
		window.display();
	}

	return EXIT_SUCCESS;
}

Selba Ward Tutorials