mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-11 18:50:58 +00:00
Improvements to target tap loader game.
This commit is contained in:
parent
5528d71da8
commit
08e98b089c
1 changed files with 217 additions and 189 deletions
|
@ -8,115 +8,104 @@ import android.view.HapticFeedbackConstants
|
|||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.graphics.toColorInt
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import kotlin.math.*
|
||||
import kotlin.random.Random
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
|
||||
class TargetTapLoaderView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : View(context, attrs) {
|
||||
private val primaryColor = "#2D63ED".toColorInt()
|
||||
private val inactiveGlobalAlpha = 110
|
||||
private val streakAccelerationStep = .3f
|
||||
private val minSpawnDelay = 200L
|
||||
private val idleSpeedMultiplier = .015f
|
||||
private val baseDeterministicDelay = 700L
|
||||
private val baseIndeterminateDelay = 1400L
|
||||
private val overshootInterpolator = OvershootInterpolator(2f)
|
||||
private val initialSpawnFactor = 2f
|
||||
private val floatAccel = .03f
|
||||
private val idleMaxSpeed = .35f
|
||||
private val idleInitialTargets = 10
|
||||
private val idleHintText = "Waiting for media to become available"
|
||||
|
||||
private var expectedDurationMs: Long? = null
|
||||
private var startTime: Long = 0L
|
||||
private var loadStartTime = 0L
|
||||
private var playStartTime = 0L
|
||||
private var loaderFinished = false
|
||||
private var forceIndeterminate = false
|
||||
private var spinnerShader: SweepGradient? = null
|
||||
private var forceIndeterminate= false
|
||||
private var lastFrameTime = System.currentTimeMillis()
|
||||
private val bounceInterpolator = android.view.animation.OvershootInterpolator(2f)
|
||||
|
||||
private val isIndeterminate: Boolean
|
||||
get() = forceIndeterminate || expectedDurationMs == null || expectedDurationMs == 0L
|
||||
private var streak = 0
|
||||
private var score = 0
|
||||
private var isPlaying = false
|
||||
|
||||
private val targets = mutableListOf<Target>()
|
||||
private val particles = mutableListOf<Particle>()
|
||||
private var score = 0
|
||||
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
textSize = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_SP, 18f, resources.displayMetrics
|
||||
)
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 18f, resources.displayMetrics)
|
||||
textAlign = Paint.Align.LEFT
|
||||
setShadowLayer(4f, 0f, 0f, Color.BLACK)
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
}
|
||||
private val progressBarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = "#2D63ED".toColorInt()
|
||||
}
|
||||
private val progressBarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = primaryColor }
|
||||
private val spinnerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = "#2D63ED".toColorInt()
|
||||
strokeWidth = 12f
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
}
|
||||
private val outerRingPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
private val middleRingPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.RED
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
private val centerDotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.YELLOW
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.argb(50, 0, 0, 0)
|
||||
color = primaryColor; strokeWidth = 12f
|
||||
style = Paint.Style.STROKE; strokeCap = Paint.Cap.ROUND
|
||||
}
|
||||
private val outerRingPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val middleRingPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val centerDotPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.argb(50, 0, 0, 0) }
|
||||
private val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val particlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.YELLOW
|
||||
}
|
||||
private val particlePaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val backgroundPaint = Paint()
|
||||
|
||||
private var spinnerShader: SweepGradient? = null
|
||||
private var spinnerAngle = 0f
|
||||
private var lastSpawnDelayMs: Long = baseDeterministicDelay
|
||||
private var currentSpawnDelayMs = baseDeterministicDelay.toFloat()
|
||||
private val DELAY_SMOOTHING = 0.7f
|
||||
private val MISS_PENALTY = 1
|
||||
|
||||
private val frameRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
invalidate()
|
||||
if (!loaderFinished) postDelayed(this, 16L)
|
||||
}
|
||||
override fun run() { invalidate(); if (!loaderFinished) postDelayed(this, 16L) }
|
||||
}
|
||||
|
||||
init {
|
||||
setOnTouchListener { _, event ->
|
||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||
handleTap(event.x, event.y)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
init { setOnTouchListener { _, e -> if (e.action == MotionEvent.ACTION_DOWN) handleTap(e.x, e.y); true } }
|
||||
|
||||
fun startLoader(durationMs: Long? = null) {
|
||||
val isAlreadyRunning = !loaderFinished
|
||||
|
||||
val newDuration = durationMs?.takeIf { it > 0L }
|
||||
|
||||
if (isAlreadyRunning && newDuration == null) {
|
||||
val alreadyRunning = !loaderFinished
|
||||
if (alreadyRunning && durationMs == null) {
|
||||
expectedDurationMs = null
|
||||
forceIndeterminate = true
|
||||
startTime = System.currentTimeMillis()
|
||||
return
|
||||
}
|
||||
|
||||
expectedDurationMs = newDuration
|
||||
expectedDurationMs = durationMs?.takeIf { it > 0 }
|
||||
forceIndeterminate = expectedDurationMs == null
|
||||
loaderFinished = false
|
||||
startTime = System.currentTimeMillis()
|
||||
isPlaying = false
|
||||
score = 0
|
||||
targets.clear()
|
||||
streak = 0
|
||||
particles.clear()
|
||||
|
||||
post { if (targets.isEmpty()) prepopulateIdleTargets() }
|
||||
|
||||
loadStartTime = System.currentTimeMillis()
|
||||
playStartTime = 0
|
||||
removeCallbacks(frameRunnable)
|
||||
post(frameRunnable)
|
||||
post { spawnTarget() }
|
||||
|
||||
if (!isIndeterminate) {
|
||||
postDelayed({
|
||||
if (!loaderFinished) {
|
||||
forceIndeterminate = true
|
||||
startTime = System.currentTimeMillis()
|
||||
spawnTarget()
|
||||
expectedDurationMs = null
|
||||
}
|
||||
}, expectedDurationMs!!)
|
||||
}
|
||||
|
@ -125,73 +114,115 @@ class TargetTapLoaderView @JvmOverloads constructor(
|
|||
fun finishLoader() {
|
||||
loaderFinished = true
|
||||
particles.clear()
|
||||
isPlaying = false
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun stopAndResetLoader() {
|
||||
if (score > 0) {
|
||||
val now = System.currentTimeMillis()
|
||||
val dt = (now - startTime) / 1000.0
|
||||
UIDialogs.toast("Nice! score was $score, ${"%.${1}f".format(score / dt).toDouble()} (per second)")
|
||||
score = 0
|
||||
val elapsed = (System.currentTimeMillis() - (if (playStartTime > 0) playStartTime else loadStartTime)) / 1000.0
|
||||
UIDialogs.toast("Nice! score $score | ${"%.1f".format(score / elapsed)} / s")
|
||||
}
|
||||
|
||||
loaderFinished = true
|
||||
isPlaying = false
|
||||
targets.clear()
|
||||
particles.clear()
|
||||
removeCallbacks(frameRunnable)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private val isIndeterminate get() = forceIndeterminate || expectedDurationMs == null || expectedDurationMs == 0L
|
||||
|
||||
private fun handleTap(x: Float, y: Float) {
|
||||
val now = System.currentTimeMillis()
|
||||
val hitIndex = targets.indexOfFirst { t -> !t.hit && hypot(x - t.x, y - t.y) <= t.radius }
|
||||
if (hitIndex >= 0) {
|
||||
val idx = targets.indexOfFirst { !it.hit && hypot(x - it.x, y - it.y) <= it.radius }
|
||||
if (idx >= 0) {
|
||||
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
val target = targets[hitIndex]
|
||||
if (!target.hit) {
|
||||
target.hit = true
|
||||
target.hitTime = now
|
||||
score += if (!isIndeterminate) 10 else 5
|
||||
spawnParticles(target.x, target.y, target.radius)
|
||||
val t = targets[idx]
|
||||
t.hit = true; t.hitTime = System.currentTimeMillis()
|
||||
streak++
|
||||
score += if (!isIndeterminate) 10 else 5
|
||||
spawnParticles(t.x, t.y, t.radius)
|
||||
|
||||
if (!isPlaying) {
|
||||
isPlaying = true
|
||||
playStartTime = System.currentTimeMillis()
|
||||
score = 0
|
||||
streak = 0
|
||||
targets.retainAll { it === t }
|
||||
spawnTarget()
|
||||
}
|
||||
}
|
||||
} else if (isPlaying) applyMissPenalty()
|
||||
}
|
||||
|
||||
private fun applyMissPenalty() { streak = max(0, streak - MISS_PENALTY) }
|
||||
|
||||
private fun spawnTarget() {
|
||||
if (loaderFinished) return
|
||||
if (width <= 0 || height <= 0) {
|
||||
post { spawnTarget() }
|
||||
return
|
||||
if (loaderFinished || width == 0 || height == 0) {
|
||||
postDelayed({ spawnTarget() }, 200L); return
|
||||
}
|
||||
|
||||
val radius = Random.nextInt(40, 80).toFloat()
|
||||
val x = Random.nextFloat() * (width - 2 * radius) + radius
|
||||
val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius
|
||||
targets.add(Target(x, y, radius, System.currentTimeMillis()))
|
||||
if (!isPlaying) { postDelayed({ spawnTarget() }, 500L); return }
|
||||
|
||||
val delay = if (isIndeterminate) 1400L else 700L
|
||||
val radius = Random.nextInt(40, 80).toFloat()
|
||||
val x = Random.nextFloat() * (width - 2 * radius) + radius
|
||||
val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius
|
||||
|
||||
val baseSpeed = Random.nextFloat() + .1f
|
||||
val speed = baseSpeed
|
||||
val angle = Random.nextFloat() * TAU
|
||||
val vx = cos(angle) * speed
|
||||
val vy = sin(angle) * speed
|
||||
val alpha = Random.nextInt(150, 255)
|
||||
|
||||
targets += Target(x, y, radius, System.currentTimeMillis(), baseAlpha = alpha, vx = vx, vy = vy)
|
||||
val delayBase = if (isIndeterminate) baseIndeterminateDelay else baseDeterministicDelay
|
||||
val streakBoost = 1f + streak * streakAccelerationStep
|
||||
val baseFactor = if (streak == 0) initialSpawnFactor else 1f
|
||||
val targetDelay = max(minSpawnDelay.toFloat(), delayBase * baseFactor / streakBoost)
|
||||
|
||||
currentSpawnDelayMs = currentSpawnDelayMs * DELAY_SMOOTHING + targetDelay * (1 - DELAY_SMOOTHING)
|
||||
val delay = currentSpawnDelayMs.roundToLong()
|
||||
lastSpawnDelayMs = delay
|
||||
postDelayed({ spawnTarget() }, delay)
|
||||
}
|
||||
|
||||
private fun prepopulateIdleTargets() {
|
||||
if (width == 0 || height == 0) {
|
||||
post { prepopulateIdleTargets() }
|
||||
return
|
||||
}
|
||||
repeat(idleInitialTargets) {
|
||||
val radius = Random.nextInt(40, 80).toFloat()
|
||||
val x = Random.nextFloat() * (width - 2 * radius) + radius
|
||||
val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius
|
||||
val angle = Random.nextFloat() * TAU
|
||||
val speed = (Random.nextFloat() * .3f + .05f) * idleSpeedMultiplier
|
||||
val vx = cos(angle) * speed
|
||||
val vy = sin(angle) * speed
|
||||
val alpha = Random.nextInt(60, 110)
|
||||
targets += Target(x, y, radius, System.currentTimeMillis(), baseAlpha = alpha, vx = vx, vy = vy)
|
||||
}
|
||||
}
|
||||
|
||||
private fun spawnParticles(cx: Float, cy: Float, radius: Float) {
|
||||
repeat(12) {
|
||||
val angle = Random.nextFloat() * 2f * PI.toFloat()
|
||||
val angle = Random.nextFloat() * TAU
|
||||
val speed = Random.nextFloat() * 5f + 2f
|
||||
val vx = cos(angle) * speed
|
||||
val vy = sin(angle) * speed
|
||||
particles.add(Particle(cx, cy, vx, vy, System.currentTimeMillis()))
|
||||
val col = ColorUtils.setAlphaComponent(primaryColor, Random.nextInt(120, 255))
|
||||
particles += Particle(cx, cy, vx, vy, System.currentTimeMillis(), col)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
drawBackground(canvas)
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val now = System.currentTimeMillis()
|
||||
val deltaMs = now - lastFrameTime
|
||||
lastFrameTime = now
|
||||
|
||||
drawBackground(canvas)
|
||||
drawTargets(canvas, now)
|
||||
drawParticles(canvas, now)
|
||||
|
||||
|
@ -200,140 +231,137 @@ class TargetTapLoaderView @JvmOverloads constructor(
|
|||
else drawDeterministicProgressBar(canvas, now)
|
||||
}
|
||||
|
||||
canvas.drawText("Score: $score", width / 2f, height - 80f, textPaint)
|
||||
if (isPlaying) {
|
||||
val margin = 24f
|
||||
val scoreTxt = "Score $score"
|
||||
val speed = 1000f / lastSpawnDelayMs
|
||||
val speedTxt = "Speed ${"%.2f".format(speed)}/s"
|
||||
val maxWidth = width - margin
|
||||
val needRight = max(textPaint.measureText(scoreTxt), textPaint.measureText(speedTxt)) > maxWidth
|
||||
|
||||
if (loaderFinished) {
|
||||
canvas.drawText("Loading Complete!", width / 2f, height / 2f, textPaint)
|
||||
val alignX = if (needRight) (width - margin) else margin
|
||||
textPaint.textAlign = if (needRight) Paint.Align.RIGHT else Paint.Align.LEFT
|
||||
|
||||
canvas.drawText(scoreTxt, alignX, textPaint.textSize + margin, textPaint)
|
||||
canvas.drawText(speedTxt, alignX, 2*textPaint.textSize + margin + 4f, textPaint)
|
||||
textPaint.textAlign = Paint.Align.LEFT
|
||||
}
|
||||
else if (loaderFinished)
|
||||
canvas.drawText("Loading Complete!", width/2f, height/2f, textPaint.apply { textAlign = Paint.Align.CENTER })
|
||||
else {
|
||||
textPaint.textAlign = Paint.Align.CENTER
|
||||
canvas.drawText(idleHintText, width / 2f, height - 48f, textPaint)
|
||||
textPaint.textAlign = Paint.Align.LEFT
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawBackground(canvas: Canvas) {
|
||||
val gradient = LinearGradient(
|
||||
backgroundPaint.shader = LinearGradient(
|
||||
0f, 0f, 0f, height.toFloat(),
|
||||
Color.rgb(20, 20, 40), Color.BLACK,
|
||||
Shader.TileMode.CLAMP
|
||||
Color.rgb(20, 20, 40), Color.BLACK, Shader.TileMode.CLAMP
|
||||
)
|
||||
backgroundPaint.shader = gradient
|
||||
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), backgroundPaint)
|
||||
}
|
||||
|
||||
private fun drawTargets(canvas: Canvas, now: Long) {
|
||||
val expireMs = if (isIndeterminate) 2500L else 1500L
|
||||
targets.removeAll { it.hit && now - it.hitTime > 300L }
|
||||
targets.removeAll { !it.hit && now - it.spawnTime > expireMs }
|
||||
|
||||
for (t in targets) {
|
||||
val scale = when {
|
||||
t.hit -> {
|
||||
1f - ((now - t.hitTime) / 300f).coerceIn(0f, 1f)
|
||||
}
|
||||
else -> {
|
||||
val spawnElapsed = now - t.spawnAnimationStartTime
|
||||
if (spawnElapsed < 300L) {
|
||||
val animProgress = spawnElapsed / 300f
|
||||
bounceInterpolator.getInterpolation(animProgress)
|
||||
} else {
|
||||
val pulseTime = ((now - t.spawnAnimationStartTime) / 1000f) * 2f * PI.toFloat() + t.idlePulseOffset
|
||||
1f + 0.02f * sin(pulseTime)
|
||||
}
|
||||
}
|
||||
val expireMsActive = if (isIndeterminate) 2500L else 1500L
|
||||
val it = targets.iterator()
|
||||
while (it.hasNext()) {
|
||||
val t = it.next()
|
||||
if (t.hit && now - t.hitTime > 300L) { it.remove(); continue }
|
||||
if (isPlaying && !t.hit && now - t.spawnTime > expireMsActive) {
|
||||
it.remove(); applyMissPenalty(); continue
|
||||
}
|
||||
t.x += t.vx; t.y += t.vy
|
||||
t.vx += (Random.nextFloat() - .5f) * floatAccel
|
||||
t.vy += (Random.nextFloat() - .5f) * floatAccel
|
||||
val speedCap = if (isPlaying) Float.MAX_VALUE else idleMaxSpeed
|
||||
val mag = hypot(t.vx, t.vy)
|
||||
if (mag > speedCap) {
|
||||
val s = speedCap / mag
|
||||
t.vx *= s; t.vy *= s
|
||||
}
|
||||
if (t.x - t.radius < 0 || t.x + t.radius > width) t.vx *= -1
|
||||
if (t.y - t.radius < 0 || t.y + t.radius > height) t.vy *= -1
|
||||
val scale = if (t.hit) 1f - ((now - t.hitTime) / 300f).coerceIn(0f,1f)
|
||||
else {
|
||||
val e = now - t.spawnAnimStart
|
||||
if (e < 300L) overshootInterpolator.getInterpolation(e/300f)
|
||||
else 1f + .02f * sin(((now - t.spawnAnimStart)/1000f)*TAU + t.pulseOffset)
|
||||
}
|
||||
val animAlpha = if (t.hit) ((1f - scale)*255).toInt() else 255
|
||||
val globalAlpha = if (isPlaying) 255 else inactiveGlobalAlpha
|
||||
val alpha = (animAlpha * t.baseAlpha /255f * globalAlpha/255f).toInt().coerceAtMost(255)
|
||||
val r = max(1f, t.radius*scale)
|
||||
val outerCol = ColorUtils.setAlphaComponent(primaryColor, alpha)
|
||||
val midCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.7f).toInt())
|
||||
val innerCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.4f).toInt())
|
||||
outerRingPaint.color = outerCol; middleRingPaint.color = midCol; centerDotPaint.color = innerCol
|
||||
|
||||
val alpha = if (t.hit) ((1f - scale) * 255).toInt().coerceAtMost(255) else 255
|
||||
val safeRadius = (t.radius * scale).coerceAtLeast(1f)
|
||||
glowPaint.shader = RadialGradient(t.x, t.y, r, outerCol, Color.TRANSPARENT, Shader.TileMode.CLAMP)
|
||||
|
||||
glowPaint.shader = RadialGradient(
|
||||
t.x, t.y, safeRadius,
|
||||
Color.YELLOW, Color.TRANSPARENT,
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
canvas.drawCircle(t.x, t.y, safeRadius * 1.2f, glowPaint)
|
||||
canvas.drawCircle(t.x + 4f, t.y + 4f, safeRadius, shadowPaint)
|
||||
|
||||
outerRingPaint.alpha = alpha
|
||||
middleRingPaint.alpha = alpha
|
||||
centerDotPaint.alpha = alpha
|
||||
|
||||
canvas.drawCircle(t.x, t.y, safeRadius, outerRingPaint)
|
||||
canvas.drawCircle(t.x, t.y, safeRadius * 0.66f, middleRingPaint)
|
||||
canvas.drawCircle(t.x, t.y, safeRadius * 0.33f, centerDotPaint)
|
||||
canvas.drawCircle(t.x, t.y, r*1.2f, glowPaint)
|
||||
canvas.drawCircle(t.x+4f, t.y+4f, r, shadowPaint)
|
||||
canvas.drawCircle(t.x, t.y, r, outerRingPaint)
|
||||
canvas.drawCircle(t.x, t.y, r*.66f, middleRingPaint)
|
||||
canvas.drawCircle(t.x, t.y, r*.33f, centerDotPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawParticles(canvas: Canvas, now: Long) {
|
||||
val lifespan = 400L
|
||||
val iterator = particles.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val p = iterator.next()
|
||||
val it = particles.iterator()
|
||||
while (it.hasNext()) {
|
||||
val p = it.next()
|
||||
val age = now - p.startTime
|
||||
if (age > lifespan) {
|
||||
iterator.remove()
|
||||
continue
|
||||
}
|
||||
val alpha = ((1f - (age / lifespan.toFloat())) * 255).toInt()
|
||||
p.x += p.vx
|
||||
p.y += p.vy
|
||||
particlePaint.alpha = alpha
|
||||
if (age > lifespan) { it.remove(); continue }
|
||||
val a = ((1f - age/lifespan.toFloat())*255).toInt()
|
||||
particlePaint.color = ColorUtils.setAlphaComponent(p.baseColor, a)
|
||||
p.x += p.vx; p.y += p.vy
|
||||
canvas.drawCircle(p.x, p.y, 6f, particlePaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawDeterministicProgressBar(canvas: Canvas, now: Long) {
|
||||
val duration = expectedDurationMs ?: return
|
||||
val rawProgress = ((now - startTime).toFloat() / duration).coerceIn(0f, 1f)
|
||||
val easedProgress = AccelerateDecelerateInterpolator().getInterpolation(rawProgress)
|
||||
|
||||
val barHeight = 20f
|
||||
val barRadius = 10f
|
||||
val barWidth = width * easedProgress
|
||||
|
||||
val rect = RectF(0f, height - barHeight, barWidth, height.toFloat())
|
||||
canvas.drawRoundRect(rect, barRadius, barRadius, progressBarPaint)
|
||||
val dur = expectedDurationMs ?: return
|
||||
val prog = ((now - loadStartTime) / dur.toFloat()).coerceIn(0f, 1f)
|
||||
val eased = AccelerateDecelerateInterpolator().getInterpolation(prog)
|
||||
val h = 20f; val r=10f
|
||||
canvas.drawRoundRect(RectF(0f, height-h, width*eased, height.toFloat()), r, r, progressBarPaint)
|
||||
}
|
||||
|
||||
|
||||
private fun drawIndeterminateSpinner(canvas: Canvas, deltaMs: Long) {
|
||||
val cx = width / 2f
|
||||
val cy = height / 2f
|
||||
val radius = min(width, height) / 6f
|
||||
val sweepAngle = 270f
|
||||
|
||||
spinnerAngle = (spinnerAngle + 0.25f * deltaMs) % 360f
|
||||
|
||||
if (spinnerShader == null) {
|
||||
spinnerShader = SweepGradient(
|
||||
cx, cy,
|
||||
intArrayOf(Color.TRANSPARENT, Color.WHITE, Color.TRANSPARENT),
|
||||
floatArrayOf(0f, 0.5f, 1f)
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawIndeterminateSpinner(canvas: Canvas, dt: Long) {
|
||||
val cx=width/2f; val cy=height/2f; val r=min(width,height)/6f
|
||||
spinnerAngle = (spinnerAngle + .25f*dt)%360f
|
||||
if(spinnerShader == null) spinnerShader = SweepGradient(cx,cy,intArrayOf(Color.TRANSPARENT,Color.WHITE,Color.TRANSPARENT),floatArrayOf(0f,.5f,1f))
|
||||
spinnerPaint.shader = spinnerShader
|
||||
|
||||
val glowPaint = Paint(spinnerPaint).apply {
|
||||
maskFilter = BlurMaskFilter(15f, BlurMaskFilter.Blur.SOLID)
|
||||
}
|
||||
|
||||
canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius, spinnerAngle, sweepAngle, false, glowPaint)
|
||||
canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius, spinnerAngle, sweepAngle, false, spinnerPaint)
|
||||
val glow = Paint(spinnerPaint).apply{ maskFilter = BlurMaskFilter(15f,BlurMaskFilter.Blur.SOLID) }
|
||||
val sweep = 270f
|
||||
canvas.drawArc(cx-r,cy-r,cx+r,cy+r,spinnerAngle,sweep,false,glow)
|
||||
canvas.drawArc(cx-r,cy-r,cx+r,cy+r,spinnerAngle,sweep,false,spinnerPaint)
|
||||
}
|
||||
|
||||
private data class Target(
|
||||
val x: Float,
|
||||
val y: Float,
|
||||
var x: Float,
|
||||
var y: Float,
|
||||
val radius: Float,
|
||||
val spawnTime: Long,
|
||||
var hit: Boolean = false,
|
||||
var hitTime: Long = 0L,
|
||||
val spawnAnimationStartTime: Long = System.currentTimeMillis(),
|
||||
val idlePulseOffset: Float = Random.nextFloat() * 2f * PI.toFloat()
|
||||
val baseAlpha: Int = 255,
|
||||
var vx: Float=0f,
|
||||
var vy:Float=0f,
|
||||
val spawnAnimStart: Long = System.currentTimeMillis(),
|
||||
val pulseOffset: Float = Random.nextFloat() * TAU
|
||||
)
|
||||
private data class Particle(
|
||||
var x:Float,
|
||||
var y:Float,
|
||||
val vx:Float,
|
||||
val vy:Float,
|
||||
val startTime:Long,
|
||||
val baseColor:Int
|
||||
)
|
||||
|
||||
private data class Particle(
|
||||
var x: Float,
|
||||
var y: Float,
|
||||
val vx: Float,
|
||||
val vy: Float,
|
||||
val startTime: Long
|
||||
)
|
||||
private companion object { private const val TAU = (2 * Math.PI).toFloat() }
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue