Friday, February 10, 2012

Accelerometer Demo

My accelerometer blog ended rather abruptly, without a demo program so today I’m returning to the topic.

To test the accelerometer control I wrote a very simple program in which you can move a rectangle around the screen by tilting your iOS device. You can test it here.

This program builds heavily upon the source code I found on Alberto Sarullo’s blog. Thanks!

The Form

FCount counts number of times the ‘motion change’ event has been called. FMotion contains the TGpW3Motion control. FRect is used to display the bouncing rectangle on the screen. FRectColor is the color of this rectangle. FRectText is the text displayed inside the rectangle. FvX and FvY are rectangle speeds in horizontal and vertical (relative to the current display orientation) direction, respectively.

type
TFormMain=class(TW3form)
private
FCount: integer;
FMotion: TGpW3Motion;
FRect: TW3GraphicControl;
FRectColor: TColor;
FRectText: string;
FvX, FvY: float;
protected
function BounceX(x: float): float;
function BounceY(y: float): float;
procedure HandleMotionChange(Sender: TObject; info: TW3MotionData);
procedure InitializeObject;override;
procedure LogMotion(info: TW3MotionData);
procedure PaintRect(sender:TObject;Canvas:TW3Canvas);
procedure RandomizeRectColor;
procedure Resize; override;
procedure StyleTagObject;override;
end;

Basic Stuff

Initialization is pretty standard. Components are created, events hooked up and initial color for the rectangle is set.

procedure TFormMain.InitializeObject;
begin
inherited;
RandomizeRectColor;
FMotion := TGpW3Motion.Create(Self);
FMotion.OnChange := HandleMotionChange;
FRect := TW3GraphicControl.Create(Self);
FRect.OnPaint := PaintRect;
end;

Nothing special happens in Resize, either. Rectangle is positioned in the middle of the screen and its velocity is set to zero.

procedure TFormMain.Resize;
begin
inherited;
FvX := 0; FvY := 0;
FRect.Width := 200;
FRect.Height := 200;
FRect.Top := (Height - FRect.Height) div 2;
FRect.Left:= (Width - FRect.Width) div 2;
FRect.Invalidate;
end;

RandomizeRectColor chooses RGB components which are random but all on the bright side so the rectangle will be of a slightly tinted light greyish tone.

procedure TFormMain.RandomizeRectColor;
begin
FRectColor := RGBToColor(255 - Round(Random*35), 255 - Round(Random*35),
255 - Round(Random*35));
end;

And that concludes the boring part.

Handling the Motion Event

TGpW3Motion.OnChange event does three distinct things. First it updates the velocity regarding the current acceleration (the speed calculation comes straight from the physics – v2 = v1 + a * t), then it logs the information about the motion data object and at the end it updates the rectangle coordinates.

Updating deserves more detailed description. I’ve wrapped it into BeginUpdate/EndUpdate calls to prevent rectangle from being redrawn twice. Speed is converted into distance (s2 = s1 + v * t) with an additional speedup factor. This factor was experimentally chosen so the application behaves nicely and responsively. Resulting offset is passed through the bounce function which bounces the rectangle off the edge of the screen.

procedure TFormMain.HandleMotionChange(Sender: TObject; info: TW3MotionData);
const
CSpeedUp = 200;
var
x, y: integer;
begin
FvX := FvX + info.DisplayAccel.Horizontal * info.Interval;
FvY := FvY + info.DisplayAccel.Vertical * info.Interval;

LogMotion(info);

FRect.BeginUpdate;
FRect.Left := Round(BounceX(FRect.Left + FvX * info.Interval * CSpeedUp));
FRect.Top := Round(BounceY(FRect.Top + FvY * info.Interval * CSpeedUp));
FRect.EndUpdate;
end;

Bouncy, Bouncy!

There are two bounce functions – one handling the horizontal edges and another the verticals – but I’ll only show one because they are pretty much the same.

function TFormMain.BounceX(x: float): float;
begin
Result := x;
if Result < 0 then
Result := -Result
else if Result > (Width - FRect.Width) then
Result := 2*(Width - FRect.Width) - Result
else
Exit;
FvX := -FvX*0.8;
RandomizeRectColor;
end;

If the rectangle falls at least partially off the edge of the screen, its position is moved back into the display area (by as much as it has fallen off the screen) and speed is reduced by 20%. Again, this is an arbitrary number which I choose purely for the effect. You could do anything here – stop the rectangle in place by setting the velocity to 0 (or both velocity components to 0) or you could simulate an active edge by giving the rectangle an additional push by increasing the velocity.

