1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 * except in compliance with the License. You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the
10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11 * KIND, either express or implied. See the License for the specific language governing
12 * permissions and limitations under the License.
13 */
14
15package com.android.egg.octo;
16
17import android.animation.TimeAnimator;
18import android.content.Context;
19import android.graphics.Canvas;
20import android.graphics.ColorFilter;
21import android.graphics.DashPathEffect;
22import android.graphics.Matrix;
23import android.graphics.Paint;
24import android.graphics.Path;
25import android.graphics.PixelFormat;
26import android.graphics.PointF;
27import android.graphics.Rect;
28import android.graphics.drawable.Drawable;
29import android.support.animation.DynamicAnimation;
30import android.support.animation.SpringForce;
31import android.support.annotation.NonNull;
32import android.support.annotation.Nullable;
33import android.support.animation.SpringAnimation;
34import android.support.animation.FloatValueHolder;
35
36public class OctopusDrawable extends Drawable {
37    private static float BASE_SCALE = 100f;
38    public static boolean PATH_DEBUG = false;
39
40    private static int BODY_COLOR   = 0xFF101010;
41    private static int ARM_COLOR    = 0xFF101010;
42    private static int ARM_COLOR_BACK = 0xFF000000;
43    private static int EYE_COLOR    = 0xFF808080;
44
45    private static int[] BACK_ARMS = {1, 3, 4, 6};
46    private static int[] FRONT_ARMS = {0, 2, 5, 7};
47
48    private Paint mPaint = new Paint();
49    private Arm[] mArms = new Arm[8];
50    final PointF point = new PointF();
51    private int mSizePx = 100;
52    final Matrix M = new Matrix();
53    final Matrix M_inv = new Matrix();
54    private TimeAnimator mDriftAnimation;
55    private boolean mBlinking;
56    private float[] ptmp = new float[2];
57    private float[] scaledBounds = new float[2];
58
59    public static float randfrange(float a, float b) {
60        return (float) (Math.random()*(b-a) + a);
61    }
62    public static float clamp(float v, float a, float b) {
63        return v<a?a:v>b?b:v;
64    }
65
66    public OctopusDrawable(Context context) {
67        float dp = context.getResources().getDisplayMetrics().density;
68        setSizePx((int) (100*dp));
69        mPaint.setAntiAlias(true);
70        for (int i=0; i<mArms.length; i++) {
71            final float bias = (float)i/(mArms.length-1) - 0.5f;
72            mArms[i] = new Arm(
73                    0,0, // arm will be repositioned on moveTo
74                    10f*bias + randfrange(0,20f), randfrange(20f,50f),
75                    40f*bias+randfrange(-60f,60f), randfrange(30f, 80f),
76                    randfrange(-40f,40f), randfrange(-80f,40f),
77                    14f, 2f);
78        }
79    }
80
81    public void setSizePx(int size) {
82        mSizePx = size;
83        M.setScale(mSizePx/BASE_SCALE, mSizePx/BASE_SCALE);
84        // TaperedPathStroke.setMinStep(20f*BASE_SCALE/mSizePx); // nice little floaty circles
85        TaperedPathStroke.setMinStep(8f*BASE_SCALE/mSizePx); // classic tentacles
86        M.invert(M_inv);
87    }
88
89    public void startDrift() {
90        if (mDriftAnimation == null) {
91            mDriftAnimation = new TimeAnimator();
92            mDriftAnimation.setTimeListener(new TimeAnimator.TimeListener() {
93                float MAX_VY = 35f;
94                float JUMP_VY = -100f;
95                float MAX_VX = 15f;
96                private float ax = 0f, ay = 30f;
97                private float vx, vy;
98                long nextjump = 0;
99                long unblink = 0;
100                @Override
101                public void onTimeUpdate(TimeAnimator timeAnimator, long t, long dt) {
102                    float t_sec = 0.001f * t;
103                    float dt_sec = 0.001f * dt;
104                    if (t > nextjump) {
105                        vy = JUMP_VY;
106                        nextjump = t + (long) randfrange(5000, 10000);
107                    }
108                    if (unblink > 0 && t > unblink) {
109                        setBlinking(false);
110                        unblink = 0;
111                    } else if (Math.random() < 0.001f) {
112                        setBlinking(true);
113                        unblink = t + 200;
114                    }
115
116                    ax = (float) (MAX_VX * Math.sin(t_sec*.25f));
117
118                    vx = clamp(vx + dt_sec * ax, -MAX_VX, MAX_VX);
119                    vy = clamp(vy + dt_sec * ay, -100*MAX_VY, MAX_VY);
120
121                    // oob check
122                    if (point.y - BASE_SCALE/2 > scaledBounds[1]) {
123                        vy = JUMP_VY;
124                    } else if (point.y + BASE_SCALE < 0) {
125                        vy = MAX_VY;
126                    }
127
128                    point.x = clamp(point.x + dt_sec * vx, 0, scaledBounds[0]);
129                    point.y = point.y + dt_sec * vy;
130
131                    repositionArms();
132               }
133            });
134        }
135        mDriftAnimation.start();
136    }
137
138    public void stopDrift() {
139        mDriftAnimation.cancel();
140    }
141
142    @Override
143    public void onBoundsChange(Rect bounds) {
144        final float w = bounds.width();
145        final float h = bounds.height();
146
147        lockArms(true);
148        moveTo(w/2, h/2);
149        lockArms(false);
150
151        scaledBounds[0] = w;
152        scaledBounds[1] = h;
153        M_inv.mapPoints(scaledBounds);
154    }
155
156    // real pixel coordinates
157    public void moveTo(float x, float y) {
158        point.x = x;
159        point.y = y;
160        mapPointF(M_inv, point);
161        repositionArms();
162    }
163
164    public boolean hitTest(float x, float y) {
165        ptmp[0] = x;
166        ptmp[1] = y;
167        M_inv.mapPoints(ptmp);
168        return Math.hypot(ptmp[0] - point.x, ptmp[1] - point.y) < BASE_SCALE/2;
169    }
170
171    private void lockArms(boolean l) {
172        for (Arm arm : mArms) {
173            arm.setLocked(l);
174        }
175    }
176    private void repositionArms() {
177        for (int i=0; i<mArms.length; i++) {
178            final float bias = (float)i/(mArms.length-1) - 0.5f;
179            mArms[i].setAnchor(
180                    point.x+bias*30f,point.y+26f);
181        }
182        invalidateSelf();
183    }
184
185    private void drawPupil(Canvas canvas, float x, float y, float size, boolean open,
186            Paint pt) {
187        final float r = open ? size*.33f : size * .1f;
188        canvas.drawRoundRect(x - size, y - r, x + size, y + r, r, r, pt);
189    }
190
191    @Override
192    public void draw(@NonNull Canvas canvas) {
193        canvas.save();
194        {
195            canvas.concat(M);
196
197            // arms behind
198            mPaint.setColor(ARM_COLOR_BACK);
199            for (int i : BACK_ARMS) {
200                mArms[i].draw(canvas, mPaint);
201            }
202
203            // head/body/thing
204            mPaint.setColor(EYE_COLOR);
205            canvas.drawCircle(point.x, point.y, 36f, mPaint);
206            mPaint.setColor(BODY_COLOR);
207            canvas.save();
208            {
209                canvas.clipOutRect(point.x - 61f, point.y + 8f,
210                        point.x + 61f, point.y + 12f);
211                canvas.drawOval(point.x-40f,point.y-60f,point.x+40f,point.y+40f, mPaint);
212            }
213            canvas.restore();
214
215            // eyes
216            mPaint.setColor(EYE_COLOR);
217            if (mBlinking) {
218                drawPupil(canvas, point.x - 16f, point.y - 12f, 6f, false, mPaint);
219                drawPupil(canvas, point.x + 16f, point.y - 12f, 6f, false, mPaint);
220            } else {
221                canvas.drawCircle(point.x - 16f, point.y - 12f, 6f, mPaint);
222                canvas.drawCircle(point.x + 16f, point.y - 12f, 6f, mPaint);
223            }
224
225            // too much?
226            if (false) {
227                mPaint.setColor(0xFF000000);
228                drawPupil(canvas, point.x - 16f, point.y - 12f, 5f, true, mPaint);
229                drawPupil(canvas, point.x + 16f, point.y - 12f, 5f, true, mPaint);
230            }
231
232            // arms in front
233            mPaint.setColor(ARM_COLOR);
234            for (int i : FRONT_ARMS) {
235                mArms[i].draw(canvas, mPaint);
236            }
237
238            if (PATH_DEBUG) for (Arm arm : mArms) {
239                arm.drawDebug(canvas);
240            }
241        }
242        canvas.restore();
243    }
244
245    public void setBlinking(boolean b) {
246        mBlinking = b;
247        invalidateSelf();
248    }
249
250    @Override
251    public void setAlpha(int i) {
252    }
253
254    @Override
255    public void setColorFilter(@Nullable ColorFilter colorFilter) {
256
257    }
258
259    @Override
260    public int getOpacity() {
261        return PixelFormat.TRANSLUCENT;
262    }
263
264    static Path pathMoveTo(Path p, PointF pt) {
265        p.moveTo(pt.x, pt.y);
266        return p;
267    }
268    static Path pathQuadTo(Path p, PointF p1, PointF p2) {
269        p.quadTo(p1.x, p1.y, p2.x, p2.y);
270        return p;
271    }
272
273    static void mapPointF(Matrix m, PointF point) {
274        float[] p = new float[2];
275        p[0] = point.x;
276        p[1] = point.y;
277        m.mapPoints(p);
278        point.x = p[0];
279        point.y = p[1];
280    }
281
282    private class Link  // he come to town
283            implements DynamicAnimation.OnAnimationUpdateListener {
284        final FloatValueHolder[] coords = new FloatValueHolder[2];
285        final SpringAnimation[] anims = new SpringAnimation[coords.length];
286        private float dx, dy;
287        private boolean locked = false;
288        Link next;
289
290        Link(int index, float x1, float y1, float dx, float dy) {
291            coords[0] = new FloatValueHolder(x1);
292            coords[1] = new FloatValueHolder(y1);
293            this.dx = dx;
294            this.dy = dy;
295            for (int i=0; i<coords.length; i++) {
296                anims[i] = new SpringAnimation(coords[i]);
297                anims[i].setSpring(new SpringForce()
298                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
299                        .setStiffness(
300                                index == 0 ? SpringForce.STIFFNESS_LOW
301                                        : index == 1 ? SpringForce.STIFFNESS_VERY_LOW
302                                                : SpringForce.STIFFNESS_VERY_LOW/2)
303                        .setFinalPosition(0f));
304                anims[i].addUpdateListener(this);
305            }
306        }
307        public void setLocked(boolean locked) {
308            this.locked = locked;
309        }
310        public PointF start() {
311            return new PointF(coords[0].getValue(), coords[1].getValue());
312        }
313        public PointF end() {
314            return new PointF(coords[0].getValue()+dx,coords[1].getValue()+dy);
315        }
316        public PointF mid() {
317            return new PointF(
318                    0.5f*dx+(coords[0].getValue()),
319                    0.5f*dy+(coords[1].getValue()));
320        }
321        public void animateTo(PointF target) {
322            if (locked) {
323                setStart(target.x, target.y);
324            } else {
325                anims[0].animateToFinalPosition(target.x);
326                anims[1].animateToFinalPosition(target.y);
327            }
328        }
329        @Override
330        public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float v, float v1) {
331            if (next != null) {
332                next.animateTo(end());
333            }
334            OctopusDrawable.this.invalidateSelf();
335        }
336
337        public void setStart(float x, float y) {
338            coords[0].setValue(x);
339            coords[1].setValue(y);
340            onAnimationUpdate(null, 0, 0);
341        }
342    }
343
344    private class Arm {
345        final Link link1, link2, link3;
346        float max, min;
347
348        public Arm(float x, float y, float dx1, float dy1, float dx2, float dy2, float dx3, float dy3,
349                float max, float min) {
350            link1 = new Link(0, x, y, dx1, dy1);
351            link2 = new Link(1, x+dx1, y+dy1, dx2, dy2);
352            link3 = new Link(2, x+dx1+dx2, y+dy1+dy2, dx3, dy3);
353            link1.next = link2;
354            link2.next = link3;
355
356            link1.setLocked(true);
357            link2.setLocked(false);
358            link3.setLocked(false);
359
360            this.max = max;
361            this.min = min;
362        }
363
364        // when the arm is locked, it moves rigidly, without physics
365        public void setLocked(boolean locked) {
366            link2.setLocked(locked);
367            link3.setLocked(locked);
368        }
369
370        private void setAnchor(float x, float y) {
371            link1.setStart(x,y);
372        }
373
374        public Path getPath() {
375            Path p = new Path();
376            pathMoveTo(p, link1.start());
377            pathQuadTo(p, link2.start(), link2.mid());
378            pathQuadTo(p, link2.end(), link3.end());
379            return p;
380        }
381
382        public void draw(@NonNull Canvas canvas, Paint pt) {
383            final Path p = getPath();
384            TaperedPathStroke.drawPath(canvas, p, max, min, pt);
385        }
386
387        private final Paint dpt = new Paint();
388        public void drawDebug(Canvas canvas) {
389            dpt.setStyle(Paint.Style.STROKE);
390            dpt.setStrokeWidth(0.75f);
391            dpt.setStrokeCap(Paint.Cap.ROUND);
392
393            dpt.setAntiAlias(true);
394            dpt.setColor(0xFF336699);
395
396            final Path path = getPath();
397            canvas.drawPath(path, dpt);
398
399            dpt.setColor(0xFFFFFF00);
400
401            dpt.setPathEffect(new DashPathEffect(new float[] {2f, 2f}, 0f));
402
403            canvas.drawLines(new float[] {
404                    link1.end().x,   link1.end().y,
405                    link2.start().x, link2.start().y,
406
407                    link2.end().x,   link2.end().y,
408                    link3.start().x, link3.start().y,
409            }, dpt);
410            dpt.setPathEffect(null);
411
412            dpt.setColor(0xFF00CCFF);
413
414            canvas.drawLines(new float[] {
415                    link1.start().x, link1.start().y,
416                    link1.end().x,   link1.end().y,
417
418                    link2.start().x, link2.start().y,
419                    link2.end().x,   link2.end().y,
420
421                    link3.start().x, link3.start().y,
422                    link3.end().x,   link3.end().y,
423            }, dpt);
424
425            dpt.setColor(0xFFCCEEFF);
426            canvas.drawCircle(link2.start().x, link2.start().y, 2f, dpt);
427            canvas.drawCircle(link3.start().x, link3.start().y, 2f, dpt);
428
429            dpt.setStyle(Paint.Style.FILL_AND_STROKE);
430            canvas.drawCircle(link1.start().x, link1.start().y, 2f, dpt);
431            canvas.drawCircle(link2.mid().x,   link2.mid().y,   2f, dpt);
432            canvas.drawCircle(link3.end().x,   link3.end().y,   2f, dpt);
433        }
434
435    }
436}
437