WDL-OL modify graphic by user input

Another article on WDL-OL, this time about relation between mouse, and graphic.

This article is more or less related to the previous one about WDL and cairo, but this time, as we will some basics about handling mouse and graphic, it’s not related to cairo, or to LICE. So we will use LICE as it’s already setup and working.

Like the previous article, if you are not familiar with WDL-OL, I recommand a lot this awesome tutorial on WDL-OL.

Idea

Let’s explain a little about the mouse, and the relation you can apply.

A mouse, when it moves, or click, or whatever, send commands to Windows. Then, Windows search for the most suitable program able to handle it, and dispatch it.

So, there is several problem with this, sometimes (depending on framework/OS/system), you will recieve the exact coordinates (from the top/left corner of your screen), sometimes you will get related to the current window, sometimes if you got more than one screen you will destroy the world…

That’s the first pitfall you can have: mis-understand where the 0:0 point is…

Then, you need to have a relationship with the graphic, which needs again to know what is the link between the coordinate system, and the graphic position (where the 0:0 of the graphic is located).

On WDL-OL, the graphic 0:0 is centered on the VST window (so you need to take care of the left/top coordinate of your IControl), and the mouse is related also to the VST window. Sweet we have all to start!

Starting a VST

Let’s start, like always, by duplicating a project:

python.exe duplicate.py IPlugEffect/ MyFirstGraphicPlugin YourName 

Enter into it, and start the MyFirstGraphicPlugin.sln as usual. Like usual too, I’m on VS2013 so you may found tiny differences on other Visual Studio.

Right click on the MyFirstGraphicPlugin-app project, create => new file. I call it MyGraphicControl.h

Here is the starting point:

#ifndef __MY_GRAPHIC_CONTROL_H__
#define __MY_GRAPHIC_CONTROL_H__

#include "IControl.h"

class MyGraphicControl : public IControl {
public:
	MyGraphicControl(IPlugBase *pPlug, IRECT pR) : IControl(pPlug, pR) {};
	~MyGraphicControl() {};

	bool IsDirty() { return true; };

	bool Draw(IGraphics *pGraphics) {
		return true;
	};
};

#endif

As usual, a simple base IControl.

Now we need to make the rendering, and of course, having some data somewhere.
We will use the magic of std by using a classic of all: std::vector.

So let’s go. We update the class to add this part:

#include <vector>

class MyGraphicControl : public IControl {
protected:
	std::vector<Point> points;
};

What? You don’t have Point class, so above, add a Point class (above the class):

class Point {
public:
	unsigned int uid;
	double x;
	double y;
	bool operator < (const Point& point) const { return (this->x < point.x); };
	bool operator >(const Point& point) const {	return (this->x > point.x); };
	bool operator == (const Point& point) const { return (this->uid == point.uid); };
};

We got here a simple uid (it can be usefull in some cases), and a x/y values. The uid can be handy to make difference between two points which got same x/y couple. So it can avoid some tiny un-pleasant results.

The operator surchage will be usefull later (to use std::sort, std::remove…). Here we define a point position by it’s axis coordinate, we define it’s uniqueness by the uid. It’s a mandatory things to not have lines going everywhere (just make the test to not sort when a user insert a point to see what I’m talking about).

SO, we got a Point class, a vector of points in our future graphic, we miss two part: drawing the graphic, handling the mouse. Let’s start by the graphic rendering, and, before, some conversion method to avoid storing in the vector, real graphic values.
We choose for that a percent system, which allows us to have control over any changes of our graphic size, with ease.

As this is pretty annoying, and framework related, I put here directly the good version (also it helps you to see what the class looks like right now), pay attention to the 4 functions convertToGraphicX, convertToPercentX, convertToGraphicY, convertToPercentY:

#ifndef __MY_GRAPHIC_CONTROL_H__
#define __MY_GRAPHIC_CONTROL_H__

#include "IControl.h"
#include <vector>

class Point {
public:
	unsigned int uid;
	double x;
	double y;
	bool operator<(Point pt) { return x < pt.x; }
};

class MyGraphicControl : public IControl {
protected:
	std::vector<Point> points;

	double convertToGraphicX(double value) {
		double min = (double) this->mRECT.L;
		double distance = (double) this->mRECT.W();
		return value * distance + min;
	};
	double convertToPercentX(double value) {
		double min = (double) this->mRECT.L;
		double position = value - min;
		double distance = (double) this->mRECT.W();
		return position / distance;
	};
	double convertToGraphicY(double value) {
		double min = (double) this->mRECT.T;
		double distance = (double) this->mRECT.H();
		// We use (1 - value) as the max value 1 is located on top of graphics and not bottom
		return (1 - value) * distance + min;
	};
	double convertToPercentY(double value) {
		double min = (double) this->mRECT.T;
		double position = value - min;
		double distance = (double) this->mRECT.H();
		// We return the 1 - distance as the value 1 is located on top of graphics and not bottom
		return 1 - position / distance;
	};

public:
	MyGraphicControl(IPlugBase *pPlug, IRECT pR) : IControl(pPlug, pR) {};
	~MyGraphicControl() {};

