摘要:比如地球自轉(zhuǎn)時播放背景音樂,動畫一旦開始則停止穿越云層后播放視頻,其他時候視頻是停止的。在上面做動畫分析的時候,是把這個開場動畫分開來設想的,但是上面的用上狀態(tài)機之后,意外的發(fā)現(xiàn)這個入場動畫可以以另外一個放進來。
上一篇知道如何制作threejs地球之后,就正式coding了,當然還是使用最心愛的Vue。本篇會有一些代碼,但是都是十幾行的獨立片段,相信你不用擔心。
布局在進入本篇主題前,要簡單看一下xplan中的自適應解決方案,即如何在不同尺寸設備中,都保證地球最合適的大小和位置,并且與其配套的一些圖片(虛線的橢圓軌道、正中心白色的圓環(huán)等)都不會顯示的錯位。
xplan用的方式簡單直接,固定大小內(nèi)作布局,然后針對不同的設備尺寸進行縮放。
固定畫布大小(375 * 600),所有和地球相關的元素都可以在這個范圍內(nèi)絕對定位,之后scale一下,保證在設備實際尺寸中是被包含(contain)的。這種方式比REM等其他的自適應方式更適合這個項目,畢竟threejs中不能使用REM單位。
感謝Vue,我得以將上面這個自行縮放的邏輯寫成一個Page組件,之后再也不用操心布局問題了。
動畫xplan中的動畫是最吸引我的地方,特別是地球放大,穿越云層的那一刻,想想還有點小激動。
其實之前看到過一些項目有做從外太空俯沖進地球表面的動畫,但是那些基本都是純圖片制作的SpriteSheet Animation,動畫的前進后退控制都很容易。但xplan項目中則不同,動畫過程中需要控制多個動畫對象,還要配合其他資源(音頻和視頻)。
分析xplan中動畫的邏輯是,在地球自轉(zhuǎn)過程中,長按按鈕,會依次發(fā)生:
地球旋轉(zhuǎn)到目的坐標
地球放大(相機推進)到該坐標
到足夠近的時候,播放云層穿越動畫
云層穿越結(jié)束后,展示對應坐標的視頻內(nèi)容
任何時刻松開長按按鈕,動畫都會回退到地球自轉(zhuǎn)的狀態(tài)
為了方便討論,將上面分析到的動畫階段命名一下:
地球自轉(zhuǎn)過程:idle階段
地球轉(zhuǎn)動到指定坐標的過程:rotating階段
地球距離被拉近拉遠的過程:zooming階段
穿越云層的過程:diving階段
云層過后的視頻展示:presenting階段
具體分析幾個過程:
在idle階段,只要touchstart,就算你只長按了0.1s,那么rotating的動畫就會完整的觸發(fā),然后狀態(tài)跳回idle(rotating沒有反向旋轉(zhuǎn))。如上示意圖。
如果長按至了zooming階段,松開手指之后,zooming動畫會立刻反向播放,直至回到idle階段。如上示意圖。
如果zooming過程松開手指后,但是在離開zooming階段前再次按下去,那么zooming動畫會再一次正向播放。如上示意圖。
diving階段貌似又回到了和rotating類似的行為,就算中途結(jié)束,也會完成當前階段的動畫。但是和rotating不一樣的是,diving階段是有反向動畫的。因此可以看到上面的示意圖。
我在考慮的過程中,陰差陽錯的誤以為還有一個條件:即除了rotating階段外,其他動畫過程都可以隨時進和退(上面的GIF就是我最終完成的動畫控制)。這個給自己添加額外的難度,困擾了我很久。
分步實現(xiàn):地球我創(chuàng)建了一個Earth類,負責3D地球(包括光線,光暈,地表的云,浮動坐標點等)的創(chuàng)建和渲染,同時向外提供幾個public方法:
setCameraPosition()
getCameraPosition()
startAutoRotation()
stopAutoRotation()
地球旋轉(zhuǎn)到指定坐標點,其實就是設置camera的position來完成了。要有流暢動畫的感覺,就使用tween去做position的更新。
new TWEEN.Tween( earth.getCameraPosition() ).to( targetCameraPosition, 1000 ).onUpdate(function () { earth.setCameraPosition(this.x, this.y, this.z) })
關于tween和threejs動畫,這里有教程。
其實最開始,這個Earth類沒有這么純粹,我在里面加了targetLocation代表當前要轉(zhuǎn)到的目標地點;還將tween的邏輯寫在了這個類里面,讓earth知道自己的目的地,控制自己的旋轉(zhuǎn)動畫。但后面發(fā)現(xiàn)對于這個項目中動畫可控制的靈活性,這樣封裝在內(nèi)部的動畫邏輯,將很難寫成清晰的代碼,讓其能和后面的云層動畫統(tǒng)一來控制起來。
分步實現(xiàn):云層決定使用SpriteSheet Animation類似的方法做云層動畫。其實有這樣的庫,比如Film(這個好像也是qq下面的團隊做的),但是我還是更想從npm中install一個,由于沒有找到合適的,就索性自己寫一個好了,于是就發(fā)布了一個小工具——image-sprite。
操作由ImageSprite類創(chuàng)建云層對象,只用到了兩個public方法,主要控制播放前一幀和后一幀:
imageSprite.next()
imageSprite.prev()
其實應該使用自動播放(play)和暫停(pause)應該也能完成,anyway
云層動畫功能單一,想把它寫的不純粹也難。個人覺得coding的藝術就在于如何去劃分這個純粹。
第一印象上面兩個關鍵動畫對象都實現(xiàn)了,用戶的行為也很簡單,只有touchstart和touchend,那么用一個touchDown標志位記錄一下就可以了。所以可以有一個中控器(controller),根據(jù)用戶產(chǎn)生的狀態(tài),來調(diào)用不同的動畫對象播放動畫。
最先開始,腦子里面第一印象是下面這樣的解決方案:
function handleTouchDown () { touchDown = true if (currentState is idle) { playRotatingForwardAnimation(handleAnimationComplete) } else if (currentState is rotating) { playZoomingForwardAnimation(handleAnimationComplete) } else if (currentState is zooming) { playDivingForwardAnimation(handleAnimationComplete) } else if (currentState is diving) { playPresentingForwardAnimation(handleAnimationComplete) } else if (currentState is presenting) { // nothing to do } } function handleTouchEnd () { touchDown = false } function handleAnimationComplete () { if (touchDown) { // 找到下一個階段,正向播放動畫 findNextState() playForwardAnimation(handleAnimationComplete) } else { // 找到上一個階段,反向播放動畫 findPrevState() play BackwardAnimation(handleAnimationComplete) } }
這樣的方案能解決動畫的大方向,即動畫階段之間的前進和后退,無法控制階段內(nèi)的每一幀的方向。而且也能看到,上面有太多的if判斷,handleTouchDown函數(shù)中的那種if情況,一定要避免,否則大項目中代碼很難維護。這樣的情況使用有限狀態(tài)機模式或者策略模式都是很容易解決的。
第一印象告訴我:
要使用狀態(tài)機設計模式
要從幀級別去做控制
狀態(tài)機寫代碼過程中肯定會遇到狀態(tài),最常見的狀態(tài)會被記錄成布爾值或者字符串常量,然后在做某個行為的時候?qū)顟B(tài)變量進行if-else判斷。如果只有2個狀態(tài),還行,但是狀態(tài)如果會變多,那么這樣的代碼就很難維護,將在主體中引入越來越多的if-else,越來越多的與特定狀態(tài)相關的變量和邏輯。
個人非常喜歡狀態(tài)機模式或者策略模式,它們本質(zhì)都一樣,都是使用組合代替繼承,完成統(tǒng)一接口下的行為的多樣性。最開心的是,這個模式將混雜在主體中的狀態(tài)量和行為抽離出來,多帶帶封裝,讓主體變的清清爽爽;還有,在JS中,你甚至連接口類都不用寫!
舉個簡單的例子,上一篇中談到的ImageSprite,用來將一系列圖片進行播放,本質(zhì)上就是繪制圖片而已。但是我這里提供兩種模式,一種繪制在canvas里,一種繪制在dom里(即image展示)。
不使用模式,可以簡單的寫成這樣:
class ImageSprite { constructor () { this.renderMode = "canvas" this.context = null this.imageElement = null this.images = [] } drawImage () { if (this.renderMode === "canvas") { this.context.drawImage() } else if (this.rendererMode === "dom") { this.imageElement.src = "..." } } }
使用了狀態(tài)機模式(這里的場景來看,叫策略模式更貼切,渲染策略不同):
class ImageSprite { constructor () { this.renderer = new CanvasRenderer(this) this.images = [] } drawImage () { this.renderer.drawImage() } } class CanvasRenderer { constructor (imageSprite) { this.imageSprite = imageSprite this.context = null } drawImage () { this.context.drawImage() } } class DomRenderer { constructor (imageSprite) { this.imageSprite = imageSprite this.imageElement = null } drawImage () { this.imageElement.src = "..." } }
可以看到使用了模式之后,context和imageElement這樣的和狀態(tài)相關的變量,還有繪制canvas圖片和繪制dom圖片的不同代碼,都從主體ImageSprite中抽離出去,多帶帶的封裝到了不同的狀態(tài)對象中去了。
想想一下如果有第三種渲染模式,比如渲染在webgl中去,在不使用模式的代碼中,要添加變量,要修改drawImage函數(shù);但是在使用了模式的代碼中,現(xiàn)有代碼都不用改變,只需要添加一個新類WebglRenderer就可以了。這就是代碼的可擴展性和可維護性的體現(xiàn)。(在Java中,還能省去代碼的重新編譯的過程)
整合回到xplan的動畫中去。在前面分析動畫階段的時候,其實就得到了每個狀態(tài),這些狀態(tài)的統(tǒng)一接口就是向前幀動畫(forward)和向后幀動畫(backward)。
先不管每個state中邏輯該怎樣,有了約定的接口,就可以把我們的中控器(Controller)寫個基本框架了:
class Controller { constructor (earth, cloud) { this.earth = earth this.cloud = cloud this.touchDown = false this.state = new IdleState(this) // 初始狀態(tài)為IdleState this._init() } _loop () { requestAnimationFrame(this._loop.bind(this)) if (this.touchDown) { // 如果touchDown,則向前一幀 this.state.forward() } else { // 否則,向后一幀 this.state.backward() } handleTouchStart () { this.touchDown = true } handleTouchEnd () { this.touchDown = false } // ... }
因為要做到幀級別的控制,因此這里用到requestAnimationFrame來制作渲染循環(huán)。代碼是不是很清晰簡單!在渲染循環(huán)中,根本不在乎動畫邏輯怎么執(zhí)行,只知道touchDown了,就做向前動畫,否則做向后動畫,其他的都在各自的狀態(tài)類里去實現(xiàn)。
下面拿兩個狀態(tài)類舉例,其他的請移步這里。
IdleState
class IdleState { constructor (controller) { this.controller = controller } forward () { this.controller.state = new RotatingState(this.controller) } backward () { // do nothing } }
這里IdleState沒有向后的動畫,因此backward()里面是空的;而該狀態(tài)下的touchDown都會讓earth開始旋轉(zhuǎn)到指定坐標,而這個過程我們知道是RotatingState該做的,所以在RotatingState的‘forward()`里會去實現(xiàn)旋轉(zhuǎn)控制。
DivingState
class DivingState { constructor (controller) { this.controller = controller } forward () { let cloud = this.controller.cloud if (cloud.currentFrame is last frame) { // 最后一幀時,進入下一個狀態(tài) this.controller.state = new PresentingState(this.controller) } else { cloud.next() // 播放下一幀 } } backward () { let cloud = this.controller.cloud if (cloud.currentFrame is first frame) { // 回退到第一幀時,進入上一個狀態(tài) this.controller.state = new ZoomingState(this.controller) } else { cloud.prev() // 播放前一幀 } } }
還記得么,diving是指穿越云層的那個過程。因此它往前(forward)是presenting,往后(backward)是zooming。而什么時候切換到下一個或者前一個狀態(tài),和往前或者往后的每一幀動畫該如何執(zhí)行,都只有這個DivingState知道,完美的邏輯封裝。
完整的動畫邏輯里,還包含著一些音頻和視頻的控制邏輯。比如地球自轉(zhuǎn)時播放背景音樂,動畫一旦開始則停止;穿越云層后播放視頻,其他時候視頻是停止的。這些邏輯,能夠很容易的添加到上面的狀態(tài)中去。比如在IdleState的contructor中播放音樂,在RotatingState的contructor中停止播放音樂;在PresentingState的constructor中播放視頻,在DivingState的contructor中停止視頻。
所以,一旦邏輯清晰了,代碼清晰了,添加功能時顯得很容易。
意外收獲完成上面的所有動畫狀態(tài)之后,我發(fā)現(xiàn)地球其實還有一個動畫,那就是開場的逆向旋轉(zhuǎn)并放大的入場動畫。在上面做動畫分析的時候,是把這個開場動畫分開來設想的,但是上面的controller用上狀態(tài)機之后,意外的發(fā)現(xiàn)這個入場動畫可以以另外一個state放進來。
入場動畫狀態(tài)類:
class EnteringState { constructor (controller) { this.controller = controller this.tween = new TWEEN.Tween({ // 起點位置 }).to({ // 終點位置 }, 1600).onUpdate(function () { // 設置earth的縮放和旋轉(zhuǎn) }).onComplete(function () { this.controller.state = new IdleState(this.controller) // 完成后進入IdleState }).easing(TWEEN.Easing.Cubic.Out).start() } forward () { TWEEN.update() } backward () { // do nothing } }
最后將Controller初始化時的第一個state賦值改為EnteringState即可。這真算是一個意外的收獲,本來是打算多帶帶(在controller之外)去實現(xiàn)的。
小結(jié)到這里就差不多了,xplan主要的東西都講到了,高(shan)仿(zhai)的過程還不錯,了解了three,順便還publish了幾個小的工具庫;有不足、也有超越。這個h5看似復雜,但是技術也沒有多高深,主要還是創(chuàng)意,還是要給xplan點個贊!
最后,個人接h5,有沒有個人或者公司啊,不要不好意思聯(lián)系我~
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/83376.html
摘要:首先是這個地球,得看看它是真還是假因為很多效果是拿雪碧圖做的,比如這里的旋轉(zhuǎn)的飛機,結(jié)果找到了并且在網(wǎng)站文件中搜到了,那就是沒跑了。 上個月底,在朋友圈看到一個號稱這可能是地球上最美的h5的分享,點進入后發(fā)現(xiàn)這個h5還很別致,思考了一會,決定要不高仿一個? 到今天為止,高仿基本完成, 線上地址 github地址 除了手機端的media控制沒有去兼容,其他的基本都給仿了。 那為了讓你...
摘要:在文末,我會附上一個可加載的模型方便學習中文藝術字渲染用原生可以很容易地繪制文字,但是原生提供的文字效果美化功能十分有限。 showImg(https://segmentfault.com/img/bVWYnb?w=900&h=385); WebGL 可以說是 HTML5 技術生態(tài)鏈中最為令人振奮的標準之一,它把 Web 帶入了 3D 的時代。 初識 WebGL 先通過幾個使用 Web...
摘要:那我這邊呢,根據(jù)技術的分類,找出其中十個有代表性的案例,給大家解析一下他們技術的實現(xiàn)方案。經(jīng)過我對線上的代碼進行修改,使這個頁面在安卓端強制使用來進行展示后發(fā)現(xiàn),在播放了一會后微信瀏覽器直接崩潰。那么這十個案例的淺析就完了,謝謝。 最近我們前端這邊搜集了50個比較優(yōu)秀的H5。 那我這邊呢,根據(jù)技術的分類,找出其中十個有代表性的案例,給大家解析一下他們技術的實現(xiàn)方案。 設計師也可以根據(jù)技...
摘要:那我這邊呢,根據(jù)技術的分類,找出其中十個有代表性的案例,給大家解析一下他們技術的實現(xiàn)方案。經(jīng)過我對線上的代碼進行修改,使這個頁面在安卓端強制使用來進行展示后發(fā)現(xiàn),在播放了一會后微信瀏覽器直接崩潰。那么這十個案例的淺析就完了,謝謝。 最近我們前端這邊搜集了50個比較優(yōu)秀的H5。 那我這邊呢,根據(jù)技術的分類,找出其中十個有代表性的案例,給大家解析一下他們技術的實現(xiàn)方案。 設計師也可以根據(jù)技...
閱讀 1982·2019-08-30 15:54
閱讀 3532·2019-08-30 15:52
閱讀 1822·2019-08-29 17:20
閱讀 2514·2019-08-29 17:08
閱讀 2346·2019-08-26 13:24
閱讀 780·2019-08-26 11:59
閱讀 2780·2019-08-23 14:50
閱讀 611·2019-08-23 14:20