Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 68c689b

Browse files
add wave transition to particle trajectory start
1 parent 6be9ae1 commit 68c689b

File tree

2 files changed

+115
-57
lines changed

2 files changed

+115
-57
lines changed

‎Tutorial1-1Basics/src/main/java/com/smarttoolfactory/tutorial1_1basics/ParticleAnimations.kt

Lines changed: 83 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ package com.smarttoolfactory.tutorial1_1basics
33
import android.graphics.Bitmap
44
import android.widget.Toast
55
import androidx.compose.animation.core.Animatable
6+
import androidx.compose.animation.core.FastOutLinearInEasing
7+
import androidx.compose.animation.core.FastOutSlowInEasing
68
import androidx.compose.animation.core.LinearEasing
79
import androidx.compose.animation.core.tween
810
import androidx.compose.foundation.Canvas
911
import androidx.compose.foundation.Image
12+
import androidx.compose.foundation.background
1013
import androidx.compose.foundation.border
1114
import androidx.compose.foundation.layout.Arrangement
1215
import androidx.compose.foundation.layout.Column
@@ -40,19 +43,21 @@ import androidx.compose.ui.graphics.Color
4043
import androidx.compose.ui.graphics.ImageBitmap
4144
import androidx.compose.ui.graphics.asAndroidBitmap
4245
import androidx.compose.ui.graphics.asImageBitmap
43-
import androidx.compose.ui.graphics.drawscope.clipRect
46+
import androidx.compose.ui.graphics.drawscope.Stroke
4447
import androidx.compose.ui.graphics.rememberGraphicsLayer
4548
import androidx.compose.ui.platform.LocalContext
4649
import androidx.compose.ui.platform.LocalDensity
4750
import androidx.compose.ui.res.painterResource
4851
import androidx.compose.ui.tooling.preview.Preview
52+
import androidx.compose.ui.unit.Velocity
4953
import androidx.compose.ui.unit.dp
5054
import androidx.compose.ui.unit.sp
5155
import com.smarttoolfactory.tutorial1_1basics.chapter6_graphics.randomInRange
5256
import com.smarttoolfactory.tutorial1_1basics.chapter6_graphics.scale
5357
import com.smarttoolfactory.tutorial1_1basics.chapter6_graphics.toPx
5458
import com.smarttoolfactory.tutorial1_1basics.ui.Pink400
5559
import kotlinx.coroutines.CancellationException
60+
import kotlin.random.Random
5661

5762

5863
@Preview
@@ -69,8 +74,6 @@ fun SingleParticleTrajectorySample() {
6974
mutableFloatStateOf(1f)
7075
}
7176

72-
val particleCount = 1
73-
7477
val density = LocalDensity.current
7578