	bool IsDirty() { return true; };

	bool Draw(IGraphics *pGraphics) {
	};
};

#endif

Now we have our convertion methods, time to Draw: we take points, modify coordinates (as we store in %, the range will be [0..1], so we need to convert it for rendering), and show lines between them:

	bool Draw(IGraphics *pGraphics) {
		IColor color(255, 50, 200, 20);
		Point previous;
		// Little trick, no "real" points got uid = 0, so we know it's
		// not a real point, and we should avoid doing line with it...
		previous.uid = 0;

		for (std::vector<Point>::iterator it = points.begin(); it != points.end(); ++it) {
			Point current = *it;
			// We draw the point
			pGraphics->DrawCircle(&color, convertToGraphicX(current.x), convertToGraphicY(current.y), 3, 0, true);

			// The previous point is a valid point, we draw line also
			if (previous.uid > 0) {
				pGraphics->DrawLine(&color,
					convertToGraphicX(previous.x), convertToGraphicY(previous.y),
					convertToGraphicX(current.x), convertToGraphicY(current.y),
					0, true);
			}

			// We update the previous point
			previous = current;
		}

		/* TODO:
		  I didn't put it here, to make you think
		  But you need few more cases to handle:
		    - when there is no points on the graphic
		    - when there is a single point on the graphic
		    - what to draw BEFORE the first point
		    - what to draw AFTER the last point
		*/
		return true;
	};

There is some TODO missing, they can be specific to all project so you must think about them, and reply with your own code to it. Just remember, graphic system is always full of surprise, there is many cases (let’s call them exception), always to handle to have a proper graphic.
For this test purpose, it will be enough, we got lines drawing and points drawing. Let’s add our code to let user add points and remove points.

For doing this, IPlug gives us some help:

  • void OnMouseDown(int x, int y, IMouseMod* pMouseMod)
  • void OnMouseDrag(int x, int y, int dX, int dY, IMouseMod* pMouseMod)
  • void OnMouseUp(int x, int y, IMouseMod* pMouseMod)
  • void OnMouseDblClick(int x, int y, IMouseMod* pMouseMod)

Like everytime, in GUI, there is things to do, and things to never do, here is the basic usage ALL users will think of:

  • Double click somewhere create a point;
  • BUT, if there is point, it will instead delete it;
  • A mousedown on a point (while keeping mouse down), will select and move a point;
  • A mouseup will stop moving a point, but will keep that point selected;
  • A mousedown nowhere, will unselect all.

That’s the base, you probably won’t another behavior as everybody is familiar with this… Related to our previous list of functions:

  • OnMouseDown will unselect a point, or select a point;
  • OnMouseDrag will move a point, if there is a point selected;
  • OnMouseUp will stop dragging, but keep point selected;
  • And OnMouseDblClick will born or kill some point.

From that point (pun), we need two more things:

  • A « Point selected » somewhere in our graphic class;
  • A « dragging » boolean, to know if user is currently dragging or not;
  • A counter, I forgot it, but the UID needs to be unique, a counter always increasing is a good candidate in this case.

We will also use a convention, a point with UID = 0, is not an existing point, it’s a fake point, and we can’t do anything with it.

So:

class MyGraphicControl : public IControl {
protected:
	std::vector<Point> points;
	Point selected;
	bool isDragging;
	unsigned int counter;

Update also the constructor for the counter (to start it at 1, as we said 0 is reserved to not existing/null Point):

	MyGraphicControl(IPlugBase *pPlug, IRECT pR) : IControl(pPlug, pR), counter(1) {};

OOOhhh, and before I forgot; the IMouseMod structure:

  • A Alt keyboard key is pressed or not
  • C Control keyboard key is pressed or not
  • L Left mouse button is pressed or not
  • R Right mouse button is pressed or not
  • S Shift keyboard key is pressed or not

You could guess that easily if you know related click functions in Windows which bring few (only few) keys keyboard with them!

Enough, time to code OnMouse* stuff, let’s start with OnMouseDblClick:

	Point getPoint(double x, double y, double epsilon) {
		for (std::vector<Point>::iterator it = points.begin(); it != points.end(); ++it) {
			Point point = *it;
			double xGraphic = convertToGraphicX(point.x);
			double yGraphic = convertToGraphicY(point.y);

			if (
				// X check
				(xGraphic - epsilon < x && xGraphic + epsilon > x) &&
				// Y check
				(yGraphic - epsilon < y && yGraphic + epsilon > y)
				) {
				return point;
			}
		}

		// Nothing found, we return a "blank" point
		Point none;
		none.uid = 0;
		return none;
	};

