Issue with vertical scrolling marquee

CMS Version

4.5.3

Player Type

Any

Marquee scroll too fast

I’m analyzing the jquery.marquee plugin block in the bundle and the two functions that call it (xiboElementsRender and xiboTextRender). .

What I verified in the code

Vertical scrolling is handled by the jquery.marquee plugin (module 59080), with this flow:

  1. Xibo sets data-speed = speed / 25 * 1000, in both xiboElementsRender and xiboTextRender. If speed is 0 it is forced to 1.
  2. As soon as the plugin reads the options, it computes duration = c.width() / speed * 1000. It always uses the container’s width, even when the direction is up or down. The vertical-direction flag (i) is only set after this line.
  3. In the vertical branch the duration is then rescaled: duration = (h + O) / O * duration, where O is the container height and h is the content height plus the gap.
  4. With seamless enabled (data-duplicated) the duration is divided by 2.

Why speed doesn’t behave as you’d expect

For vertical scrolling, the actual speed in pixels per second works out to speed * 40 * (container height / container width), rather than speed * 40. The result therefore depends on the container’s aspect ratio. A tall, narrow ticker scrolls much faster than it should, while a wide, short one scrolls much slower. This is an inference from the code, and something that can be observed at runtime in anyplayer.

Why duration has no effect

In the marqueeUp and marqueeDown branches of both functions, the widget’s duration parameter is never used. It only appears in the cycle branch (e.duration / numItems), i.e. for slide effects. For the marquee, the scroll time depends on speed and the content height, while the widget duration only determines when the layout moves on to the next widget. If the text is longer than the scroll can complete in the configured time, it gets cut off. This is the current code’s behavior, not necessarily the intended one.

By example with a 240 × 740 container, the formula puts the vertical scroll at about 3.1 times the speed it should have. This is a calculation from the code, not a runtime measurement. I’m assuming those are the dimensions of the element the marquee runs on (.scroll or its parent) and that O in the code equals 740.

The calculation

  • The plugin computes the base duration from the width: 240 / (speed × 40) × 1000 ms. For example, with speed = 2 (the default in the elements renderer) this is about 2.99 s.
  • It then multiplies by (h + 740) / 740, where h is the content height plus gap. The distance travelled is h + 740, so the actual velocity is 740 / base duration, which cancels out h.
  • With speed = 2 that gives about 248 px/s instead of the intended 80 px/s.
  • In general, the actual velocity is speed × 40 × (740 / 239), which is speed × 40 × 3.096. That is about 124 px/s per speed unit instead of 40.

I’m treating data-speed as pixels per second because that is what the horizontal case produces: there, width / duration works out to exactly data-speed. This is my reading of the intended behavior, not something documented.

Consequences

  • Since speed = 0 is forced to 1, the slowest scroll you can get with a similar aspect ratio is about 124 px/s.
  • The vertical scroll will never match the horizontal one, because the correction factor depends on the aspect ratio.

I have prepared a small python script to patch bundle.min.js to take into account height instead of width for vertical scolling plugins:


#!/usr/bin/env python3

"""Patch jquery.marquee in Xibo bundle: use height for vertical scroll."""

import sys
OLD = 's.speed&&(s.duration=parseInt(c.width(),10)/s.speed*1e3),i="up"===s.direction||"down"===s.direction,'

NEW = 'i="up"===s.direction||"down"===s.direction,s.speed&&(s.duration=parseInt(i?c.height():c.width(),10)/s.speed*1e3),'

src, dst = sys.argv[1], sys.argv[2]
data = open(src, encoding="utf-8").read()

n = data.count(OLD)
if n != 1:
sys.exit(f"ERROR: Expected 1 occurrences, found {n}. Not modifying.")
open(dst, "w", encoding="utf-8").write(data.replace(OLD, NEW))
print("Patch have been applied:", dst)

Usage: python3 apply_marquee_patch.py bundle_min.js bundle_min.patched.js
And then substitute the bundle…