@@ -3,10 +3,13 @@ package com.smarttoolfactory.tutorial1_1basics
3
3
import android.graphics.Bitmap
4
4
import android.widget.Toast
5
5
import androidx.compose.animation.core.Animatable
6
+ import androidx.compose.animation.core.FastOutLinearInEasing
7
+ import androidx.compose.animation.core.FastOutSlowInEasing
6
8
import androidx.compose.animation.core.LinearEasing
7
9
import androidx.compose.animation.core.tween
8
10
import androidx.compose.foundation.Canvas
9
11
import androidx.compose.foundation.Image
12
+ import androidx.compose.foundation.background
10
13
import androidx.compose.foundation.border
11
14
import androidx.compose.foundation.layout.Arrangement
12
15
import androidx.compose.foundation.layout.Column
@@ -40,19 +43,21 @@ import androidx.compose.ui.graphics.Color
40
43
import androidx.compose.ui.graphics.ImageBitmap
41
44
import androidx.compose.ui.graphics.asAndroidBitmap
42
45
import androidx.compose.ui.graphics.asImageBitmap
43
- import androidx.compose.ui.graphics.drawscope.clipRect
46
+ import androidx.compose.ui.graphics.drawscope.Stroke
44
47
import androidx.compose.ui.graphics.rememberGraphicsLayer
45
48
import androidx.compose.ui.platform.LocalContext
46
49
import androidx.compose.ui.platform.LocalDensity
47
50
import androidx.compose.ui.res.painterResource
48
51
import androidx.compose.ui.tooling.preview.Preview
52
+ import androidx.compose.ui.unit.Velocity
49
53
import androidx.compose.ui.unit.dp
50
54
import androidx.compose.ui.unit.sp
51
55
import com.smarttoolfactory.tutorial1_1basics.chapter6_graphics.randomInRange
52
56
import com.smarttoolfactory.tutorial1_1basics.chapter6_graphics.scale
53
57
import com.smarttoolfactory.tutorial1_1basics.chapter6_graphics.toPx
54
58
import com.smarttoolfactory.tutorial1_1basics.ui.Pink400
55
59
import kotlinx.coroutines.CancellationException
60
+ import kotlin.random.Random
56
61
57
62
58
63
@Preview
@@ -69,8 +74,6 @@ fun SingleParticleTrajectorySample() {
69
74
mutableFloatStateOf(1f )
70
75
}
71
76
72
- val particleCount = 1
73
-
74
77
val density = LocalDensity .current
75
78
76
79
val sizeDp = with (density) {
@@ -94,13 +97,11 @@ fun SingleParticleTrajectorySample() {
94
97
),
95
98
initialSize = Size (5 .dp.toPx(), 5 .dp.toPx()),
96
99
endSize = Size (sizePx, sizePx),
97
- displacement = Offset (
98
- sizePxHalf,
99
- sizePxHalf
100
+ velocity = Velocity (
101
+ x = sizePxHalf,
102
+ y = sizePxHalf* 4
100
103
),
101
- trajectoryProgressRange = trajectoryProgressStart.. trajectoryProgressEnd,
102
- row = 0 ,
103
- column = 0
104
+ trajectoryProgressRange = trajectoryProgressStart.. trajectoryProgressEnd
104
105
)
105
106
)
106
107
}
@@ -189,6 +190,7 @@ fun ParticleAnimationSample() {
189
190
Column (
190
191
modifier = Modifier
191
192
.fillMaxSize()
193
+ .background(Color .DarkGray )
192
194
.verticalScroll(rememberScrollState())
193
195
.padding(horizontal = 16 .dp, vertical = 100 .dp),
194
196
verticalArrangement = Arrangement .spacedBy(8 .dp),
@@ -244,15 +246,12 @@ fun ParticleAnimationSample() {
244
246
245
247
data class Particle (
246
248
val initialCenter : Offset ,
247
- val displacement : Offset ,
248
249
val initialSize : Size ,
249
250
val endSize : Size ,
250
251
val color : Color ,
251
252
val trajectoryProgressRange : ClosedRange <Float > = 0f ..1f ,
252
- var velocity : Float = 4 * displacement.y,
253
- var acceleration : Float = -2 * velocity,
254
- val column : Int ,
255
- val row : Int
253
+ var velocity : Velocity = Velocity (0f, 0f),
254
+ var acceleration : Float = -2 * velocity.y
256
255
) {
257
256
var currentPosition: Offset = initialCenter
258
257
internal set
@@ -279,6 +278,8 @@ fun Modifier.explode(
279
278
val density = LocalDensity .current
280
279
val particleSizePx = with (density) { particleState.particleSize.roundToPx() }
281
280
281
+ val progress = particleState.progress
282
+
282
283
LaunchedEffect (animationStatus) {
283
284
if (animationStatus == AnimationStatus .Initializing ) {
284
285
val imageBitmap = graphicsLayer
@@ -319,14 +320,12 @@ fun Modifier.explode(
319
320
320
321
if (animationStatus != AnimationStatus .Idle ) {
321
322
322
- val animatedProgress = particleState.progress
323
-
324
323
particleState.particleList.forEach { particle ->
325
324
326
- particleState.updateParticle(animatedProgress , particle)
325
+ particleState.updateParticle(progress , particle)
327
326
328
327
val color = particle.color
329
- val radius = particle.currentSize.width * .7f
328
+ val radius = particle.currentSize.width * .65f
330
329
val position = particle.currentPosition
331
330
val alpha = particle.alpha
332
331
@@ -336,21 +335,29 @@ fun Modifier.explode(
336
335
color = color,
337
336
radius = radius,
338
337
center = position,
339
- // alpha = alpha
338
+ alpha = alpha
340
339
)
341
340
}
342
341
342
+ // TODO disintegrate image non-uniformly, with blend mode of particles
343
343
// clipRect(
344
- // left = animatedProgress * size.width
344
+ // left = progress * size.width
345
345
// ) {
346
346
// this@onDrawWithContent.drawContent()
347
347
// }
348
+
349
+ // For debugging
350
+ drawRect(
351
+ color = Color .Black ,
352
+ topLeft = Offset (progress * size.width, 0f ),
353
+ size = Size (size.width - progress * size.width, size.height),
354
+ style = Stroke (4 .dp.toPx())
355
+ )
348
356
}
349
357
}
350
358
}
351
359
}
352
360
353
-
354
361
@Composable
355
362
fun rememberParticleState (): ParticleState {
356
363
return remember {
@@ -406,35 +413,41 @@ class ParticleState internal constructor() {
406
413
val pixel: Int = bitmap.getPixel(pixelCenterX, pixelCenterY)
407
414
val color = Color (pixel)
408
415
409
- // println(
410
- // "Column: $column, " +
411
- // "row: $row," +
412
- // " pixelCenterX: $pixelCenterX, " +
413
- // "pixelCenterY: $pixelCenterY, color: $color"
414
- // )
415
-
416
416
if (color != Color .Unspecified ) {
417
-
418
417
val initialCenter = Offset (pixelCenterX.toFloat(), pixelCenterY.toFloat())
419
418
val horizontalDisplacement = randomInRange(- 50f , 50f )
420
- val verticalDisplacement = randomInRange(- height * .1f , height * .2f )
421
419
422
- val velocity = verticalDisplacement
420
+ val verticalDisplacement = randomInRange( - height * . 1f , height * . 2f )
423
421
val acceleration = randomInRange(- 2f , 2f )
424
- val progressStart = (initialCenter.x / width).coerceAtMost(.7f )
425
- val progressEnd = progressStart + 0.3f
422
+
423
+ val fractionToImageWidth = initialCenter.x / width
424
+ val fractionToImageHeight = initialCenter.y / height
425
+
426
+ // Get trajectory for each 5 percent of the image in x direction
427
+ // This creates wave effect where particles at the start animation earlier
428
+ var trajectoryProgressRange =
429
+ valueInRange(fractionToImageWidth, .05f )
430
+
431
+ // Add some vertical randomization for trajectory so particles don't start
432
+ // animating vertically as well. Particles with smaller y value in same x
433
+ // coordinate tend to start earlier
434
+ trajectoryProgressRange =
435
+ trajectoryProgressRange.start + fractionToImageHeight * Random .nextFloat()
436
+ .coerceAtMost(.2f ).. trajectoryProgressRange.endInclusive
437
+
438
+ val endSize = randomInRange(0f , particleSize.toFloat() * .5f )
426
439
427
440
particleList.add(
428
441
Particle (
429
442
initialCenter = initialCenter,
430
- displacement = Offset (horizontalDisplacement, verticalDisplacement),
431
443
initialSize = Size (particleSize.toFloat(), particleSize.toFloat()),
432
- // trajectoryProgressRange = progressStart..progressEnd ,
433
- endSize = Size . Zero ,
444
+ trajectoryProgressRange = trajectoryProgressRange ,
445
+ endSize = Size (endSize, endSize) ,
434
446
color = color,
435
- column = column / particleSize,
436
- row = row / particleRadius,
437
- velocity = velocity,
447
+ velocity = Velocity (
448
+ x = horizontalDisplacement,
449
+ y = verticalDisplacement
450
+ ),
438
451
acceleration = acceleration
439
452
)
440
453
)
@@ -447,7 +460,6 @@ class ParticleState internal constructor() {
447
460
}
448
461
449
462
fun updateParticle (progress : Float , particle : Particle ) {
450
-
451
463
particle.run {
452
464
// Trajectory progress translates progress from 0f-1f to
453
465
// visibilityThresholdLow-visibilityThresholdHigh
@@ -457,31 +469,28 @@ class ParticleState internal constructor() {
457
469
458
470
// Each 0.1f change in trajectoryProgress 0.5f total range
459
471
// corresponds to 0.2f change of current time
460
-
461
472
setTrajectoryProgress(progress)
462
473
463
474
currentTime = trajectoryProgress
464
- // .mapInRange(0f, 1f, 0f, 1.4f)
465
475
466
476
// Set size
467
- val width = initialSize.width + (endSize.width - initialSize.width) * currentTime
468
- val height = initialSize.height + (endSize.height - initialSize.height) * currentTime
469
- // currentSize = Size(width, height)
477
+ val width =
478
+ initialSize.width + (endSize.width - initialSize.width) * currentTime * .5f
479
+ val height =
480
+ initialSize.height + (endSize.height - initialSize.height) * currentTime * .5f
470
481
471
- // Set alpha
472
- // While trajectory progress is less than 70% have full alpha then slowly cre
473
- // alpha = if (trajectoryProgress == 0f) 0f
474
- // else if (trajectoryProgress < .7f) 1f
475
- // else scale(.7f, 1f, trajectoryProgress, 1f, 0f)
482
+ currentSize = Size (width, height)
476
483
477
- // Set position
478
- // acceleration = randomInRange(-5f, 5f)
479
- // velocity = 1f * Random.nextInt(10)
484
+ // Set alpha
485
+ // While trajectory progress is less than 80% have full alpha then slowly
486
+ // reduce to zero for particles to disappear
487
+ alpha = if (trajectoryProgress == 0f ) 1f
488
+ else if (trajectoryProgress < .8f ) 1f
489
+ else scale(.8f , 1f , trajectoryProgress, 1f , 0f )
480
490
481
- val maxHorizontalDisplacement = displacement.x
482
- val horizontalDisplacement = maxHorizontalDisplacement * trajectoryProgress
491
+ val horizontalDisplacement = velocity.x * trajectoryProgress
483
492
val verticalDisplacement =
484
- velocity * currentTime + 0.5f * acceleration * currentTime * currentTime
493
+ velocity.y * currentTime + 0.5f * acceleration * currentTime * currentTime
485
494
currentPosition = Offset (
486
495
x = initialCenter.x + horizontalDisplacement,
487
496
y = initialCenter.y - verticalDisplacement
@@ -516,7 +525,7 @@ class ParticleState internal constructor() {
516
525
suspend fun animate () {
517
526
try {
518
527
animatable.snapTo(0f )
519
- animatable.animateTo(1f , tween(durationMillis = 2000 , easing = LinearEasing ))
528
+ animatable.animateTo(1f , tween(durationMillis = 2400 , easing = FastOutSlowInEasing ))
520
529
// animationStatus = AnimationStatus.Idle
521
530
} catch (e: CancellationException ) {
522
531
println (" FAILED: ${e.message} " )
@@ -525,6 +534,23 @@ class ParticleState internal constructor() {
525
534
}
526
535
}
527
536
537
+ fun valueInRange (
538
+ input : Float ,
539
+ range : Float ,
540
+ minValue : Float = 0f,
541
+ maxValue : Float = 1f
542
+ ): ClosedRange <Float > {
543
+
544
+ if (range == 0f || range > 1f ) return minValue.. maxValue
545
+ val remainder = input % range
546
+ val multiplier = input / range
547
+
548
+ return (range * (multiplier - 4f ) - remainder)
549
+ .coerceAtLeast(0f ).. (range * (multiplier + 15f ) - remainder)
550
+ .coerceAtMost(maxValue)
551
+
552
+ }
553
+
528
554
enum class AnimationStatus {
529
555
Idle , Initializing , Playing
530
556
}
0 commit comments