	void OnMouseDblClick(int x, int y, IMouseMod* pMouseMod) {
		Point imHere = getPoint(x, y, 6);

		// As we said, the uid = 0 means no point
		if (imHere.uid == 0) {
			Point newPoint;
			newPoint.x = convertToPercentX(x);
			newPoint.y = convertToPercentY(y);
			newPoint.uid = counter++;
			points.push_back(newPoint);
			// And we sort it!
			std::sort(points.begin(), points.end());
			SetDirty();
		} else {
			// We delete the point
			points.erase(std::remove(points.begin(), points.end(), imHere), points.end());
			SetDirty();
		}
	};

As I said above, the vector has to be sorted (there is no meaning in audio to have a point going backward on the graphic), so when we add a new point, we sort it, therefore, compare to what’s written here, getPoint could be improve (if the current point has it’s X coordinate way above the searched X, we could stop). The second important things is the epsilon, it’s a mandatory things, as user are not precise to a single pixel, we must have an epsilon of 3/4 px is usually good (jump to 15/20 px if you are on iOs/finger products).

But, here we got a DrawCircle (in Draw function) of 3 px, so instead of a 3/4 px margin, jump to a 6px margin (actually a good value is usually, for tiny elements 3px around more than the render size). So here we got a render of 3 px, so we take an espilon of 6.

=> You can create a #define EPSILON 6 if you want to change that easily later.

Also, never forget to call for SetDirty when you want to see some changes on the GUI, here you will have no problem as we override IsDirty, but on real system you should avoid overriding it like this by always setting it to true.

Enough, let’s already try what we got there. Make some changes into MyFirstGraphicPlugin.cpp, apply those changes in the constructor:

  IGraphics* pGraphics = MakeGraphics(this, kWidth, kHeight);
  //pGraphics->AttachPanelBackground(&COLOR_RED);
  pGraphics->AttachPanelBackground(&COLOR_WHITE);

  pGraphics->AttachControl(new MyGraphicControl(this, IRECT(0, 0, kWidth, kHeight)));

  /*IBitmap knob = pGraphics->LoadIBitmap(KNOB_ID, KNOB_FN, kKnobFrames);

  pGraphics->AttachControl(new IKnobMultiControl(this, kGainX, kGainY, kGain, &knob));*/

We remove the color red to switch to white, and we add our new IControl, also don’t forget to add it has include:

#include "MyGraphicControl.h"

Build it, you should arrive on a blank page, and then, make some double click, you should see some points and line appearing:

MyFirstGraphicPlugin

If you double click a point, you should see it disapear!

Now, we miss the moving part, lets go back to our superb MyGraphicControl.h, let’s start with the simple one again, OnMouseUp:

	void OnMouseUp(int x, int y, IMouseMod* pMouseMod) {
		isDragging = false;
	};

Yup, done. Let’s do OnMouseDown now:

	void OnMouseDown(int x, int y, IMouseMod* pMouseMod) {
		Point imHere = getPoint(x, y, 6);

		if (imHere.uid == 0) {
			// We erase selected
			Point none;
			none.uid = 0;
			selected = none;
			// Not needed, but who knows.
			isDragging = false;
			SetDirty();
		} else {
			selected = imHere;
			isDragging = true;
			SetDirty();
		}
	};

If we didn’t have any point behind this x/y couple, we unselect everything. On the other side, if something is selected, we mark it as selected and allow dragging.

Note: there is plenty of things you can do here, like keeping the previous, and next point related to the selected point (will avoid some compute when dealing with constraints), also you can simply store the uid for example, and start diggind into std::find function how to get it always.

That’s way beyond the scope of this article which is an introduction for graphics in WDL-OL, but as you see, even with a simple one, you can have many many cases to handle for having a well done graphic…

Let’s finish with the move one:

	void OnMouseDrag(int x, int y, int dX, int dY, IMouseMod* pMouseMod) {
		if (selected.uid == 0 || isDragging == false) {
			// Nothing to do
			return;
		}

		// We search for our points
		// As selected is not a pointer
		// If we modify it, it will be meaningless...
		std::vector<Point>::iterator it = std::find(points.begin(), points.end(), selected);

		if (it != points.end()) {
			(&(*it))->x = convertToPercentX(x);
			(&(*it))->y = convertToPercentY(y);
		}

		// And we ask to render
		SetDirty();
	};

We’re done, we got a basic graphic where user can do what he wants. Unfortunately from that point you have much… MUCH more things to do for making it professional:

  • Handle specific cases (no points, one points)
  • Before and after first point (what to draw)
  • When we move a point, avoiding going before or after current selected point (in terms of X coordinate), again, as we are in audio stuff, having a point going backward is a non-sense
  • Having selected points a different color, which inform the user of selection he mades
  • Many improvements, for example this system is not able to handle many points selected
  • Can the first, and the last points, move on X axis?
  • Also, are they linked (if you move point 0 on Y, the last point has the same Y)?
  • How about bezier or arc curves? They got middle points which are created at the same time as user click…
  • How about some constraints, like the curve cannot go backward (mostly in bezier cases)?
  • How to link it with audio processing (hint: the percent system done here is a good start)?

Too many things to explain them like this, you need to think and test a lot, to find an algorithm which match all the cases you need/care…

And many of them will probably be linked (at least related) to other parts of your program…

As always, you can find the full project on github: here

Publicités

Un commentaire

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :