《你的性格主導(dǎo)色》揭秘

介紹

《你的性格主導(dǎo)色》是今年網(wǎng)易云音樂(lè)前端團(tuán)隊(duì)開(kāi)發(fā)的一款測(cè)試用戶(hù)主導(dǎo)色的 H5 應(yīng)用,上線后反響很好,刷爆了微博和朋友圈。

項(xiàng)目的主要開(kāi)發(fā)者 imyzf 發(fā)表了一篇文章《官方揭秘!你的顏色是這樣算出來(lái)的》,解釋了一些動(dòng)效和最后主導(dǎo)色的計(jì)算方面的問(wèn)題。但由于涉及到了具體的業(yè)務(wù),所以作者沒(méi)有開(kāi)源出源碼,但是熱心的作者給了很多的提示。我就是根據(jù)這些提示,揭秘了我比較感興趣的部分。

在線 Demo

由于一直沒(méi)有在生產(chǎn)環(huán)境中使用Vue3.0vite,所以源碼部分我使用了 Vue3.0+vite實(shí)現(xiàn)。

頁(yè)面預(yù)加載

答題類(lèi)頁(yè)面與一般的 H5 頁(yè)面的不同之處在于,用戶(hù)的操作路徑是確定的,即每個(gè)頁(yè)面的下一頁(yè)路由是固定的,所以在 router 層面做了優(yōu)化,提前預(yù)加載了下一個(gè)頁(yè)面

由于活動(dòng)頁(yè)面使用了大量的視頻和動(dòng)效等,所以想在用戶(hù)閱讀選擇題目的過(guò)程中把下一頁(yè)的頁(yè)面渲染完畢,這樣切換到下一頁(yè)面的時(shí)候會(huì)很流暢,體驗(yàn)很好。

最初就想著怎么利用 vue-router 完成頁(yè)面的預(yù)加載。但是搞了一圈發(fā)現(xiàn),都是基于webpack或者vite的懶加載,提前加載了一些資源,并不會(huì)提前渲染出頁(yè)面。

后來(lái)通過(guò)看vue-router文檔,才找到了靈感,利用命名視圖,同時(shí)展示 2 個(gè)視圖,使用css隱藏下一頁(yè),這時(shí)候雖然不顯示,但是頁(yè)面已經(jīng)渲染出來(lái)了。

通過(guò)修改router-viewname 屬性,完成頁(yè)面的切換。也就是說(shuō),其實(shí)我的路由是沒(méi)有變化的。

// App.vue
<template>
  <router-view :name="currentViewName"></router-view>
  <router-view :name="nextViewName"></router-view>
</template>

// 注意 ,這里使用兩個(gè) viewName 完成了頁(yè)面的跳轉(zhuǎn),next 的頁(yè)面被預(yù)加載
const currentViewName = computed(() => store.getters.currentViewName);
const nextViewName = computed(() => store.getters.nextViewName);

// router的定義部分
const routes = [
  {
    path: '/',
    components: {
      default: Index1,
      index2: Index2,
      session1: Session1,
      session2: Session2,
      session5: Session5
    }
  }
];

看上面的代碼,Index1、Index2Session1等其實(shí)就是每一頁(yè)的組件了,通過(guò)修改currentViewNamenextViewName就可以達(dá)到頁(yè)面切換的目的。

最終的效果是下圖這樣的,下一頁(yè)已經(jīng)提前渲染出來(lái):

image

翻頁(yè)動(dòng)效

作者提示說(shuō)使用canvas實(shí)現(xiàn)了頁(yè)面切換時(shí)候的幕布拉動(dòng)效果,主要運(yùn)用了最核心的 canvas APIbezierCurveTo。

通過(guò)查詢(xún)得知,bezierCurveTo 需要 3 個(gè) 點(diǎn)用來(lái)繪制三次貝賽爾曲線,在線體驗(yàn)

看下圖,想要實(shí)現(xiàn)拉動(dòng)動(dòng)畫(huà),P1 P2 P3X軸坐標(biāo)需要持續(xù)變化,然后繪制曲線,就能夠?qū)崿F(xiàn)拉動(dòng)的效果了。

image

