Skip to content

Instantly share code, notes, and snippets.

@alexjlockwood
Created September 9, 2020 03:21
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save alexjlockwood/50a04490a39e7f60e000497d173699ad to your computer and use it in GitHub Desktop.
Save alexjlockwood/50a04490a39e7f60e000497d173699ad to your computer and use it in GitHub Desktop.
A composable wave square animation.
package com.alexjlockwood.wavesquare
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.animatedFloat
import androidx.compose.animation.core.AnimationConstants
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.repeatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.onActive
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.unit.dp
import kotlin.math.*
private const val N = 720
private const val NUM_LINES = 18
private const val NUM_WAVES = 18
private const val PI = Math.PI.toFloat()
private const val TWO_PI = 2 * PI
private const val LINE_LENGTH = 500f
private const val WAVE_HEIGHT = 20f
private const val SPACING = 27f
private const val CURL_AMOUNT = 12f
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WaveSquare(modifier = Modifier.fillMaxSize().padding(16.dp))
}
}
}
/**
* Creates a composable wave square animation.
*/
@Composable
fun WaveSquare(modifier: Modifier = Modifier) {
val animatedProgress = animatedFloat(0f)
onActive {
animatedProgress.animateTo(
targetValue = 1f,
anim = repeatable(
iterations = AnimationConstants.Infinite,
animation = tween(durationMillis = 5000, easing = LinearEasing),
),
)
}
val path = remember { Path() }.apply { reset() }
val t = animatedProgress.value
for (l in 0 until NUM_LINES) {
for (n in 0 until N) {
val qq = n.toFloat() / (N - 1)
val phase = map(n.toFloat(), 0f, N - 1f, 0f, TWO_PI * NUM_WAVES) - TWO_PI * t
var x = lerp(-LINE_LENGTH / 2f, LINE_LENGTH / 2f, qq)
var y = (SPACING * (l - 0.5f * (NUM_LINES - 1)))
val amount = ease(map(cos(TWO_PI * t + atan2(x, y) - 0.01f * dist(x, y, 0f, 0f)), 1f, -1f, 0f, 1f))
y += 0.5f * WAVE_HEIGHT * sin(phase + PI * l) * amount - 0.2f * WAVE_HEIGHT * amount
x -= CURL_AMOUNT * cos(phase + PI * l) * amount
if (n == 0) {
path.moveTo(x, y)
} else {
path.lineTo(x, y)
}
}
}
Canvas(modifier = modifier) {
withTransform({
scale(min(size.width, size.height) / LINE_LENGTH)
translate(size.width / 2f, size.height / 2f)
}, {
drawPath(
path = path,
color = Color.Black,
style = Stroke(width = 2f, miter = 1f),
)
})
}
}
private fun map(value: Float, start1: Float, stop1: Float, start2: Float, stop2: Float): Float {
return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1))
}
private fun lerp(a: Float, b: Float, t: Float): Float {
return a + (b - a) * t
}
private fun dist(x1: Float, y1: Float, x2: Float, y2: Float): Float {
return sqrt(((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)))
}
private fun ease(p: Float): Float {
return 3 * p * p - 2 * p * p * p
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment