blob: 5dde6e1152688d998db196b0e2bcfad26ad72e78 [file] [log] [blame]
Dan Sandler49ddb0d2017-06-08 23:52:45 -04001/*
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}