Dan Sandler | 49ddb0d | 2017-06-08 23:52:45 -0400 | [diff] [blame] | 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 | |
| 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 | } |