介紹
《你的性格主導(dǎo)色》是今年網(wǎng)易云音樂(lè)前端團(tuán)隊(duì)開(kāi)發(fā)的一款測(cè)試用戶主導(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.0
和vite
,所以源碼部分我使用了 Vue3.0
+vite
實(shí)現(xiàn)楼眷。
頁(yè)面預(yù)加載
答題類(lèi)頁(yè)面與一般的 H5 頁(yè)面的不同之處在于铲汪,用戶的操作路徑是確定的,即每個(gè)頁(yè)面的下一頁(yè)路由是固定的摩桶,所以在 router 層面做了優(yōu)化桥状,提前預(yù)加載了下一個(gè)頁(yè)面
由于活動(dòng)頁(yè)面使用了大量的視頻和動(dòng)效等,所以想在用戶閱讀選擇題目的過(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-view
的 name
屬性,完成頁(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
爱咬、Index2
和Session1
等其實(shí)就是每一頁(yè)的組件了尺借,通過(guò)修改currentViewName
和nextViewName
就可以達(dá)到頁(yè)面切換的目的。
最終的效果是下圖這樣的台颠,下一頁(yè)已經(jīng)提前渲染出來(lái):
翻頁(yè)動(dòng)效
作者提示說(shuō)使用canvas
實(shí)現(xiàn)了頁(yè)面切換時(shí)候的幕布拉動(dòng)效果褐望,主要運(yùn)用了最核心的 canvas API
是 bezierCurveTo
。
通過(guò)查詢得知串前,bezierCurveTo
需要 3 個(gè) 點(diǎn)用來(lái)繪制三次貝賽爾曲線,在線體驗(yàn)
看下圖实蔽,想要實(shí)現(xiàn)拉動(dòng)動(dòng)畫(huà)荡碾,P1 P2 P3
的X
軸坐標(biāo)需要持續(xù)變化,然后繪制曲線局装,就能夠?qū)崿F(xiàn)拉動(dòng)的效果了坛吁。
我這里使用了比較輕量的JavaScript
動(dòng)畫(huà)庫(kù)animejs
,用來(lái)控制上面幾個(gè)點(diǎn)的持續(xù)移動(dòng)铐尚。3 個(gè)動(dòng)畫(huà)效果分別移動(dòng)了P1 P2 P3
的X
軸坐標(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();
}
});
最終完成的效果是這樣的:
這個(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ò)程院水。
源碼
最后放上源碼腊徙,感興趣的同學(xué)可以看一下,歡迎 Star 和提出建議檬某。