DynaDraw, Part One

Hello, everyone! I'm Michael Morrissey, the new DTS engineer
here at Be. I've already enjoyed working with several of you
during my first month on the job, and I hope that one day we'll
meet at a developer conference or demo.

The first time I saw a Silicon Graphics machine (*), when I was
a freshman in college, I was blown away. The graphics were so fast,
so smooth! Those great SGI demos captivated me, and I spent hours
tweaking the parameters to the programs, just to amuse myself.

Eventually, I wanted  to write my own graphics programs. I looked at
some of the sample code, but couldn't seem to get started. C wasn't
the problem, it was the GL library -- I didn't understand the building
blocks necessary to writing a basic graphics program with it. Some
older students who saw I was struggling set me straight and got me going.

I imagine a lot of people are in the same predicament with the Intel
release of the BeOS that I was in with that SGI machine -- tired of
tweaking the demos and sample code, wanting to write their own code,
but unsure how to start and feeling a little overwhelmed. I'd like to
help newcomers to BeOS programming by building a small application which
emphasizes the fundamentals but is large enough for experimentation.

The application we'll build (actually, port and modify) is DynaDraw,
written originally in C and GL by Paul Haeberli of Silicon Graphics
(fittingly, the person who first told me about BeOS). It's a fun little
paint program which models the dynamics of a brush with mass on a paper
with drag. (Don't worry, we'll skip the physics!)

DynaDraw turns your mouse into a calligraphy pen, and lets you make
beautiful, smooth strokes easily. Rather than concentrating on
any one aspect  of the OS, we'll talk about good program structure
and use objects that are common to almost all programs.

The sample code for this application can be found at:

	<ftp://ftp.be.com/pub/samples/obsolete/interface_kit/dynadraw.zip>

Paul's original C source is also included in this zip file, and I
highly recommend checking out his web page on DynaDraw:

	<http://www.sgi.com/grafica/dyna/index.html>

When designing or porting an application, it's often easiest to
write the program in stages, starting with minimal functionality,
and building up from there. Keep in mind that you'll add more
features later, and don't paint yourself into a corner.

For minimal functionality DynaDraw needs a window to draw in. But,			
since  BWindow objects can't draw, we need a BView object as a child		
of the BWindow.

We'll want to draw with the left mouse button down. The BView  API
has a MouseDown() function, so if we derive a class from BView, we
can override that function to handle mouse events the way we'd like.

Another important consideration is the ability to redraw the
image if part or all of the view becomes invalidated, for example,
if another window is on top of ours temporarily, or if we minimize
and then restore our window. BViews can't redraw themselves; you
have to tell them how -- and for this, we'll override the BView::Draw()
function.

Something else to consider is  where to put the state-specific
variables for the "filter" -- things such as brush mass, drag,
velocity, etc. -- which were stored in the filter structure in Paul's
original program. We'd like them to be as close as possible to the
functions that need them -- the functions that draw (and redraw) --
so we'll put them in our BView descendant. By the same logic, we'll
also put the functions that operate on these variables in the same
class (such as the Apply() function).

That's our minimum functionality, so let's look at what we'll need to			
implement it:

* We always want an instance of the BApplication object, but we don't
need to specialize any of its functionality, so we can use the class
as it is.

* We'll want a window. We don't need to specialize it -- but we do
have to override one of it's functions, QuitRequested(). I'll return to
this in a moment.

* We'll need a BView, but since we need to specialize functions such as
MouseDown() and Draw(), so we'll want to create a new class -- let's
call it FilterView -- derived from BView.

Returning to override BWindow::QuitRequested(): even in the				
simplest apps, with just one window, you need to override
QuitRequested(). This is because the last window to close needs
to alert the BApplication object to quit. BWindow::QuitRequested()
doesn't do this automatically, because a window can't assume that
it's the last one open.  So we'll override QuitRequested() by
creating a simple class derived from BWindow, called DDWindow:

class DDWindow : public BWindow
{
  public:
  	DDWindow(BRect R, const char* title, window_type type, uint32 flags);
  	bool QuitRequested();
};

DDWindow::DDWindow(BRect R, const char* title, window_type type, uint32
flags)
	: BWindow(R, title, type, flags)
{
	// do nothing
}

bool
DDWindow::QuitRequested()
{
	/* we're the last window open, so shut down the application */
 	be_app->PostMessage(B_QUIT_REQUESTED);
	return true;
}

The constructor for the DDWindow doesn't really do anything but
pass the arguments along to the BWindow. In the QuitRequested()
function, we post a message to our BApplication object to quit.
Remember, be_app is a global variable set in BApplication constructor
to point to the instance of your application object.

Okay! Now on to the main() function:

int main()
{
	BApplication App("application/x-vnd.Be-dynadraw");
	DDWindow* W = new DDWindow(BRect(50,50,800,600), "DynaDraw!",
B_TITLED_WINDOW, 0);

	FilterView* F = new FilterView(W->Bounds());
	W->AddChild(F);

	W->Show();
	App.Run();
	return B_NO_ERROR;;
}

This actually does an enormous amount of work for us. First, it
connects us to the application sever and sets up our application
identifier. Next, we create a DDWindow: the top-left corner is at
(50,50) and the lower-right corner is at (800,600); the window title
is "DynaDraw!"; the window has a yellow tab; and finally, the user
can move, resize, close, and zoom the window. Remember, though, this
window isn't displayed yet.

Next, we create an instance of our (still undefined) FilterView class,
passing in an important BRect, namely, the bounds of the window it
will be attached to. Then we display the window, and start the
application's message loop.

The FilterView class should look like this:

class FilterView : public BView
{
 public:
	/* overridden functions from BView */
 	FilterView(BRect R);
 	void MouseDown(BPoint point);
	void Draw(BRect updateRect);

 private:
	/* state variables, formerly from the filter structure */
 	float curmass, curdrag, width;
 	float velx, vely, vel;
 	float accx, accy, acc;
 	float angx, angy;
 	BPoint odel, m, cur, last;

	/* a list of polygons which make up our brushstokes */
	BList polyList;

 	/* this is where the calculations get done, and the drawing */
	void DrawSegment();
 	bool Apply(BPoint m, float curmass, float curdrag);

	/* little helper functions */
	inline float flerp(float f0, float f1, float p);
 	inline void setpos(BPoint point);
};

Note that all the variables that were originally in the filter
struct are now in the private section of the class. This is fine,
since the only functions that need these variables are also in the class.
Our brushstrokes are made up of polygons. We'll want to keep a list of
them (so that we can redraw), so I've decided to use a BList object as  a
container. Every time we make a stroke, we'll add an item to this BList.

Then we have the two main functions, which remain largely unchanged
from the original program. The first is Apply(), which decides whether
or not a segment needs to be drawn. If it does, the other main function,
DrawSegment() is called, and it draws the segment. Finally, there are
two small helper functions, which don't do anything special.

The constructor for this class looks like this:

FilterView::FilterView(BRect R)
	: BView(R, "filter", B_FOLLOW_ALL_SIDES, B_WILL_DRAW)
{
	curmass = 0.50;
	curdrag = 0.46;
	width = 0.50;
}

Now we give the constructor a BRect object, which it passes on to the
BView constructor. We name the view "filter", and instruct it to
follow all sides. We'll do some drawing, so we need update
notifications sent to us. Inside the constructor, we set initial
values for the mass, drag, and width variables.

The MouseDown() function looks like this:

void
FilterView::MouseDown(BPoint point)
{
	uint32 buttons=0;
  	bool flag=0;
  	float p;
  	float mx, my;
	BRect B = Bounds();

  	GetMouse(&point, &buttons, true);

	if(buttons == B_PRIMARY_MOUSE_BUTTON)
	{
		mx = (float)point.x / B.right;
        		my = (float)point.y / B.bottom;
        		m.Set(mx,my);
        		setpos(m);
        		odel.Set(0,0);

		while(buttons == B_PRIMARY_MOUSE_BUTTON)
        		{
    			GetMouse(&point, &buttons, true);

			mx = (float)point.x / B.right;
		    	my = (float)point.y / B.bottom;
	      		m.Set(mx,my);

		      	if(Apply(m, curmass, curdrag)) DrawSegment();
          		snooze(15000);
		}
	}
	else if (buttons == B_SECONDARY_MOUSE_BUTTON)
	{
		int32 count = polyList.CountItems();
		for(int i=0; i < count; i++)
		{
			delete(polyList.ItemAt(0);
			polyList.RemoveItem(0L);
		}
		Invalidate();
	}
}

The MouseDown() function is called when the parent window
receives a mouse down message. In our case, we want to track
the cursor as long as the primary mouse button is held down. If
the second mouse button is pressed, we want to clear the screen.
(For anyone using a one-button mouse, don't despair; we'll add a
general clear-screen feature later on.)

First we decide which button is being pressed. If it's the primary
button, we initialize some points. Then we enter a loop which will
continue until the button is released. In that loop, we get the mouse
position, update our point, and call the Apply() function to determine
if a segment needs to be drawn. (I'll skip the Apply() function body,
since it's all calculations, and identical to the original version.)
If we need to draw the segment, we call DrawSegment(), which I'll get
to in a moment. Finally, we need to snooze() between mouse calls;
otherwise, the responsiveness would be too high.

If the secondary mouse button was pressed, we want to clear the
screen. We do this by deleting all the polygons in the polyList, and
invalidating the whole view (meaning the Draw() function is called on
the entire view). There are three things to note here: first, the
RemoveItem() function does not free the objects it holds pointers to --
you must do that. Second, calling RemoveItem() compacts the list, so
the length of the list decreases by one every time you call it. Third,
you'll notice that I called RemoveItem() with an argument of 0L --
this is because RemoveItem is overloaded, one version taking a void*,
the other taking an int32. Calling it with a 0 is ambiguous; calling
it with a 0L makes it clear we want the int32 version.

Now, back to DrawSegment. Calculations removed, it looks like this:

void
FilterView::DrawSegment()
{
	/* calculations removed */

	SetHighColor(0,0,0);

	polypoints[0].Set((B.right*(px+odel.x)), (B.bottom*(py+odel.y)));
	polypoints[1].Set((B.right*(px-odel.x)), (B.bottom*(py-odel.y)));
	polypoints[2].Set((B.right*(nx-del.x)), (B.bottom*(ny-del.y)));
	polypoints[3].Set((B.right*(nx+del.x)), (B.bottom*(ny+del.y)));

	polyList.AddItem(new BPolygon(polypoints, 4));
	FillPolygon(polypoints, 4);
	StrokePolygon(polypoints, 4);

	odel = del;

}

We call SetHighColor, which sets the pen color to black. Next, we set
the coordinates for the four BPoints which make up the polygon. Then,
we make a new BPolygon, constructed with our four-point array, and add
it to our polygon list. Finally, we make a call to FillPolygon(), but
also one to StrokePolygon. The reason is that if our polygon gets extremely
small, flattened to the point were it lies on a single line,  FillPolygon()
will not draw anything (because the area is, after all, zero). 
StrokePolygon, on the other hand, will draw the line, which is what we need.

The only thing left to look at is the Draw() function, which is called
if part or all of the view is invalidated. Lifting the hood reveals a
trivial function:

void
FilterView::Draw(BRect updateRect)
{
	BPolygon* bp;
	int32 count = polyList.CountItems();
	for(int i =0; i < count; i++)
	{
		bp = (BPolygon*)polyList.ItemAt(i);
		FillPolygon(bp);
		StrokePolygon(bp);
	}
}

All we do here is loop through the polygon list, getting one item at a
time, and calling FillPolygon() and StrokePolygon() for the item. This
reconstructs our drawing.

We now have a small program that draws nice calligraphic strokes. Play with
it for a while to see how it feels and how it reacts to movements. Adjust
the mass, drag, and width parameters in the FilterView constructor and see
how it changes the program.

There's lots more to do with this program. To start with, we should have
a simple menu bar at the top that lets us clear the screen, bring up an
About box, and quit (without using the close button on the window tab).
Since I'm a "tweak-freak," I'd like a window where I can adjust the mass,
drag, width, snooze factor, and other things on the fly, without
recompiling. Being able to change the color of the pen would be nice, too,
so we'll add a color preference panel.

I'll be back in two weeks with an article which adds these features.
It will also show how starting with a simple, clean framework makes
expanding your programs much easier. In the next article we'll be
moving at a faster pace, so you may want to get a head start by checking
out the BSlider class, the BColorControl class, and simple messaging.

See you in two weeks!

(*) - That first SGI machine was an Iris 4D, for the curious.
