[貝聊科技] 一個(gè)炫酷大屏展示頁(yè)的打造過(guò)程

作者:韓永豪 移動(dòng)開(kāi)發(fā)部 前端開(kāi)發(fā)工程師

今年的11月初,我們公司參加了「2017年亞洲幼教年會(huì)(APEAC)」并取得了很不錯(cuò)的成果。本人有幸負(fù)責(zé)關(guān)于這次展示頁(yè)的前端開(kāi)發(fā),特以此文記錄開(kāi)發(fā)過(guò)程中的關(guān)鍵環(huán)節(jié)。

展示頁(yè)分為三大模塊:數(shù)據(jù)展示、動(dòng)態(tài)展示和地圖展示。效果如下:

image

數(shù)據(jù)展示

image

此模塊展示我們公司至今為止的各項(xiàng)數(shù)據(jù),通過(guò)異步請(qǐng)求定時(shí)更新。

數(shù)字過(guò)渡的動(dòng)態(tài)效果為類(lèi)似于老虎機(jī)的效果,對(duì)應(yīng)數(shù)位的新數(shù)字從下至上替換舊數(shù)字,如果該位數(shù)的數(shù)字沒(méi)有發(fā)生變化,則沒(méi)有過(guò)渡效果。

要實(shí)現(xiàn)這種效果,第一步要把數(shù)字按位切分:

// 分離每個(gè)數(shù)字
function split(num) {
    return (num || 0).toString().split('');
}

然后,增加千分位,即從個(gè)位開(kāi)始,每隔三位插入一個(gè)逗號(hào),實(shí)現(xiàn)代碼如下:

function toThousands(num) {
    var num = (num || 0).toString(), result = '';
    while (num.length > 3) {
        result = ',' + num.slice(-3) + result;
        num = num.slice(0, num.length - 3);
    }
    if (num) { result = num + result; }
    return result;
}

最后利用樣式控制過(guò)渡動(dòng)畫(huà),關(guān)鍵代碼如下:

<ul id="main" class="number">

    <li class="group">
        <span class="old">1</span>
        <span class="new">1</span>
    </li>

    <li class="group">
        <span class="old">,</span>
        <span class="new">,</span>
    </li>

    <li class="group">
        <span class="old">4</span>
        <span class="new">1</span>
    </li>

    <li class="group">
        <span class="old">5</span>
        <span class="new">5</span>
    </li>

    <li class="group">
        <span class="old">6</span>
        <span class="new">2</span>
    </li>

</ul>
.number li {
    width: 0.18rem;
    height: 0.24rem;
    line-height: 0.24rem;
    display: inline-block;
    overflow: hidden;
}

.number li span {
    display: block;
    transform: translateY(0%);
}

.number li.active span {
    animation: move 0.3s;
    animation-fill-mode: forwards; // 讓動(dòng)畫(huà)結(jié)束后保持最后一幀
}

@keyframes move {
    from {
        transform: translateY(0);
    }
    to {
        transform: translateY(-100%);
    }
}
var $main = document.querySelector('#main');

// 填充數(shù)字
function update(fromArr, toArr) {

    // 從個(gè)位數(shù)開(kāi)始對(duì)齊位數(shù)
    fromArr = fromArr.reverse();
    toArr = toArr.reverse();
    
    if (fromArr.length > toArr.length) {
        toArr.length = fromArr.length
    } else {
        fromArr.length = toArr.length
    }
    
    fromArr = fromArr.reverse();
    toArr = toArr.reverse();

    // 渲染節(jié)點(diǎn)并激活動(dòng)畫(huà)
    var numberHTML = ''
    for (var i = 0; i < toArr.length; i++) {
        // 如果該位數(shù)的數(shù)字沒(méi)有發(fā)生變化,則沒(méi)有過(guò)渡效果
        if (formArr[i] !== toArr[i]) {
            numberHTML += ('<li class="group active">' +
                '<span class="old">' + formArr[i] || '' + '</span>' +
                '<span class="new">' + toArr[i] || '' + '</span>' +
            '</li>');
        } else {
            numberHTML += ('<li class="group">' +
                '<span class="old">' + formArr[i] || '' + '</span>' +
                '<span class="new">' + toArr[i] || '' + '</span>' +
            '</li>');
        }
    }
    
    if (numberHTML) {
        $main.innerHTML = numberHTML;
    }
}

