前言
最近做項目的過程中,碰到一些不太尋常的需求——將 gif 圖貼到三維球上跨算。
首先我們分析下這一需求爆土,朝 cesium 三維球上貼圖并非難事,很多方式都能做到诸蚕。
采用多邊形步势、方形、甚至于 SingleTileImageryProvider api背犯,都能做到坏瘩。
但是問題最麻煩的地方在于,不僅僅要貼圖片漠魏,而是需要貼 gif 動圖倔矾。
如果不是動圖,而是一系列靜態(tài)圖片的話蛉幸,實現(xiàn)起來的難度反而小一些破讨。
無非是,設(shè)置一些定時器奕纫,定時將當(dāng)前加載的圖片切換為下一張。筆者之前烫沙,就做過這種輪播效果匹层。
但是現(xiàn)在,問題的難點在于锌蓄,如何針對一張 gif 也實現(xiàn)這種播放的效果呢升筏?
一些思考
稍微梳理一下思路,便不難得出瘸爽,我們需要考慮以下幾點問題:
- 瀏覽器是否提供現(xiàn)成的api您访,直接讓 canvas 支持 gif 動圖的加載。
- 如果 canvas 不支持動圖剪决,那么前端是否有現(xiàn)成的工具灵汪,可以將 gif 里的每一幀拆分出來檀训,拆分出每一幀后,我們就可以像上面的做法享言,控制這些幀來實現(xiàn)輪播的效果了峻凫。
- 如果找不到現(xiàn)成的可以拆分 gif 幀的工具,那么就只有采用將 gif 處理成一系列的圖片的方式來實現(xiàn)了览露。
按照這樣的思路荧琼,我們來一條條的尋找解決方案。
第一條差牛,直接可以從原理上否決掉命锄,目前沒有任何一個瀏覽器會支持該功能。原因偏化,其實也很容易能想明白累舷。canvas 其實就是一個畫布,它被設(shè)計出來就是干類似于繪畫的工作夹孔。你要是想要在畫布上實現(xiàn)動畫效果被盈,就只有通過改變每一幀畫布上要繪制的內(nèi)容,來實現(xiàn)搭伤。
canvas 的 api 只提供較為底層只怎,較為基礎(chǔ)的繪畫功能,繪制點線面等等怜俐,webgl 也不提供動圖的支持身堡,因為無論是 canvas 還是 webgl,通俗意義上講拍鲤,都是一張畫布贴谎,它實質(zhì)是不變的,想要變化季稳,則需要人為的去控制擅这。
既然第一條思路行不通,那么第二條思路景鼠,可以解決我們的問題么仲翎?
著手解決問題
帶著這個思路,筆者在網(wǎng)上找尋了一番铛漓,果然找到了一個有力的工具:https://themadcreator.github.io/gifler/
它可以支持將 gif 解析成幀溯香,并且還封裝了一些動畫接口,方便供我們調(diào)用浓恶。
默認(rèn)情況下玫坛,直接調(diào)用 animate 方法即可為我們在目標(biāo)的 canvas 上繪制出該 gif 動畫。
https://codepen.io/wuzhiqin/pen/vYZabwy
當(dāng)然包晰,還有另外的使用方式湿镀,通過調(diào)用 frame api 或者 get api炕吸。
下面這個 demo,是使用 frame api 調(diào)用的方式肠骆。
https://codepen.io/wuzhiqin/pen/YzQjgyZ
可以看出來算途,兩種使用方式,大同小異蚀腿,結(jié)果也并無差別嘴瓤,唯一不同的是,第二種方式莉钙,方便我們?nèi)タ刂泼恳粠瑪?shù)據(jù)廓脆。
那么怎么將該工具應(yīng)用于我們 cesium 貼圖中去,實現(xiàn)動畫效果呢磁玉?
首先停忿,結(jié)合 gifler 和 cesium,不難寫出如下代碼
let canvas = document.createElement("canvas");
let url =
"https://media.giphy.com/media/VbEq7lhC0gVMFUX819/giphy-downsized.gif?cid=ecf05e471i9fq42unyxtjoci88jd019z2aana25ytggjay33&rid=giphy-downsized.gif&ct=g";
let gifImageLayer;
let rectangle = Cesium.Rectangle.fromDegrees(...[92.07, 27.67, 118.66, 39.45]);
function onDrawFrame(ctx, frame) {
ctx.canvas.width = frame.width;
ctx.canvas.height = frame.height;
ctx.drawImage(frame.buffer, 0, 0);
const provider = new Cesium.SingleTileImageryProvider({
url: canvas.toDataURL(),
rectangle
});
gifImageLayer = viewer.imageryLayers.addImageryProvider(provider);
}
gifler(url).frames(canvas, onDrawFrame);
基本邏輯就是蚊伞,在動圖每一幀調(diào)用的時候席赂,都將像素點繪制在 canvas 上,然后將 canvas 導(dǎo)出成圖片时迫,然后再調(diào)用 cesium 的 SingleTileImageryProvider api颅停,將該幀以圖層的方式,貼到球面上掠拳。
最終呈現(xiàn)出來的效果是這樣的:
雖然看起來還不錯癞揉,但是這段代碼,實質(zhì)上是有一些問題的溺欧。
比如喊熟,頻繁的添加新的 layer,卻沒有處理之前重復(fù)的 layer姐刁,短期內(nèi)好像看不出大問題芥牌,但是當(dāng)頁面運行時間長了,會占用大量的內(nèi)存龙填,造成頁面內(nèi)存泄漏胳泉。
那有沒有改進(jìn)的方案呢?
世界上本沒有路岩遗,走的人多了也便成了路。
對于解決問題的方案凤瘦,同樣如此一般宿礁。
方案當(dāng)然有很多,但是唯有不斷嘗試蔬芥,才能找到問題的最佳解決方案梆靖。
既然一直增加不行控汉,那么,為了防止內(nèi)存泄漏返吻,每次增加之前姑子,將上一個刪除不就行了么?
function onDrawFrame(ctx, frame) {
// 不斷地將之前添加的 layer 從 imageryLayers 中刪除掉
viewer.imageryLayers.remove(gifImageLayer, true);
ctx.canvas.width = frame.width;
ctx.canvas.height = frame.height;
ctx.drawImage(frame.buffer, 0, 0);
const provider = new Cesium.SingleTileImageryProvider({
url: canvas.toDataURL(),
rectangle
});
gifImageLayer = viewer.imageryLayers.addImageryProvider(provider);
}
如上所示测僵,改改我們的代碼街佑,看看運行以后,會出現(xiàn)什么結(jié)果:
結(jié)果發(fā)現(xiàn)捍靠,這個效果完全不行沐旨,因為切換幀的時候,沒有過渡榨婆,會導(dǎo)致出現(xiàn)閃屏的效果磁携。
既然先移除再增加不行,那么先增加再移除是否可行呢良风?
按照思路谊迄,再改改我們的代碼:
function onDrawFrame(ctx, frame) {
ctx.canvas.width = frame.width;
ctx.canvas.height = frame.height;
ctx.drawImage(frame.buffer, 0, 0);
const provider = new Cesium.SingleTileImageryProvider({
url: canvas.toDataURL(),
rectangle
});
let layer = viewer.imageryLayers.addImageryProvider(provider);
// 不斷地將之前添加的 layer 從 imageryLayers 中刪除掉
viewer.imageryLayers.remove(gifImageLayer, true);
gifImageLayer = layer;
}
沒想到,再次運行我們的代碼烟央,結(jié)果比之前反而閃爍的更嚴(yán)重了:
不要氣餒统诺,按照道理來說,這個思路并沒有問題吊档,問題篙议,應(yīng)該出現(xiàn)在,增加上一個圖層和刪除下一個圖層這兩步操作怠硼,應(yīng)該并不是同步操作鬼贱,而是異步的。
對于這個問題香璃,不要著急这难,我們也有好的解決方案,每次增加一幀后,不再刪除上一幀了,而是改成梁肿,刪除倒數(shù)第二幀富岳、倒數(shù)第三幀......
一個個試下去,不愁試不出比較理想的方案脏毯。
實際在操作的過程中,你會發(fā)現(xiàn),其實剪个,在每次刪除倒數(shù)第二幀的時候,過渡就已經(jīng)很自然了版确,但是為了效果更好扣囊,我們每次刪除倒數(shù)第三幀:
function onDrawFrame(ctx, frame) {
ctx.canvas.width = frame.width;
ctx.canvas.height = frame.height;
ctx.drawImage(frame.buffer, 0, 0);
const provider = new Cesium.SingleTileImageryProvider({
url: canvas.toDataURL(),
rectangle
});
let layer = viewer.imageryLayers.addImageryProvider(provider);
// 不斷地將之前添加的 layer 從 imageryLayers 中刪除掉
viewer.imageryLayers.remove(gifImageLayer3, true);
gifImageLayer3 = gifImageLayer2;
gifImageLayer2 = gifImageLayer;
gifImageLayer = layer;
}
改進(jìn)以后乎折,最終的效果是這樣的:
可以看出來,效果已經(jīng)很自然了侵歇。
下面是完整的 demo:
https://codepen.io/wuzhiqin/pen/xxrJBgV
精益求精
一般情況下骂澄,按照套路來說,童鞋們可能會覺得我這篇文章這就寫完了惕虑。
但是坟冲,其實細(xì)細(xì)的思索一番,還有一種方式枷遂,可以把代碼寫的更優(yōu)雅一些樱衷。
為什么要不斷的移除之前的圖層,再重復(fù)創(chuàng)建新的圖層酒唉,而不將之前的圖層緩存起來呢矩桂?
我們知道的是,js 里面不能手動釋放內(nèi)存痪伦,即使將對象設(shè)置為 null侄榴,它也會在之后的某次垃圾回收的時候才會被清除掉。如果加一層緩存网沾,就可以減少創(chuàng)建新的對象癞蚕,使我們的代碼性能更高效。
所以辉哥,我們可以再次修改下我們的代碼桦山,將每個新圖層做一個緩存:
function onDrawFrame(ctx, frame) {
let { data_offset } = frame;
// 如果有緩存,則直接將緩存的圖層頂?shù)阶钌厦娲椎环駝t恒水,將創(chuàng)建新的圖層,并加入緩存
if (gifImageLayerList[data_offset]) {
viewer.imageryLayers.raiseToTop(gifImageLayerList[data_offset]);
} else {
ctx.canvas.width = frame.width;
ctx.canvas.height = frame.height;
ctx.drawImage(frame.buffer, 0, 0);
const provider = new Cesium.SingleTileImageryProvider({
url: canvas.toDataURL(),
rectangle
});
let layer = viewer.imageryLayers.addImageryProvider(provider);
gifImageLayerList[data_offset] = layer;
}
}
可以看到饲齐,最后的效果其實跟之前的一般無二钉凌,但是效率比之前肯定要高一些。
https://codepen.io/wuzhiqin/pen/oNwPboM
后記
既然我們采用第二種方式捂人,實現(xiàn)了我們想要的效果御雕,那么前面的第三點思考到這里,就不用再深入下去了滥搭。
雖然酸纲,看起來,這篇文章寫下來瑟匆,實現(xiàn)這個功能好像很輕松福青,但是事實上,在解決這個問題的過程中脓诡,還是碰到了一些特別需要思考的問題无午。
- gif 貼上去,動畫速度太快祝谚,怎么控制
- gif 圖片太大的時候宪迟,加載速度很慢,該怎么處理
第二個問題交惯,其實沒有太好的解決方案次泽,畢竟資源擺在那兒,靠前端不太好整出合理的解決方案席爽。
而第一個問題其實挺有意思的意荤,一開始,博主以為是 gifler 框架里面沒有處理時間間隔的問題只锻,于是想了很多方案玖像,比如將 onDrawFrame 改成 async/await 函數(shù),里面插入一個自己封裝的 sleep 函數(shù)齐饮,開始 sleep 的時候捐寥,暫停動畫,sleep 結(jié)束以后再恢復(fù)執(zhí)行動畫祖驱。這一過程握恳,就達(dá)到了將后面的進(jìn)程阻塞一段時間再切換 layer,似乎能達(dá)到比較理想的效果捺僻。
但是這個解決方案乡洼,在需要不斷切換 gif 圖片的時候,就失效了匕坯。切換的時候束昵,直接就沒有辦法阻塞住進(jìn)程,這個問題困擾了我很久醒颖,最后還是沒找到太好的解決方案妻怎。
就在我以為進(jìn)行不下去的時候,沒想到抓到了問題的癥結(jié)所在泞歉。
看了 gifler 工具的源碼逼侦,才發(fā)現(xiàn),gif 播放的太快腰耙,本質(zhì)上是 gif 圖片本身的問題榛丢。
可以看到,在源碼里面挺庞,作者其實想到了這個問題晰赞,做了細(xì)致的處理的。
所以,這個問題最方便的解決方案掖鱼,其實是然走,調(diào)整下 gif, 將 gif 每一幀播放的間隔時間拉長一些就行了戏挡。
當(dāng)然芍瑞,上面我采用的方案是使用 layer 的方式來解決的,其實用 polygon 或者 rectangle 也能實現(xiàn)褐墅。
亦或者推廣一下拆檬,不用在 cesium 里面,而用在其他 webgl 框架或者 canvas 框架中妥凳,采用上面的思路竟贯,都能實現(xiàn)這樣的效果。