共用方式為


Phone magic

If you've never come across Scam School before, do take a look at it - it normally offers a bit of entertainment on a Thursday. One in particular caught my eye recently: there have been a few episodes using smart phones as props in magic tricks, but in this one Greg Rostami does something clever with the accelerometer. He's got a bunch of cunning apps there but, alas, not for Windows Phone - so I thought I's create a vastly simplified and minimalist version of his iForce for WP. I'm deliberately making it very minimal to not step on his toes, and because I'm primarily interested in the core of the app, handling the accelerometer data.

The basis of the trick is to get someone to think of a number from 1 to 8; you run a drawing app on the phone and doodle a prediction, then place the phone face down; ask the person for their prediction and you turn the phone face up to show the same number. The way it works is that the drawing app is much more than that: when the phone is rotated from face down to up, the direction and speed of rotation causes the app to choose one of 8 images to show. I do feel a bit guilty at giving away the secret of the magic trick but Greg and Brian Brushwood pretty much completely did that in the Scam School episode anyway!

The root of the app, as mentioned, is the accelerometer and the easiest way to see how it works is to grab the demo code from the MSDN samples site. If you run that app, you'll see that it reads X=0, Y=0, Z=1 when you hold the phone face down, as shown below:

Actually, you'll see values close to 1 and 0 - there will be some noise. If you tilt the phone along the long axis, you'll get X=1 (or -1 depending on which direction you tilt) and Y=0, Z=0, as shown below:

 

As you might expect, while transitioning from horizontal to vertical, Z will decrease and X (or Y, if tilting along the short axis) will get further from 0. All I need to do in this app is note which axis is increasing and in which direction as Z changes from close to 1 to a little bit from 0 - I want to make sure I've made my choice well before Z reaches 0, so that the victim can't see an incorrect screen. If you've been paying attention, you'll note that this gives only four values (positive X, positive Y and the two negatives): I also look at the speed of the transition - fast or slow - to give an additional four. Incidentally, I've no idea how the details here match up with iForce - I've not seen the source for that, nor have I looked any more closely at the app than what appeared in the video mentioned above. I've just thrown together something that kinda does the same job.

The accelerometer is started with:

 this.accelerometer.CurrentValueChanged += Accelerometer_CurrentValueChanged;
this.accelerometer.Start();

The callback is where the, er, magic happens:

         void Accelerometer_CurrentValueChanged(object sender, SensorReadingEventArgs<AccelerometerReading> e)
        {
            this.accumulator = this.accumulator * 0.8f + e.SensorReading.Acceleration * 0.2f;
            if (accumulator.Z > 0.9f)
            {
                this.startToMove = DateTime.Now;
                this.started = true;
            }
            else if (accumulator.Z < 0.2f && this.started)
            {
                done = true;
                bool isLong = (DateTime.Now - this.startToMove).TotalSeconds > 1.0;
                var scores = new double[] { -accumulator.Y, accumulator.Y, accumulator.X, -accumulator.X };
                var best = 0;
                for (int i = 1; i < scores.Length; ++i)
                    if (scores[i] > scores[best])
                        best = i;
                if (isLong) best += 4;
                this.started = false;
                this.Dispatcher.BeginInvoke(() =>
                {
                    // SET DISPLAY TO <best>
                });
            }
        }

After applying a simple bit of smoothing to reduce the effect of noise, there are two phases: if the Z value is close to 1, assume the phone is face down and store the current time (as startToMove); if the Z value is close-ish to 0, work out which of the 4 directions of rotation happened, simply by looking for the maximum magnitude of the X and Y readings, and look at the time taken to get from face down to almost vertical. While I could pre-can 8 images and select one at the end of that callback, the rest of this short application deals with collecting 8 doodles from the user and persisting them across runs of the application.

The obvious mechanism to use is the InkPresenter. I capture "mouse" down (i.e., finger touching the screen) and moves to capture drawing actions, and then convert those to stroke data to store on the ink presenter:

 private void Ink_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    this.Ink.CaptureMouse();
 
    this.stroke = new Stroke();
    this.stroke.StylusPoints.Add(e.StylusDevice.GetStylusPoints(this.Ink));
    this.stroke.DrawingAttributes.Color = System.Windows.Media.Colors.White;
    this.stroke.DrawingAttributes.Width = 3.0;
    this.Ink.Strokes.Add(this.stroke);
}
 
private void Ink_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
    if (this.stroke != null)
        this.stroke.StylusPoints.Add(e.StylusDevice.GetStylusPoints(this.Ink));
}
 
private void Ink_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    this.stroke = null;
    this.Ink.ReleaseMouseCapture();
}

(Ink is the InkPresenter, stroke is a Stroke object which is added to the StrokeCollection rendered on the presenter.)

The app has an edit page which iterates over 8 drawings, thus giving me a collection of the 8 stroke collections to be displayed. The final lines of the accelerometer change routine above select the appropriate set of strokes and loads them into the ink presenter.

The final bit of polish on the app is to persist the stroke data, instead of requiring the user to do the 8 drawings every time. Alas the stroke collection class is not itself serializable, so I need to convert to some string format to persist in phone storage. Being lazy, I let the XmlSerializer handle the final conversion, and just convert the 8 stroke collections into a multi dimensional array of points (an array of stroke collections, each of which is an array of strokes, each of which is an array of points - I'm ignoring stroke attributes such as colour and thickness here).

         internal void Save()
        {
            var pts = new Point[this.strokes.Length][][];
            for (int i = 0; i < this.strokes.Length; ++i)
            {
                var sc = this.strokes[i];
                pts[i] = new Point[sc.Count][];
                for (int j = 0; j < sc.Count; ++j)
                    pts[i][j] = sc[j].StylusPoints.Select(p => new Point(p.X, p.Y)).ToArray();
            }
            var ser = new XmlSerializer(pts.GetType());
            using (var s = IsolatedStorageFile.GetUserStoreForApplication().CreateFile("Strokes"))
            {
                ser.Serialize(s, pts);
            }
        }

        internal void Load()
        {
            try
            {
                using (var s = IsolatedStorageFile.GetUserStoreForApplication().OpenFile("Strokes", FileMode.Open))
                {
                    var ser = new XmlSerializer(typeof(Point[][][]));
                    var pts = (Point[][][])ser.Deserialize(s);
                    this.strokes = new StrokeCollection[pts.GetLength(0)];
                    for(int i = 0; i < strokes.Length; ++i)
                    {
                        var sc = new StrokeCollection();
                        foreach(var s in pts[i])
                        {
                           var stroke = new Stroke();
                            stroke.DrawingAttributes.Color = System.Windows.Media.Colors.White;
                            stroke.DrawingAttributes.Width = 3.0;
                            foreach (var p in s)
                                stroke.StylusPoints.Add(new StylusPoint(p.X, p.Y));
                            sc.Add(stroke);
                        }
                        this.strokes[i] = sc;
                    }
                }

            }
            catch
            {
                this.strokes = new StrokeCollection[8];
                for (int i = 0; i < 8; ++i)
                    this.strokes[i] = new StrokeCollection();
            }
        }

If loading fails (as it will the first time the app is run, for example), an empty set of stroke data is created.

So there you have it - a very poor sibling to Greg's iForce, but functional. (I'll put it in the store soon, and will update this article with a pointer.)