Skip to content

Animations from 1st principles Part 1

Updated: at 05:09 PM (6 min read)

Getting started

Start a server from python for web illustration

python -m http.server 8000

I am trying to reproduce all the samples of coding math from this youtube channel. Do checkout his videos for its web implementation.

My web repository for the same

Welcome to Part ONE of this series

Task 1 - lets draw lines in canvas randomly

Something like this

Image


@Preview
@Composable
fun BasicLineDraws() {
    val numberOfLines by remember {
        mutableStateOf(50)
    }
    Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
        val canvasWidth = size.width
        val canvasHeight = size.height

        for(i in 1..numberOfLines){
            val startOffsetX = Random.nextFloat()*canvasWidth
            val startOffsetY = Random.nextFloat()*canvasHeight

            val endOffsetX = Random.nextFloat()*canvasWidth
            val endOffsetY = Random.nextFloat()*canvasHeight

            drawLine(
                start = Offset(
                    x = startOffsetX,
                    y = startOffsetY,
                ),
                end = Offset(
                    x = endOffsetX,
                    y = endOffsetY,
                ),
                color = Color.Black,
                strokeWidth = 5.dp.toPx(),
            )
        }
    })
}

For Android we have

Image

Task 2 - Draw a sin Wave

Something like this. We can create bounce effect on basis of sin wave, object shrink and grow animation, all this is possible because sin value is continuous and between -1 and 1 and it repeats itself infinitely. So we can use this property in lots of places. Image

How do you create wave like this?

Image

The starting code is this but the result would be like



@Preview
@Composable
fun SimpleSinWave() {
    Canvas(modifier = Modifier
        .fillMaxSize(),
        onDraw = {
            val canvasHeight = size.height * 0.3

            val sinStartY = canvasHeight / 2
            var startAngle = 0.0f
            val finalAngle = (2 * Math.PI).toFloat()
            while (startAngle < finalAngle) {
                val pointX = startAngle * 100
                // add vertical offset
                val pointY = sin(startAngle.toDouble()) * 100 + sinStartY
                drawCircle(
                    color = Color.Black,
                    radius = 5f,
                    center = Offset(
                        x = pointX,
                        y = pointY.toFloat(),
                    )
                )
                startAngle += 0.01f
            }
        })
}

Image

WHY is it so?

because according to mobile cartesian system y axis is positive when we go down, and -ve when we go up, as compared to maths cartesian system which is inverse (+ve as we go up, and -ve as we go down)

==So that is why graph is flipped around X axis.==

How to fix it??

Options

Yes and its done

Image



@Preview
@Composable
fun SimpleSinWave() {
    Canvas(modifier = Modifier
        .fillMaxSize(),
        onDraw = {
            val canvasHeight = size.height * 0.3

            val sinStartY = canvasHeight / 2
            var startAngle = Math.PI
            val finalAngle = (3 * Math.PI).toFloat()
            while (startAngle < finalAngle) {
                val pointX = startAngle * 100
                // add vertical offset
                val pointY = sin(startAngle) * 100 + sinStartY
                drawCircle(
                    color = Color.Black,
                    radius = 5f,
                    center = Offset(
                        x = pointX.toFloat(),
                        y = pointY.toFloat(),
                    )
                )
                startAngle += 0.01f
            }
        })
}

Another approach

There has to be a better way of creating sin curve or curves in android, rather than drawing 1000’s points.

Can Bezier curve help?

Try it out??

Divide sin wave into 2 bezier curves, such that it pivots at 0

Image


Canvas(
    modifier = Modifier
        .fillMaxSize()
        .padding(
            all = 40.dp,
        ),
    onDraw = {
        val canvasHeight = size.height
        val canvasWidth = size.width

        Log.d(TAG, "SimpleSinWave: width -> $canvasWidth and height -> $canvasHeight")

        val path = Path()

        // first half
        path.moveTo(0f, (canvasHeight * 0.5f))
        path.quadraticBezierTo(
            canvasWidth * 0.25f,
            0f,
            canvasWidth * 0.5f,
            canvasHeight * 0.5f,
        )

        path.moveTo(canvasWidth * 0.5f, (canvasHeight * 0.5f))
        // second half
        path.quadraticBezierTo(
            canvasWidth * 0.75f,
            canvasHeight,
            canvasWidth,
            canvasHeight * 0.5f,
        )

        drawPath(
            path = path,
            color = Color.Black,
            style = Stroke(
                width = 1.dp.toPx(),
            )
        )
    },
)

Task 3 - Bounce animation

In web ![[bounce animation.mov]]

In web it is done using something like this,

var centerY = height * 0.5,
  centerX = width * 0.5,
  offset = height * 0.4,
  speed = 0.1,
  angle = 0;

bounceAnimation();

/**

* this function will bounce the ball

*/

function bounceAnimation() {
  let y = centerY + Math.sin(angle) * offset;

  // clear canvas

  context.clearRect(0, 0, width, height);

  context.beginPath();

  // create circle

  context.arc(centerX, y, 50, 0, 2 * Math.PI, false);

  context.fill();

  angle += speed;

  requestAnimationFrame(bounceAnimation);
}

requestAnimationFrame tells browser that we want animation, and hence it calls this callback depending upon machine’s refresh rate which is 60FPS or 60Hz. Docs

Similar can be achieved in android, but that would be ==non performant==, so let’s do something from compose APIs to create the same effect.


@Composable
fun Dp.dpToPx() = with(LocalDensity.current) { this@dpToPx.toPx() }


@Preview
@Composable
fun BouncingBall(){
    BoxWithConstraints(modifier = Modifier.fillMaxSize()){
        val infiniteTransition = rememberInfiniteTransition("bouncing effect")

        val ballRadius = 40.dp

        val width = maxWidth.dpToPx()
        val height = maxHeight.dpToPx() - (ballRadius*2).dpToPx()
        val ballRadiusInPx = ballRadius.dpToPx()

        val offset by infiniteTransition.animateFloat(
            targetValue = height,
            initialValue = 0f,
            label = "animate-offset",
            animationSpec = infiniteRepeatable(
                animation = tween(
                    durationMillis = 3_000,
                    easing = LinearEasing,
                ),
                repeatMode = RepeatMode.Reverse,
            )
        )
        Box(
            modifier = Modifier
                .size(ballRadius * 2)
                .offset {
                    IntOffset(
                        x = ((width * 0.5f).toInt() - ballRadiusInPx).toInt(),
                        y = offset.toInt(),
                    )
                }
                .clip(
                    CircleShape
                )
                .background(
                    color = Color.Black,
                )
        )
    }

}

Well, well, and well, Grab a coffee and play around with different ease in Effects

In compose chaining does matter, change the changing order in Black box, like put offset to last, you would see that box never moves, actually its moving

We did following things

Task 4 - Circular motion

We would be using the following diagram to create effect like this

Image

img.png

Now with extra offset added, we would have motion starting from center right.

Full code here

Task 5 - Object in circular path

Well this one was tricky, and had to do some sort of mirroring across the y axis to achieve the results

Image

Image

Full code here

Task 6 - Elliptical Path

Motion in Elliptical Path, just a change in circular path.

Full code here

Task 7 - Pointer pointing to you

Image

img.png

Full code here

Code

The Repository for all code is here