動(dòng)態(tài)展示

image

此模塊是游客現(xiàn)場(chǎng)互動(dòng)的區(qū)域,只要掃一下二維碼點(diǎn)贊,就會(huì)新增一條動(dòng)態(tài)。
頁(yè)面運(yùn)行過(guò)程中,可能同時(shí)有多個(gè)人發(fā)動(dòng)態(tài),所以要有一條線程定時(shí)請(qǐng)求數(shù)據(jù),并把數(shù)據(jù)保存在隊(duì)列中:

image

同時(shí),要有另一條線程讀取隊(duì)列的數(shù)據(jù)進(jìn)行渲染:

image

關(guān)鍵代碼如下:

var cacheList = []; // 隊(duì)列列表

var CHECK_INTERVAL = 2000; // 每個(gè)兩秒檢查一下隊(duì)列
var UPDATE_INTERVAL = 1000; // 插入數(shù)據(jù)間隔
var MIN_CACHE = 10; // 儲(chǔ)備數(shù)

// 檢查隊(duì)列
function checkCache() {
    if (cacheList.length < MIN_CACHE) {
        // 異步請(qǐng)求數(shù)據(jù)
        ajax(function(res) {
            if (res && res.length) {
                cacheList = cacheList.concat(res); // 把新的數(shù)據(jù)合并到隊(duì)列列表
                setTimeout(checkCache, CHECK_INTERVAL); // 輪詢(xún)檢查數(shù)據(jù)
            }
        }
    }
}

// 開(kāi)始加載
function loadData() {
    if (cacheList.length > 0) {
        render(cacheList[0]);
        cacheList = cacheList.splice(0, 1);
    }
    setTimeout(start, UPDATE_INTERVAL); // 輪詢(xún)讀取數(shù)據(jù)
}

loadData(); // 數(shù)據(jù)啟動(dòng)
checkCache(); // 隊(duì)列啟動(dòng)

地圖展示

image

此模塊由中國(guó)地圖、固定的光點(diǎn)、閃爍的光點(diǎn)和動(dòng)態(tài)氣泡構(gòu)成。
因?yàn)楣恻c(diǎn)和氣泡都與地理位置(精確到省份)有關(guān)聯(lián),所以首先要在地圖上劃分出每一個(gè)省份。然而,省份的占位是不規(guī)則的,劃分起來(lái)會(huì)有一定的難度。
剛開(kāi)始的想法是每個(gè)省份有固定幾個(gè)點(diǎn),氣泡和光點(diǎn)只會(huì)固定出現(xiàn)在那幾個(gè)位置。雖然能實(shí)現(xiàn)效果,但是看起來(lái)比較僵硬,并沒(méi)有達(dá)到設(shè)計(jì)的效果。因此,又換了一種方案。
微積分在計(jì)算不規(guī)則圖形的面積時(shí),就是把一大塊不規(guī)則圖形切分成若干塊小矩形,以此鋪滿(mǎn)整個(gè)不規(guī)則圖形。同理,只需要按省份畫(huà)出其對(duì)應(yīng)的幾個(gè)小矩形,并記錄下來(lái),就可以根據(jù)這些坐標(biāo)讓光點(diǎn)和氣泡出現(xiàn)在對(duì)應(yīng)省份的位置。為了便于畫(huà)出這些小矩形,我做了一個(gè)小工具,效果如下圖:

image

因?yàn)轫?yè)面是根據(jù)屏幕分辨率自適應(yīng)寬高的,因此地圖在頁(yè)面上的尺寸是不固定的(但是比例是固定的),所以這里做了一點(diǎn)調(diào)整,生成的坐標(biāo)為百分比而不是像素值。例如:

