模仿網易四字魔咒:PixiJS實現h5一鏡到底

目錄

前言

1.代碼地址與demo效果圖

2.項目技術架構分析

3. PixiJS

3.1 pixi常見概念介紹

3.2 創建應用

3.3 預加載資源

3.4 初始化場景

3.5 初始化精靈

4. AlloyTouch

5. TimelineMax

6. 初始化動畫

7. 舞臺的整體縮放與旋轉

8.添加背景音樂

總結

後記

// 續更 2019-09-08

(1)運動屬性的最小值應該怎麼計算?

(2)爲什麼後面會劇烈抖動??

(3)動畫的delay與duration如何計算???


前言

網易四字魔咒:http://news.163.com/special/fdh5_tolerance/(手機掃描以下二維碼查看)

偶然機會看到網易的四字魔咒,覺得so cool!一開始以爲只是簡單的CSS3動畫,後來一研究,確實不簡單。CSS3動畫有一定的侷限性,實現不了這麼流暢的一鏡到底的效果。那就入坑吧,來研究一波如何實現一鏡到底。

 

 

1.代碼地址與demo效果圖

先掛上最終實現的效果圖跟倉庫地址:https://gitee.com/wuchunling/pixiJS.git

 

 

2.項目技術架構分析

項目主要用到了PixiJS(https://www.pixijs.com/),一個2D動畫引擎,看官方文檔稍微喫力,也有一個翻譯的中文教程:https://github.com/Zainking/learningPixi,中英文配合看,基本pixiJS的API就能初步掌握。

 

先來總結一下項目的實現整體的實現步驟:

(1) 創建PIXI應用預加載圖片資源,資源加載完成後將PIXI應用插入真實DOM中,進行下一步

(2) 初始化場景:定義每個場景的寬高、位置等屬性數據,通過new PIXI.Container()函數創建每個場景,將他們加入PIXI舞臺中

(3) 初始化精靈:定義每個精靈的位置等屬性數據,將其加到對應的場景中

(4) 進行到第三步,所有的精靈都繪製出來了,但是此時屏幕還不可以拖動,這裏需要用到一個滑動的庫,我用的是AlloyTouch,網易四字魔咒用的是ScrollerJS。通過AlloyTouch可以檢測用戶滑動的距離,在對應的滑動的change回調函數裏可以改變舞臺的位置app.stage.position.x來實現舞臺的拖動效果。這樣用戶就可以通過滑動查看了。

(5) 進行到第四步,一切的效果都是靜態的,我們的元素還沒有動起來。要實現隨着用戶的滑動播放對應的動畫效果,這裏需要用到一個庫TimelineMax。這是管理動畫播放進度條的庫。直接new 一個主時間軸timeline。

(6) 給精靈加上動畫,這裏用到一個庫TweenMax,可以用來創建補間動畫。我們只需定義精靈的起始狀態,最終狀態,它能輕鬆幫助我們進行狀態的過渡。用TweenMax創建完動畫,將動畫加到時間軸timeline對應的位置。delay:0.1表示在動畫長度的百分十處開始播放。

(7) 在第三步的時候,用戶滑動的回調函數加上 timeline.seek(progress)就可以實現滑動到某個位置播放對應的動畫。

 

這裏,先解釋一下每個庫的作用

1) PixiJS: 繪圖,其中包括舞臺、場景、普通精靈、動畫精靈、平鋪精靈、定時器等。

2) TweenMax:製作過渡動畫

3) TimelineMax:管理整個舞臺的動畫播放進度

4) AlloyTouch:實現滑動效果,監聽用戶滑動

PixiJS+TweenMax+TimelineMax+AlloyTouch就能實現一鏡到底的套路,每個庫發揮它自己應有的作用,相互配合。

 

 

3. PixiJS

3.1 pixi常見概念介紹

①舞臺/Stage:舞臺只有一個,app.stage就是我們的舞臺。所有要被顯示的東西最終都是放到舞臺裏去的。

②精靈/Sprite:精靈就是舞臺裏的每一個元素,例如一隻鳥、一張背景圖、一段文本等。可以通過操作精靈的某些屬性達到我們想要的動畫效果。同時,精靈也分很多種類,普通精靈Sprite、平鋪精靈TilingSprite,動畫精靈AnimatedSprite。平鋪精靈一般用於小圖片需要平鋪成背景,動畫精靈一般用於製作幀動畫。

③容器/Container:容器,可以用來放置精靈,舞臺的本質也是一個容器,所以容器有的屬性舞臺也有,舞臺繼承自容器。當我們舞臺裏的精靈元素太多太雜,可以通過設置多個容器,把精靈放到對應的容器裏,再把容器放到舞臺裏,這樣就方便管理了。這也就是一鏡到底會用到的多場景切換。每個場景其實就是一個容器,每個場景有自己的精靈。舞臺只有一個(舞臺繼承自容器),但容器可以有多個。

④加載器/Loader:因爲要用到大量的圖片,圖片的加載比較費時間,所以需要進行一次性的預加載,這也就是在網易四字魔咒開頭看到的進度條的由來,利用Loader可以預加載圖片,同時跟蹤進度。

 

3.2 創建應用

項目的一開始,需要創建一個pixi應用,直接通過一個語句 new PIXI.Application(),這樣我們的PIXI應用就創建完成了。

    let app = new PIXI.Application({
      width:1334, 
      height:750
    });

一般把舞臺的寬高設置成跟設備屏幕寬高等同。注意舞臺的背景色是0x寫法。例如十六進制色值#000555;在這裏寫成 0x000555

    // 創建PIXI應用
    const w = document.body.clientWidth,
        h = document.body.clientHeight;
    const app = new PIXI.Application({
      width:w,
      height:h,
      backgroundColor:0xd7a664
    });

 

3.3 預加載資源

loader.add可以鏈式調用,將全部的資源進行預加載。

progress回調函數幫助我們監聽加載的進度;complete回調函數表示資源全部加載完成,這時候就可以把PIXI應用插入到真實DOM中了。

定義好加載器的所有東西,loader.load()就開始加載資源了。加載好的資源可以通過loader.resources調用

(代碼中涉及到舞臺的整體縮放跟旋轉的問題留到最後講。)

 // 創建資源加載器loader ,進行資源預加載
  const loader = new PIXI.loaders.Loader();

  // 鏈式調用添加圖片資源
  loader.add('bg1', './imgs/bg1.png')
       .add('bg_desk', './imgs/bg_desk.png')
       .add('bg_person','./imgs/bg1_person.png')

  // 監聽加載進度,顯示加載進度
  loader.on("progress", function(target, resource) {  //加載進度
    document.getElementById('percent').innerText = parseInt(target.progress)+"%";
  });

  // 監聽加載完畢
  loader.once('complete', function(target, resource) {  //加載完成
    document.getElementById('loading').style.display = 'none';  // 隱藏進度條
    document.body.appendChild(app.view);   // 將pixi應用插入真實DOM中

    initScenes(); // 初始化場景
    initSprites();  // 初始化精靈
    initAnimation(); // 初始化動畫 
    app.stage.scale.set(scale,scale);  // 根據設備屏幕實際寬高縮放舞臺
    if (w<h) {   // 豎屏,旋轉舞臺
      app.stage.rotation = 1.57;
      app.stage.pivot.set(0.5);
      app.stage.x = w;
      initTouch(true,'y');   // 初始化滑動
    } else {   // 橫屏
      initTouch(false,'x');   // 初始化滑動
    } 
  });
   
  // 開始加載資源
  loader.load();
   

3.4 初始化場景

根據設計圖,得出每個場景的長寬與位置。新建場景容器,等建完場景,纔可以進行下一步的操作,將精靈放進場景裏。

建場景的操作比較簡單,定義每個場景的數據,新建Container對象並加入舞臺中。這裏的實現主要有三個東西:

①場景數據 scenesOptions:定義每個場景的數據

②對象集合 scenes:PIXI.Container對象,在初始化精靈的時候需要用到,所以需要將每個場景對象進行存儲

③循環函數 initScenes:初始化場景並加入舞臺裏

  const scenesOptions = [ // 場景數據:定義每個場景的寬高,x/y距離
    {
      name:"scene1",
      x:0,y:0,
      width:2933,height:750
    },
    {
      name:"scene2",
      x:2933,y:0,
      width:1617,height:750
    },
    ....
  ];
   
  const scenes = {};  // 場景集合 - pixi對象
   
   
  function initScenes(){ // 初始化場景
    for (let i = scenesOptions.length-1; i >= 0 ; i--) {
      scenes[scenesOptions[i].name] = new PIXI.Container({
        width:scenesOptions[i].width,
        height:scenesOptions[i].height
      });
      scenes[scenesOptions[i].name].x = scenesOptions[i].x;
      app.stage.addChild(scenes[scenesOptions[i].name]);
    }
  }

 

3.5 初始化精靈

這一步的操作與上一步操作一致:數據集,對象集,循環函數。

只是在這裏我把精靈的初始化拆分成兩個函數,一個是循環精靈數組,一個是循環每一個精靈的屬性。再加了一個特殊屬性的函數。根據需要,可以對某些精靈進行特殊的操作,都統一放在initSpecialProp函數裏。

  const spritesOptions = [ // 精靈數據:定義每個精靈的座標
    { // 第一個場景的精靈
      bg1:{
        position:{x:0,y:0}
      },
      bg_person:{
        position:{x:0,y:19},
        anchor:{x:0.5,y:0.5}
      },
      ....
    },
    { // 第二個場景的精靈
      bg_desk:{
        position:{x:2213,y:38}
      },
      ....
    }
  ];
  const sprites = {}; // 精靈集合 - pixi對象
   
  function initSprites(){  // new出所有精靈對象,並交給函數initSprite分別賦值
    for (let i = 0; i < spritesOptions.length; i++) {
      let obj = spritesOptions[i];
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          sprites[key] = PIXI.Sprite.fromImage(key);
          initSprite(sprites[key],obj[key],i+1);
        }
      }
    }
    initSpecialProp();
  }
  function initSprite(sprite,prop,i){  // 初始化單個精靈的屬性並加入對應的場景中
    for (let key in prop) {
      if (prop.hasOwnProperty(key)) {
        sprite[key] = prop[key];
      }
    }
    scenes['scene'+i].addChild(sprite);
  }
  function initSpecialProp(){  // 若有特殊精靈要處理特殊屬性,可在此函數內處理
    // sprites.mother_left.pivot.set(0,51);
    // sprites.mother_right.pivot.set(95,50)
  }

 

4. AlloyTouch

進行到上面那一步,舞臺場景精靈一應俱全,可以看到繪製出來的效果,但是此時頁面還不可以拖動,還需要一個滑動的庫來配合。

new AlloyTouch({ ... }):

①touch定義觸摸的DOM對象,在這裏我們就直接是body了;

②vertical定義觸摸的方向(橫向滑動,還是豎向滑動。這裏涉及設備是橫屏還是豎屏,所以通過值vertical傳進來。橫屏則爲false,豎屏則爲true);

③可滾動的最大距離max跟最小距離min。因爲我們都是往左滑,往上滑,所以爲負距離,所以最大值max爲0。最小值爲舞臺的整體寬度再減去一整屏的寬度,然後再取負值;

④關鍵點在於change函數,他可以返回實時滾動的距離。通過計算可以得到當前滾動的距離佔全部距離的百分比,這個百分比就是我們當前的進度。拿到這個百分比。(默認總進度爲1)就可以通過timeline.seek函數就可以隨時改變播放的進度。這就是爲什麼往回滑動動畫會往回撤的原因。想一下我們平時看視頻,我們滾動進度條的行爲就是用戶滑動頁面的行爲,所以這就是實現的關鍵點。

  let alloyTouch;
   
  function initTouch(vertical, val) {
    let scrollDis = app.stage.width-max;
    alloyTouch = new AlloyTouch({
      touch:"body", //反饋觸摸的dom
      vertical: vertical, //不必需,默認是true代表監聽豎直方向touch
      min: -app.stage.width + max, //不必需,運動屬性的最小值
      maxSpeed: 1,
      max: 0, //不必需,滾動屬性的最大值
      bindSelf: false,
      initialValue: 0,
      change:function(value){  
        app.stage.position[val] = value;
        let progress = -value/scrollDis;
        progress = progress < 0 ? 0 : progress;
        progress = progress > 1 ? 1 : progress;
        timeline.seek(progress);      
      }
   })
  }

 

5. TimelineMax

TimelineMax是GSAP動畫庫中的動畫組織、排序、管理工具,可創建時間軸(timeline)作爲動畫或其他時間軸的容器,這使得整個動畫控制和精確管理時間變得簡單。

  const timeline = new TimelineMax({  // 整個舞臺的時間軸
    paused: true
  });

最後,將4跟5步驟合在一起如下

  const w = document.body.clientWidth,
      h = document.body.clientHeight;
   
  const min = (w<h)?w:h;
  const max = (w>h)?w:h;
   
   
  const timeline = new TimelineMax({  // 整個舞臺的時間軸
    paused: true
  });
   
  let alloyTouch;
   
  function initTouch(vertical, val) {
    let scrollDis = app.stage.width - max;
    alloyTouch = new AlloyTouch({
      touch:"body", //反饋觸摸的dom
      vertical: vertical, //不必需,默認是true代表監聽豎直方向touch
      min: -app.stage.width + max, //不必需,運動屬性的最小值
      maxSpeed: 1,
      max: 0, //不必需,滾動屬性的最大值
      bindSelf: false,
      initialValue: 0,
      change:function(value){  
        app.stage.position[val] = value;
        let progress = -value/scrollDis;
        progress = progress < 0 ? 0 : progress;
        progress = progress > 1 ? 1 : progress;
        timeline.seek(progress);      
      }
   })
  }

 

6. 初始化動畫

時間軸準備好了。就可以定義動畫,將動畫加到時間軸上了,表示在時間軸的哪個位置開始播放動畫。動畫這裏用到的是TweenMax庫,當然,也可以用別的庫,只要能實現補間動畫即可。TweenMax常用的三個函數如下:

①TweenMax.to(target,duration,statusObj) ——目標target從當前狀態到statusObj狀態過渡

②TweenMax.from(target,duration,statusObj) ——目標target從statusObj狀態到當前狀態過渡

③TweenMax.fromTo(target,duration,statusObjFrom,statusObjTo)——目標target從statusObjFrom狀態到statusObjTo狀態過渡

 

duration表示過渡時長。如果duration=0.1則表示過渡時長佔滾動總長的10%,即佔時間軸的10%。

delay跟duration的計算規則:

1) delay = 開始播放動畫時的滾動距離 / 可滾動總長度

delay是動畫開始的時間

2) duration = (結束播放動畫時的滾動距離 - 開始播放動畫時的滾動距離) / 可滾動總長度

duration是動畫持續的時間

 const animationsOptions = {  // 精靈動畫集合
    windows:[{
      prop:'scale',  // 這裏有個prop,有些人沒有注意到這是什麼
      delay:0.05,
      duration:0.3,
      to:{x:3,y:3,ease:Power0.easeNone}  // 在這裏注意一下 ease:Power0.easeNone 是緩動函數
    },{
      delay:0.1,
      duration:0.1,
      to:{alpha:0}
    }],
    talk_1:[{
      delay:0.15,
      duration:0.1,
      from:{width:0,height:0,ease:Power0.easeNone}
    }]
  }
  function initAnimation(){
    // delay=0.1 表示滾動到10%開始播放動畫
    // duration=0.1 表示運動時間佔滾動的百分比
    for (let key in animationsOptions) {
      if (animationsOptions.hasOwnProperty(key)) {
        let obj = animationsOptions[key];
        for (let i = 0; i < obj.length; i++) {
          let act;
          let target;
          if (obj[i].prop) {
            target = sprites[key][obj[i].prop];
          } else {
            target = sprites[key];
          }
          if (obj[i].from & obj[i].to) {
            act = TweenMax.fromTo(target,obj[i].duration,obj[i].from,obj[i].to);
          } else if (obj[i].from) {
            act = TweenMax.from(target,obj[i].duration,obj[i].from);
          } else if (obj[i].to) {
            act = TweenMax.to(target,obj[i].duration,obj[i].to);
          }
          let tm = new TimelineMax({delay:obj[i].delay});
          tm.add(act,0);
          tm.play();
          timeline.add(tm,0);
        }
      }
    }
    // 特殊動畫特殊處理
    let act = TweenMax.to(scenes.scene1,0.3,{x:2400});
    let tm = new TimelineMax({delay:0.25});
    tm.add(act,0);
    timeline.add(tm,0);
  }

在這裏注意一下 ease:Power0.easeNone 是緩動函數 戳鏈接:https://www.tweenmax.com.cn/api/tweenmax/ease

我一開始沒有寫緩動函數,動畫出現嚴重的卡幀效果,真醜陋!我還以爲是設備問題,是pixi問題。最後發現了這個緩動函數。給動畫加上線性緩動,完美解決了卡幀的醜陋效果。

 

tip!!!!!(有熱心網友問到我上面的prop是幹什麼用的)

注意一下,TweenMax.from(target,duration,statusObj)等方法只識別target的屬性。舉個例子,精靈sprite的width從0變到100

TweenMax.to(sprite, 0.1, { width: 100})    // 即 sprite.width變爲100

那麼問題來了,要改變屬性的屬性呢?例如sprite.scale.x變爲2,則target應該變爲sprite.scale

TweenMax.to(sprite.scale, 0.1, { x: 2})    // 即 sprite.scale.x變爲100

爲了兼容這兩種情況,就需要加入prop。我可真是個小機靈鬼。

拿上面的animationsOptions.window舉個例子。可以觀察到animationsOptions.window其實是一個數組,數組裏面的一個個對象代表一個個動畫,所以組成精靈window的動畫集合。我們需要對精靈window做很多動畫的話,例如用時位移+縮放+變透明等等,如果是delay跟duration相同並且是window的一級屬性(即window.prop),可以放在一個對象裏,如果delay或者duration不同,就需要設置成不同的對象。爲了兼容二級屬性,即window.prop.prop,我特意加了一個參數prop,達到兼容二級屬性的效果。

在下面初始化動畫的函數initAnimation裏,纔有這一步,先判斷是否是二級屬性

          if (obj[i].prop) {
            target = sprites[key][obj[i].prop];
          } else {
            target = sprites[key];
          }

 

7. 舞臺的整體縮放與旋轉

設計圖是 1334*750. 不修改圖片大小,直接將圖片精靈加入舞臺裏,最後對這個舞臺進行整體縮放,這樣就省心省力很多。需要對當前設備的橫屏還是豎屏進行檢測。

實現步驟:

1)獲取整個屏幕的寬高;

  const w = document.body.clientWidth,   // 或者  window.innerWidth
      h = document.body.clientHeight;  // 或者  window.innerHeight

 

2)判斷寬高大小,取最小的邊 作爲舞臺的高,與 750做比較,得出縮放比例;

  const min = (w<h)?w:h;
  const max = (w>h)?w:h;
  let scale = min/750;  // 根據設計稿尺寸進行縮放比例調整
  console.log(w,h,min,"放大係數:",scale);
  app.stage.scale.set(scale,scale);  // 根據屏幕實際寬高放大舞臺

 

3)對舞臺進行旋轉;

  if (w<h) {   // 根據橫屏豎屏效果旋轉舞臺   // 當前是豎屏
    app.stage.rotation = 1.5708;  // 這個角度很迷。微調出來最好的效果了
    app.stage.pivot.set(0.5);
    app.stage.x = w;
    initTouch(true,'y');
  } else {   // 當前是橫屏
    initTouch(false,'x');
  }

 

4)判斷設備狀態的切換

  //判斷手機橫豎屏狀態:
  window.addEventListener("onorientationchange" in window ? "orientationchange" : "resize", function() {
      if (window.orientation === 180 || window.orientation === 0) {
          alert('豎屏狀態!');
          location.reload();
      }
      if (window.orientation === 90 || window.orientation === -90 ){
          alert('橫屏狀態!');
          location.reload();
      }
  }, false); 

 

8.添加背景音樂

在不同的進度處播放不同的背景音樂,只需在之前的initTouch的change回調函數裏調用播放背景音樂的函數 playAudio(progress),重點處理一下playAudio(progress)函數的編寫即可。

 function initTouch(vertical,val){
    let scrollDis = app.stage.width-max-max;
    alloyTouch = new AlloyTouch({
      touch:"body",//反饋觸摸的dom
      vertical: vertical,//不必需,默認是true代表監聽豎直方向touch
      min:-app.stage.width+max+max,
      maxSpeed: 1,
      max: 0, //不必需,滾動屬性的最大值
      bindSelf: false,
      initialValue: 0,
      change:function(value){
        app.stage.position[val] = value - max;
        let progress = -value/scrollDis;
        progress = progress < 0 ? 0 : progress;
        progress = progress > 1 ? 1 : progress;
        timeline.seek(progress);
        console.log(value,progress);
       
        // 播放背景音樂
        playAudio(progress);
   
      }
   })
  }

 

播放背景音樂的處理相對比較簡單粗暴,將就看吧。

function playAudio(progress){
  if (progress>=0.08 && progress<=0.081) {
    playBgmAfterLoading('wechat');
    setTimeout(function(){
      tickerPhone.stop();
      playBgmAfterLoading('talk_5','talk_6',2000);
    },2000)
  }
  if (progress>=0.227 && progress<=0.23) {
    playBgmAfterLoading('talk_7');
  }
  if (progress>=0.357&& progress<=0.36) {
    playBgmAfterLoading('talk_8','talk_9',3000);
  }
  if (progress>=0.444 && progress<=0.45) {
    playBgmAfterLoading('talk_10');
  }
  if (progress>=0.433 && progress<=0.44) {
    actTrumpet.play();
    playBgmAfterLoading('train','train1',4000);
    setTimeout(function(){
      actTrumpet.pause();
    },8000)
  }
}
 
function playBgmAfterLoading(e,next,wait) {
    playBgm(e);
    if (next) {
      setTimeout(function(){
        playBgm(next);
      },wait);
    }
}
 
function playBgm(e){
  let audio = document.getElementById(e);
  if (typeof WeixinJSBridge == "object" && typeof WeixinJSBridge.invoke == "function") {
      WeixinJSBridge.invoke('getNetworkType', {}, function (res) {
          // 在這裏拿到 e.err_msg, 這裏面就包含了所有的網絡類型
          // alert(res.err_msg);
          audio.play();
      });
  } else {
    audio.play();
  }
}

 

總結

完結撒花,待優化。下次遇到新奇玩意就來續更。

 

後記

還有一個更加完整的demo作品(代碼倉庫地址):https://gitee.com/wuchunling/life-amp-activity

(示例:掃碼查看完整demo)

或者直戳:http://pixi.chunling.online/

 

// 續更 2019-09-08

最近陸陸續續收到網友的疑問,對於一些值的計算不甚清楚,我也覺得自己這篇博文寫得不夠好,沒有把一些細節展開說。有必要整理一下這些疑問。

很高興收到一封郵件,有人對pixiJS的demo提出了疑問,我也回頭看了自己的代碼,代碼確實有bug。因爲這只是我的練手demo,很多細節沒有去深究,我還是建議大家看https://gitee.com/wuchunling/life-amp-activity這個倉庫的代碼。代碼差不多,但是這個更完整,更完善,也修復了一些小bug,源代碼看public文件夾即可,不需要看dist文件夾哦,要看dist文件夾,建議clone代碼後再gulp dist一下。

(1)運動屬性的最小值應該怎麼計算?

  function initTouch(vertical,val){
    let scrollDis = app.stage.width-max-max;
    alloyTouch = new AlloyTouch({
      touch:"body",//反饋觸摸的dom
      vertical: vertical,//不必需,默認是true代表監聽豎直方向touch
      min: -app.stage.width + max,  // 這個值應該怎麼計算呢??????????????????????
      maxSpeed: 1,
      max: 0, 
      bindSelf: false,
      initialValue: 0,
      change:function(value){
        app.stage.position[val] = value - max;
        let progress = -value/scrollDis;
        progress = progress < 0 ? 0 : progress;
        progress = progress > 1 ? 1 : progress;
        timeline.seek(progress);
        console.log(value,progress);
       
        // 播放背景音樂
        playAudio(progress);
   
      }
   })
  }

看看這段代碼,new AlloyTouch({})裏的min-max其實就是可滾動的最大值跟最小值。

 

向左滾動,滾動距離爲負數,向右滾動,滾動距離爲正數。所以,假如我們的整個動畫場景的可滾動距離爲x。則 min爲-x,max爲0。

