聊聊蘋果營銷頁中幾個有趣的交互動畫

前言

前兩天在瀏覽 蘋果 16寸 營銷頁面 的時候,發現了幾個比較有意思的交互,內心想着本身雖然是一個窮逼,可是知識是無界限的呀,因而便研究了一波。javascript

文章主要講交互效果,因此文中會有不少 gif 圖,你們最好連上無線再看,示例代碼連接我放在了文章底部,有須要自取。css

 

兩個效果

翻蓋效果

一個是屏幕慢慢打開的效果,在屏幕打開的過程當中,電腦圖片 是在屏幕中固定不動的,直到打開完畢或者關閉完畢的時候再讓 電腦圖片 隨着滾動條滾動。html

縮放圖片

開始時是一張全屏的圖片,在滾動過程當中慢慢變成另外一張圖片,接着這張圖片以屏幕正中間爲基準點慢慢縮小,在縮小的過程當中,這張圖是定在屏幕中央的,縮小到必定值的時候,圖片隨着滾動條滾動。java

 

前置知識

再動手寫代碼以前,咱們須要瞭解幾個在接下來代碼中要用到的知識點。react

粘性定位 sticky

能夠簡單的認爲是 相對定位 relative固定定位 fixed 的混合,元素在跨越指定範圍前爲相對定位,以後爲固定定位。css3

sticky 元素固定的相對偏移是相對於離它最近的具備滾動框的祖先元素,若是祖先元素都不能夠滾動,那麼是相對於 viewport 來計算元素的偏移量。git

一個例子

以下代碼,html 結構以下:github

<body>
  <h1>我是 sticky 的第一個 demo</h1>
  <nav>
    <h3>導航A</h3>
    <h3>導航B</h3>
    <h3>導航C</h3>
  </nav>
  <article>
    <p>...</p>
    <p>...</p>
    // ...
  </article>
</body>  
複製代碼

樣式以下:canvas

nav {
  display: table;
  width: 100%;
  position: sticky;
  top: 0;
}
複製代碼

在代碼中 nav 元素會根據 body 進行粘性定位,在 viewport 視口滾動到元素 top 距離小於 0px 以前,元素爲相對定位,也就是說他會隨着文檔滾動。以後,元素將固定在與頂部距離 0px 的位置。c#

原理

sticky 原理你們能夠看一下張鑫旭老師的 深刻理解position sticky粘性定位的計算規則,能夠先簡單看一下老師講解 sticky 時用的這個圖:

圖片引自 張鑫旭 深刻理解position sticky

其中 <nav>sticky 元素,藍色框區域是 sticky 的爸爸元素,用於承載 sticky 元素,紅色區域是 <nav> 相對的能夠滾動的元素。

  • 當整個藍色區域在紅色區域中的時候,sticky 元素是沒有粘性效果的(如圖一);

  • 當慢慢的向上滑的時候,藍色的盒子超過了紅色的滾動元素,那麼 sticky 元素就會在藍色的框中向下滑,實現粘性效果(如圖2、三);

  • 當藍色的盒子劃出紅色的盒子的時候,由於 sticky 元素在藍色的框子中,因此也就直接被一波帶走了,沒有粘性效果(如圖三)。

其實這樣咱們就能夠很清楚的知道爲何 sticky 元素的高度爲何不能等於它爸爸的高度了,由於若是相等的話,粘性定位元素已經徹底沒有了實現粘性效果的空間,也就至關於失效了。

以上原理參考了張鑫旭老師的 深刻理解position sticky粘性定位的計算規則,文章中有講解 流盒粘性約束矩形 的概念解釋,以及具體的代碼結構和 css 實現,你們能夠查看原文。

經常使用例子

在業務中咱們可能會遇到這樣一種場景:即一個列表,列表中的數據須要根據時間顯示,並且時間須要在滾動的時候固定在最頂部,這個時候咱們就可使用 sticky 來解決這個問題:

具體 html 結構以下:

<body>
  <h1>時間固定demo</h1>
  <div className={styles.wrapper}>
    <section>
      <h4>5月20日</h4>
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
      </ul>
    </section>
    <section>
      <h4>5月19日</h4>
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
      </ul>
    </section>
    // ...
