马黑黑 发表于 2025-7-2 13:19

Tween动画链接补间及其播放停止暂停继续

本帖最后由 马黑黑 于 2025-7-2 13:21 编辑 <br /><br /><style>
        .artBox { font-size: 18px; }
        .artBox > p { margin: 10px 0; line-height: 30px; }
        .artBox mark { padding: 4px 6px; background: lightblue; }
        #prevBox { position: fixed; top: 0; right: 0; bottom: 0; left: 0; background: #eee; display: none; padding: 0; overflow: hidden; z-index: 1000; margin: 0; }
        #prevBox::after { position: absolute; content: '关闭预览'; bottom: 10px; left: calc(50% - 40px); padding: 0 4px; width: 80px; height: 30px; line-height: 30px; text-align: center; border: 1px solid #efe; border-radius: 6px; background: #eee; font-size: 14px; box-shadow: 2px 2px 6px rgba(0,0,0,.25); cursor: pointer; }
        iframe { position: relative; width: 100%; height: 100%; border: none; outline: none; box-sizing: border-box; margin: 0; }
</style>

<div id="prevBox"></div>
<div class="artBox">
        <p>当我们只有一个 tween 动画,若需要动画永恒持续运行,我们除了设计一个<mark>repeat(Infinity)</mark>机制,还可以使用链式补间方法来加以实现,<mark>tween.chain(tween)</mark>,这是 tween 动画自己链接自己,这将在 tween 动画结束时重新启动 tween 动画。依此原理,假如我们有两个或更多的动画,我们可以按次序依次相互链接补间,最后一个动画则链接到第一个动画,这就形成了闭环式的按次序执行的永动动画链。例如:</p>
        <div class="hEdiv"><pre class="hEpre">
// 假如我们有三个动画 tweenA、tweenB、tweenC
tweenA.start(); // 需要启动第一个动画,其余的不要start()
tweenA.chain(tweenB); // 动画A链接动画B
tweenB.chain(tweenC); // 动画B链接动画C
tweenC.chain(tweenA); // 动画C链接动画A
        </pre></div>
        <p>永动不意味着动画不可控。我们可能已经知道,<mark>tween.start()</mark>是启动一个动画,<mark>tween.stop()</mark>则停止动画。动画启动后可以暂停,<mark>tween.pause()</mark>,暂停之后可以继续,<mark>tween.resume()</mark>。停止(stop)和暂停(pause)以及开始(start)和继续(resume)是有区别的:停止后再启动动画,动画将以全新的方式开始,暂停后继续动画,动画从暂停的地方继续;开始总是以新的动画方式启动动画,继续则从上一个动画时间线上的点继续动画。另外,停止针对正在进行中的动画,继续则仅仅针对暂停有效。知道这些,我们就可以给多个动画加入控制机制,我们要做的就是遍历动画,检查它们处于什么状态,从而有针对性地、精准地控制其运动状态。</p>
        <p>以下示例我们设计三个球形div元素,它们将依次运行从左到右再返回的平移运动,通过链式(chain)补间实现动画接力及不间断运行。动画可以通过点击相应按钮进行交互控制——</p>
        <div class="hEdiv"><pre class="hEpre" id="pre1">
&lt;style&gt;
        #papa {margin: 30px auto; width: 900px; height: 360px; border: 1px solid gray; position: relative; }
        #b1, #b2, #b3 { position: absolute; left: 10px; top: 20px; width: 60px; height: 60px; border-radius: 50%; background: olive; }
        #b2 { top: 120px; }
        #b3 { top: 220px; }
        .btnsWrap { position: absolute; bottom: 10px; width: 100%; text-align: center; }
        .btnsWrap button { min-width: 70px; margin: 5px; padding: 4px; }
&lt;/style&gt;

&lt;div id="papa"&gt;
        &lt;div id="b1"&gt;&lt;/div&gt;
        &lt;div id="b2"&gt;&lt;/div&gt;
        &lt;div id="b3"&gt;&lt;/div&gt;
        &lt;div class="btnsWrap"&gt;
                &lt;button type="button" value="start"&gt;开始&lt;/button&gt;
                &lt;button type="button" value="stop"&gt;停止&lt;/button&gt;
                &lt;button type="button" value="pause"&gt;暂停&lt;/button&gt;
                &lt;button type="button" value="resume"&gt;继续&lt;/button&gt;
        &lt;/div&gt;
&lt;/div&gt;

