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 
15 package com.android.egg.octo;
16 
17 import android.animation.TimeAnimator;
18 import android.content.Context;
19 import android.graphics.Canvas;
20 import android.graphics.ColorFilter;
21 import android.graphics.DashPathEffect;
22 import android.graphics.Matrix;
23 import android.graphics.Paint;
24 import android.graphics.Path;
25 import android.graphics.PixelFormat;
26 import android.graphics.PointF;
27 import android.graphics.Rect;
28 import android.graphics.drawable.Drawable;
29 import android.support.animation.DynamicAnimation;
30 import android.support.animation.SpringForce;
31 import android.support.annotation.NonNull;
32 import android.support.annotation.Nullable;
33 import android.support.animation.SpringAnimation;
34 import android.support.animation.FloatValueHolder;
35 
36 public 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 
randfrange(float a, float b)59     public static float randfrange(float a, float b) {
60         return (float) (Math.random()*(b-a) + a);
61     }
clamp(float v, float a, float b)62     public static float clamp(float v, float a, float b) {
63         return v<a?a:v>b?b:v;
64     }
65 
OctopusDrawable(Context context)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 
setSizePx(int size)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 
startDrift()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 
stopDrift()138     public void stopDrift() {
139         mDriftAnimation.cancel();
140     }
141 
142     @Override
onBoundsChange(Rect bounds)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
moveTo(float x, float y)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 
hitTest(float x, float y)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 
lockArms(boolean l)171     private void lockArms(boolean l) {
172         for (Arm arm : mArms) {
173             arm.setLocked(l);
174         }
175     }
repositionArms()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 
drawPupil(Canvas canvas, float x, float y, float size, boolean open, Paint pt)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
draw(@onNull Canvas canvas)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 
setBlinking(boolean b)245     public void setBlinking(boolean b) {
246         mBlinking = b;
247         invalidateSelf();
248     }
249 
250     @Override
setAlpha(int i)251     public void setAlpha(int i) {
252     }
253 
254     @Override
setColorFilter(@ullable ColorFilter colorFilter)255     public void setColorFilter(@Nullable ColorFilter colorFilter) {
256 
257     }
258 
259     @Override
getOpacity()260     public int getOpacity() {
261         return PixelFormat.TRANSLUCENT;
262     }
263 
pathMoveTo(Path p, PointF pt)264     static Path pathMoveTo(Path p, PointF pt) {
265         p.moveTo(pt.x, pt.y);
266         return p;
267     }
pathQuadTo(Path p, PointF p1, PointF p2)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 
mapPointF(Matrix m, PointF point)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 
Link(int index, float x1, float y1, float dx, float dy)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         }
setLocked(boolean locked)307         public void setLocked(boolean locked) {
308             this.locked = locked;
309         }
start()310         public PointF start() {
311             return new PointF(coords[0].getValue(), coords[1].getValue());
312         }
end()313         public PointF end() {
314             return new PointF(coords[0].getValue()+dx,coords[1].getValue()+dy);
315         }
mid()316         public PointF mid() {
317             return new PointF(
318                     0.5f*dx+(coords[0].getValue()),
319                     0.5f*dy+(coords[1].getValue()));
320         }
animateTo(PointF target)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
onAnimationUpdate(DynamicAnimation dynamicAnimation, float v, float v1)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 
setStart(float x, float y)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 
Arm(float x, float y, float dx1, float dy1, float dx2, float dy2, float dx3, float dy3, float max, float min)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
setLocked(boolean locked)365         public void setLocked(boolean locked) {
366             link2.setLocked(locked);
367             link3.setLocked(locked);
368         }
369 
setAnchor(float x, float y)370         private void setAnchor(float x, float y) {
371             link1.setStart(x,y);
372         }
373 
getPath()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 
draw(@onNull Canvas canvas, Paint pt)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();
drawDebug(Canvas canvas)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