</body>
複製代碼

樣式以下:

body {
  margin: 0px;
  padding: 100px;
  height: 2000px;
}

h4 {
  margin: 2em 0 0;
  background-color: #333;
  color: #fff;
  padding: 10px;
  top: 0;
  z-index: 1;
  position: sticky;
}
複製代碼

代碼如上,其中每一塊是一個 <section>,而後給 <h4> 設置 sticky 定位,這樣就能夠實現上述效果了。

注意點

固然使用 sticky 的時候,咱們須要注意幾個點:

  • 父級元素不能有任何 overflow:visible 之外的 overflow 設置,不然沒有粘滯效果。若是你設置的 sticky 沒有效果,能夠看看父級元素們有沒有設置 overflow:hidden,去掉就能夠了。

  • 必須指定 topbottomleftright 4 個值之一,不然只會處於相對定位。

  • 父元素的高度不能低於 sticky 元素的高度(參考上面原理解釋)

  • sticky 元素僅在其父元素內生效(參考上面原理解釋)

還有一個不得不提的就是兼容性,咱們能夠在 Can I use 官網看看 sticky 的兼容性,一片紅:

IE 下徹底是廢了,若是你的項目須要考慮 IE 的話,你就須要使用 fixed 來兼容了。

 

滾動視差 background-attachment

什麼是滾動視差,來看一下下面這個例子就明白了:

視差滾動(Parallax Scrolling)是指讓多層背景以不一樣的速度移動,造成立體的運動效果,帶來很是出色的視覺體驗。

上圖中的效果,咱們只須要一行 css 就能夠實現了,不須要寫複雜的 js 代碼,直接設置 background-attachment: fixed 就完成了。

html 結構

<body>
  <section className={`${styles.gImg} ${styles.gImg1}`}>IMG1</section>
  <section className={`${styles.gImg} ${styles.gImg2}`}>IMG2</section>
  <section className={`${styles.gImg} ${styles.gImg3}`}>IMG3</section>
</body>
複製代碼

樣式代碼

section {
  height: 100vh;
}

.gImg {
  background-attachment: fixed;
  background-size: cover;
  background-position: center center;
  width: 100%;
}

.gImg1 {
  background-image: url(@/assets/mac1.jpg);
}

.gImg2 {
  background-image: url(@/assets/mac2.jpg);
}

.gImg3 {
  background-image: url(@/assets/mac4.jpg);
}
複製代碼

經過滾動視差這個 css 咱們基本上能夠實現第二個動畫了。

關於滾動視差的講解,你們能夠參考這篇文章 滾動視差?CSS 不在話下,寫的很詳細。

 

Canvas 畫圖

其實第二個動畫咱們也可使用 canvas 畫圖來實現,咱們能夠在一塊畫布中畫出兩張圖片,根據滾動的距離,去顯示兩張圖片在畫布中的比例。

能夠經過 canvas 提供的 drawImage 方法來進行畫圖,這個方法提供了多種方式在 Canvas 上繪製圖像。

好比咱們須要實現的畫出以下圖:

其實咱們就須要截取第一章圖片的上半部分,下一張圖片的下半部分,而後進行拼接就 ojbk 了,看看參數解釋圖:

這裏咱們須要傳入 7 個參數,來實現咱們須要的效果:

ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
複製代碼

具體參數的意思筆者也就再也不這裏細講了,你們能夠參考一下 drawImage() MDN 文檔

思路大體是首先繪製第一張圖片,做爲底圖,而後咱們經過繪製第二張圖片,覆蓋掉部分第一張圖片,這樣就能夠實現前面提到的效果。假設咱們圖片的原始寬高爲 2048*1024,畫布的大小爲 544*341,在滾動的時候的偏移距離爲 offsetTop,這樣咱們就能夠寫出以下代碼:

function drawImage() {
  context.drawImage(img1, 0, 0, 2048, 1024, 0, 0, 544, 341);
  context.drawImage(img2, 0, 滾動偏移距離 * 1024 / 341, 2048, 1024, 0, 滾動偏移距離, 544, 341);
}
複製代碼

以前筆者使用過 ctx.drawImage(image, dx, dy, dWidth, dHeight),能夠參考筆者寫的 使用 React Hooks 實現仿石墨的圖片預覽插件,此次用到了 7 個參數,你們能夠參考這篇文章 將圖片畫到canvas 上的幾種方法,寫的很詳細。

 

transform 中的 matrix

CSS3 中使用 transform 能夠對元素進行變換。其中包含:位移、旋轉、偏移、縮放。 transform 可使用 translate/rotate/skew/scale 的方式來控制元素變換,也可使用 matrix 的方式來控制元素變換。

舉個例子:

// 代碼一
transform: matrix(1.5, 0, 0, 1.5, 0, 190.5);
// 代碼二
transform: scale(1,5, 1.5) translate(0, 190.5)
複製代碼

上面兩行代碼的意思是同樣的,以後咱們作到第二個動畫的時候會用到這個屬性。

若是你們想要深刻了解這個屬性,能夠參考:大學沒學過數學也要理解 CSS3 transform 中的 matrix

 

開擼

初始化項目

工欲善其事,必先利其器。筆者使用 react Hooks 來完成這兩個動畫效果,並使用 umi 快速初始化一個項目,具體的初始化步驟能夠參考筆者寫的 dva理論到實踐——幫你掃清dva的知識盲點,裏面詳細介紹瞭如何使用腳手架快速搭建一個項目。

搭建完成以後,筆者將以前講到的例子都放在這裏,方便你們點進去查看。

翻蓋效果

翻蓋效果其實很簡單,大家絕對想不到,蘋果營銷頁是怎麼作的?

它用了 120 張圖片,根據滾動距離來畫出對應的在這個滾動位置上該展現的圖片,對,你沒有聽錯。我以前也覺得應該是 css3 控制蓋的角度從而實現翻蓋效果的,是我想多了,哈哈哈。

思路

那樣咱們實現就很簡單了,咱們只須要作如下幾點:

  • 首先要定義一個常量,規定從蓋着到徹底打開須要 滾動多少距離 來完成,咱們這裏定義爲 400px
  • 咱們須要知道何時開始進行翻蓋或者合蓋操做,這個可讓圖片在屏幕正中間的時候,讓其開始動畫。
// 開始動畫的 scrollTop
// $('#imgWrapper') 放圖片的容器,html 結構下面有
startOpen = $('#imgWrapper').offset().top - (window.innerHeight / 2 - $('#imgWrapper').height() / 2);
複製代碼
  • 當翻蓋或者合蓋的時候,咱們須要將電腦固定在視口中,等到徹底翻開或者合上的時候,再讓起隨滾動條滾動,這裏咱們可使用 position: sticky

html 結構

<body>
 // ...
 <div className={styles.stickyContainer}>
   <div className={styles.stickyWrapper}>
     <div id="imgWrapper" className={styles.imgWrapper}>
       <img src={require(`@/assets/${asset}.jpg`)} alt="圖片1" />
     </div>
   </div>
 </div>
 // ...
</body>
複製代碼

其中動態引入圖片咱們能夠經過 require(圖片路徑) 來完成,如上面的代碼,咱們只須要計算出對應滾動距離所須要展現的圖片名字便可。

樣式代碼

.stickyContainer {
  height: 150vh;
}

.stickyWrapper {
  height: 100vh;
  position: sticky;
  top: 100px;
}

.imgWrapper {
  width: 100vh;
  height: 521px;
  margin: 0 auto;
}

.imgWrapper img {
  width: 100%;
}
複製代碼

接着就是在滾動的過程當中計算出當先須要顯示的圖片是那一張,咱們上面提到:120 張圖片,在 400px 的滾動距離中完成動畫。

首先咱們再加載完成後能夠得出,咱們能夠得出開始動畫的距離文檔頂部的滾動值 startOpen,所以咱們能夠得出以下代碼:

useEffect(() => {
  // 綁定事件
  window.addEventListener('scroll', scrollEvent, false);

  // 開始動畫的滾動距離
  // startOpen
  startOpen = $('#imgWrapper').offset().top - (window.innerHeight / 2 - $('#imgWrapper').height() / 2);

  return ()=>{
    window.removeEventListener('scroll', scrollEvent, false);
  }
}, []);

// 滾動事件
const scrollEvent = () => {
  // 實時的 scrollTop
  const scrollTop = $('html').scrollTop();
  let newAsset = ''

  if (scrollTop > startOpen && scrollTop < startOpen + 400) {
    let offset = Math.floor((scrollTop - startOpen) / 400 * 120);
    
    if (offset < 1) {
      offset = 1;
    } else if (offset > 120) {
      offset = 120;
    }

    if (offset < 10) {
      newAsset = `large_000${offset}`;
    } else if (offset < 100) {
      newAsset = `large_00${offset}`;
    } else {
      newAsset = `large_0${offset}`;
    }
  }

  // 邊界值判斷
  // ....
  
  // 設置圖片 url
  setAsset(newAsset);
};
複製代碼

預覽效果

這個翻蓋動畫很簡單,120張圖片換着來,實時渲染對應的圖片,其實沒有什麼技術含量,你們也能夠嘗試一下用其餘的方法實現一波。

 

縮放圖片

縮放圖片到屏幕這個動畫咱們能夠用兩個方式實現,一個是 滾動視差 實現,一個是 canvas 在滾動過程當中實時渲染圖片。

開始以前咱們來看一下沒有放大的以前圖,以下:

它由兩張圖片組成,屏幕中顯示的圖片,他與 電腦外殼 的上間距是 18px,當放大了以後,圖片與電腦外殼圖片 的上邊距應該是 18 * 放大比率

電腦外殼圖片,以下:

接下來咱們就開始介紹兩種實現方式。

 

Canvas 實現

Canvas 實現是將屏幕中顯示的這張圖片由 Canvas 來畫。

思路

其實這個動畫有兩部分組成,一個是 圖片覆蓋,一個是 圖片縮小

  • 圖片覆蓋

使用 Canvas 來解決,使用 Canvas 實現咱們須要使用 drawImage 方法將兩張圖片畫到一張畫布上。只須要經過滾動的距離,對應計算出具體某個時候畫布應該畫多少比例的第一張圖,畫多少比例的第二張圖,就能夠解決了。只須要知道何時開始圖片覆蓋。

  • 圖片縮小

咱們使用 transform: matrix 來實現,其中圖片縮小是基於屏幕正中央的點進行縮放的。

咱們根據滾動的距離相應的計算出相應放大比率和 translate 的值,以下圖,實時改變 transform: matrix 的參數值就好了。

這裏咱們須要計算出幾個臨界點的值,好比最大/小的放大比率,最大/小偏移值,開始縮小的點等。

  • 在進行動畫的時候,canvas 包裹容器應該是 sticky 定位在視口中的,直到動畫結束,canvas 包裹容器纔會隨着滾動條滾動。
一些重要的值

這裏咱們須要知道幾個值:

  • 定義的常量
// canvas 顯示的圖片寬度
const CANVAS_WIDTH = 544;

// canvas 顯示的圖片高度
const CANVAS_HEIGHT = 341;

// 動畫持續的距離
const ZOOM_SCROLL_RANGE = 400;

// canvas 顯示的圖片 實際寬度
const IMG_NATURAL_WIDTH = 2048;

// canvas 顯示的圖片 實際高度
const IMG_NATURAL_HEIGHT = 1024;
複製代碼
  • 放大比率(curScale),用於 matrixscale

最小的放大比率爲 1,便是自身。

最大的放大比率是屏幕的高度除以屏幕顯示圖片的比率,這裏筆者將 canvas 畫出來的圖片寬高定位 544 * 341

const CANVAS_WIDTH = 544;
const CANVAS_HEIGHT = 341;

const scaleRadio = window.innerHeight / CANVAS_HEIGHT;
複製代碼

因此放大比率的區間應該是 1 ~ scaleRadio 之間。

  • 偏移距離(translate),用於 matrix 的 偏移值

最大的偏移距離,應該是當 curScale 爲 1 的時候,包裹元素距離視口頂部的距離,咱們的縮放一直都是基於屏幕正中央這個點來進行放大/縮小的,因此能夠很簡單的得出:

// 最大的 translate
let StartScale = 0;
StartScale = window.innerHeight / 2 - $('#img-wrapper').height() / 2;
複製代碼

最小的偏移距離,應該是在 curScalescaleRadio 時,包裹元素距離視口頂部的距離,這個時候,咱們就須要用到以前提到的視屏圖片到電腦外殼的 top = 18px 這個值了,由於圖片進行了放大,因此最小的偏移距離應該爲:

miniTranslate = - 18 * scaleRadio
複製代碼

因此偏移距離的區間應該是 miniTranslate ~ StartScale 之間。

  • 開始縮放操做的起始點(NewStartScale

其實很簡單咱們須要在第二章圖片徹底覆蓋掉第一張的圖片的時候就進行開始縮放,這個值能夠經過 Canvas 包裹元素距離頂部文檔的top值 加上 一屏的高度 就能計算出。

let NewStartScale = 0;

NewStartScale = $('#section-sticky-hero').offset().top + window.innerHeight;
複製代碼
核心代碼

核心代碼就是滾動時候的計算:

const scrollEvent = () => {
  // 當前的 scrollTop
  const scrollTop = $('html').scrollTop();
  // 放大比率 默認爲最大
  let curScale = scaleRadio;
  // 偏移距離 默認爲最小
  let translate = -scaleRadio * 18;

  // StartScale:最大的偏移距離
  // NewStartScale:開始縮放操做的起始點
  // 沒有就 return
  if (!NewStartScale || !StartScale)  return;

  // 計算 當前的 curScale
  // (scaleRadio - 1) / ZOOM_SCROLL_RANGE): 每 1px 放大多少
  // scrollTop + scaleRadio * 18 - NewStartScale:當前滾動了多少
  curScale = scaleRadio - ((scaleRadio - 1) / ZOOM_SCROLL_RANGE) * (scrollTop + scaleRadio * 18 - NewStartScale);

  // 邊界值處理
  if (curScale > scaleRadio) {
    curScale = scaleRadio;
  } else if (curScale < 1) {
    curScale = 1;
  }

  // 計算 當前的 translate
  // 從 scaleRadio * 18 開始
  // all = scaleRadio * 18 + StartScale
  // 滑動過程當中不斷相加
  translate = -scaleRadio * 18 + ((scrollTop + scaleRadio * 18 - NewStartScale) / ZOOM_SCROLL_RANGE * (scaleRadio * 18 + StartScale));

  // 邊界值處理
  if (translate > StartScale) {
    translate = StartScale;
  } else if (translate < -scaleRadio * 18) {
    translate = - scaleRadio * 18;
  }

  // 使用 canvas 畫圖
  if (image1 && image2) {
    // 在圖片覆蓋階段
    // curScale 仍是最大的比率
    if (curScale === scaleRadio) {
      drawImage({
        img1: image1,
        img2: image2,
        secTop: CANVAS_HEIGHT * (scrollTop + 18 * scaleRadio - NewStartScale) / window.innerHeight,
      });
    } else {
      // 若是不是最大的比率,說明圖片已經覆蓋完了
      // 直接顯示第二章
      drawImage({
        img1: image1,
        img2: image2,
        secTop: 0,
      });
    }
  }
	
  // 設置樣式
  $('#img-wrapper').css({
    transform: `matrix(${curScale}, 0, 0, ${curScale}, 0, ${translate})`,
  });
};
複製代碼

html 結構以下:

<body>
  // ... 其餘內容
  <div id="section-sticky-hero" className={styles.stickyContainer}>
    <div className={styles.componentContainer}>
      <div className={styles.imgWrapper} id="img-wrapper">
        <canvas ref={canvasRef} id="canvas" className={styles.canvas}></canvas>
      </div>
    </div>
  </div>
  // ... 其餘內容
</body>
複製代碼

篇幅有限,筆者只列舉了滾動事件的代碼和 html 結構,其餘的代碼,好比 drawImage 這個方法,你們有興趣的話,能夠參考源碼。

預覽效果圖

 

滾動視差實現

前面咱們也講了滾動視差的原理,有了這個 background-attachment: fixed 屬性,第二個動畫基本上已經實現一半了。

實現思路

和上面的 canvas 畫圖相比的話,其實就是圖片覆蓋的這一步不同,其餘基本上都是相似的,包括邊界值的計算。

  • 圖片覆蓋

這裏咱們須要將兩張圖片都設置爲背景圖片,同時咱們須要給第二張圖片套上 電腦外殼圖片

當第一張圖片充滿屏幕的時候,就給兩張圖片同時加上 background-attachment: fixed 屬性,不能一開始的時候就加上這個屬性,否則就會變成下面這個效果:

  • 圖片縮小

這裏咱們不使用 transform: matrix 來作這個放大縮小,咱們使用 background-positionbackground-size 來進行圖片的 縮小/放大和偏移

  • 其餘的都與 Canvas 實現的原理大同小異。
核心代碼

滾動邏輯代碼以下:

const CANVAS_WIDTH = 544;
const CANVAS_HEIGHT = 341;

const WRAPPER_WIDTH = 694;
const WRAPPER_HEIGHT = 408;

const ZOOM_SCROLL_RANGE = 400;

// scalaRadio
// 圖片放大的最大的倍數
const scaleRadio = window.innerHeight / CANVAS_HEIGHT;

const scrollEvent = () => {
  const scrollTop = $('html').scrollTop();
  let curScale = scaleRadio;
  let translate = -scaleRadio * 18;

  if (!imgFixFixed || !StartScale) return;

  // 第一張圖片的 距離文檔的頂部的距離爲 imgFixFixed
  // 第一章圖片的高度爲 100vh,即一屏的高度
  // 因此第二章圖片的 scrollTop 爲 imgFixFixed + window.innerHeight
  if (scrollTop > imgFixFixed && scrollTop < imgFixFixed + window.innerHeight) {
    // 設置 fixed 屬性
    setFixImg(true);
  } else {
    setFixImg(false);
  }

  // 假設咱們縮放的距離是 400
  // 那麼咱們能夠計算出 每 1px 縮放的比例
  // 接着一這個比例乘以滾動的距離
  curScale = scaleRadio - ((scaleRadio - 1) / ZOOM_SCROLL_RANGE) * (scrollTop - imgFixFixed - window.innerHeight);

  // curScale 邊界值處理
  // ...

  // 從 scaleRadio * 18 開始
  // all = scaleRadio * 18 + StartScale
  // 滑動過程當中不斷相加
  translate = -scaleRadio * 18 + ((scrollTop - imgFixFixed - window.innerHeight) / ZOOM_SCROLL_RANGE * (scaleRadio * 18 + StartScale));

  // translate 邊界值處理
  // ...

  // 設置圖片的 css 樣式
  // 進行圖片基於中心點的縮放
  $('#g-img2').css({
    "width": curScale * CANVAS_WIDTH,
    "height": curScale * CANVAS_HEIGHT,
    "margin-top": `${translate + 18 * curScale}px`,
  });

  $('#img-wrapper').css({
    "width": scaleRadio * WRAPPER_WIDTH,
    "height": scaleRadio * WRAPPER_HEIGHT,
    "background-size": `${curScale * WRAPPER_WIDTH}px ${curScale * WRAPPER_HEIGHT}px`,
    "background-position": `center ${translate}px`,
  });
};
複製代碼

html 結構以下:

<body>
  // ... 其餘內容
  <section id="g-img" className={`${styles.gImg} ${styles.gImg1} ${fixImg ? styles.fixed : ''}`}>IMG1</section>

  <div className={styles.stickyContainer}>
    <div className={styles.componentContainer}>
      <div className={styles.imgWrapper} id="img-wrapper">
        <section id="g-img2" className={`${styles.gImg} ${styles.gImg2} ${fixImg ? styles.fixed : ''}`}>IMG2</section>
      </div>
    </div>
  </div>
  // ... 其餘內容
</body>
複製代碼
預覽效果圖

 

總結

今天講了兩個蘋果營銷頁面的動畫,文章沒什麼難點,主要是對幾個基礎知識點的運用。粘性定位滾動視差Canvas 畫圖matrix 屬性的使用 等等,但願對你們有所幫助。

實不相瞞,想要個贊!

 

參考資料

 

示例代碼

相關文章
相關標籤/搜索