Thanks for sharing your solution. I’ve had a good play with different solutions to achieve a smooth ticker that can handle text updates gracefully.
GSAP seemed promising to move the text smoothly, and I had a working version of a looping ticker that was buttery smooth, but had issues when it came to updating the text and keeping the position of the text correct. This is because GSAP uses durations for its movements. You can calculate the duration by doing ‘length of text / speed = duration’ and modify the GSAP timeline with the new time and x position data. You could probably get around this but it all became more effort than it was worth.
GSAP uses requestAnimationFrame
at core so though I might as well use this too and use a constant speed (speed being how many pixels to move the text by per frame).
I’ve basically gone down the route of ‘see how long the text is, move it to the left at a constant speed until it is past the end point, then move it back’. using javascript to move it. You get the current position and move the text by getting/setting the element transform matrix(...)
.
When this plays in Chrome it doesn’t look very smooth but looks perfect when playing in CasparCG (tested in the latest version d3e3efef596c03dd834007de5b100b3e1d6e08ae)
In my setup I have the lottie config + webCG in one file and the ticker text movement in another file, which is why I use a simple but effective way of call functions like this:
if (window.playStarted) window.playStarted();
I’ve included both lottie config + webCG and ticker movement code below, you will most likely be interested in the “Ticker text bit”.
The AE animation has a single text line named `.f0’.
///////////////////////////////////////////////////////////////////////////////
// Lottie Player bit //////////////////////////////////////////////////////////
var animData = window.animationData; // Path to the animation json
var animContainer = document.getElementById('lottie-container');
var anim = lottie.loadAnimation({
container: animContainer,
renderer: 'svg',
loop: false,
autoplay: false,
animationData: animData,
});
// on Update / Data
webcg.on('data', function (data) {
if (window.updateStarted) window.updateStarted();
// Update text here...
if (window.updateComplete) window.updateComplete();
});
// on Play
webcg.on('play', function () {
if (window.playStarted) window.playStarted();
anim.removeEventListener('complete');
// Play animation intro
anim.playSegments([0, 50], true);
anim.addEventListener('complete', function _playComplete(e) {
anim.removeEventListener(e.type, _playComplete);
if (window.playComplete) window.playComplete();
});
});
// on Stop
webcg.on('stop', function () {
if (window.stopStarted) window.stopStarted();
anim.removeEventListener('complete');
// Play animation outro
anim.playSegments([51, 100], true);
anim.addEventListener('complete', function _stopComplete(e) {
anim.removeEventListener(e.type, _stopComplete);
if (window.stopComplete) window.stopComplete();
});
});
///////////////////////////////////////////////////////////////////////////////
// Ticker text bit ////////////////////////////////////////////////////////////
var tickerInitialized = false;
var tickerRunning = false;
var tickerSpeed = 8; // pixels per frame
var tickerPadding = 100;
var animContainerWidth = 0;
var tickerTextEle;
var tickerTextWidth = 0;
var tickerContainerWidth = 0;
var tickerTextTransformString;
var tickerTextTransformMatrixArray;
var tickerTextTransformMatrixObj = {};
var tickerXStartPosition;
var tickerXFinishPosition;
var animationFrame;
window.updateComplete = function () {
initTicker();
};
window.playStarted = function () {
if (!tickerRunning) {
initTicker();
// If the ticker is not running, move the text to the start position.
tickerTextTransformMatrixObj.tx = tickerXStartPosition;
updateTransform(tickerTextEle, tickerTextTransformMatrixObj);
// start the movement
animationFrame = requestAnimationFrame(moveTicker);
tickerRunning = true;
}
};
window.stopComplete = function () {
cancelAnimationFrame(animationFrame);
tickerRunning = false;
};
function initTicker() {
if (!tickerInitialized) {
animContainerWidth = animContainer.getBoundingClientRect().width;
tickerTextEle = document.getElementsByClassName('f0')[0];
tickerTextTransformString = tickerTextEle.getAttribute('transform'); // The 'transform' value needs to be only 'matrix(a,b,c,d,tx,ty)'. Fingers crossed BodyMovin does not change this.
tickerTextTransformMatrixArray = JSON.parse(
tickerTextTransformString.replace(/^\w+\(/, '[').replace(/\)$/, ']') // This will only work if the the 'transform' value is only 'matrix()'
);
tickerTextTransformMatrixObj.a = tickerTextTransformMatrixArray[0];
tickerTextTransformMatrixObj.b = tickerTextTransformMatrixArray[1];
tickerTextTransformMatrixObj.c = tickerTextTransformMatrixArray[2];
tickerTextTransformMatrixObj.d = tickerTextTransformMatrixArray[3];
tickerTextTransformMatrixObj.tx = tickerTextTransformMatrixArray[4];
tickerTextTransformMatrixObj.ty = tickerTextTransformMatrixArray[5];
tickerXInitPosition = tickerTextTransformMatrixObj.tx;
tickerXStartPosition = animContainerWidth;
tickerInitialized = true;
}
// We need to wait for the text to be updated before we can get the text width
// The timeout duration needs to be atleast 1 frame
setTimeout(() => {
tickerTextWidth = tickerTextEle.getBoundingClientRect().width;
tickerXFinishPosition =
tickerXInitPosition - (tickerTextWidth + tickerPadding);
}, 60);
}
function moveTicker() {
tickerTextTransformMatrixObj.tx =
tickerTextTransformMatrixObj.tx - tickerSpeed;
// Move the ticker back to the start position if it has moved past the finish position
if (tickerTextTransformMatrixObj.tx < tickerXFinishPosition)
tickerTextTransformMatrixObj.tx = tickerXStartPosition;
updateTransform(tickerTextEle, tickerTextTransformMatrixObj);
// loop the movement
animationFrame = requestAnimationFrame(moveTicker);
}
function updateTransform(ele, m) {
var matrixString = 'matrix(' + m.a + ',' + m.b + ',' + m.c + ',' + m.d +',' + m.tx + ',' + m.ty + ')';
ele.setAttribute('transform', matrixString);
}
I like your idea of using an array of text, you could incorporate that into the above by joining them into a single string:
var textString = textFields.join(' ')
Sorry for the lengthy post, hope this is of some use!