我這里使用了比較輕量的JavaScript 動(dòng)畫(huà)庫(kù)animejs,用來(lái)控制上面幾個(gè)點(diǎn)的持續(xù)移動(dòng)。3 個(gè)動(dòng)畫(huà)效果分別移動(dòng)了P1 P2 P3X軸坐標(biāo) ,再配合曲線的繪制,就達(dá)到了基本的拉動(dòng)幕布效果。

  const heights = [0, 0.5 * pageHeight, pageHeight];
  points = {
    p1: {
      x: pageWidth,
      y: heights[0]
    },
    p2: {
      x: pageWidth,
      y: heights[1]
    },
    p3: {
      x: pageWidth,
      y: heights[2]
    },
    p4: {
      x: pageWidth,
      y: heights[2]
    },
    p5: {
      x: pageWidth,
      y: heights[0]
    }
  };

  // P1點(diǎn)的變化
  anime({
    targets: points.p1,
    x: 0,
    easing: 'easeInQuart',
    delay: 50,
    duration: 500
  });

  // P2點(diǎn)的變化
  anime({
    targets: points.p2,
    x: 0,
    easing: 'easeInSine',
    duration: 500
  });

  anime({
    targets: points.p2,
    y: 0.6 * pageHeight,
    easing: 'easeInSine',
    duration: 500
  });

  // P3點(diǎn)的變化
  anime({
    targets: points.p3,
    x: 0,
    easing: 'easeInQuart',
    delay: 50,
    duration: 500
  });

  // 畫(huà)曲線
  anime({
    duration: 550,
    update: function () {
      // 清除上一次的繪制
      ctx.clearRect(0, 0, pageWidth, pageHeight);
      ctx.beginPath();
      ctx.moveTo(points.p1.x, points.p1.y);
      // 幕布的上半?yún)^(qū)域
      ctx.bezierCurveTo(
        points.p1.x,
        points.p1.y,
        points.p2.x,
        points.p2.y - 0.2 * pageHeight,
        points.p2.x,
        points.p2.y
      );
      // 幕布的下半?yún)^(qū)域
      ctx.bezierCurveTo(
        points.p2.x,
        points.p2.y + 0.2 * pageHeight,
        points.p3.x,
        points.p3.y,
        points.p3.x,
        points.p3.y
      );
      // 已拉動(dòng)部分的矩形區(qū)域
      ctx.lineTo(points.p4.x, points.p4.y);
      ctx.lineTo(points.p5.x, points.p5.y);
      ctx.closePath();
      ctx.fill();
      ctx.strokeStyle = '#f1f1f1';
      ctx.stroke();
    }
  });

最終完成的效果是這樣的:

image

這個(gè)動(dòng)效由于每一頁(yè)都需要使用,所以考慮完成一個(gè)通用的全局組件。

考慮到使用的時(shí)候一般組件需要寫(xiě)到vue 模板上面,很不方便,所以最好通過(guò)一個(gè)全局函數(shù)直接顯示這段動(dòng)效,類(lèi)似于showAnimation();

首先需要完成一個(gè)獨(dú)立的組件,由于想覆蓋掉頁(yè)面的所有信息,所以使用了 Vue3.0 最新提供的teleport 組件:

<!-- 這個(gè)canvas會(huì)被渲染為 app 的子級(jí) -->
  <teleport to="#app">
    <canvas class="mask-canvas" ref="canvas" :class="{ 'mask-canvas-posi': isShow }"></canvas>
  </teleport>

然后需要把組件通過(guò) Vue 插件的方式注冊(cè)到全局屬性,由于我想使用 Composition API ,所以最終決定使用 provide+ inject 的方式注冊(cè)和使用全局 property。一般的情況下使用app.config.globalProperties就可以了,但是這種配合Composition API寫(xiě)起來(lái)會(huì)比較麻煩,不推薦。

(Mask as any).install = (app: App): void => {
  // Vue3 的 Composition API 建議使用 provide + inject 的方式注冊(cè)和使用全局 property
  app.provide('mask', Mask);
};

// 使用的時(shí)候
const Mask = inject('mask');

最后,由于翻頁(yè)動(dòng)效和路由都在一起使用,就繼續(xù)封裝了個(gè)useNext函數(shù),這樣在一般的view組件使用的話,就非常簡(jiǎn)單了,同時(shí)做了翻頁(yè)動(dòng)效和翻頁(yè)的操作:

nextPage();

到這里我可以夸夸Composition API了,非常的簡(jiǎn)單和方便,通過(guò)這個(gè)全局通用組件的封裝,我徹底喜歡上了這種方式。

云層動(dòng)效

這部分是我覺(jué)得最有趣的,以前用three.js實(shí)現(xiàn)過(guò)一個(gè) 3D 照片墻,但是這個(gè)云層動(dòng)效真的牛,也是最難破解的,還好被我搞定了,下篇詳細(xì)說(shuō)明破解的過(guò)程。

image

源碼

最后放上源碼,感興趣的同學(xué)可以看一下,歡迎 Star 和提出建議。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容