The extendable and framework agnostic animation library with timeline inspector.
pnpm add light-trails
# or
yarn add light-trails
# or
npm install light-trails
- Highly extendable with a lot of small elements which you can replace
- Advanced animation inspector as a separate package
- Easy composition even for completely different renderers
- Timeline based so you can control the animation using seek(t)
- Written in TypeScript with pretty good typings
- Rock-solid declarative way of doing animations
- Build for large animation sets - previously as part of Phenomenon web slides engine, now a stand-alone package
import { lightTrails, trail, fromTo, delay, val, color, parallel } from 'light-trails'
const bodyTrail = trail('body', [
fromTo({
backgroundColor: color('#FF0000', '#00FF00')
}, 1500)
])
const titleTrail = trail('#title', [
delay(500),
fromTo({
y: val(0, 50, 'px'),
opacity: val(0, 1),
rotate: val(0, 360, 'deg'),
}, 500)
])
const animation = lightTrails(
parallel([
bodyTrail,
titleTrail,
])
)
animation.play()
First you have to install inspector:
yarn add --dev light-trails-inspector
# or
npm install --save-dev light-trails-inspector
And just put lightTrails
instance into inspector
function:
import { inspector } from 'light-trails-inspector'
// ...
const animation = lightTrails(/* … */)
inspector(animation)
First of all, to make an animation we have to start with trail()
function.
Under this name, you can find a combination of renderer (by default it's a HTML/CSS renderer) and a set of operators discarding animation steps.
There is a example of fade in trail:
const fadeInTrail = trail('h1', [
fromTo({ opacity: val(0, 1) }, 400)
])
You are probably wander what fromTo()
or val()
are.
Let me explain what's going on.
The fromTo
is one of the operators which takes an object with functions that can return a value in a specific time. Parameter n
here is the percentage value from 0 to 1:
fromTo({ opacity: (n) => … }, 400)
For example, if the animation will be at 200ms this function will be called with n = 0.5
.
Depending on what you want to do you can make your own value function or use one of build-in. The most common are val(0, 100, 'px')
and color('#FFF', '#000')
.
There are more operators which can work together, for example set()
when you want to change a value immediately or delay()
:
const fadeInTrail = trail('h1', [
set({ display: ['none', 'block']}),
delay(1000),
fromTo({ opacity: val(0, 1) }, 400),
])
Previously mentioned trail
function carries information about animation only for the specific element.
To run our animation we have to use controller lightTrails
:
const fadeInTrail = trail('h1', [
set({ display: ['none', 'block']}),
delay(1000),
fromTo({ opacity: val(0, 1) }, 400)
])
const animation = lightTrails(fadeInTrail)
animation.play()
Plural function name reveals one of the key features of this library, it's not about one trail but about composition.
To run related trails together you have to use composition functions, the simplest ones are sequence
and parallel
which I think are self-explanatory.
For example: we have two different trails, one for body
background color and a second one for h1
fade-in animation:
const bodyTrail = trail('body', [
fromTo({ backgroundColor: color('#000', '#FFF') }, 1400)
])
const fadeInTrail = trail('h1', [
set({ display: ['none', 'block']}),
delay(1000),
fromTo({ opacity: val(0, 1) }, 400)
])
Assume we want to run them one after anther, let's use sequence
function.
const combinedTrails = sequence([bodyTrail, fadeInTrail])
Now we have a single trail so we can finally run it using a controller as before:
const animation = lightTrails(combinedTrails)
animation.play()
Composition functions have exactly the same type as a trail, so you can combine them together e.g. parallel
inside a sequence
.
const combinedTrails = sequence([
bodyTrail,
parallel([
topTrail,
contentTrail,
footerTrail,
])
])
const animation = lightTrails(trail, {
onPlay() {
// animation starts playing
},
onComplete() {
// animation is completed
},
onPause(manual) {
// animation paused by `animation.pause()` (manual = true) or `pause()` operator
},
onUpdate() {
// triggered on every frame
}
})
Starts the animation
animation.play()
Pauses the animation
animation.pause()
Moves the animation head to a specific point in time
animation.seek(200)
Prepares the animation by assigning initial values, it is useful when you do not play the animation immediately.
animation.prepare()
// Return object with current animation status
animation.getStatus()
{
playing: boolean
ended: boolean
started: boolean
currentTime: number
currentTimeIndex: number
total: number
}
Finds next or prev pause time.
animation.findNextPause()
animation.findPrevPause()
Trail is a combination of renderer function and operators array.
If you put string
or HTMLElement
as first argument it will use build-in HTML/CSS renderer.
trail(renderer: string | HTMLElement | Function, operators: Array)
Example:
const myTrail1 = trail('#my', [ … ])
const myTrail2 = trail(document.getElementById('my'), [ … ])
const myTrail3 = trail(document.body, [ … ])
const myTrail4 = trail(myCustomRendererFunction, [ … ])
The second argument is an array of operators, for example:
const myTrail = trail('#my', [
set({ display: ['none', 'block']}),
fromTo({ opacity: val(0, 1) }, 1000),
])
lightTrails(myTrail).play()
For more information, see the operators section.
It's possible to make your own renderer function. Let's assume that we want to "render" values as object. This may be useful for libraries like ThreeJS.
Full example:
const position = { x: 0 }
const myTrail = trail(
(values) => {
position.x = values.posX;
},
[
fromTo({ posX: val(0, 100) }, 1000),
]
)
lightTrails(myTrail).seek(500)
console.log(position.x) // → 50
Joins trails together.
Stack trails to run at the same time
const bodyTrail = trail('body', [ … ])
const fadeInTrail = trail('h1', [ … ])
// → aaaa
// → bbbb
// → cccc
const combinedTrails = parallel([bodyTrail, fadeInTrail, …])
lightTrails(combinedTrails).play()
Stack trails one after the other
// → aaaa
// → bbbb
// → cccc
const combinedTrails = sequence([bodyTrail, fadeInTrail, …])
Cascade with offset based on the index
// → aaaa
// → bbbb
// → bbbb
const combinedTrails = cascade([bodyTrail, fadeInTrail, …], {
offset: (i) => i * 300
})
Smoothly changes values within a specified time.
Values object with functions which receive the percentage value from 0 to 1
{ valueName: (n: number) => any }
Example:
fromTo({
height: val(0, 100, 'px'),
width: (n) => Math.sin(n) * 100 + 'px',
})
Sets a specific value depends on where animation head is.
Values object is array with two items (tuple)
{ valueName: [any, any] }
Example:
set({
display: ['none', 'block'],
height: [0, 'auto']
})
Delays next operators by given time
Example:
const elTrail = trail(el, [
delay(200),
fromTo({ opacity: val(1, 0) }, 200),
delay(100),
set({ display: ['block', 'none'] })
])
Pauses animation in specific place:
const elTrail = trail(el, [
fromTo({ opacity: val(0, 1) }, 200),
pause(),
fromTo({ opacity: val(1, 0) }, 200),
])
"Values" are small functions built specifically for fromTo
operator. Basically it is a linear interpolation between values so you can use them wherever you like.
const opacity = val(0.2, 0.8)
const size = val(100, 200, 'px')
opacity(0) // 0.2
size(0.5) // '150px'
Example:
const size = val(100, 200, 'px')
const elTrail = trail(el, [
fromTo({
opacity: val(0.2, 0.8),
width: size,
height: size,
}, 200),
])
Same thing as val
but for colors.
const color1 = color('#FFF', '#000')
const color2 = color('rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 1)')
color1(0.5) // 'rgba(180, 180, 180, 1)'
color2(0.5) // 'rgba(0, 0, 0, 0.5)'
Example:
const elTrail = trail(el, [
fromTo({
color: color('#FFF', '#000'),
backgroundColor: color('rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 1)'),
}, 200),
])
valChain(a: number, suffix?: string)
colorChain(a: string)
Sometimes you have to change the same value multiple times starting from the previous value, for example:
const elTrail = trail(el, [
fromTo({
color: color('#FFF', '#000'),
height: val(0, 50, 'px'),
}, 200),
fromTo({
color: color('#000', '#ABC123'),
height: val(50, 100, 'px'),
}, 200),
])
Look at the next example, valChain
or colorChain
can be useful in this case:
const elColor = colorChain('#FFF') // First, define starting values
const elHeight = valChain(0, 'px')
const elTrail = trail(el, [
fromTo({
color: elColor('#000'), // Put the next value
height: elHeight(50),
}, 200),
fromTo({
color: elColor('#ABC123'), // And so on
height: elHeight(100),
}, 200),
])
There is no built-in option for that but you can play the animation again after completion, for example:
const bodyTrail = trail('body', [
fromTo({ backgroundColor: color('#000', '#FFF') }, 1000)
])
const animation = lightTrails(bodyTrail, {
onComplete() {
animation.play()
}
})
animation.play()
Yep, simply use useRef
and useEffect
hooks:
const MyComponent = () => {
const ref = useRef(null)
useEffect(() => {
const elTrail = trail(ref.current, [
fromTo({ opacity: val(0, 1) }, 1000)
])
const animation = lightTrails(elTrail)
animation.play()
return () => animation.pause()
}, [])
return <div ref={ref}>Test</div>
}