Skip to main content

Crawler

info

If you are not familiar with the concept of middleware, make sure to check the middlewares explanation. Also, make sure that you are familiar with the Actions concept.

caution

This example interacts with the Actions API that will be re-designed in the future (keyframes). This example will be regularly updated so that it is always up-to-date with the latest version of the Loopic.

Crawler is one of the most popular ways to show text messages to the viewers. However, despite looking very simple and straightforward, implementing crawler animation in HTML has always been tricky and was almost impossible to do without 3rd party animation library. This guide will demonstrate how you can create advance crawler animations just by using Loopic - without any 3rd party libraries!

Prerequisites

For this example, you'll need one Composition with just one single Text element with the _text key. Make sure that the Text element has "auto size" option turned on since it will be important for calculating the length of the text. Duration of the Composition is not important since it will be dynamically changed using the Actions API.

The logic

Alright, so how do we implement a crawler? Should you set the Composition duration to, for example, 100 frames, and animate text going from left to right? Well, that would work if you had only static text. But when changing the text dynamically, we don't know how long the text will be, between which positions exactly it should animate, and how fast it should go.

To begin with, we'll create a new middleware that will be triggered each time our _text receives new content. This middleware should be added inside the Composition action, and that will make sure that our middleware is registered before it receives any new text content.

Composition action
loopic.useOnUpdate("_text", (key, value, next) => {
// Middleware code
});

Now inside the middleware, we should get the reference to the text element and get it's width. We'll also store the width of the composition and our desired crawler speed in dedicated variables. But be careful here - we want to get the width of the text content that has just been sent to the template, which means that we should first set the content of the Text element. The default text middleware (which is in this case referenced with the next variable) can do that job for us.

Part of the middleware code
next();
const SPEED = 200;
const compWidth = 1920;

const textEl = this.findElementByKey("_text");
const textWidth = textEl.style.width.value;

Now let's see between which X positions our Text element should animate. startX contains the starting X position of the Text element, finalX contains the final X position of the Text element, totalDistance is the distance (in pixels) which our Text element will travel, and duration contains information about how long our animation should be. Duration is not fixed here, because the duration for crawling very short text and very long text is different - very long text needs much more time to travel between the starting and final positions.

Part of the middleware code
const startX = compWidth + textWidth / 2;
const finalX = -textWidth / 2;
const travelDistance = textWidth + compWidth;
const duration = parseInt((travelDistance / SPEED) * this.fps);

Once we know our new duration of the animation, we need to change the duration of the Composition, as well as the duration of the layer which contains the Text element.

Part of the middleware code
textEl.layer.duration = duration;
this.duration = duration;

And we are almost done - now we just need to add keyframes to our animation.

Adding keyframes

The code below will delete any existing keyframes on the X-style property and it will add two new keyframes for animating our crawler. In the end, we'll need to trigger the compute method of the composition to tell the Loopic that something has changed and that it needs to recompute all the animations.

Part of the middleware code
textEl.style.x.keyframes = [];
textEl.style.x.keyframes.push({
id: "start",
_value: startX,
frame: 0,
easing: { p1x: 1, p1y: 1, p2x: 1, p2y: 1 },
});
textEl.style.x.keyframes.push({
id: "final",
_value: finalX,
frame: duration,
easing: { p1x: 1, p1y: 1, p2x: 1, p2y: 1 },
});
this.compute();

Final code

Here is what the final middleware inside the Composition action should look like.

Final Composition action
loopic.useOnUpdate("_text", (key, value, next) => {
const SPEED = 200;
const compWidth = 1920;

const textEl = this.findElementByKey("_text");
textEl.setContent(value);
const textWidth = textEl.style.width.value;

const startX = compWidth + textWidth / 2;
const finalX = -textWidth / 2;
const travelDistance = textWidth + compWidth;
const duration = parseInt((travelDistance / SPEED) * this.fps);

textEl.layer.duration = duration;
this.duration = duration;

textEl.style.x.keyframes = [];
textEl.style.x.keyframes.push({
id: "start",
_value: startX,
frame: 0,
easing: { p1x: 1, p1y: 1, p2x: 1, p2y: 1 },
});
textEl.style.x.keyframes.push({
id: "final",
_value: finalX,
frame: duration,
easing: { p1x: 1, p1y: 1, p2x: 1, p2y: 1 },
});
this.compute();
});

Bonus: Looping

Quite often, you want your crawler to keep looping. To achieve this, we will use the setInterval function, which starts the crawler animation every time it reaches the end.

The final code with looping now looks like this:

let timer;

loopic.useOnUpdate("_text", (key, value, next) => {
if (timer) {
clearInterval(timer);
}

next();
const SPEED = 200;

const textEl = this.findElementByKey("_text");
const textWidth = textEl.style.width.value;
const coreElement = this.findElementByKey("_core");
const coreComp = coreElement.composition;
const coreWidth = coreElement.style.width.value;

const startX = coreWidth + textWidth / 2;
const finalX = -textWidth / 2;

const travelDistance = textWidth + coreWidth;
const duration = parseInt((travelDistance / SPEED) * this.fps);

textEl.layer.duration = duration;
coreComp.duration = duration;

textEl.style.x.keyframes = [
{
id: "start",
_value: startX,
frame: 0,
easing: { p1x: 1, p1y: 1, p2x: 1, p2y: 1 },
},
{
id: "final",
_value: finalX,
frame: duration,
easing: { p1x: 1, p1y: 1, p2x: 1, p2y: 1 },
},
];
coreComp.compute();

const durationInSeconds = duration / this.fps;
const startAnimation = () => {
coreComp.goToAndPlay(0);
};
startAnimation();
timer = setInterval(startAnimation, durationInSeconds * 1000);
});

loopic.useOnStop(() => {
if (timer) {
clearInterval(timer);
}

this.play({ reverse: true });
});

If you have any questions or suggestions regarding this example, feel free to contact us at info@loopic.io.