7679
val sizeDp = with(density) {
@@ -94,13 +97,11 @@ fun SingleParticleTrajectorySample() {
9497
),
9598
initialSize = Size(5.dp.toPx(), 5.dp.toPx()),
9699
endSize = Size(sizePx, sizePx),
97-
displacement = Offset(
98-
sizePxHalf,
99-
sizePxHalf
100+
velocity = Velocity(
101+
x =sizePxHalf,
102+
y =sizePxHalf*4
100103
),
101-
trajectoryProgressRange = trajectoryProgressStart..trajectoryProgressEnd,
102-
row = 0,
103-
column = 0
104+
trajectoryProgressRange = trajectoryProgressStart..trajectoryProgressEnd
104105
)
105106
)
106107
}
@@ -189,6 +190,7 @@ fun ParticleAnimationSample() {
189190
Column(
190191
modifier = Modifier
191192
.fillMaxSize()
193+
.background(Color.DarkGray)
192194
.verticalScroll(rememberScrollState())
193195
.padding(horizontal = 16.dp, vertical = 100.dp),
194196
verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -244,15 +246,12 @@ fun ParticleAnimationSample() {
244246

245247
data class Particle(
246248
val initialCenter: Offset,
247-
val displacement: Offset,
248249
val initialSize: Size,
249250
val endSize: Size,
250251
val color: Color,
251252
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
256255
) {
257256
var currentPosition: Offset = initialCenter
258257
internal set
@@ -279,6 +278,8 @@ fun Modifier.explode(
279278
val density = LocalDensity.current
280279
val particleSizePx = with(density) { particleState.particleSize.roundToPx() }
281280

281+
val progress = particleState.progress
282+
282283
LaunchedEffect(animationStatus) {
283284
if (animationStatus == AnimationStatus.Initializing) {
284285
val imageBitmap = graphicsLayer
@@ -319,14 +320,12 @@ fun Modifier.explode(
319320

320321
if (animationStatus != AnimationStatus.Idle) {
321322

322-
val animatedProgress = particleState.progress
323-
324323
particleState.particleList.forEach { particle ->
325324

326-
particleState.updateParticle(animatedProgress, particle)
325+
particleState.updateParticle(progress, particle)
327326

328327
val color = particle.color
329-
val radius = particle.currentSize.width * .7f
328+
val radius = particle.currentSize.width * .65f
330329
val position = particle.currentPosition
331330
val alpha = particle.alpha
332331

@@ -336,21 +335,29 @@ fun Modifier.explode(
336335
color = color,
337336
radius = radius,
338337
center = position,
339-
// alpha = alpha
338+
alpha = alpha
340339
)
341340
}
342341

342+
// TODO disintegrate image non-uniformly, with blend mode of particles
343343
// clipRect(
344-
// left = animatedProgress * size.width
344+
// left = progress * size.width
345345
// ) {
346346
// this@onDrawWithContent.drawContent()
347347
// }
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+
)
348356
}
349357
}
350358
}
351359
}
352360

353-
354361
@Composable
355362
fun rememberParticleState(): ParticleState {
356363
return remember {
@@ -406,35 +413,41 @@ class ParticleState internal constructor() {
406413
val pixel: Int = bitmap.getPixel(pixelCenterX, pixelCenterY)
407414
val color = Color(pixel)
408415

409-
// println(
410-
// "Column: $column, " +
411-
// "row: $row," +
412-
// " pixelCenterX: $pixelCenterX, " +
413-
// "pixelCenterY: $pixelCenterY, color: $color"
414-
// )
415-
416416
if (color != Color.Unspecified) {
417-
418417
val initialCenter = Offset(pixelCenterX.toFloat(), pixelCenterY.toFloat())
419418
val horizontalDisplacement = randomInRange(-50f, 50f)
420-
val verticalDisplacement = randomInRange(-height * .1f, height * .2f)
421419

422-
val velocity = verticalDisplacement
420+
val verticalDisplacement = randomInRange(-height * .1f, height * .2f)
423421
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)
426439

427440
particleList.add(
428441
Particle(
429442
initialCenter = initialCenter,
430-
displacement = Offset(horizontalDisplacement, verticalDisplacement),
431443
initialSize = Size(particleSize.toFloat(), particleSize.toFloat()),
432-
// trajectoryProgressRange = progressStart..progressEnd,
433-
endSize = Size.Zero,
444+
trajectoryProgressRange = trajectoryProgressRange,
445+
endSize = Size(endSize, endSize),
434446
color = color,
435-
column = column / particleSize,
436-
row = row / particleRadius,
437-
velocity = velocity,
447+
velocity = Velocity(
448+
x = horizontalDisplacement,
449+
y = verticalDisplacement
450+
),
438451
acceleration = acceleration
439452
)
440453
)
@@ -447,7 +460,6 @@ class ParticleState internal constructor() {
447460
}
448461

449462
fun updateParticle(progress: Float, particle: Particle) {
450-
451463
particle.run {
452464
// Trajectory progress translates progress from 0f-1f to
453465
// visibilityThresholdLow-visibilityThresholdHigh
@@ -457,31 +469,28 @@ class ParticleState internal constructor() {
457469

458470
// Each 0.1f change in trajectoryProgress 0.5f total range
459471
// corresponds to 0.2f change of current time
460-
461472
setTrajectoryProgress(progress)
462473

463474
currentTime = trajectoryProgress
464-
// .mapInRange(0f, 1f, 0f, 1.4f)
465475

466476
// 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
470481

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)
476483

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)
480490

481-
val maxHorizontalDisplacement = displacement.x
482-
val horizontalDisplacement = maxHorizontalDisplacement * trajectoryProgress
491+
val horizontalDisplacement = velocity.x * trajectoryProgress
483492
val verticalDisplacement =
484-
velocity * currentTime + 0.5f * acceleration * currentTime * currentTime
493+
velocity.y * currentTime + 0.5f * acceleration * currentTime * currentTime
485494
currentPosition = Offset(
486495
x = initialCenter.x + horizontalDisplacement,
487496
y = initialCenter.y - verticalDisplacement
@@ -516,7 +525,7 @@ class ParticleState internal constructor() {
516525
suspend fun animate() {
517526
try {
518527
animatable.snapTo(0f)
519-
animatable.animateTo(1f, tween(durationMillis = 2000, easing = LinearEasing))
528+
animatable.animateTo(1f, tween(durationMillis = 2400, easing = FastOutSlowInEasing))
520529
// animationStatus = AnimationStatus.Idle
521530
} catch (e: CancellationException) {
522531
println("FAILED: ${e.message}")
@@ -525,6 +534,23 @@ class ParticleState internal constructor() {
525534
}
526535
}
527536

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+
528554
enum class AnimationStatus {
529555
Idle, Initializing, Playing
530556
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.smarttoolfactory.tutorial2_1unit_testing
2+
3+
import org.junit.Test
4+
import kotlin.math.max
5+
6+
7+
class AnimationOperationTest {
8+
@Test
9+
fun animationTest() {
10+
val result = valueInRange(0.51f, 0.2f)
11+
12+
println("🔥 Result: $result")
13+
14+
}
15+
16+
fun valueInRange(
17+
input: Float,
18+
range: Float,
19+
minValue: Float = 0f,
20+
maxValue: Float = 1f
21+
): ClosedRange<Float> {
22+
23+
if (range == 0f || range > 1f) return minValue..maxValue
24+
val remainder = input % range
25+
val multiplier = input / range
26+
27+
return (range * (multiplier) - remainder)
28+
.coerceAtLeast(0f)..(range * (multiplier + 1f) - remainder)
29+
.coerceAtMost(maxValue)
30+
31+
}
32+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /