syn
syn

Reputation: 13

Marquee text effect. Same scrolling speed no matter the length of the text

I have a element on my website similar to a marquee in that it slides from the right side to the left side of the screen. The html looks like this:

<div class='text-scroller-container'>
    <p class='message'></p>
</div>

There can be many different messages inside the scroller. Some range from a single word to a full sentence.

The way I am handling the scrolling is by setting left: 100% and adding a transition: left 5s. Then setting the left to 0 within js.

The problem im facing now is that messages that are super short scroll very slowly while very long messages scroll fast because theyre all binded to the 5s transition duration.

Im sure there is a way to instead, calculate a speed relative to the elements offsetWidth so that it scrolls at roughly the same speed no matter what the length of the message is.

My initial solution for this was to instead use a setInterval/requestAnimationFrame and move the element 1px at a time until its width is completely off screen. However, i now need to improve performance on my webapp so I am switching back to using transitions.

Does anyone have experience with this?

Upvotes: 1

Views: 2541

Answers (2)

Alex
Alex

Reputation: 1309

I have modified the answer by @Emiel Zuurbier to be more dynamic. Because the 200px width will pose a problem when you use this code on multiple containers and using several widths. Also I think it is useful to have a dynamic speed plus starting point.

I hope my code adaptation is useful to others.

/**
 * Select all the messages
 * @type {NodeList}
 */
const messages = document.querySelectorAll('.marqueemessage');

/**
 * For each message, calculate the duration based on the lenght of the message.  
 * Then set the animation-duration of the animation.
 */
messages.forEach((message, index) => {
    /**
    * Get the custom speed in time (in milliseconds) of a single pixel from the data-timer attribute
    * Changing this value will change the speed.
    * @type {number}
    */
    let timePerPixel = parseInt(message.dataset.timer, 10);

    // Get the starting position ('r' for right, 'l' for left)
    let startPosition = message.dataset.start;

    // Get the closest div element
    let container = message.closest('div');

    // Get the message height
    let messageHeight = message.offsetHeight;

    // Set the container height
    container.style.height = messageHeight + 'px';

    // Get the container width
    let containerWidth = container.offsetWidth;

    // Set the message width
    let messageWidth = message.offsetWidth;
    let distance = messageWidth + containerWidth;

    // Set the duration
    let duration = timePerPixel * distance;

    // Duplicate the message content
    message.innerHTML += ' ' + message.innerHTML;

    // Adjust the width of the message to fit the duplicated content
    message.style.width = `${messageWidth * 2}px`;

    // Set the message animation duration
    message.style.animationDuration = `${duration}ms`;

    // Create a unique name for the keyframes based on the index
    let animationName = `marquee-animation-${index}`;

    // Create dynamic keyframes with the full message width
    let keyframes = `
        @keyframes ${animationName} {
            0% {
                transform: translate3d(${startPosition === 'r' ? 0 : -messageWidth}px, 0, 0);
            }
            100% {
                transform: translate3d(${startPosition === 'r' ? -messageWidth : 0}px, 0, 0);
            }
        }
    `;

    // Apply the animation to the message
    message.style.animationName = animationName;

    // Check if the style element for keyframes already exists
    let styleElement = document.getElementById('dynamic-keyframes');
    if (!styleElement) {
        styleElement = document.createElement('style');
        styleElement.type = 'text/css';
        styleElement.id = 'dynamic-keyframes';
        document.head.appendChild(styleElement);
    }

    // Append the new keyframes to the style element
    styleElement.sheet.insertRule(keyframes, styleElement.sheet.cssRules.length);
});
.text-scroller-container {
    position: relative;
    width: 100%;
    overflow: hidden;
}
.marqueemessage {
    display: block;
    position: absolute;
    top: 0;
    right: 0;
    margin: 0;
    white-space: nowrap;
    overflow: hidden; /* Hide the overflow to prevent scrollbars */
    /* Starting postition */
    transform: translate3d(100%, 0, 0);
    /* Animation settings */
    animation-iteration-count: infinite;
    animation-timing-function: linear;
}
<div class="text-scroller-container">
  <p class="marqueemessage" style="font-size: 52px; font-weight: 900;" data-timer="50" data-start="r">This is a sentence. I"m a long sentence.</p>
</div>

<div class="text-scroller-container">
  <p class="marqueemessage" style="font-size: 28px; font-weight: 800;" data-timer="10" data-start="l">This is a short sentence.</p>
</div>

<div class="text-scroller-container">
  <p class="marqueemessage" data-timer="20" data-start="r">This is a very long sentence. This sentence is going to be the longest one of them all.</p>
</div>

Yet again... This code is a copy from Emiel Zuurbier and modified to support more dynamic options.

Upvotes: 0

Emiel Zuurbier
Emiel Zuurbier

Reputation: 20934

This sounds more like an an animation, than a transition. Where a transition runs only once when a state changes, animation can loop forever, creating that marquee effect.

What you'll need is an animation loop. You can do that with CSS Keyframes. With it you can specify a start and an end state, then loop those states infinitely.

Now the problem here is the speed. The speeds needs to be calculated. CSS can't do that so we'll need to add some JavaScript which will take care of that.

The calculation for the speed is amount of pixels per second * (width of message + container width). So the amount of distance travelled within a period of time times the distance. The bigger the message, the bigger the duration.

The example below shows three marquee's, each with different messages of different lengths. JavaScript loops over each message, makes the calculation, and sets the animationDuration in milliseconds for each message.

/**
 * The speed in time (in milliseconds) of a single pixel.
 * Changing this value will change the speed.
 * @type {number}
 */
const timePerPixel = 20;

/**
 * Width of the container.
 * Hardcoded for simplicity' sake.
 * @type {number}
 */
const containerWidth = 200;

/**
 * Select all the messages
 * @type {NodeList}
 */
const messages = document.querySelectorAll('.message');

/**
 * For each message, calculate the duration based on the lenght of the message.  
 * Then set the animation-duration of the animation.
 */
messages.forEach(message => {
  const messageWidth = message.offsetWidth;
  const distance = messageWidth + containerWidth;
  const duration = timePerPixel * distance;

  message.style.animationDuration = `${duration}ms`;
});
.text-scroller-container {
  position: relative;
  width: 200px;
  height: 20px;
  border: 1px solid #d0d0d0;
  border-radius: 3px;
  background-color: #f0f0f0;
  overflow: hidden;
  margin-bottom: 10px;
}

.message {
  display: block;
  position: absolute;
  top: 0;
  right: 0;
  margin: 0;
  white-space: nowrap;
  
  /* Starting postition */
  transform: translate3d(100%, 0, 0);
  
  /* Animation settings */
  animation-name: marquee-animation;
  animation-iteration-count: infinite;
  animation-timing-function: linear;
}

@keyframes marquee-animation {
  from {
    /* Start right out of view */
    transform: translate3d(100%, 0, 0);
  }
  
  to {
    /* Animate to the left of the container width */
    transform: translate3d(-200px, 0, 0);
  }
}
<div class='text-scroller-container'>
  <p class='message'>This is a sentence. I'm a long sentence.</p>
</div>

<div class='text-scroller-container'>
  <p class='message'>This is a short sentence.</p>
</div>

<div class='text-scroller-container'>
  <p class='message'>This is a very long sentence. This sentence is going to be the longest one of them all.</p>
</div>

If you're looking for performant animations, then use the transform property instead of left. While changing left will repaint the entire page, transform will only re-render only the portion that is affected by the transformation.

Upvotes: 4

Related Questions