web中的動畫主要分爲屬性動畫
和幀動畫
兩種,屬性動畫
是經過改變dom元素的屬性如寬高、字體大小或者transform的scale、rotate等屬性,在一段時間內,屬性值按照時間函數變化來實現的。幀動畫
是經過在一段時間內按照必定速率替換圖片的方式來實現,這個和傳統的動畫方式一致。javascript
幀動畫
和屬性動畫
各有優缺點:屬性動畫
不須要加載什麼資源,只須要不斷改變屬性值,觸發瀏覽器的從新計算和渲染就能夠了。幀動畫
可以實現更爲複雜的動畫效果,好比遊戲角色的技能特效等,但須要加載一些圖片。css
web中的交互動畫特效通常都比較簡單,因此屬性動畫
用的更多,幀動畫
比較少。遊戲中的動畫效果追求絢麗,基本都會用幀動畫
,部分會結合屬性動畫
。html
AE
全稱After Effets,是Adobe公司推出的用於處理視頻和圖形的軟件。ui界面中的動畫效果不少都是用AE
來作的。 java
Spine
是針對軟件和遊戲中的2d動畫的,製做動畫比AE
更專業。遊戲中用的比較多。ios
Lottie是Airbnb推出的能夠解析AE導出的包含動畫信息的json文件的庫,支持Android、iOS,React Native等平臺。git
Spine Runtime是Spine提供的Spine導出的動畫解析的庫,支持各類遊戲引擎,如egret、cocos2d-x等。github
Lottie渲染時須要提供一系列的圖片,渲染不一樣幀的時候會使用組合不一樣的圖片。Spine Runtime使用一個小圖片合成的大圖片,渲染時會取不一樣的部分來渲染。web
此外,Lottie支持svg、dom、canvas三種渲染方式,而Spine Runtime只支持canvas。json
實際開發中,Lottie在應用中用的多,Spine Runtime在遊戲中用的多。但並不表明他們不能在另外的場景中使用。canvas
需求中涉及到動畫,設計師沒有使用AE,而是使用Spine來設計的。導出的文件也是Spine特有的格式,因而我就對Spine進行了調研。
通過調研我發現Spine的Runtime中有Html Canvas,這就是他可用在web應用中的基礎。
我把demo下下來看了一下,經過閱讀代碼,替換對應的資源文件,刪減部分無用代碼以後,對Spine Canvas Runtime的使用有了一些心得。
Spine導出的文件有3個,xxx.atlas、xxx.json、xxx.png
xxx.json是動畫的描述文件,分爲skeleton、bones、slots、skins、animations這5部分
咱們不必去詳細瞭解,只須要知道這裏的animations下有一個叫作animation的動畫就能夠了。
xxx.png是圖片文件,由於圖片整合到了一塊兒,全部有一個xxx.atlas文件來描述哪一個小圖片在什麼地方。
資源就這3個文件,接下來就是動畫實現的代碼了。
通過分析,總體流程就是加載資源後,經過不斷的重繪來顯示一幀幀的圖片,圖片的更新是經過時間的毫秒數來驅動的。
不斷重繪的邏輯:
改變繪製內容的邏輯:
每次繪製傳入兩次繪製的時間差,spine runtime會計算出當前應該渲染的內容是什麼。
上面是核心的不斷重繪的機制和更新渲染內容的機制,總體的流程以下:
先加載資源,而後不斷re-render。
總體代碼以下:
<!-- saved from url=(0068)http://esotericsoftware.com/files/runtimes/spine-ts/examples/canvas/ -->
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
<script src="./js/spine-canvas.js"></script>
<style> * { margin: 0; padding: 0; } body, html { height: 100% } canvas { position: absolute; width: 100% ;height: 100%; } </style>
</head>
<body>
<canvas id="canvas" width="398" height="588"></canvas>
<script> var lastFrameTime = Date.now() / 1000; var canvas, context; var assetManager; var skeleton, state, bounds; var skeletonRenderer; // var skelName = "spineboy-ess"; var skelName = "pk_list_flash"; // var animName = "walk"; var animName = "animation"; function init () { canvas = document.getElementById("canvas"); canvas.width = window.innerWidth; canvas.height = window.innerHeight; context = canvas.getContext("2d"); skeletonRenderer = new spine.canvas.SkeletonRenderer(context); // enable debug rendering skeletonRenderer.debugRendering = false; // enable the triangle renderer, supports meshes, but may produce artifacts in some browsers skeletonRenderer.triangleRendering = false; assetManager = new spine.canvas.AssetManager(); assetManager.loadText("assets/" + skelName + ".json"); assetManager.loadText("assets/" + skelName.replace("-pro", "").replace("-ess", "") + ".atlas"); assetManager.loadTexture("assets/" + skelName.replace("-pro", "").replace("-ess", "") + ".png"); requestAnimationFrame(run); } function run () { if (assetManager.isLoadingComplete()) { var data = loadSkeleton(skelName, animName, "default"); skeleton = data.skeleton; state = data.state; bounds = data.bounds; requestAnimationFrame(render); } else { requestAnimationFrame(run); } } function loadSkeleton (name, initialAnimation, skin) { if (skin === undefined) skin = "default"; // Load the texture atlas using name.atlas and name.png from the AssetManager. // The function passed to TextureAtlas is used to resolve relative paths. atlas = new spine.TextureAtlas(assetManager.get("assets/" + name.replace("-pro", "").replace("-ess", "") + ".atlas"), function(path) { return assetManager.get("assets/" + path); }); // Create a AtlasAttachmentLoader, which is specific to the WebGL backend. atlasLoader = new spine.AtlasAttachmentLoader(atlas); // Create a SkeletonJson instance for parsing the .json file. var skeletonJson = new spine.SkeletonJson(atlasLoader); // Set the scale to apply during parsing, parse the file, and create a new skeleton. var skeletonData = skeletonJson.readSkeletonData(assetManager.get("assets/" + name + ".json")); var skeleton = new spine.Skeleton(skeletonData); skeleton.flipY = true; var bounds = calculateBounds(skeleton); skeleton.setSkinByName(skin); // Create an AnimationState, and set the initial animation in looping mode. var animationState = new spine.AnimationState(new spine.AnimationStateData(skeleton.data)); animationState.setAnimation(0, initialAnimation, true); animationState.addListener({ event: function(trackIndex, event) { // console.log("Event on track " + trackIndex + ": " + JSON.stringify(event)); }, complete: function(trackIndex, loopCount) { // console.log("Animation on track " + trackIndex + " completed, loop count: " + loopCount); }, start: function(trackIndex) { // console.log("Animation on track " + trackIndex + " started"); }, end: function(trackIndex) { // console.log("Animation on track " + trackIndex + " ended"); } }) // Pack everything up and return to caller. return { skeleton: skeleton, state: animationState, bounds: bounds }; } function calculateBounds(skeleton) { var data = skeleton.data; skeleton.setToSetupPose(); skeleton.updateWorldTransform(); var offset = new spine.Vector2(); var size = new spine.Vector2(); skeleton.getBounds(offset, size, []); return { offset: offset, size: size }; } function render () { var now = Date.now() / 1000; var delta = now - lastFrameTime; lastFrameTime = now; resize(); state.update(delta); state.apply(skeleton); skeleton.updateWorldTransform(); skeletonRenderer.draw(skeleton); requestAnimationFrame(render); } function resize () { var w = canvas.clientWidth; var h = canvas.clientHeight; if (canvas.width != w || canvas.height != h) { canvas.width = w; canvas.height = h; } // magic var centerX = bounds.offset.x + bounds.size.x / 2; var centerY = bounds.offset.y + bounds.size.y / 2; var scaleX = bounds.size.x / canvas.width; var scaleY = bounds.size.y / canvas.height; var scale = Math.max(scaleX, scaleY) * 1.2; if (scale < 1) scale = 1; var width = canvas.width * scale; var height = canvas.height * scale; context.setTransform(1, 0, 0, 1, 0, 0); context.scale(1 / scale, 1 / scale); context.translate(-centerX, -centerY); context.translate(width / 2, height / 2); } (function() { init(); }()); </script>
</body></html>
複製代碼
web中的動畫有屬性動畫和幀動畫兩種,幀動畫經常使用的庫有Lottie和Spine Runtime,用哪種取決於動效師使用的是AE仍是Spine,其中Spine多用於遊戲的動畫。
從圖片資源的管理方式、支持的渲染方式和平臺這幾個方面比較了Lottie和Spine Runtime的區別:Spine 多用於遊戲,圖片資源整合到一塊兒而且提供atlas文件來標明對應圖片位置,支持canvas的渲染方式,支持各類遊戲引擎。Lottie多用於應用,圖片資源分開存放,支持canvas、svg、dom三種渲染方式,而且支持Android、ios、React Native等平臺。僅從canvas角度看,二者區別並不大。
由於動效師選擇了Spine來設計動效,因此我調研了Spine Runtime的動畫實現方案,研究了Spine的動畫資源和Spine Cavas Runtime的代碼實現、運行流程,所有代碼見github。