&lt;script type="module"&gt;
        import TWEEN from 'https://638183.freep.cn/638183/3dev/examples/jsm/libs/tween.module.js';

        const btns = papa.querySelectorAll('button'); // 获取按钮
        const balls = ; // 球数组
        const poses = [{ x: 10 }, { x: 10 }, { x: 10 }]; // 初始位置数组
        const endPoses = [{ x: }, { x: }, { x: }]; // 目标位置数组
        const tweens = []; // 动画数组

        // 为每一个球创建tween动画
        balls.forEach( (b, k) =&gt; {
                const tween = new TWEEN.Tween(poses)
                        .to(endPoses, 4000)
                        .onUpdate( () =&gt; b.style.transform = `translate(${poses.x}px)` );
                if (k === 0) tween.start(); // 第一个球自动运行动画(不需要自动运行时注释掉或删掉)
                tweens.push(tween);
        });
       
        // 动画链式相互调用
        tweens.forEach( (t, k) =&gt; t.chain(tweens[(k + 1) % balls.length]) );

        // 循环运行TWEEN动画
        const animate = () =&gt; {
                TWEEN.update();
                requestAnimationFrame(animate);
        };

        // Tween运行、暂停状态
        const tweenState = () =&gt; {
                let play = 0, pause = 0; // 播放与暂停的tween动画数量
                tweens.forEach(tw =&gt; {
                        play += tw.isPlaying() ? 1 : 0;
                        pause += tw.isPaused() ? 1 : 0;
                });
                return {play: play &gt; 0, pause: pause &gt; 0 }; // 返回结果
        };

        // 设置按钮状态
        const btnState = () =&gt; {
                const state = tweenState(); // 获取当前动画运行状态
                // 播放状态为真
                if (state.play) {
                        btns.disabled = true;
                        btns.disabled = false;
                        btns.disabled = state.pause ? true : false;
                        btns.disabled = state.pause ? false : true;
                // 暂停状态为真
                } else btns.forEach( (b,k) =&gt; b.disabled = k === 0 ? false : true);
        };

        // 按钮点击事件
        btns.forEach(btn =&gt; {
                btn.onclick = () =&gt; {
                        const state = tweenState(); // 获取当前动画运行状态
                        switch (btn.value) {
                                case 'start': // 开始
                                        if (!state.play) tweens.start();
                                        break;
                                case 'stop': // 停止
                                        if (state.play) tweens.forEach(tw =&gt; tw.stop());
                                        break;
                                case 'pause': // 暂停
                                        if (!state.pause) tweens.forEach(tw =&gt; tw.pause());
                                        break;
                                case 'resume': // 继续
                                        if (!state.pause) return;
                                        tweens.forEach(tw =&gt; {
                                                if (tw.isPaused()) tw.resume();
                                        });
                                        break;
                        }
                        btnState(); // 设置按钮状态
                }
        });

        animate(); // 启动动画
        btnState(); // 初始化按钮状态
&lt;/script&gt;
        </pre></div>
        <blockquote><button id="btnPrev1">运行代码</button></blockquote>
        <p>代码量偏多,但不是Tween动画层面的实现问题,而是在处理按钮状态、按钮交互逻辑层面。其实按钮状态可以忽略,只是为了提升交互的友好性,我们花了一些精力对按钮的交互功能做了一些逻辑和状态方面的处理机制。</p>
        <p>最后提一下:chain 链式补间可以一对多,例如:<mark>tweenA.chain(tweenB, tweenC, tweenD);</mark>。</p>
</div>

<script type="module">
        import hlight from 'https://638183.freep.cn/638183/web/helight/helight1.js';
        const pres = document.querySelectorAll('.hEpre');
        const divs = document.querySelectorAll('.hEdiv');
        divs.forEach( (div, key) => hlight.hl(div, pres));
       
        const preView = (htmlCode, targetBox) => {
                if (targetBox.innerHTML) return;
                const iframe = document.createElement('iframe');
                htmlCode = htmlCode + '<style>body {margin: 0; }</style>';
                iframe.srcdoc = htmlCode;
                targetBox.appendChild(iframe);
                targetBox.style.display = 'block';
                targetBox.onclick = () => {
                        targetBox.innerHTML = '';
                        targetBox.style.display = 'none';
                }
        };

        const btns = ;
        const idPrevs = ;
        btns.forEach( (btn, key) => {
                btn.onclick = () => preView(idPrevs.textContent, prevBox);
        });
</script>

杨帆 发表于 2025-7-2 13:50

马老师您辛苦了!感谢老师经典、精彩讲授{:4_190:}


梦江南 发表于 2025-7-2 14:12

老师辛苦,谢谢详细讲解。{:4_187:}

花飞飞 发表于 2025-7-2 16:31

自己链自己真是太牛了,先点运行看效果比较好玩~{:4_173:}
无论是停止还是暂停,只要点开始,就是重新从第一个球开始运行。继续只对暂停状态。。
也可以第二个球暂停之时,让第一个球体重新开始。。多种运动形式组合。。

花飞飞 发表于 2025-7-2 16:33

设置目示数组时,用了x: 先到820处,再返回到原点10处,
这样就可以任意设定每个球体的到达位置和返回位置了。{:4_173:}
如果错落有致的也好看。。