剩下的疑問就是怎麼計算這個可滾動距離??

 

以demo的四字魔咒舉例,場景一二的長寬如下:

 

 

本來如果是簡單的設計,整個舞臺的長度就是場景一的長度加場景二的長度。

但是因爲場景一與場景二存在疊加,母親的精靈圖要放置在窗戶處。所以整個舞臺的長度就是4971

 

 

從上圖可以得出設計圖的舞臺總長度就是4971 ,但是實際的總長度經過縮放比的計算後有所變化,通過app.stage.width可以取得舞臺的實際總長度。

所以,我們要知道舞臺的實際總長度,不需要去計算設計圖每個場景的長度,直接通過app.stage.width去獲取就可以了。

知道了舞臺的實際總長度,那麼舞臺的可滑動距離就是舞臺的總長度減去一整屏的長度。(不知道這句話好理解不。。。。)

屏幕長度如何獲取呢?

// 創建PIXI應用
const w = document.body.clientWidth,
      h = document.body.clientHeight;
let app = new PIXI.Application({
  width:w,
  height:h,
  backgroundColor:0xd7a664,
  forceCanvas:true
});

// 獲取屏幕寬高,判斷橫屏還是豎屏
const min = (w<h)?w:h;
const max = (w>h)?w:h;

let scale = min/750;  // 根據設計稿尺寸進行縮放比例調整
console.log(w,h,min,"放大係數:",scale);

 

這個max就是我們的屏幕長度,所以可滑動距離就是 app.stage.width - max

而min需要取負值,就是 -app.stage.width + max

 

(2)爲什麼後面會劇烈抖動??

劇烈抖動是因爲可滾動距離計算錯誤,這個問題就回到了第一個問題。

我當初在寫練手dmeo時,很多錯誤沒有及時糾正,因爲後續工作忙就沒有去改正。我也沒想料想到有那麼多人關注我的博文。現在代碼已經更正錯誤。

但是我還是建議大家看“春節溫暖回家路”的作品的源代碼。相信大家看了就會有疑問,爲什麼這個作品的min,反而是 -app.stage.width + max + max??因爲這個作品的設計,第一屏是不允許滾動,而是通過一個點擊事件,直接進入第二屏。所以可滾動距離會減掉兩個屏的長度。

沒有特殊設計,一般情況下就是-app.stage.width + max

 

(3)動畫的delay與duration如何計算???

delay:動畫開始的時間

duration:動畫持續的時間

在h5一鏡到底中,時間的概念其實就是滑動距離。總滑動距離就是時間1。舉個例子,假如可滑動的總距離是100m,滑動距離爲20m時開始播放動畫,即delay爲 0.2。滑動到50m時停止播放動畫,那麼動畫持續時間就是0.5-0.2=0.3

其實這個相對簡單,我是建議大家不要去計算太麻煩了,直接通過console.log去獲取。滑動的回調函數會實時返回滑動的距離,通過計算可得出當前的滑動進度progress。我們通過拖動,可以得出輸出結果。拖動到要開始播放動畫的地方,獲取progress1,這個動畫的delay就是progress;拖動到動畫要停止的地方,獲取progress2。所以動畫的duration就是progress2-progress1

function initTouch(vertical,val){
  let scrollDis = app.stage.width - max
  console.log('舞臺總長度', app.stage.width)
  console.log('屏幕長度', max)
  console.log('可滾動的距離', scrollDis)
  alloyTouch = new AlloyTouch({
    touch:"body",//反饋觸摸的dom
    vertical: vertical,//不必需,默認是true代表監聽豎直方向touch
    min: -scrollDis, //不必需,運動屬性的最小值
    maxSpeed: 1,
    max: 0, //不必需,滾動屬性的最大值
    bindSelf: false,
    initialValue: 0,
    change:function(value){
      app.stage.position[val] = value;
      let progress = -value / scrollDis;
      console.log(progress)  // 這個console超級有用!!!!幫助我們得出動畫的delay與duration!!!!!
      progress = progress < 0 ? 0 : progress;
      progress = progress > 1 ? 1 : progress;
      timeline.seek(progress);
    }
 })
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章