“This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!”

preface

When we’re working on a website or a game, we’ve seen a lot of cool “slow motion” animations.

What is slow animation?

In a certain frequency, the property value of the target object is irregularly modified, so that it can do translation, rotation, zoom, explicit and implicit operation animation. You must have used some CSS properties:

  • transition
  • animation

Can achieve the effect of animation.

But today we’re going to talk about how to make a “timeline animation”.

When your animation is finished or halfway through, you want the animation to continue “easing” back to its original form or somewhere in the middle.

Do you have any good ideas at this time?

Maybe it’s a little vague, but let’s take a look at a GIF:

What should we do to achieve the image? Here’s how to write a library of “Timeline animations” from zero to one.

First, the core idea

The principle of our “timeline animation” is to add “tasks” of different time periods on the timeline, “tasks” is each animation, “tasks” has the start time, the duration.

The realization idea has already taken shape, so let’s think about it further:

First, we need to create a global timeline with a time length property,

Second, we need to add slow animation, which requires the following parameters:

  • Target The target for which we execute the animation
  • Paused whether the animation is paused
  • At the same time, our animation needs to be in linear motion, and it needs to have a duration.
  • A linear property on an object
  • Animation duration

We are considering implementing a “timeline animation library” called TweenTimeLine. When initializing an instance, register an animation. You also need to declare a static array _timeTweens to store each animation. Next we need to implement three methods:

  • Add adds a single animation to the timeline
  • To to achieve the animation’s slow motion direction
  • Seek Triggers the time node

Here comes the most important question

How do we make the animation move.

We can now use the to method to get the start state and end state of an animation as well as the start time and duration.

Use seek to get the current time node.

Can we use these “known conditions” to calculate the animation state of our current time node?

The answer is yes!

Known conditions:

  • The initial time of an animation is T ₀
  • The duration of an animation dt
  • The object start attribute v₀
  • Object end attribute v₁
  • Current time CT

You get the increment from the initial state.

∆v = (cT-t ₀)/dt * (V ₁ -V ₀)Copy the code

With that in mind, we can do it in code.

Two, code implementation

For now, we’re just going to do the animations above, and we’ll keep the interfaces and methods simple, but we’ll continue to refine the library later.

Declare two simple interfaces

Defines the types of parameters passed to the constructor and declares the types of animation property values.

/** * Timeline animation parameters *@export
 * @interface TweenTimeLineProps* /
export interface TweenTimeLineProps {
  /** Pause state */paused? : boolean;/** Animation delay */delay? : number; }/** Slow operation parameter */
export type AniProps = { [k: string]: number }
Copy the code

2. Create oneTweenTimeLineclass

First, we need to define a TweenTimeLine class.

It has a private attribute, _target, and each TweenTimeLine has an object to operate on.

Next, we define two private methods:

  • To defines the start to end and duration of the animation
  • Render updates the animation status

Finally, define two static methods:

  • Add adds each animation to the timeline
  • Seek is triggered externally, listening to the time node of each time

It is clear that our library structure is as follows:

export default class TweenTimeLine {
    /** The object to be eased */
    private _target: Object = null;
    constructor(target: Object, props: TweenTimeLineProps){}/** * The final state of slow motion *@param {AniProps} props
     * @param {number} [duration]
     * @memberof TweenTimeLine* /
    public to(props: AniProps, duration: number = 0){}/** * Jump to a point in time *@static
     * @param {number} delay
     * @memberof TweenTimeLine* /
    public static seek(delay: number): void{}/** * Timeline render *@private
     * @param {number} delay
     * @memberof TweenTimeLine* /
    private render(delay: number){}/** * Add animation *@private
     * @param {TweenTimeLine} act
     * @memberof TweenTimeLine* /
    public static add(act: TweenTimeLine){}}Copy the code

3. Constructor

We initialize a timeline animation that stores the target object, the start time of the animation, and the paused state.

const { paused = false, delay = 0 } = props;
this.paused = paused;
this.startDelay = delay;
this.durationSum = this.startDelay;
Copy the code

DurationSum is the total duration of the animation, starting with the passed parameter delay.

4. Implement the to method

The to method is used to define the end state and duration of an animation. Means that a timeline animation can have multiple animation state changes.

With this approach, we do two main things:

  • 1. Store the state of the initial and final animation.
  • 2. Record the steps of each animation.

Define two private methods:

this._appendProps(nextProps);
this._addStep({ duration, curProps, nextProps });
Copy the code

Define the _appendProps method to store the final animation state and the initial state:

for (let i in props) {
  this._origintargetProps[i] = this._curTargetProps[i] = this._target[i];
  this._curTargetProps[i] = props[i];
}
Copy the code

Define the _addStep method to store the animation of each step into an array _steps:

this._steps.push(props);
props.startFlagTime = this.durationSum;
this.durationSum += props.duration;
Copy the code

StartFlagTime is the start time of the animation, and durationSum is the total duration of the multiple to animation steps.

Implement the render method

First, when the time node is less than 0, we default to 0;

How to tell when the animation is over

The animation ends when the externally triggered time node is greater than the sum of durations.

// Animation done - need to pause
if (_t >= this.durationSum) {
  _t = this.durationSum;
  end = true;
}
Copy the code

You can animate when you are within the duration of the animation.

if (this._target && this._steps.length > 0) {
  let _l = this._steps.length;
  let _curStepIndex = -1;
  // Find the current animation
  for (let i = 0; i < _l; i++) {
    _curStepIndex = i;
    if (
      this._steps[i].startFlagTime <= _t &&
      this._steps[i].duration + this._steps[i].startFlagTime >= _t
    ) {
      break}}// Update properties
  if (_curStepIndex >= 0) {
    let step = this._steps[_curStepIndex];
    if (_t >= step.startFlagTime && _t <= step.startFlagTime + step.duration)
      this.updateTargetProps(
        step,
        Math.min((_t - step.startFlagTime) / step.duration || 0.1),)}}Copy the code

By judging that the incoming time node is greater than the start time, and the incoming “current time node < start time + duration”, the current animation interval can be obtained.

if (
  this._steps[i].startFlagTime < _t &&
  this._steps[i].duration + this._steps[i].startFlagTime >= _t
) {
  break;
}
Copy the code

Then we define a method to update the properties of the target object, updateTargetProps.

private updateTargetProps(step: any, ratio: number) {
    let { curProps, nextProps } = step;
    let v0: number, v1: number, delatV: number;
    if(! step && ratio ==1) {
        curProps = nextProps = this._curTargetProps;
    }
    for (let i in this._origintargetProps) {
        if ((v0 = curProps[i]) == null) {
            curProps[i] = v0 = this._origintargetProps[i];
        }
        if ((v1 = nextProps[i]) == null) {
            nextProps[i] = v1 = v0;
        }
        const delta = (nextProps[i] - curProps[i]) * ratio;

        let deltaV: number;
        deltaV = v0 + delta;
        this._target[i] = deltaV; }}Copy the code

Here is the implementation of how the animation works. Can be calculated by “known conditions” :

ratio = Math.min((_t - step.startFlagTime) / step.duration || 0.1);
delta = (nextProps[i] - curProps[i]) * ratio;
deltaV = v0 + delta;
this._target[i] = deltaV;
Copy the code

This changes the value of the _target attribute.

So how do we call the render method?

6. Implement the ADD method

We define the static method Add to add an animation to the timeline.

let tweenAnis: TweenTimeLine[] = TweenTimeLine._timeTweens;
if (tweenAnis.indexOf(act) == -1) {
  tweenAnis.push(act);
}
Copy the code

Define an _timeTweens array to hold all of our animations. To update each animation, all we need to do is walk through the tweentimeline._timetweens array and call his own render method.

7. Implement the SEEK method

The seek method is used to find the animation corresponding to the incoming time node.

let aniList: TweenTimeLine[] = TweenTimeLine._timeTweens.concat();
let _l = aniList.length - 1;
for (let i = _l; i >= 0; i--) {
  let ani: TweenTimeLine = aniList[i];
  ani.render(delay);
}
Copy the code

Now we can perform seek to jump to our corresponding animation state. Of course, we press a certain frequency of continuous call seek can achieve the animation of play, pause and reverse. Next, let’s look at how to use it.

3. Practical application

Let’s implement the text in the GIF above. First, prepare a small poem, ode to Geese by King Luobin of the Tang Dynasty.

Here we use the Canvas engine to draw the view. The engines are basically available on the market, pixi, Phaser, Egret, etc. After all, this “Timeline animation” is just a library. It just operates on a linear property on an object.

1. Configure text

To implement the above different entry mode for each text, we need to define a configuration item for the text. Declare text properties:

  • The text text
  • Fill text color
  • Blod bold
  • X position
  • Y position
  • Size Font size
  • TextAligin is centered or not
const textCfg = [
  {
    text: 'xu goose'.fill: '# 000000'.blod: true.x: 250.y: 420.size: 50.textAligin: 'center',},... Omit {heretext: 'Anthurium clears the waves. '.fill: '# 000000'.blod: true.x: 250.y: 440.size: 40.textAligin: 'center',},]Copy the code

2. Create a literal instance

Use the engine’s API to create the literals, iterate over each one, and assign them a corresponding attribute value. In the example below, game.textField () is in a custom engine, just like any other literal API such as pixi.

let textList = [];
for (let i = 0; i < textCfg.length; i++) {
  let curText = textCfg[i];
  var allText = curText.text.split(' ');
  let _x = 0;
  if ((curText.textAligin == 'center') != null) {
    _x = (750 - allText.length * curText.size) / 2;
  }
  // Each text
  allText.forEach((item, index) = > {
    let realY = curText.y + (curText.size + 20) * i;
    let _text = con.addChild(new GAME.TextField());
    _text.x = curText.textAligin;
      ? _x + curText.size * index
      : curText.x + (curText.size + 15) * index;
    _text.y = realY;
    _text.text = item;
    _text.size = curText.size;
    _text.fillColor = curText.fill;
    _text.alpha = 0;
    _text.rotation = 0;
    _text.textWidth = curText.size;
    _text.anchorX = _text.anchorY = 0.5* curText.size; _text.textAlign = FYGE.TEXT_ALIGN.CENTER; textList.push(_text); })}Copy the code

Then, for convenience, we mount the final properties of the two animations on each text.

let _random = Math.random();
let status1 = {
  alpha: 0.x: curText.textAligin
    ? _x + curText.size * index
    : curText.x + (curText.size + 15) * index,
  y: _random > 0.5 ? realY - Math.random() * 180 : realY,
  rotation: _random > 0.6 ? 360 * Math.random() : 0};let status2 = {
  alpha: 0.x: curText.x + (curText.size + 15) * index + 500.y: realY,
  rotation: 0};let status3 = {
  alpha: 0.x: curText.textAligin
    ? _x + curText.size * index
    : curText.x + (curText.size + 15) * index,
  y: 0.rotation: 0.scaleX: 0.1.scaleY: 0.1};if (i == 0) {
  _text['to1'] = status3;
} else if (i == 1) {
  _text['to1'] = status2;
} else {
  _text['to1'] = status1;
}

_text['to2'] = {
  alpha: 1.x: curText.textAligin
    ? _x + (curText.size + 10) * index
    : curText.x + (curText.size + 15) * index,
  y: realY,
  rotation: 0.scaleX: 1.scaleY: 1};Copy the code

Status1, Status2, and Status3 correspond to three animation states respectively. In this way, we render this ancient poem on the page.

All that is needed is the east wind. Pull out the timeline animation library we wrote above.

3. Add timeline animation

We now have three methods, one to, one add, one seek; Add our previous text list to the timeline.

textList.forEach((item, index) = > {
    let timeLine = new TweenTimeLine(item, { paused: false.delay: 0 / 1624 * 2 });
    timeLine.to(item.to1, 0 / 1624 * 2);
    timeLine.to(item.to2, 200 / 1624 * 2);
    TweenTimeLine.add(timeLine);
});
Copy the code

We set the animation to start at time zero. The first animation has a duration of 0, which is the first animation and we’ll just set it to item.to1. The second animation lasts 200/1624 * 2 and ends in the state of item.to2.

Why is it 200/1624 * 2

Because our timeline is 1624 * 2/1624 * 2 = 1, that’s 2 screen heights applied to the page.

So 200/1624 times 2 is when the screen goes 200 distance.

So here’s the question, we’re talking about a timeline animation, how far away are you from me?

In fact, when our animation speed is the same as the screen speed, then the distance is not representing the time?

t = s / v
Copy the code

After adding the “timeline animation”, it is time to trigger the event seek of the animation “move”.

TweenTimeLine.seek(120 / 1624*2);
Copy the code

After running the above code, we can see that the page has jumped directly to the distance of 120. The details are as follows:

So how do we animate the opening text?

We just need to keep firing seek as the screen scrolls. Here we refer to a library “PhyTouch” to scroll our pages. We then trigger our timeline animation each time we scroll change.

//@ts-ignore
window.alloyTouchs = null;
var lastValue = 0;
//@ts-ignore
window.alloyTouchs = new PhyTouch({
    touch: "body".// The dom of the feedback touch
    property: "translateY".// Properties of being moved
    min: -1624.// Not required, the minimum value of the motion attribute
    step: 45.// Used to calibrate to integer multiples of step
    sensitivity: 0.65.// Not required. The sensitivity of the touch area, which defaults to 1, can be negative
    maxSpeed: 1.max: 0.// Not required, the maximum value of the scroll property
    bindSelf: false.value: 0.change: function (value) {
        // Trigger the animation
        TweenTimeLine.seek(-value / 1624 * 2); }});Copy the code

Here we are, watching the animation play, pause, and fall back as we slide our mouse or finger up and down.

Interested students can also think about the “slide line” and “boat into the” animation implementation, is also very simple.

Looking forward to

A simple “timeline animation” is done. But as you can see, we only have two or three ways to go, and there’s a lot more that can be expanded. For example, we can define some useful methods:

  • Set Sets the initial properties of the animation
  • FromTo defines where animations come from and where they go
  • Call animation callback
  • Callback when the change animation is updated
  • Remove remove animation
  • .

We can also set a number of properties:

  • Repeat Number of times the animation is repeated
  • You can also encapsulate a tween
  • I’m going to stagger it
  • Yoyo animation round trip
  • .

This way we are almost ready to complete an animation library of our own. We will continue to improve this library, please keep an eye on it.

This article is shallow, please don’t hesitate your comments and likes ~ note: this article is the author’s painstaking work, reprint must declare