1package com.android.dreamtheater;
2
3import android.animation.PropertyValuesHolder;
4import android.animation.TimeAnimator;
5import android.app.Activity;
6import android.content.Context;
7import android.content.Intent;
8import android.graphics.Canvas;
9import android.graphics.Matrix;
10import android.graphics.Paint;
11import android.graphics.RectF;
12import android.os.Bundle;
13import android.util.AttributeSet;
14import android.util.Log;
15import android.view.MotionEvent;
16import android.view.View;
17import android.view.Gravity;
18import android.view.ViewGroup;
19import android.widget.Button;
20import android.widget.FrameLayout;
21import android.widget.ImageView;
22
23import java.util.LinkedList;
24import java.util.HashMap;
25
26public class BouncyDroid extends Activity {
27    static final boolean DEBUG = true;
28    static final boolean CENTER_DROID = true;
29
30    public static class BouncyView extends FrameLayout
31    {
32        boolean mShowDebug = false;
33
34        static final int RADIUS = 100;
35
36        static final boolean HAS_INITIAL_IMPULSE = true;
37        static final boolean HAS_GRAVITY = true;
38        static final boolean HAS_FRICTION = false;
39        static final boolean HAS_EDGES = true;
40
41        static final boolean STICKY_FINGERS = true;
42
43        static final float MAX_SPEED = 5000f;
44
45        static final float RANDOM_IMPULSE_PROB = 0.001f;
46
47        public static class World {
48            public static final float PX_PER_METER = 100f;
49            public static final float GRAVITY = 500f;
50            public static class Vec {
51                float x;
52                float y;
53                public Vec() {
54                    x = y = 0;
55                }
56                public Vec(float _x, float _y) {
57                    x = _x;
58                    y = _y;
59                }
60                public Vec add(Vec v) {
61                    return new Vec(x + v.x, y + v.y);
62                }
63                public Vec mul(float a) {
64                    return new Vec(x * a, y * a);
65                }
66                public Vec sub(Vec v) {
67                    return new Vec(x - v.x, y - v.y);
68                }
69                public float mag() {
70                    return (float) Math.hypot(x, y);
71                }
72                public Vec norm() {
73                    float k = 1/mag();
74                    return new Vec(x*k, y*k);
75                }
76                public String toString() {
77                    return "(" + x + "," + y + ")";
78                }
79            }
80            public static class Body {
81                float m, r;
82                Vec p = new Vec();
83                Vec v = new Vec();
84                LinkedList<Vec> forces = new LinkedList<Vec>();
85                LinkedList<Vec> impulses = new LinkedList<Vec>();
86                public Body(float _m, Vec _p) {
87                    m = _m;
88                    p = _p;
89                }
90                public void applyForce(Vec f) {
91                    forces.add(f);
92                }
93                public void applyImpulse(Vec f) {
94                    impulses.add(f);
95                }
96                public void clearForces() {
97                    forces.clear();
98                }
99                public void removeForce(Vec f) {
100                    forces.remove(f);
101                }
102                public void step(float dt) {
103                    p = p.add(v.mul(dt));
104                    for (Vec f : impulses) {
105                        v = v.add(f.mul(dt/m));
106                    }
107                    impulses.clear();
108                    for (Vec f : forces) {
109                        v = v.add(f.mul(dt/m));
110                    }
111                }
112                public String toString() {
113                    return "Body(m=" + m + " p=" + p + " v=" + v + ")";
114                }
115            }
116            LinkedList<Body> mBodies = new LinkedList<Body>();
117            public void addBody(Body b) {
118                mBodies.add(b);
119            }
120
121            public void step(float dt) {
122                for (Body b : mBodies) {
123                    b.step(dt);
124                }
125            }
126        }
127
128
129        TimeAnimator mAnim;
130        World mWorld;
131        ImageView mBug;
132        View mShowDebugView;
133        HashMap<Integer, World.Vec> mFingers = new HashMap<Integer, World.Vec>();
134        World.Body mBody;
135        World.Vec mGrabSpot;
136        int mGrabbedPointer = -1;
137
138        public BouncyView(Context context, AttributeSet as) {
139            super(context, as);
140
141            /*
142            setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
143                @Override
144                public void onSystemUiVisibilityChange(int visibility) {
145                    if (visibility == View.STATUS_BAR_VISIBLE) {
146                        ((Activity)getContext()).finish();
147                    }
148                }
149            });
150            */
151
152            setBackgroundColor(0xFF444444);
153
154            mBug = new ImageView(context);
155            mBug.setScaleType(ImageView.ScaleType.MATRIX);
156            addView(mBug, new ViewGroup.LayoutParams(
157                        ViewGroup.LayoutParams.WRAP_CONTENT,
158                        ViewGroup.LayoutParams.WRAP_CONTENT));
159
160            if (DEBUG) {
161                Button b = new Button(getContext());
162                b.setText("Debugzors");
163                b.setBackgroundColor(0); // very hard to see! :)
164                b.setOnClickListener(new View.OnClickListener() {
165                    @Override
166                    public void onClick(View v) {
167                        setDebug(!mShowDebug);
168                    }
169                });
170                addView(b, new FrameLayout.LayoutParams(
171                            ViewGroup.LayoutParams.WRAP_CONTENT,
172                            ViewGroup.LayoutParams.WRAP_CONTENT,
173                            Gravity.TOP|Gravity.RIGHT));
174            }
175        }
176
177        public void setDebug(boolean d) {
178            if (d != mShowDebug) {
179                if (d) {
180                    mShowDebugView = new DebugView(getContext());
181                    mShowDebugView.setLayoutParams(
182                        new ViewGroup.LayoutParams(
183                            ViewGroup.LayoutParams.MATCH_PARENT,
184                            ViewGroup.LayoutParams.MATCH_PARENT
185                        ));
186                    addView(mShowDebugView);
187
188                    mBug.setBackgroundColor(0x2000FF00);
189                } else {
190                    if (mShowDebugView != null) {
191                        removeView(mShowDebugView);
192                        mShowDebugView = null;
193                    }
194                    mBug.setBackgroundColor(0);
195                }
196                invalidate();
197                mShowDebug = d;
198            }
199        }
200
201        private void reset() {
202            mWorld = new World();
203            final float mass = 100;
204            mBody = new World.Body(mass, new World.Vec(200,200));
205            mBody.r = RADIUS;
206            mWorld.addBody(mBody);
207            mGrabbedPointer = -1;
208
209            mAnim = new TimeAnimator();
210            mAnim.setTimeListener(new TimeAnimator.TimeListener() {
211                public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
212                    if (deltaTime > 0) {
213                        int STEPS = 5;
214                        final float dt = deltaTime / (float) STEPS;
215                        while (STEPS-->0) {
216                            mBody.clearForces();
217
218                            if (HAS_INITIAL_IMPULSE) {
219                                // initial oomph
220                                if (totalTime == 0) {
221                                    mBody.applyImpulse(new World.Vec(400000, -200000));
222                                }
223                            }
224
225                            if (HAS_GRAVITY) {
226                                // gravity points down
227                                mBody.applyForce(new World.Vec(0, mass * World.GRAVITY));
228                            }
229
230                            if (mGrabbedPointer >= 0) {
231                                World.Vec finger = mFingers.get(mGrabbedPointer);
232                                if (finger == null) {
233                                    // let go!
234                                    mGrabbedPointer = -1;
235                                } else {
236                                    // never gonna let you go
237                                    World.Vec newPos = finger.add(mGrabSpot);
238                                    mBody.v = mBody.v.add(newPos.sub(mBody.p).mul(dt));
239                                    mBody.p = newPos;
240                                }
241                            } else {
242                                // springs
243                                // ideal Hooke's Law plus a maximum force and a minimum length cutoff
244                                for (Integer i : mFingers.keySet()) {
245                                    World.Vec finger = mFingers.get(i);
246                                    World.Vec springForce = finger.sub(mBody.p);
247                                    float mag = springForce.mag();
248
249                                    if (STICKY_FINGERS && mag < mBody.r*0.75) {
250                                        // close enough; we'll call this a "stick"
251                                        mGrabbedPointer = i;
252                                        mGrabSpot = mBody.p.sub(finger);
253                                        mBody.v = new World.Vec(0,0);
254                                        break;
255                                    }
256
257                                    final float SPRING_K = 30000;
258                                    final float FORCE_MAX = 10*SPRING_K;
259                                    mag = (float) Math.min(mag * SPRING_K, FORCE_MAX); // Hooke's law
260            //                    float mag = (float) (FORCE_MAX / Math.pow(springForce.mag(), 2)); // Gravitation
261                                    springForce = springForce.norm().mul(mag);
262                                    mBody.applyForce(springForce);
263                                }
264                            }
265
266                            if (HAS_FRICTION) {
267                                // sliding friction opposes movement
268                                mBody.applyForce(mBody.v.mul(-0.01f * mBody.m));
269                            }
270
271                            if (HAS_EDGES) {
272                                if (mBody.p.x - mBody.r < 0) {
273                                    mBody.v.x = (float) Math.abs(mBody.v.x) *
274                                        (HAS_FRICTION ? 0.95f : 1f);
275                                } else if (mBody.p.x + mBody.r > getWidth()) {
276                                    mBody.v.x = (float) Math.abs(mBody.v.x) *
277                                        (HAS_FRICTION ? -0.95f : -1f);
278                                }
279                                if (mBody.p.y - mBody.r < 0) {
280                                    mBody.v.y = (float) Math.abs(mBody.v.y) *
281                                        (HAS_FRICTION ? 0.95f : 1f);
282                                } else if (mBody.p.y + mBody.r > getHeight()) {
283                                    mBody.v.y = (float) Math.abs(mBody.v.y) *
284                                        (HAS_FRICTION ? -0.95f : -1f);
285                                }
286                            }
287
288                            if (MAX_SPEED > 0) {
289                                if (mBody.v.mag() > MAX_SPEED) {
290                                    mBody.v = mBody.v.norm().mul(MAX_SPEED);
291                                }
292                            }
293
294                            // ok, Euler, do your thing
295                            mWorld.step(dt / 1000f); // dt is in sec
296                        }
297                    }
298                    mBug.setTranslationX(mBody.p.x - mBody.r);
299                    mBug.setTranslationY(mBody.p.y - mBody.r);
300
301                    Matrix m = new Matrix();
302                    m.setScale(
303                        (mBody.v.x < 0)    ? -1 : 1,
304                        (mBody.v.y > 1500) ? -1 : 1, // AAAAAAAAAAAAAAAA
305                        RADIUS, RADIUS);
306                    mBug.setImageMatrix(m);
307                    if (CENTER_DROID) {
308                        mBug.setImageResource(
309                            (Math.abs(mBody.v.x) < 25)
310                                ? R.drawable.bouncy_center
311                                : R.drawable.bouncy);
312                    }
313
314                    if (mShowDebug) mShowDebugView.invalidate();
315                }
316            });
317        }
318
319        @Override
320        protected void onAttachedToWindow() {
321            super.onAttachedToWindow();
322            setSystemUiVisibility(View.STATUS_BAR_HIDDEN);
323
324            reset();
325            mAnim.start();
326        }
327
328        @Override
329        protected void onDetachedFromWindow() {
330            super.onDetachedFromWindow();
331            mAnim.cancel();
332        }
333
334        @Override
335        public boolean onTouchEvent(MotionEvent event) {
336            int i;
337            for (i=0; i<event.getPointerCount(); i++) {
338                switch (event.getActionMasked()) {
339                    case MotionEvent.ACTION_DOWN:
340                    case MotionEvent.ACTION_MOVE:
341                    case MotionEvent.ACTION_POINTER_DOWN:
342                        mFingers.put(event.getPointerId(i),
343                                new World.Vec(event.getX(i), event.getY(i)));
344                        break;
345
346                    case MotionEvent.ACTION_UP:
347                    case MotionEvent.ACTION_POINTER_UP:
348                        mFingers.remove(event.getPointerId(i));
349                        break;
350
351                    case MotionEvent.ACTION_CANCEL:
352                        mFingers.clear();
353                        break;
354                }
355            }
356            // expired pointers
357    //        for (; i<mFingers.length; i++) {
358    //            mFingers[i] = null;
359    //        }
360            return true;
361        }
362
363        @Override
364        public boolean isOpaque() {
365            return true;
366        }
367
368        class DebugView extends View {
369            public DebugView(Context ct) {
370                super(ct);
371            }
372
373            private void drawVector(Canvas canvas,
374                    float x, float y, float vx, float vy,
375                    Paint pt) {
376                final float mag = (float) Math.hypot(vx, vy);
377
378                canvas.save();
379                Matrix mx = new Matrix();
380                mx.setSinCos(-vx/mag, vy/mag);
381                mx.postTranslate(x, y);
382                canvas.setMatrix(mx);
383
384                canvas.drawLine(0,0, 0, mag, pt);
385                canvas.drawLine(0, mag, -4, mag-4, pt);
386                canvas.drawLine(0, mag, 4, mag-4, pt);
387
388                canvas.restore();
389            }
390
391            @Override
392            protected void onDraw(Canvas canvas) {
393                super.onDraw(canvas);
394
395                Paint pt = new Paint(Paint.ANTI_ALIAS_FLAG);
396                pt.setColor(0xFFCC0000);
397                pt.setTextSize(30f);
398                pt.setStrokeWidth(1.0f);
399
400                for (Integer id : mFingers.keySet()) {
401                    World.Vec v = mFingers.get(id);
402                    float x = v.x;
403                    float y = v.y;
404                    pt.setStyle(Paint.Style.FILL);
405                    canvas.drawText("#"+id, x+38, y-38, pt);
406                    pt.setStyle(Paint.Style.STROKE);
407                    canvas.drawLine(x-40, y, x+40, y, pt);
408                    canvas.drawLine(x, y-40, x, y+40, pt);
409                    canvas.drawCircle(x, y, 40, pt);
410                }
411                pt.setStyle(Paint.Style.STROKE);
412                if (mBody != null) {
413                    float x = mBody.p.x;
414                    float y = mBody.p.y;
415                    float r = mBody.r;
416                    pt.setColor(0xFF6699FF);
417                    RectF bounds = new RectF(x-r, y-r, x+r, y+r);
418                    canvas.drawOval(bounds, pt);
419
420                    pt.setStrokeWidth(3);
421                    drawVector(canvas, x, y, mBody.v.x/100, mBody.v.y/100, pt);
422
423                    pt.setColor(0xFF0033FF);
424                    for (World.Vec f : mBody.forces) {
425                        drawVector(canvas, x, y, f.x/1000, f.y/1000, pt);
426                    }
427                }
428            }
429        }
430    }
431
432    @Override
433    public void onStart() {
434        super.onStart();
435        setContentView(new BouncyView(this, null));
436    }
437}
438