canvas繪製經典星空連線效果

廢話不說先上圖:
這個有點醜node

關於這個效果我第一次見是在
https://www.mengxiaozhu.cn/
後來知乎登陸頁也開始用了
https://www.zhihu.com/
網絡上還有不少地方都在用,效果仍是不錯的。
我見了以後以爲挺有意思的就研究了一下原理
下面開始coding:
先寫個canvas標籤canvas

<canvas height="620" width="1360" id="canvas"></canvas>

加上一些默認的樣式:數組

*{
    margin:0;
    padding:0;
}
body{
    overflow: hidden;
}

這裏的overflow:hidden是爲了防止出現滾動條
下面開始寫JS:
首先咱們要獲得那個 canvas 並獲得繪製上下文:瀏覽器

var canvasEl = document.getElementById('canvas');
var ctx = canvasEl.getContext('2d');
var mousePos = [0, 0];

緊接着咱們聲明兩個變量,分別用於存儲「星星」和邊:網絡

var nodes = [];
var edges = [];

而後咱們定義一些其餘的變量:dom

var easingFactor = 5.0;  //緩動因子
var backgroundColor = '#000'; //背景顏色
var nodeColor = '#fff'; //點顏色
var edgeColor = '#fff'; //邊顏色
var pageWidth = window.innerWidth, //窗口寬度 
    pageHeight = window.innerHeight; //窗口高度

設置畫布的大小鋪滿整個屏幕:函數

window.onresize = function () {
    canvasEl.width = pageWidth;
    canvasEl.height = pageHeight;

    if (nodes.length == 0) {
        constructNodes();
    }

    render();
};

window.onresize();

準備工做完成,咱們要開始構建點了:spa

function constructNodes() {
    for (var i = 0; i < 100; i++) {
        var node = {
            drivenByMouse: i == 0,
            x: Math.random() * canvasEl.width,
            y: Math.random() * canvasEl.height,
            vx: Math.random() * 1 - 0.5,
            vy: Math.random() * 1 - 0.5,
            radius: Math.random() > 0.9 ? 3 + Math.random() * 3 : 1 + Math.random() * 3
        };

        nodes.push(node);
    }

    nodes.forEach(function (e) {
        nodes.forEach(function (e2) {
            if (e == e2) {
                return;
            }

            var edge = {
                from: e,
                to: e2
            }

            addEdge(edge);
        });
    });
}

先建立100個點,每一個點設置6個屬性,drivenByMouse屬性只有第一個點爲true,其餘的點爲false,第一個點做爲鼠標跟隨點,不顯示出來,能夠與其餘點連線。x,y做爲點的初始位置,取得是畫布內的隨機點,vx,vy表示點的初始速度,範圍爲-0.5到0.5之間的隨機數,radius表示點的半徑,大部分的點爲小的,少數的點爲大的。code

點都構建完畢了,就要構建點與點之間的連線了,咱們用到雙重遍歷,把兩個點捆綁成一組,放到 edges 數組中。注意這裏我用了另一個函數來完成這件事,而沒有直接用 edges.push() ,爲何?blog

假設咱們以前鏈接了 A、B兩點,也就是外側循環是A,內側循環是B,那麼在下一次循環中,外側爲B,內側爲A,是否是也會建立一條邊呢?而實際上,這兩個邊除了方向不同之外是徹底同樣的,這徹底沒有必要並且佔用資源。所以咱們在 addEdge 函數中進行一個判斷:

function addEdge(edge) {
    var ignore = false;

    edges.forEach(function (e) {
        if (e.from == edge.from & e.to == edge.to) {
            ignore = true;
        }

        if (e.to == edge.from & e.from == edge.to) {
            ignore = true;
        }
    });

    if (!ignore) {
        edges.push(edge);
    }
}

至此,咱們的準備工做就完畢了,下面咱們要讓點動起來:

function step() {
    nodes.forEach(function (e) {
        if (e.drivenByMouse) {
            return;
        }

        e.x += e.vx;
        e.y += e.vy;

        function clamp(min, max, value) {
            if (value > max) {
                return max;
            } else if (value < min) {
                return min;
            } else {
                return value;
            }
        }

        if (e.x <= 0 || e.x >= canvasEl.width) {
            e.vx *= -1;
            e.x = clamp(0, canvasEl.width, e.x)
        }

        if (e.y <= 0 || e.y >= canvasEl.height) {
            e.vy *= -1;
            e.y = clamp(0, canvasEl.height, e.y)
        }
    });

    adjustNodeDrivenByMouse();
    render();
    window.requestAnimationFrame(step);
}

function adjustNodeDrivenByMouse() {
    nodes[0].x += (mousePos[0] - nodes[0].x) / easingFactor;
    nodes[0].y += (mousePos[1] - nodes[0].y) / easingFactor;
}

這段代碼就是遍歷粒子,而且更新其狀態。根據一個簡單的物理公式 s = s + v,每次執行都會 更新到點的下一步的狀態。
adjustNodeDrivenByMouse函將第一個點做爲鼠標的跟隨點,easingFactor爲緩動因子可讓點的運動比鼠標運動的稍慢一點。
而後咱們要讓整個粒子系統連續地運轉起來就須要一個timer了,可是十分不提倡你們使用 setInterval,而是儘量使用 requestAnimationFrame,它能保證你的幀率鎖定在當前瀏覽器的頻率下,通常爲60HZ。

剩下的就是繪製了

function render() {
    ctx.fillStyle = backgroundColor;
    ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);

    edges.forEach(function (e) {
        var l = lengthOfEdge(e);
        var threshold = canvasEl.width / 8;

        if (l > threshold) {
            return;
        }

        ctx.strokeStyle = edgeColor;
        ctx.lineWidth = (1.0 - l / threshold) * 2.5;
        ctx.globalAlpha = 1.0 - l / threshold;
        ctx.beginPath();
        ctx.moveTo(e.from.x, e.from.y);
        ctx.lineTo(e.to.x, e.to.y);
        ctx.stroke();
    });
    ctx.globalAlpha = 1.0;

    nodes.forEach(function (e) {
        if (e.drivenByMouse) {
            return;
        }

        ctx.fillStyle = nodeColor;
        ctx.beginPath();
        ctx.arc(e.x, e.y, e.radius, 0, 2 * Math.PI);
        ctx.fill();
    });
}
function lengthOfEdge(edge) {
    return Math.sqrt(Math.pow((edge.from.x - edge.to.x), 2) + Math.pow((edge.from.y - edge.to.y), 2));
}

繪製的時候咱們要判斷線的長度若是大於某一個值,則不繪製該線了,若是在範圍以內粗細,與顏色的透明度都與線的長度相關,點除了第一個鼠標跟隨點,其餘的畫入便可。
最後加入鼠標移動事件,啓動定時器:

window.onmousemove = function (e) {
    mousePos[0] = e.clientX;
    mousePos[1] = e.clientY;
}

window.requestAnimationFrame(step);

大功告成!!

相關文章
相關標籤/搜索