如何在網(wǎng)頁中做出炫酷的動畫(使用Spine)

屬性動畫和幀動畫

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
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地回。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末扁远,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子刻像,更是在濱河造成了極大的恐慌畅买,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件细睡,死亡現(xiàn)場離奇詭異谷羞,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)溜徙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門湃缎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蠢壹,你說我怎么就攤上這事嗓违。” “怎么了图贸?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵蹂季,是天一觀的道長。 經(jīng)常有香客問我求妹,道長乏盐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任制恍,我火速辦了婚禮,結(jié)果婚禮上神凑,老公的妹妹穿的比我還像新娘净神。我一直安慰自己何吝,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布鹃唯。 她就那樣靜靜地躺著爱榕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪坡慌。 梳的紋絲不亂的頭發(fā)上黔酥,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機(jī)與錄音洪橘,去河邊找鬼跪者。 笑死,一個胖子當(dāng)著我的面吹牛熄求,可吹牛的內(nèi)容都是我干的渣玲。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼弟晚,長吁一口氣:“原來是場噩夢啊……” “哼忘衍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起卿城,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤枚钓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后瑟押,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秘噪,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年勉耀,在試婚紗的時候發(fā)現(xiàn)自己被綠了指煎。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡便斥,死狀恐怖至壤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情枢纠,我是刑警寧澤像街,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站晋渺,受9級特大地震影響镰绎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜木西,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一畴栖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧八千,春花似錦吗讶、人聲如沸燎猛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽重绷。三九已至,卻和暖如春膜毁,著一層夾襖步出監(jiān)牢的瞬間昭卓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工瘟滨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留候醒,地道東北人。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓室奏,卻偏偏與公主長得像火焰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子胧沫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容