Logging

LogMotion method merely sets a very long string which uses <br/> tags for line breaks. The reason for this is that initially I was displaying motion data with a TW3Label component which handles <br/> nicely.

procedure TFormMain.LogMotion(info: TW3MotionData);
begin
Inc(FCount);
FRectText := Format('%d: %.2f<br/>x:%.4f y:%.4f z:%.4f<br/>' +
'x:%.4fg y:%.4fg z:%.4fg<br/>'+
'a:%.4f b:%.4f g:%.4f<br/><br/>' +
'h:%.4f v:%.4f<br/><br/>' +
'vx:%.4f vy:%.4f',
[FCount, info.Interval,
info.Acceleration.X, info.Acceleration.Y, info.Acceleration.Z,
info.AccelInclGravity.X, info.AccelInclGravity.Y, info.AccelInclGravity.Z,
info.RotationRate.Alpha, info.RotationRate.Beta, info.RotationRate.Gamma,
info.DisplayAccel.Horizontal, info.DisplayAccel.Vertical,
FvX, FvY]);
end;

Painting

PaintRect method paints the rectangle. First it fills it with the current color and then it displays the FRectText variable. There are some complication, though, and they are caused not by Smart but by the HTML5 canvas.

The only way to draw text on the HTML5 canvas is the FillText function and it doesn’t handle multiline text. To circumvent this limitation, PaintRect uses w3_StrToArray built-in function to split the text into an array of lines and then paints lines one by one.

procedure TFormMain.PaintRect(sender:TObject;Canvas:TW3Canvas);
var
firstY: integer;
graph: TW3GraphicControl;
iText: integer;
text: TStringArray;
begin
graph := sender as TW3GraphicControl;
Canvas.FillStyle := ColorToWebStr(FRectColor);
Canvas.FillRectF(0, 0, Width, Height);

Canvas.TextAlign := 'center';
Canvas.TextBaseLine := 'middle';
Canvas.FillStyle := '#000000';
Canvas.Font := '12px sans-serif';
text := w3_StrToArray(FRectText, '<br/>');
firstY := Round((graph.Height / 2) - 15 * ((text.Length-1) / 2));
for iText := 0 to text.Length - 1 do
Canvas.FillTextF(text[iText], graph.Width div 2, firstY + 15 * iText, 0);
end;

And that’s how it looks on your screen.

image

Further Development

If you try out the demo, you’ll see that the data coming from the accelerometer is very jittery. If you put the device on a perfectly flat surface, you won’t get zero from the accelerometer but the value will jump around zero, sometimes it will be positive and sometimes it will be negative. To reduce the impact of this jitter on your application I intend to implement a smoothing algorithm in the TGpW3Motion component.

Another planned change is the calibration capability. iOS offers no support for accelerometer calibration so you had to do it in your code. The procedure is simple – just add (subtract) the ‘would be zero’ acceleration values to (from) the accelerometer data – but it would be even simpler if the component would handle it.

4 comments:

  1. Anonymous10:10

    Impressive! tested on 1st Gen 1Pad.

    ReplyDelete
  2. Works on an iPad2 as well.. if you rotate the screen too much, the orientation of the browser window changes, but I guess that cannot be controlled.

    Cool demo though.

    ReplyDelete
  3. @Wouter, that cannot be controlled from the JavaScript but you can lock device orientation with the button on the iPad.

    ReplyDelete
  4. These are two different effects and event stacks. When the orientation changes javascript actually fires a resize event and an orientation event (but safari has further event callbacks which are extentions to the webkit base). In the preliminary RTL i designed i opted to capture the "basic" events. This will ofcourse be altered to include as much as possible in the final release - much thank to Primoz's brilliance on the topic. So when i update the RTL i hope to add some compiler switches that makes the most of both android, chrome and safari mobile. So unless Apple have disabled the "requestFullScreen" method with the parameter for orientation (to lock the orientation like native apps do) we should be able to handle it.

    Worst case scenario and i'll fork phonegap and provide it myself.

    I must say we have been extremely lucky to have Primoz on the test team. I dont think I have ever seen a programmer absorb so much information and adapting so quickly to new territory as Primoz.

    And as always you take on new technology with such eye for details and structure -that it's an inspiration for the rest of us.

    /Lennart

    ReplyDelete