scrollLeft

闲来无事。发现自己没有……没有……没有亲手写过 无缝滚动 !!! ZZ 一样的混等。

scrollLeft
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>
scroll Left (¬_¬) Right
</title>
<style>
#demo {
width: 500px;
border: 1px dashed red;
overflow: hidden;
overflow-x: scroll;
}

table {
border-collapse: collapse;
}

td {
padding: 0;
}

.div {
width: 125px;
height: 100px;
background-image: linear-gradient(32deg, aqua, black, cyan, deeppink, fuchsia, gold, hotpink, indigo);
}

</style>
</head>
<body>
<!-- 容器 -->
<div id="demo" data-comment="this is scroll container">
<!-- 放个表格,样式很简洁,宽度自动增加。其中第一列为需要滚动的内容。 -->
<table>
<tr>
<!-- 展示内容要比容器长,否则,没有必要滚动,(尽管也可以滚动) -->
<!-- 细节,比如第一张图片还没有出去;新复制的第一张也不该显示出来 -->
<td id="listA" data-comment="origin content, column one">
<table>
<!-- 如果内容直接复制在 #tRow 中,就不需要外层表格了 -->
<tr id="tRow">
<td>
<div class="div">
1
</div>
</td>
<td>
<div class="div">
2
</div>
</td>
<td>
<div class="div">
3
</div>
</td>
<td>
<div class="div">
4
</div>
</td>
<td>
<div class="div">
5
</div>
</td>
</tr>
</table>
</td>
<td id="listB" data-comment="cloned content, column two">
</td>
</tr>
</table>
</div>
<script>
(async function 复制一份滚动内容() { //没什么卵用。随意写。
listB.innerHTML = listA.innerHTML;
}());

//假设 每秒移动 100px,
//再假设 移动频率 25, 即 40ms 移动一次。
//那么移动量 5
var deltaT = 40; //时间间隔
var deltaX = 5; //这就是每次移动量,正整数。
function goLeft() {
if(demo.scrollLeft >= listA.offsetWidth) {
demo.scrollLeft -= listA.offsetWidth;
}

demo.scrollLeft += deltaX;
}

function goRight() {
if(demo.scrollLeft < listA.offsetWidth - demo.clientWidth) {
demo.scrollLeft += listA.offsetWidth;
}

demo.scrollLeft += -deltaX;
}

//setInterval(goLeft, deltaT);
</script>
</body>
</html>

分析一下

我们这里用 scrollLeft 来实现。分析清楚以后,offsetLeft细节虽然不同,但道理是一样。

  1. 假设容器宽度为  W ,内容宽度为  L 。(满足 L > W,我觉得 L <= W 没必要无缝滚动,你非要滚动请自行设计。)
    假设容器的scrollLeft值为 x,开始,x = 0;然后 x 不停增加,一直移动到右边 x = L - W(滚不动了,x大于 L-W,也会被设置为 L-W)。
    x ∈ [0, L-W] ⊂ N。

    • Element.scrollLeft,那个rtl我在 Chrome 71.0.3578.98 测试,和描述不符。不兼容吧。我们这里不要理会 rtl。
  2. 我们要的效果

为了能让红色块滚出去,我们把它复制一份,蓝色的。 此时 x ∈ [0, 2L-W]。

显然,开始x=0,不断增加 x,内容就向左滚动了,当 x>=L 时,红色块滚了出去,显示的是蓝色块部分内容。关键是,蓝色块和红色块的长相是一样的。如果我们滚出了红色块,进入了蓝色块,我们只要马上跳转到相应的红色块位置即可。

当 x ∈ [L, 2L-W],显示的是蓝色块。 显示范围对应于 [x, x+W-1],由于周期性,它与 [x-L, (x+W-1)-L] 显示完全一样。 这就是无缝而动的本质。

那么这一句跳回去,

1
2
3
if(demo.scrollLeft >= listA.offsetWidth) {
demo.scrollLeft -= listA.offsetWidth;
}

可以改为

1
2
3
4
5
6
7
8
9
10
11
12
const L = listA.offsetWidth;
const W = demo.clientWidth;

const MIN = L;
const MAX = 2*L - W;
const RANGE = MAX - MIN;

// 跳转条件
const BOUNDARY = MIN + Math.round(RANGE * Math.random());
if(demo.scrollLeft >= BOUNDARY) {
demo.scrollLeft -= listA.offsetWidth;
}

动起来?

  1. deltaX 多大? 每一次移动,走多少距离合适?

因为 x ∈ [0, 2L-W],显然,1 <= deltaX <= 2L-W。

  • deltaX=1 看起来就很好用。

  • 再看deltaX=2L-W。你一下滚到头了。步子迈得太大,容易扯蛋。初始 x=0

    • 第一次 goLeft 后,x=2L-W,
    • 第二次 goLeft 后,x=2L-W,(满足跳转条件,x减去L,即 x=L-W,加个deltaX,3L-2W,过头了,又变成了 2L-W,x=2L-W)
    • 第三次 goLeft 后,x=2L-W,同上
    • ……
      完全没有动画效果,而且我认为这个deltaX无效,为啥?后面每次都加过头了。瞎几把搞。
  • deltaX上限多少?就是每次加上去都不会过头。

也就是每次 goLeft 都满足表达式 x + deltaX <= 2L-W
如果用 demo.scrollLeft >= BOUNDARY,会很复杂。我们就简化一下,还是用原来的,demo.scrollLeft >= listA.offsetWidth,即 x 大于等于 L 时,就需要减去 L,然后再计算上面的表达式。

假设:
xm-1 < L,
xm >= L,
其中 x0=0,xn = xn-1 + deltaX,

由于 xm 满足跳转条件,此时 x=xm - L,

(xm - L) + deltaX <= 2L-W, 令 xm=2L-W,得 deltaX <= L;
(xm-1 + delataX - L) + deltaX <= 2L-W, 令 xm-1=L-1,得 deltaX <= L - 0.5W + 0.5

同时 deltaX = xm - xm-1 = (2L-W) - (L-1) = L - W + 1

所以 当 deltaX 小于 L-W+1 时,是有效的;而当 deltaX 小于 2L-W 时,是合理的。因为 scrollLeft 有自己的上限。

  1. 怎么动起来?我们取 deltaX 为个位数就行了,一般 L 都要比 W 大。频率取值大于24(人眼?),小于60(显示器刷新?)即可。 即16ms < deltaT < 42ms。若 deltaT=25,deltaX=4,大概每秒能移动160距离。当 deltaT不变时,deltaX变化就能调整速度,比如 deltaX=10 或者 deltaX=Math.round((L-W+1)*Math.random());

然后, setInterval(goLeft, deltaT) 就完事了。

向右,向上,向下,斜着滚动,都可以做同样的分析。