avatar

Coderek's blog

Loneliness is the gift of life

Tween Animation

Recently I revamped my website by making a number of overdue improvements. One of the improvements was to add animation to the top bar since moderate amount of animation makes a website look more polished.

Animations on webpages can be done in several ways. By CSS, we can use simple transitions. For more controlled animation we can use keyframe. By JavaScript, we can manipulate props directly. We also can use canvas to animation in a high performance way.

The animation I want to do is simple. The Top Nav consists of avatar on the left and texts on the right. The texts on the right consists of a headline title, followed by a caption at the bottom. The height is about 77 pixels which is too much on mobile. So I would like it to shrink while user scrolls downwards until it reaches a minimum heigh on mobile. The shrinking should also be accompanied by changing size of the avatar as well as fading out of the caption. As simple as that.

Which method should I use?

CSS is the fastest. But I can't use it, because CSS animation is based on time while I need to animate with scroll. Canvas is an overkill to me as I just want to animate a couple of UIs. So I just need some simple JS to achieve it.

I need to build a simple tween function. The naive assumption is: given a scroll y value, I need to know what the property value I need to assign to an element.

Then I can plugin in this function in a scroll listener and call it when never scroll position is updated.

document.addEventListener('scroll', ev => {
  const pos = window.scrollY
  header.style.height = headerHeightTween(pos)
})

The question is how to derive the updated value from scroll position. There should be a relation between the scroll position and derived value. This relation can be linear or non-linear. Let's take a linear relation first.

So let's say our range of attribute is [attrStart, attrEnd]. Our range of scroll is [baseStart, baseEnd]. And the position value is value then our linear relation can be represented as:

attrValue = (value - baseStart) / (baseEnd - baseStart) * (attrEnd - attrStart) + attrStart

There are a couple of assumptions here

  1. all values here are numeric value.
  2. value is between [baseStart, baseEnd]

The formula has to be adjusted for the assumptions do not hold. So other than the position value we have four more parameters needed for the tween animation. We can use JavaScript closure to hold them.

const tween = (baseStart, baseEnd, attrStart, attrEnd) => {
  return value => (value - baseStart) / (baseEnd - baseStart) * (attrEnd - attrStart) + attrStart
}

Then we can use it like

const headerHeightTween = tween(0, header.clientHeight, 50, 30)

document.addEventListener('scroll', ev => {
  const pos = window.scrollY
  header.style.height = headerHeightTween(pos)
})
...

Very simple!

A couple of interesting things to note here.

  1. this tween function can run tween on only ordered sequence of values
  2. we can run it on discrete values as long as values has total ordering
  3. the linear function can be modified to apply a non-linear speed of change
  4. we should also consider how to handle out of range values; do we repeat? or just stop?
  5. we can combine attributes so run animation on them all at once
  6. if based on time, requestAnimationFrame should be used to boost performance

As you can see, tween function is really easy to understand and implement.

(End of article)