|

An MVC Christmas Tree in C++ and ncurses

While playing with C++ templates lately, I came up with an Oscillator class, which executes a custom task with a given frequency. In fact, it’s a family of classes, given that the frequency is parametric. Moreover, I can give the constructor a delay (in milliseconds), in case I need to postpone the first task execution:

template<typename Hz>
class Oscillator {

	private:
	
		thread taskLoop;
		atomic_bool active=true;

		using invertedHz=ratio<Hz::den,Hz::num>; // 1/Hz
		using period_t=chrono::duration<int64_t,invertedHz>;
		static constexpr period_t period{1};
		
	protected:
	
		virtual void task()=0;

	public:
	
		Oscillator(chrono::milliseconds delay=0ms) {

			taskLoop=thread([this,delay](){ 
			
				this_thread::sleep_for(delay);
			
				while(active) {
					auto t=chrono::steady_clock::now();
					task();
					this_thread::sleep_until(t+period);
				}
				
			});
			
		}
		
		~Oscillator() {
			active=false;
			taskLoop.join();
		}

};

I was looking for a way to test it and, since Christmas is right around the corner, I figured I’d make a Christmas tree with flashing lights and twinkling stars. It will be an ncurses MVC application, similar to this one. The view will utilize the oscillator to update itself at a 50Hz frequency, while the model will be responsible for providing updated intensities for the lights and the stars.

But first of all I needed a Christmas tree in ascii art. I found one here and downloaded it in a text file called xmas_tree:

      
                 *             ,
                       _/^\_
                      <     >
     *                 /.-.\         *
              *        `/&\`                   *
                      ,@.*;@,
                     /_o.I %_\    *
        *           (`'--:o(_@;
                   /`;--.,__ `')             *
                  ;@`o % O,*`'`&\ 
            *    (`'--)_@ ;o %'()\      *
                 /`;--._`''--._O'@;
                /&*,()~o`;-.,_ `""`)
     *          /`,@ ;+& () o*`;-';\
               (`""--.,_0 +% @' &()\
               /-.,_    ``''--....-'`)  *
          *    /@%;o`:;'--,.__   __.'\
              ;*,&(); @ % &^;~`"`o;@();         *
              /(); o^~; & ().o@*&`;&%O\
        jgs   `"="==""==,,,.,="=="==="`
           __.----.(\-''#####---...___...-----._
         '`         \)_`"""""`
                 .--' ')
               o(  )_-\
                 `"""` `

The 14 asterisks around the tree are stars, while the 10 little os inside it will be the light bulbs.

The View

Before we deal with our specific view, I want to make a more general template that will make any kind of view render itself with a constant refresh rate:

template <class V,typename Hz>
class ViewRefreshRate: public V, public Oscillator<Hz> {

	private:

		mutex renderMutex;

	protected:

		virtual bool getRenderData()=0;
		void task() override { render(); }

	public:
	
		void hide() override {
			lock_guard<mutex> lock(renderMutex);
			V::hide();
		}

		void render() override {
			lock_guard<mutex> lock(renderMutex);
			if(!V::showing()) return;
			if(getRenderData()) V::render();
		}
		
		using CtxT=typename V::ctx_t;
		ViewRefreshRate(CtxT *c,chrono::milliseconds d=0ms): 
        	V(c), Oscillator<Hz>(d) { }
		
};

The oscillator’s task is to simply render the view. Since now render() can be called by different threads, we must protect it with a mutex. This excludes overlapping calls to the method, as well as hiding the view while being rendered. On the other hand, getRenderData() will be used to get an updated version of the view’s dynamic data and decide if it needs to be rendered or not.

Now, our specific view will be of type NCursesScreen, with a 50Hz refresh rate. It needs to know the positions of the stars and lights, as well as their intensities and colors. All stars will be white, but light bulbs will be colored in six different ways. So I collected all positions in two vectors, along with zero initial intensities and a color for each light bulb:

class JollyView: public ViewRefreshRate<NCursesScreen,ratio<50>> {

	private:
	
		string treeLines;
		
		struct Pos {int y,x,intenScale;};
		vector<Pos> stars={
			{1,17,0},{4,5,0},{4,37,0},{5,14,0},
			{5,47,0},{7,34,0},{8,8,0},{9,45,0},
			{11,12,0},{11,40,0},{14,5,0},{16,40,0},
			{17,10,0},{18,48,0},
		};
		
		struct LightBulbPos: public Pos {int color;};
		vector<LightBulbPos> lights={
			{7,23,0,COLOR_RED},{8,26,0,COLOR_GREEN},
			{10,21,0,COLOR_BLUE},{11,27,0,COLOR_YELLOW},
			{13,23,0,COLOR_MAGENTA},{14,28,0,COLOR_CYAN},
			{17,19,0,COLOR_RED},{18,33,0,COLOR_GREEN},
			{19,19,0,COLOR_CYAN},{19,29,0,COLOR_YELLOW},
		};
		
		void renderLight(int,int,char,int,int);

	protected:
	
		void produceContent() override;
		
	public:
	
		JollyView(NCursesCtx*,string);
		
};

void JollyView::renderLight(int y,int x,char c,int color,int intensity) {
	int intensAttr[]={A_INVIS,A_DIM,A_NORMAL,A_BOLD};
	attrset(intensAttr[intensity]|COLOR_PAIR(color));
	mvprintw(y,x,"%c",c);
}

void JollyView::produceContent() {

	attrset(A_NORMAL);
	printw("%s",treeLines.c_str());
	
	for(auto &i:stars) renderLight(i.y,i.x,'*',COLOR_WHITE,i.intenScale);
	for(auto &i:lights) renderLight(i.y,i.x,'o',i.color,i.intenScale);
	
}

JollyView::JollyView(NCursesCtx *c,string lines): ViewRefreshRate(c), treeLines(lines) {
	srand(time({}));
	random_shuffle(lights.begin(),lights.end());
}

To render the view, we must first draw the tree image and then place the stars and light bulbs with their current intensities. We are using ncurses’ invisible, dim, normal and bold attributes to represent four different tiers of intensity, which means that the intensity field in the vectors must be an integer from 0 to 3.

The treeLines field is given as a parameter to the constructor, which also shuffles the light bulb vector. The reason for the latter is that I want half of the light bulbs to oscillate at a fast frequency, and the others at a slow one. But I don’t want either half to be at the top of the tree and the other at the bottom. This will be more apparent later on, when we discuss the model.

It’s worth noticing that I am using COLOR_PAIR() indexed by the actual color. This implies a color-to-pair relation like the following, which we will implement in the controller:

COLOR_XXX --> (COLOR_XXX,COLOR_BLACK)

The last aspect to cover is the implementation of getRenderData(). This is the point where our view will communicate with the model. The method should obtain a vector of light intensities that have changed since a point in time:

struct LightIntensity {
	int index;
	double intensity;
};

typedef vector<LightIntensity> LightIntensities;

using time_point_t=chrono::time_point<chrono::steady_clock>;

typedef function<LightIntensities*(time_point_t)> updateEvent;

class JollyView: public ViewRefreshRate<NCursesScreen,ratio<50>> {

	private:
  		
		/* ... */
	
		time_point_t lastChecked=time_point_t::min();
		updateEvent onGetIntensities;

	protected:
	
		/* ... */
	
		bool getRenderData() override;
		
	public:
	
		/* ... */
	
		void setOnGetIntensities(updateEvent);
		
};

void JollyView::setOnGetIntensities(updateEvent f) { onGetIntensities=f; }

bool JollyView::getRenderData() { 

	auto *changes=onGetIntensities(lastChecked);
	if(!changes->size()) return false;
	
	lastChecked=chrono::steady_clock::now();

	for(auto &i:*changes) {
	
		auto intenScale=lround(3*i.intensity);

		auto idx=i.index;
		if(idx<stars.size()) stars[idx].intenScale=intenScale;
		else lights[idx-stars.size()].intenScale=intenScale;
		
	}
	
	return true;
	
}

The controller will eventually use setOnGetIntensities() to bind the onGetIntensities event with the actual model’s event handler, which in turn will produce a vector of indexes between 0 and 23, and intensities between 0 and 1. The first 14 indexes will correspond to the stars and the rest to light bulbs. The job of getRenderData() is to transform those intensities to integral values from 0 to 3 and update the corresponding internal vectors accordingly.

The Model

The model itself is pretty simple. The constructor must create the stars and light bulbs, the destructor must delete them and there should be a getIntensities() method for views to request the latest intensity changes:

class JollyModel {

	private:
	
		vector<TwinkleStar*> stars;
		vector<SlowLightBulb*> slowLights;
		vector<FastLightBulb*> fastLights;
		
		LightIntensities changedIntensities;

	public:
	
		JollyModel();
		~JollyModel();
		
		LightIntensities *getIntensities(time_point_t);

};

JollyModel::JollyModel() {

	srand(time({}));

	for(int i=0;i<14;i++) 
		stars.push_back(new TwinkleStar(1s*rand()%12));

	for(int i=0;i<5;i++) 
		fastLights.push_back(new FastLightBulb(1ms*rand()%5000));

	for(int i=0;i<5;i++) 
		slowLights.push_back(new SlowLightBulb(1s*rand()%8));

}

JollyModel::~JollyModel() {
	for(auto &i:stars) delete i;
	for(auto &i:fastLights) delete i;
	for(auto &i:slowLights) delete i;
}

LightIntensities *JollyModel::getIntensities(time_point_t lastChecked) {

	changedIntensities.clear();
	
	int i=0;
	
	for(auto &l:stars) {
		if(l->getLastChanged()>lastChecked) {
			auto li=LightIntensity{i,l->getIntensity()};
			changedIntensities.push_back(li);
		}
		i++;
	}
		
	for(auto &l:fastLights) {
		if(l->getLastChanged()>lastChecked) {
			auto li=LightIntensity{i,l->getIntensity()};
			changedIntensities.push_back(li);
		}
		i++;
	}
		
	for(auto &l:slowLights) {
		if(l->getLastChanged()>lastChecked) {
			auto li=LightIntensity{i,l->getIntensity()};
			changedIntensities.push_back(li);
		}
		i++;
	}
		
	return &changedIntensities;
	
}

Each star and light bulb is given a random delay, while the changedIntensities field is used as a buffer that holds intensity changes that should be passed to the requesting view.

The tricky parts are the stars and lights themselves. Our Oscillator is not enough for this case. We need a new one that will execute a task (namely change the intensity) every fraction of the actual frequency. We will call this class a FracOscillator:

template<typename Hz,typename Frac>
class FracOscillator: public Oscillator<ratio_divide<Hz,Frac>> {

	using Oscillator<ratio_divide<Hz,Frac>>::Oscillator;

	private:
	
		int step=0;

	protected:
	
		double percent;
	
		virtual void fracTask()=0;
        
		void task() override {
			step=(step+Frac::num)%Frac::den;
			percent=(double)step/Frac::den;
			fracTask();
		}

};

Given that the Frac parameter will typically be a ratio less than 1, the internal frequency is essentially multiplied. The percent field holds the current percentage of the real period, and we now have to implement fracTask() instead. For our stars and light bulbs, it would look something like the following:

template<typename Hz,typename Frac>
class FlickeringLight: public FracOscillator<Hz,Frac> {

	using FracOscillator<Hz,Frac>::FracOscillator;

	private:
	
		double intensity;
		time_point_t lastChanged;
		
	protected:
	
		void fracTask() override {
			auto perc2Pi=2.0*numbers::pi*this->percent;
			intensity=(1-cos(perc2Pi))/2;
			lastChanged=chrono::steady_clock::now();
		}
		
	public:
	
		double getIntensity() { return intensity; }
		time_point_t getLastChanged() { return lastChanged; }

};

class TwinkleStar: public FlickeringLight<ratio<1,12>,ratio<1,100>> { 
	using FlickeringLight::FlickeringLight;
};

class FastLightBulb: public FlickeringLight<ratio<1>,ratio<1,100>> { 
	using FlickeringLight::FlickeringLight;
};

class SlowLightBulb: public FlickeringLight<ratio<1,5>,ratio<1,100>> { 
	using FlickeringLight::FlickeringLight;
};

The intensity property follows a cosine curve and the two public methods get the intensity and the point in time it changed, respectively. All of them calculate their intensity 100 times within their period, but stars oscillate at 1/12Hz, fast lights at 1Hz and slow ones at 1/5Hz.

The Controller

The controller acts as the application’s entry point by showing the view:

class JollyController {

	private:
	
		NCursesCtx ctx;
	
		JollyView *view;
		JollyModel *model;
		
	public:
	
		JollyController();
		~JollyController();
		
		void showView();
		
};

void JollyController::showView() { view->show(); }

In the constructor we read the image file and feed it to the view, and then we bind event handlers to its events. After that, we configure the color pairs with the relation we talked about in the view section:

ollyController::JollyController() { 

	model=new JollyModel();

	ifstream file("xmas_tree");
	string treeLines,str; 
	while (getline(file,str)) treeLines+=str+"\n";
	
	view=new JollyView(treeLines);

	view->setOnExit([this](){ view->hide(); });
	view->setOnUnhandledKey([](int){ beep(); });
	view->setOnGetIntensities(bind(&JollyModel::getIntensities,model,_1));
	
	start_color();
	auto colors={COLOR_WHITE,COLOR_RED,COLOR_GREEN,COLOR_BLUE,COLOR_YELLOW,COLOR_CYAN,COLOR_MAGENTA,};
	for(auto &i:colors) init_pair(i,i,COLOR_BLACK);

}

The destructor deletes both the view and the model:

JollyController::~JollyController() {
	delete view;
	delete model;
}

And finally, main() merely calls the controller’s showView() method:

int main() { JollyController().showView(); }

The result is something like this:

Merry Christmas!

Leave a Reply

Your email address will not be published. Required fields are marked *