花飞飞 发表于 2025-7-2 16:39

实例中动画互相调用 t.chain(tweens跟小例子中的tween.chain(tween)比,
只写成t也行,还可以这么省啊。。感觉代码写法很灵活,怎么少写反正系统都认得。。{:4_173:}

requestAnimationFrame(animate)循环调用也很厉害,结束后重新开始运行。。。

最头疼的下方的用按纽进行运行暂停的控制。。。
烧脑,我直接看晕了。{:4_170:}

花飞飞 发表于 2025-7-2 16:40

每天进行一场头脑风暴,嗯,不容易痴呆{:4_173:}

马黑黑 发表于 2025-7-2 17:08

花飞飞 发表于 2025-7-2 16:40
每天进行一场头脑风暴,嗯,不容易痴呆

{:4_189:}

马黑黑 发表于 2025-7-2 17:09

杨帆 发表于 2025-7-2 13:50
马老师您辛苦了!感谢老师经典、精彩讲授

{:4_191:}

马黑黑 发表于 2025-7-2 17:14

花飞飞 发表于 2025-7-2 16:39
实例中动画互相调用 t.chain(tweens跟小例子中的tween.chain(tween)比,
只写成t也行,还可以这么省啊。。 ...
关于 t 之类的问题它是有运行小环境的。假设 tweens 是一个代表有多个动画的数组:

tweens.forEach( ... ) 的意思是,遍历每一个 tweens 元素,干啥是小括号里的事情;

tweens.forEach( (t, k) => { ... } ) 的意思是,遍历 tweens 数组元素时,参数 (t, k) 中的 t 代表 tweens 数组元素中的单个元素,k 是元素在数组中的序号,=> 是箭头函数,(t, k) 里的 t 和 k 就是箭头函数的参数了,花括号里是函数体(即具体要干什么的代码)。

马黑黑 发表于 2025-7-2 17:16

花飞飞 发表于 2025-7-2 16:33
设置目示数组时,用了x: 先到820处,再返回到原点10处,
这样就可以任意设定每个球体的到达位置 ...

这个地方我没有解释,你自己看懂了,厉害

花飞飞 发表于 2025-7-2 17:37

马黑黑 发表于 2025-7-2 17:08


你说看看,脑细胞在学习过程中是烧亖的多呢还是进化得更强壮的多呢{:4_173:}

花飞飞 发表于 2025-7-2 17:39

马黑黑 发表于 2025-7-2 17:14
关于 t 之类的问题它是有运行小环境的。假设 tweens 是一个代表有多个动画的数组:

tweens.forEach( . ...

一句话,没看懂。。
先就这么着吧,基础太差{:4_170:}一路走一路晃悠

花飞飞 发表于 2025-7-2 17:40

马黑黑 发表于 2025-7-2 17:16
这个地方我没有解释,你自己看懂了,厉害

艾玛,看得正吃力呢,你这表扬我一下还是给口气的,总算能看对一丢丢了。。{:4_173:}

马黑黑 发表于 2025-7-2 18:13

花飞飞 发表于 2025-7-2 17:40
艾玛,看得正吃力呢,你这表扬我一下还是给口气的,总算能看对一丢丢了。。

这个相关只是,只有在过去的SVG动画里讨论过。原理相同,方式手段不同。

马黑黑 发表于 2025-7-2 18:14

花飞飞 发表于 2025-7-2 17:39
一句话,没看懂。。
先就这么着吧,基础太差一路走一路晃悠

数组遍历,箭头匿名函数,都是一大堆JS里的东东,这个没有基本功只能慢慢去理解了

马黑黑 发表于 2025-7-2 18:14

花飞飞 发表于 2025-7-2 17:37
你说看看,脑细胞在学习过程中是烧亖的多呢还是进化得更强壮的多呢

一般和小强一样的

红影 发表于 2025-7-2 19:19

按钮状态和按钮交互逻辑的确挺难理解的,看运行效果,如果第三个走到一半停止,再开始的时候是第一个开始运作,第三个还是在停的地方上,这个不太好理解。
应该是链接补间造成的吧,如果是独立的一个,应该就从头开始了。

马黑黑 发表于 2025-7-2 19:23

红影 发表于 2025-7-2 19:19
按钮状态和按钮交互逻辑的确挺难理解的,看运行效果,如果第三个走到一半停止,再开始的时候是第一个开始运 ...
stop 将动画停止在当下的状态,start() 重新开始动画、没有能力销毁先前的动画状态但所有的动画开始时都是从头再来

红影 发表于 2025-7-2 19:24

不光是小球的运行,这么多按钮和它们的对应判断也够复杂的{:4_204:}
页: [1] 2 3 4 5 6 7
查看完整版本: Tween动画链接补间及其播放停止暂停继续