屬性動畫和幀動畫
web中的動畫主要分為屬性動畫
和幀動畫
兩種菱属,屬性動畫
是通過改變dom元素的屬性如寬高、字體大小或者transform的scale损痰、rotate等屬性土至,在一段時間內(nèi),屬性值按照時間函數(shù)變化來實(shí)現(xiàn)的啡浊。幀動畫
是通過在一段時間內(nèi)按照一定速率替換圖片的方式來實(shí)現(xiàn)觅够,這個和傳統(tǒng)的動畫方式一致。
幀動畫
和屬性動畫
各有優(yōu)缺點(diǎn):屬性動畫
不需要加載什么資源巷嚣,只需要不斷改變屬性值喘先,觸發(fā)瀏覽器的重新計算和渲染就可以了。幀動畫
能夠?qū)崿F(xiàn)更為復(fù)雜的動畫效果涂籽,比如游戲角色的技能特效等苹祟,但需要加載一些圖片。
web中的交互動畫特效一般都比較簡單,所以屬性動畫
用的更多树枫,幀動畫
比較少直焙。游戲中的動畫效果追求絢麗,基本都會用幀動畫
砂轻,部分會結(jié)合屬性動畫
奔誓。
AE和Spine
AE
全稱After Effets,是Adobe公司推出的用于處理視頻和圖形的軟件搔涝。ui界面中的動畫效果很多都是用AE
來做的厨喂。
Spine
是針對軟件和游戲中的2d動畫的,制作動畫比AE
更專業(yè)庄呈。游戲中用的比較多蜕煌。
Lottie和Spine Runtime
Lottie是Airbnb推出的可以解析AE導(dǎo)出的包含動畫信息的json文件的庫,支持Android诬留、iOS斜纪,React Native等平臺。
Spine Runtime是Spine提供的Spine導(dǎo)出的動畫解析的庫文兑,支持各種游戲引擎盒刚,如egret、cocos2d-x等绿贞。
Lottie渲染時需要提供一系列的圖片因块,渲染不同幀的時候會使用組合不同的圖片。Spine Runtime使用一個小圖片合成的大圖片籍铁,渲染時會取不同的部分來渲染涡上。
此外,Lottie支持svg寨辩、dom吓懈、canvas三種渲染方式歼冰,而Spine Runtime只支持canvas靡狞。
實(shí)際開發(fā)中,Lottie在應(yīng)用中用的多隔嫡,Spine Runtime在游戲中用的多甸怕。但并不代表他們不能在另外的場景中使用。
在web應(yīng)用中使用Spine Runtime
需求中涉及到動畫腮恩,設(shè)計師沒有使用AE梢杭,而是使用Spine來設(shè)計的。導(dǎo)出的文件也是Spine特有的格式秸滴,于是我就對Spine進(jìn)行了調(diào)研武契。
經(jīng)過調(diào)研我發(fā)現(xiàn)Spine的Runtime中有Html Canvas,這就是他可用在web應(yīng)用中的基礎(chǔ)。
我把demo下下來看了一下咒唆,通過閱讀代碼届垫,替換對應(yīng)的資源文件,刪減部分無用代碼之后全释,對Spine Canvas Runtime的使用有了一些心得装处。
動畫資源
Spine導(dǎo)出的文件有3個,xxx.atlas浸船、xxx.json妄迁、xxx.png
xxx.json是動畫的描述文件,分為skeleton李命、bones登淘、slots、skins封字、animations這5部分
我們沒必要去詳細(xì)了解形帮,只需要知道這里的animations下有一個叫做animation的動畫就可以了。
xxx.png是圖片文件周叮,因?yàn)閳D片整合到了一起辩撑,所有有一個xxx.atlas文件來描述哪個小圖片在什么地方。
資源就這3個文件仿耽,接下來就是動畫實(shí)現(xiàn)的代碼了合冀。
動畫實(shí)現(xiàn)代碼
經(jīng)過分析,整體流程就是加載資源后项贺,通過不斷的重繪來顯示一幀幀的圖片君躺,圖片的更新是通過時間的毫秒數(shù)來驅(qū)動的。
不斷重繪的邏輯:
改變繪制內(nèi)容的邏輯:
每次繪制傳入兩次繪制的時間差开缎,spine runtime會計算出當(dāng)前應(yīng)該渲染的內(nèi)容是什么棕叫。
上面是核心的不斷重繪的機(jī)制和更新渲染內(nèi)容的機(jī)制,整體的流程如下:
先加載資源奕删,然后不斷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>
總結(jié)
web中的動畫有屬性動畫和幀動畫兩種,幀動畫常用的庫有Lottie和Spine Runtime完残,用哪一種取決于動效師使用的是AE還是Spine伏钠,其中Spine多用于游戲的動畫。
從圖片資源的管理方式谨设、支持的渲染方式和平臺這幾個方面比較了Lottie和Spine Runtime的區(qū)別:Spine 多用于游戲熟掂,圖片資源整合到一起并且提供atlas文件來標(biāo)明對應(yīng)圖片位置,支持canvas的渲染方式扎拣,支持各種游戲引擎赴肚。Lottie多用于應(yīng)用素跺,圖片資源分開存放,支持canvas誉券、svg亡笑、dom三種渲染方式,并且支持Android横朋、ios仑乌、React Native等平臺。僅從canvas角度看琴锭,兩者區(qū)別并不大晰甚。
因?yàn)閯有熯x擇了Spine來設(shè)計動效,所以我調(diào)研了Spine Runtime的動畫實(shí)現(xiàn)方案决帖,研究了Spine的動畫資源和Spine Cavas Runtime的代碼實(shí)現(xiàn)厕九、運(yùn)行流程,全部代碼見github地回。