// 記錄的省份數(shù)據(jù)
var positionData = {
    新疆: [
        {
            startX: '6.7164179104477615%',
            endX: '34.07960199004975%',
            startY: '8.602941176470589%',
            endY: '19.77941176470588%'
        },
        {
            startX: '25.37313432835821%',
            endX: '35.69651741293532%',
            startY: '3.5049019607843137%',
            endY: '8.602941176470589%'
        },
        ...
    ],
    廣東: [
        {
            ...
        },
        ...
    ],
    ...
};

最后,只需要在劃分好的小矩形中選取某一塊,然后從這塊小矩形的面積中隨機(jī)抽一個(gè)點(diǎn)顯示光點(diǎn)和氣泡:

// 地圖
var $map = document.querySelector('#map');

// 隨機(jī)獲取一個(gè)點(diǎn)
function getPosition(province) {
    // 獲取省里面隨機(jī)一個(gè)小矩形
    function getPositionArea(areaArray) {
        return areaArray[Math.round(Math.random() * areaArray.length)];
    }
    
    // 選定一個(gè)小矩形
    var area = getPositionArea(positionData[province]);
    
    var x = parseFloat(area.endX) - parseFloat(area.startX);
    var y = parseFloat(area.endY) - parseFloat(area.startY);

    return {
       x: parseFloat(area.startX) + (Math.random() * x) + '%',
       y: parseFloat(area.startY) + (Math.random() * y) + '%'
    };
}

// 獲取新的光點(diǎn)
function getPoint($point, province) {
    if (!$point) {
        $point = document.createElement('div');

        // 如果動(dòng)畫(huà)結(jié)束則移除光點(diǎn),并重新開(kāi)始
        $point.addEventListener('animationend', function() {
            $point.classList.remove('active');
            start($point);
        });
    }
    
    // 更新節(jié)點(diǎn)坐標(biāo)和其他樣式
    var position = getPosition(provice);
    ...
    
    return $point;
}

// 刷新樣式并隨機(jī)加入地圖
function start($point) {
    $point = getPoint($point);
    setTimeout(function() {
        $point.classList('active');
    }, Math.round(Math.radom() * 1000))
}

// 創(chuàng)建六百個(gè)光點(diǎn)
var POINT_COUNT = 600;
for (var i = 0; i < POINT_COUNT; i++) {
    start();
}

這樣就完成了。

應(yīng)急處理

由上可見(jiàn),頁(yè)面上的數(shù)據(jù)和動(dòng)畫(huà)都非常多,展會(huì)的設(shè)備未必能滿(mǎn)足這樣的性能要求。如果遇到性能比較低的機(jī)器,就需要減少數(shù)據(jù)或減弱動(dòng)畫(huà)效果。為了能讓現(xiàn)場(chǎng)工作人員方便地調(diào)整,此頁(yè)面支持通過(guò)URL參數(shù)指定性能選項(xiàng)。例如:

/exhibition?pointCount=300

pointCount參數(shù)是用于修改光點(diǎn)數(shù)量,默認(rèn)為600個(gè)。此外還有其他參數(shù),就不再逐一列出了。
最后,一起來(lái)看看效果吧!

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,076評(píng)論 25 708
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,200評(píng)論 4 61
  • 游泳對(duì)于水感好、膽子大的人來(lái)說(shuō),根本不是什么難事,可是對(duì)于像我這種即怕水、又膽小的人來(lái)講,就是難上加難的事了。經(jīng)常...
    清清草園閱讀 1,857評(píng)論 14 3
  • 中國(guó)的家長(zhǎng)教育孩子,要聽(tīng)話,不要闖禍,別不務(wù)正業(yè)。 中國(guó)的學(xué)校要求孩子整齊劃一,不能驕傲,不要標(biāo)新立異。 美國(guó)的教...
    manbanpaiing閱讀 552評(píng)論 2 3
  • 2017年4月5日,這一天的日子太沉重,我的腦子始終處于慢十二拍的節(jié)奏。 中午12:50分,我在鳳縣檢查路況時(shí),小...
    一笑而過(guò)2023閱讀 504評(píng)論 2 2