/* * Copyright (C) 2016 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.apps.santatracker.doodles.shared; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import java.util.ArrayList; import java.util.List; /** * Represents ball in Tennis. */ public class BallActor extends Actor { private static final float MIN_SHADOW_SCALE = 0.4f; private static final float MAX_SHADOW_SCALE = 0.7f; // Highest the ball can be. Going higher than this may mess up scaling & shadow positioning. public static final int BALL_MAX_HEIGHT = 75; private static final int MAX_STREAK_LENGTH = 10; // Max # of frames back the streak can go. // Slightly less than the sprite's frameWidth because there are semi-transparent pixels. public static final int BALL_RADIUS = 14; private static final double STREAK_ALPHA = 0.25; // 1 = opaque, 0 = transparent private static final float BALL_MIN_SCALE = 0.5f; // Ball is scaled between BALL_MIN_SCALE and 1. private AnimatedSprite ball; private AnimatedSprite fireball; // Flaming version of ball sprite (optional). private AnimatedSprite shadow; // Might be null, if ball shouldn't have a shadow. private float height = 0; // Height above the court. // Whether or not the ball should be scaled larger at higher heights (to aid illusion of ball // arcing through the air) public boolean shouldScaleWithHeight; private final Paint debugPaint; // The streak is drawn by storing a history of positions & scales going back streakLength // updates into the past. private List<Vector2D> streakPositions = new ArrayList<>(); private List<Float> streakHeights = new ArrayList<>(); private int streakIndex = 0; // This is the actual streak length. On iOS, it must be <= MAX_STREAK_LENGTH in order to fit in // the arrays, so we're keeping it <= here too. private int streakLength = 10; private boolean shouldDrawStreak = true; private Paint streakPaint = new Paint(); private Paint streakDebugPaint = new Paint(); // This is a field so that we don't have to create a new one every frame. Not threadsafe, // but draw() shouldn't be called from multiple threads so it should be ok. private Path path = new Path(); // Fireball is optional. public BallActor(AnimatedSprite shadow, AnimatedSprite ball, AnimatedSprite fireball, int streakLength) { this.ball = ball; ball.setAnchor(ball.frameWidth / 2, ball.frameHeight); if (fireball != null) { fireball.setAnchor(fireball.frameWidth / 2, BALL_RADIUS); this.fireball = fireball; } this.shadow = shadow; this.streakLength = streakLength; if (streakLength > MAX_STREAK_LENGTH) { throw new IllegalArgumentException("Error: Streak length exceeds MAX_STREAK_LENGTH"); } shouldScaleWithHeight = true; debugPaint = new Paint(); debugPaint.setColor(0xff000000); streakPaint.setColor(android.graphics.Color.WHITE); streakPaint.setStyle(Paint.Style.FILL); streakPaint.setAlpha((int) (STREAK_ALPHA * 256)); streakDebugPaint.setColor(android.graphics.Color.BLACK); streakDebugPaint.setStyle(Paint.Style.STROKE); clearStreak(); } @Override public void update(float deltaMs) { super.update(deltaMs); ball.update(deltaMs); if (fireball != null) { fireball.update(deltaMs); } if (shadow != null) { shadow.update(deltaMs); } streakIndex++; streakPositions.get(streakIndex % streakLength).set(position); streakHeights.set(streakIndex % streakLength, height); // Prepare sprites for drawing. if (shadow != null) { float shadowScale = MIN_SHADOW_SCALE + (MAX_SHADOW_SCALE - MIN_SHADOW_SCALE) * (1 - Math.min(BALL_MAX_HEIGHT, height) / BALL_MAX_HEIGHT); if (!shouldScaleWithHeight) { shadowScale = MAX_SHADOW_SCALE * scale; } shadow.setScale(shadowScale, shadowScale); shadow.setPosition(position.x - shadowScale * shadow.frameWidth / 2, position.y - shadowScale * shadow.frameHeight / 2); } float ballScale = calculateScale(height); ball.setScale(ballScale, ballScale); ball.setRotation(rotation); ball.setPosition(position.x, position.y - height); if (fireball != null) { fireball.setScale(ballScale, ballScale); fireball.setRotation((float) (Math.atan2(velocity.y, velocity.x) + Math.PI / 2)); fireball.setPosition(position.x, position.y - height - BALL_RADIUS); } // Streak. Start by working down the left side of the streak. path.rewind(); for (int i = 0; i < streakLength; i++) { int index = (streakIndex - i) % streakLength; Vector2D pos = streakPositions.get(index); float ballRadius = BALL_RADIUS * calculateScale(streakHeights.get(index)); float taperedRadius = ballRadius * (1 - i / (float) streakLength); float x = pos.x - taperedRadius; float y = pos.y - ballRadius - streakHeights.get(index); if (i == 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } } // Now finish by going back up right side of streak. for (int i = streakLength - 1; i >= 0; i--) { int index = (streakIndex - i) % streakLength; Vector2D pos = streakPositions.get(index); float ballRadius = BALL_RADIUS * calculateScale(streakHeights.get(index)); float taperedRadius = ballRadius * (1 - i / (float) streakLength); float x = pos.x + taperedRadius; float y = pos.y - ballRadius - streakHeights.get(index); path.lineTo(x, y); } path.close(); } @Override public void draw(Canvas canvas) { super.draw(canvas); // Shadow, ball, and streak are all drawn together here, so nothing can be drawn above shadow // but below the ball. if (!hidden) { if (shadow != null) { shadow.draw(canvas); } ball.draw(canvas); if (fireball != null) { fireball.draw(canvas); } if (shouldDrawStreak) { canvas.drawPath(path, streakPaint); } if (Debug.DRAW_POSITIONS) { canvas.drawPath(path, streakDebugPaint); canvas.drawCircle(position.x, position.y, 2, debugPaint); canvas.drawCircle(position.x, position.y - height, 2, debugPaint); } } } public void clearStreak() { streakPositions.clear(); streakHeights.clear(); for (int i = 0; i < streakLength; i++) { streakPositions.add(Vector2D.get(position)); streakHeights.add(height); } streakIndex = streakLength; // Start here so we never get negative indexes. } public void setHeight(float height) { this.height = height; } public float getHeight() { return height; } public void setColorForDebug(int color) { debugPaint.setColor(color); } public void showFireball(boolean shouldShowFireball) { if (fireball != null) { fireball.setHidden(!shouldShowFireball); ball.setHidden(shouldShowFireball); shouldDrawStreak = !shouldShowFireball; } } private float calculateScale(float height) { if (!shouldScaleWithHeight) { return scale; } return BALL_MIN_SCALE + (1 - BALL_MIN_SCALE) * height / BALL_MAX_HEIGHT; } }