The premise behind this application was an etch-a-sketch style application I saw on a friend’s iPhone and a related comment they made about a missing feature…also, to be honest, my desire to prove that my Windows Mobile based phone could be just as “snazzy.”
Rather than trying to re-invent the wheel I set off of a grand adventure to track down an API for the HTC sensors (mainly the G-Sensor, GSensor, Grav Sensor or tilt sensor, depending on what you want to call it). The hunt began with a Google search for “htc fuze sensor api.” Additionally, my hunt ended with a Google search for “htc fuze sensor api.” On the first page of results I found a link to a blog post on Enterprise Mobile that appeared to be exactly what I needed (their information was greatly assisted by information from another blog that had reverse engineered the HTC dll that controlled the interaction with the sensors on the Fuze).
I played around for a bit and everything worked well but I had issues with the grav sensor not properly detecting when it was “at rest” on a level surface as well as some flakiness in other areas. I cracked open the source for the wrapper and decided to play around with how it returned the vectors from the sensor. After several tweaks and adjustments I settled on the following change:
Original code in HTCGSensor.cs:
[cclN_csharp]
public override GVector GetGVector()
{
GVector ret = new GVector();
HTCGSensorData data = GetRawSensorData();
ret.X = data.TiltX;
ret.Y = data.TiltY;
ret.Z = data.TiltZ;
// HTC’s Sensor returns a vector which is around 1000 in length on average..
// but it really depends on how the device is oriented.
// When simply face up, my Diamond returns a vector of around 840 in length.
// While face down, it returns a vector of around 1200 in length.
// The vector direction is fairly accurate, however, the length is clearly not extremely precise.
double htcScaleFactor = 1.0 / 1000.0 * 9.8;
return ret.Scale(htcScaleFactor);
}
[/cclN_csharp]
New version I’m using:
[cclN_csharp]
public override GVector GetGVector()
{
GVector ret = new GVector();
HTCGSensorData data = GetRawSensorData();
ret.X = Math.Round((data.TiltX/50.0), 1);
ret.Y = Math.Round((data.TiltY/50.0), 1);
ret.Z = Math.Round((data.TiltZ/50.0), 1);
return ret;
}
[/cclN_csharp]
I round off the double value to 1 digit post decimal in order to keep the readings smooth. Using it this way I was able to get fluid-like smoothness in my sensor data as well as accurate readings for “at rest” in any position. I created a couple test apps with a little ball that could bounce off the walls and make the phone vibrate when a wall was hit at a certain velocity and so forth until I was satisfied that the sensor readings were going to provide me the results I was looking to get.
My next step was to create the tilt-to-draw application. To keep it simple I decided to just stick with good ol’ fashioned bitmap objects. The key things I wanted to achieve were:
- Selectable color from a palette
- Adjustable line thickness
- Smooth drawing with grav sensor
- Ability to save your drawing to the phone
And so I began by mocking up a quick layout that I would use
I decided to make the tilt drawing aspect of it a toggle so that you could also draw with your finger / stylus if you so desired and settled on a slider for line thickness and 4 colors (red, blue, black and green). Before wiring up any of the controls I went ahead and did a test deploy to get an idea of how it was going to look and was dismayed at how the windows mobile task bar cluttered things up. First order of business…hide that task bar while the app is running!
To achieve this goal I needed to import to functions from coredll.dll and then make the proper calls within my initialization (and turn it back when the app was closing):
[cclN_csharp]
[DllImport(“coredll.dll”)]
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport(“coredll.dll”)]
private static extern IntPtr ShowWindow(IntPtr hWnd, int visible);
public SampleDrawing()
{
InitializeComponent();
ShowWindow(FindWindow(“HHTaskBar”, null), 0); //0=hide, 1=show
}
[/cclN_csharp]
With that out of the way I began wiring everything up. I’ve attached the solution at the bottom of the post so I’m going to bypass listing out the generic code and just show the pertinent bits.
You’ll notice the reference to curPos in that function. I maintained two vector objects: curPos and lastPos to enable the smooth drawing of the lines around the screen. The variables were updated either by the “mouse” being moved around the screen or the grav sensor telling it the position needed to update (for this function I also used a check to keep the paintbrush within the bounds of the screen).
[cclN_csharp]
private void SampleDrawing_MouseDown(object sender, MouseEventArgs e)
{
lastPos.X = curPos.X = ((MouseEventArgs)e).X;
lastPos.Y = curPos.Y = ((MouseEventArgs)e).Y;
}
private void SampleDrawing_MouseMove(object sender, MouseEventArgs e)
{
if (!tiltEnable.Checked && allowDrawing)
{
curPos.X = e.X;
curPos.Y = e.Y;
drawGraphics();
}
}
void CalculatePhysics(GVector gVector, int elapsedTime)
{
int maxX = ClientSize.Width – (int)myThickness;
int maxY = ClientSize.Height – 45 – (int)myThickness;
gVector = gVector.Scale(1 / friction);
if (!(curPos.X <= 0 && gVector.X < 0) && !(curPos.X >= maxX && gVector.X > 0))
{
velocity.X = gVector.X * elapsedTime / 1000;
curPos.X += velocity.X * elapsedTime / 1000;
}
if (!(curPos.Y <= 0 && gVector.Y < 0) && !(curPos.Y >= maxY && gVector.Y > 0))
{
velocity.Y = gVector.Y * elapsedTime / 1000;
curPos.Y += velocity.Y * elapsedTime / 1000;
}
}
[/cclN_csharp]
The actual drawing of the line happens the same regardless of how the cursor was moved and, to be honest, I had a bit of trouble determining an efficient method for drawing the line. I couldn’t just use the standard line methods because I wanted to be able to have variable thickness and that doesn’t mesh well with the normal line draw methods. To get around the issue of jaggy and gap-filled lines I opted to use the FillEllipse method. This gave the undesired effect of giving me variably sized dotted lines. My final solution uses a bit of generic math and a loop to draw the ellipses in a way that will create a “filled in” line. I’m certain there’s a better way to do this but darned if I was able to come up with it (Suggestions are VERY welcome).
[cclN_csharp]
void drawGraphics()
{
// get tick count so we can calculate the time elapsed since last paint
int thisPaint = Environment.TickCount;
// put the vector into screen space
if (tiltEnable.Checked)
{
GVector gvector = mySensor.GetGVector();
gvector = gvector.Scale(htcDPI*friction);
CalculatePhysics(gvector, thisPaint – lastPaint);
}
CalculateBounds();
double rise = (lastPos.Y – curPos.Y);
double run = (lastPos.X – curPos.X);
double mult = (myThickness/2);
if (Math.Abs(rise) > Math.Abs(run) && Math.Abs(rise) > mult)
mult /= Math.Abs(rise);
else if (Math.Abs(run) > mult)
mult /= Math.Abs(run);
rise *= mult;
run *= mult;
if (Math.Abs(run) > 0 || Math.Abs(rise) > 0)
{
while (((rise < 0 && lastPos.Y < curPos.Y) ||
(rise > 0 && lastPos.Y > curPos.Y)) ||
((run < 0 && lastPos.X < curPos.X) ||
(run > 0 && lastPos.X > curPos.X))
)
{
backBuffer.FillEllipse(myBrush, (int)lastPos.X, (int)lastPos.Y, (int)myThickness, (int)myThickness);
lastPos.X -= run;
lastPos.Y -= rise;
}
}
lastPos = curPos;
g.DrawImage(backBufferImage, 0, 0);
lastPaint = thisPaint;
}
[/cclN_csharp]
This worked well enough to give me a smoothly flowing line on my Fuze and provided a decent-enough response time on things that it felt like you really were drawing as you tilted the screen around. At this point I had 3 of my 4 requirements completed and just needed the ability to save out the image. Luckily this ended up being trivial due to my choice of the bitmap object for drawing. I simply wired up the save button with the following code and was set.
[cclN_csharp]
private void btnSave_Click(object sender, EventArgs e)
{
SaveFileDialog tmpDialog = new SaveFileDialog();
tmpDialog.Filter = “Bitmap (*.bmp)|*.bmp|JPEG (*.jpg)|*.jpg|GIF (*.gif)|*.gif|PING (*.png)|*.png”;
if (tmpDialog.ShowDialog() == DialogResult.OK)
{
string tmpFileName = tmpDialog.FileName;
ImageFormat tmpFormat = ImageFormat.Bmp;
switch (tmpFileName.Substring(tmpFileName.Length – 4).ToLower())
{
case “.bmp”:
tmpFormat = ImageFormat.Bmp;
break;
case “.jpg”:
case “jpeg”:
tmpFormat = ImageFormat.Jpeg;
break;
case “.gif”:
tmpFormat = ImageFormat.Gif;
break;
case “.png”:
tmpFormat = ImageFormat.Png;
break;
}
backBufferImage.Save(tmpFileName, tmpFormat);
}
if (backBufferImage != null)
{
backBufferImage.Dispose();
backBufferImage = null;
}
if (backBuffer != null)
{
backBuffer.Dispose();
backBuffer = null;
}
backBufferImage = new Bitmap(ClientSize.Width, ClientSize.Height, PixelFormat.Format16bppRgb565);
backBuffer = Graphics.FromImage(backBufferImage);
backBuffer.Clear(Color.White);
g.DrawImage(backBufferImage, 0, 0);
}
[/cclN_csharp]
I was fairly happy with the application but then the dreaded snarky comment was made: “Yeah, that’s neat, but you can’t turn it upside down and shake it to clear the screen like you can with a real etch-a-sketch.” I hate snark with a deep fiery passion…ok, that’s a lie I absolutely love it, and I also enjoy new ideas. I added in the feature of turning it upside down and “shaking it” to clear the screen. I put “shaking it” in quotes because, well, you don’t really have to shake it, you just have to turn it upside down (please don’t tell).
[cclN_csharp]
void mySensor_OrientationChanged(IGSensor sender)
{
if (sender.Orientation == ScreenOrientation.FaceDown)
{
btnClear_Click(null, null);
allowDrawing = false;
}
else if (!allowDrawing)
{
allowDrawing = true;
curPos = lastPos = new GVector(ClientSize.Width / 2, ClientSize.Height / 2, 0);
lastPaint = Environment.TickCount;
}
}
[/cclN_csharp]
And there you have it. A cute windows mobile app that will let you draw around on the screen to your heart’s content and save out your beautiful creations to send as an MMS to friends and family the world over. As promised I’ve attached the entire solution for the project here. It includes my compiled version of the Sensors dll. If you’d like to get a copy of the DLL source code yourself you can grab it from the links at the top of the post and tweak it til you go blind.