以一個實際案例入門canvas

為了更直觀的展示使用canvas開發(fā)向图,本文以繪制一個項目中常見的步驟圖來展示效果粉楚,大致的構(gòu)圖如下


image.png

需求如下:

  1. 根據(jù)實際情況向后端請求數(shù)據(jù)并刷新頁面,本文用setTimeout模擬為6s刷新一次
  2. 本步驟完成時,本步驟節(jié)點及之前的節(jié)點和線條全部點亮
  3. 在進行中狀態(tài)的節(jié)點會有動畫效果顯示
  4. 點擊節(jié)點時顯示節(jié)點信息,配合其他相關操作
    模擬數(shù)據(jù):
        // status表示節(jié)點當前狀態(tài)驯杜,-1表示未完成,0表示進行中做个,1表示已完成
        let data1 = [
            { nodeId: "001x", nodeName: "第一步", status: "0" },
            { nodeId: "002x", nodeName: "第二步", status: "-1" },
            { nodeId: "003x", nodeName: "第三步", status: "-1" }
        ]
        let data2 = [
            { nodeId: "001x", nodeName: "第一步", status: "1" },
            { nodeId: "002x", nodeName: "第二步", status: "0" },
            { nodeId: "003x", nodeName: "第三步", status: "-1" }
        ]
        let data3 = [
            { nodeId: "001x", nodeName: "第一步", status: "1" },
            { nodeId: "002x", nodeName: "第二步", status: "1" },
            { nodeId: "003x", nodeName: "第三步", status: "0" }
        ]
        let data4 = [
            { nodeId: "001x", nodeName: "第一步", status: "1" },
            { nodeId: "002x", nodeName: "第二步", status: "1" },
            { nodeId: "003x", nodeName: "第三步", status: "1" }
        ]

頁面拆解:
由上面的圖片及需求鸽心,我們可以分析出,我們在繪制時需要繪制五個部分居暖,分別是圓再悼,矩形,線段膝但,交互以及動畫

使用canvas開發(fā)

第一步:

在頁面上加上canvas標簽并添加上Id

<canvas id="myCanvas"></canvas>

第二步

獲取DOM及上下文,(canvas的寬高可隨意調(diào)整谤草,本例的操作是將父節(jié)點的寬高賦給canvas)

        
        let canvasParent = document.getElementById('canvas-demo')
        var c = document.getElementById("myCanvas");
        // 使canvas的寬高等于父節(jié)點的寬高
        c.width = canvasParent.clientWidth
        c.height = canvasParent.clientHeight
        // 獲取上下文
        let ctx = c.getContext("2d")

獲取上下文之后跟束,我們就可以在canvas上面正式操作了

第三步

開始繪制圓,矩形丑孩,線段以及文本填充

         //繪制圓形
        function renderCircle(x, y, fillColor, lable) {
            ctx.fillStyle = fillColor || "#FFA500";
            ctx.strokeStyle = fillColor || "#FFA500";
            ctx.beginPath();
            ctx.arc(x, y, 50, 0, 2 * Math.PI);
            ctx.fill();
            ctx.closePath();
            ctx.stroke();
            fillText(lable.text, lable.x, lable.y)
        }
         // 繪制矩形 
        function renderRect(x, y, fillColor, lable) {
            let width = 200, height = 100;
            ctx.fillStyle = fillColor || "#fff";
            ctx.strokeStyle = "#000";
            ctx.beginPath();
            ctx.fillRect(x, y, width, height)
            ctx.stroke();
            fillText(lable.text, lable.x, lable.y, lable.fillColor)
        }
                // 繪制連線
        function renderLine(from, to, fillColor) {
            ctx.strokeStyle = '#000'//控制線段的顏色
            ctx.lineWidth = 1
            ctx.beginPath();
            ctx.moveTo(from.x, from.y);
            ctx.lineTo(to.x, to.y);
            ctx.stroke();
        }

        // 文本填充
        function fillText(text, x, y, fillColor) {
            ctx.font = "16px Arial";
            ctx.textAlign = "center"
            ctx.fillStyle = fillColor || "white"
            ctx.fillText(text, x, y);
        }

這些畫好之后開始連接起來

        function renderCanvasDemo(data) {

            if (data.length > 0) {
                renderCircle(100, 150, "#FFA500", { text: "開始", x: 100, y: 150 })
                let len = data.length
                let pre = { x: 150, y: 100 }//保存開始節(jié)點的最右側(cè)點的坐標
                for (let i = 0; i < len; i++) {
                    let x = pre.x, y = 150;
                    renderLine({ x, y }, { x: x + 100, y: y }, "gray")
                    
                    let f_color = data[i].status > 0 ? "green" : "#FFA500"http://當該步驟完成時冀宴,節(jié)點填充顏色變?yōu)榫G色
                    renderRect(x + 100, y - 50, f_color, { text: data[i].nodeName, x: x + 200, y, fillColor: '#000' })
                    pre.x += 300;
                }
                let x = pre.x
                renderLine({ x, y: 150 }, { x: x + 100, y: 150 }, "gray")
                renderCircle(x + 150, 150, "#FFA500", { text: "結(jié)束", x: x + 150, y: 150 })

            } else {
                console.error("錯誤處理或者給用戶提示")
            }
        }

現(xiàn)在靜態(tài)的步驟圖算是完工了


image.png

第四步 添加交互

這個步驟就稍微有點難度了,因為我們只能在canvas上面綁定事件温学,并不能在里面的元素上面綁定略贮,而且canvas的上下文是過程式執(zhí)行的,執(zhí)行后就會將節(jié)點信息銷毀,這樣的話我們就只能拿到最后一個節(jié)點信息逃延,所以览妖,即使我們能夠獲取點擊時的坐標,但是并不知道這個坐標在哪個元素里面揽祥。
這個時候讽膏,我們可以換個角度,在繪制的時候保存所有的節(jié)點信息拄丰,這樣的話府树,當點擊canvas獲取到坐標之后,將canvas重新繪制料按,然后再去判斷這個坐標點是否在元素的繪制范圍內(nèi)奄侠,這樣我們就成功的解決了事件綁定了,具體代碼如下:

let drawCanvasArr = []//---------存儲繪制的節(jié)點

function renderCanvasDemo(data) {

    if (data.length > 0) {
        renderCircle(100, 150, "#FFA500", { text: "開始", x: 100, y: 150 })

        drawCanvasArr.push({ cb: renderCircle, args: [100, 150, "#FFA500", { text: "開始", x: 100, y: 150 }] })
        let len = data.length
        let pre = { x: 150, y: 100 }//保存開始節(jié)點的最右側(cè)點的坐標
        for (let i = 0; i < len; i++) {
            let x = pre.x, y = 150;
            renderLine({ x, y }, { x: x + 100, y: y }, "gray")

            let f_color = data[i].status > 0 ? "green" : "#FFA500"
            renderRect(x + 100, y - 50, f_color, { text: data[i].nodeName, x: x + 200, y, fillColor: '#000' })

            pre.x += 300;
            drawCanvasArr.push(
                { cb: renderLine, args: [{ x, y }, { x: x + 100, y }, "gray"] },
                {
                    cb: renderRect, args: [x + 100, 100, f_color, { text: data[i].nodeName, x: x + 200, y, fillColor: '#000' }],
                    nodeInfo: { id: data[i].nodeId, x: x + 100, y: 100 }
                }
            )
        }
        let x = pre.x
        renderLine({ x, y: 150 }, { x: x + 100, y: 150 }, "gray")
        renderCircle(x + 150, 150, "#FFA500", { text: "結(jié)束", x: x + 150, y: 150 })

        drawCanvasArr.push(
            { cb: renderLine, args: [{ x, y: 150 }, { x: x + 100, y: 150 }, "gray"] },
            { cb: renderCircle, args: [x + 150, 150, "#FFA500", { text: "結(jié)束", x: x + 150, y: 150 }] }
        )
    } else {
        console.error("錯誤處理或者給用戶提示")
    }
}
        
c.addEventListener("click", function (e) {
            clearCanvas(ctx, c)
            drawCanvasArr.forEach(item => {
                item.cb.apply(undefined, item.args)
                if (item.nodeInfo) {
                    let x, y;
                    ({ x, y } = item.nodeInfo)
                    // 矩形的寬高分別為200载矿,100,當點擊的坐標點x大于矩形的起點x并且小于矩形的終點x+200,y同理垄潮,那么即可判定
                    // 點擊后的坐標點在矩形節(jié)點上
                    if ((e.layerX > x && e.layerX < x + 200) && (e.layerY > y && e.layerY < y + 100)) {
                        console.log(item.nodeInfo.id)
                    }
                }
            })
        });
// 清空區(qū)域內(nèi)所有的元素
function clearCanvas(ctx, el) {
    ctx.clearRect(0, 0, el.width, el.height);
}

這個時候我們再去點擊canvas的時候就可以得到想要的信息了


image.png

第五步 添加動畫

這一步才算是真正的看到canvas動畫的尾燈了,不知道大家留意過沒有恢准,一般有canvas動畫場景的魂挂,一般canvas元素不止一個,比如畫圖方面比較出名的process on


image.png

這是因為在展示的時候馁筐,canvas是分主次的涂召,主canvas相當于舞臺的作用,而那些次canvas才是真正在上面演出的(請原諒我找不到更好的詞語來形容了敏沉,暫時就這樣)
所以我們也是同樣的操作果正,在原有的canvas后面新增一個,并且將其浮動到原有的上面

<div style="position: relative;height: 300px;">
    <canvas id="myCanvas"></canvas>
    <canvas id="myCanvas1" style="position: absolute; top: 0; left: 0;  z-index: 2;"></canvas>
</div>

雖然是新增了一個標簽盟迟,但是操作是一樣的,接下來我們就在第二個canvas上面實現(xiàn)第三個需求
首先我們要做一個矩形從左到右移動的簡易動畫秋泳,具體思路為借助setInterval來定時刷新坐標點,然后定時清空攒菠,然后重繪這一過程迫皱,來實現(xiàn)動畫的效果,具體代碼如下

// 第二層canvas辖众,用于制作動畫以及放點擊事件
let c_ani = document.getElementById("myCanvas1");

c_ani.height = canvasParent.clientHeight
c_ani.width = canvasParent.clientWidth

let context = c_ani.getContext("2d");

function canvasAnimationMask(x, y, fillColor) {
    let width = 50, height = 100;
    context.fillStyle = fillColor
    context.beginPath();
    context.fillRect(x, y, width, height)
    context.stroke();
}

function canvasAnimation(x, y, fillColor, condition) {
    let end = x + 200, start = x;
    return function () {
        if (condition) {
            timer = setInterval(() => {
                clearCanvas(context, c_ani)
                canvasAnimationMask(start, y, fillColor)
                if (start == end - 50) {
                    start = end - 200
                } else {
                    start += 50
                }
                // console.log(start)
            }, 500);
        }
    }
}

動畫已經(jīng)完成了卓起,這個時候我們需要將這個動畫挪到我們預期的節(jié)點上,也就是說凹炸,我們需要獲取到需要動畫的節(jié)點的信息,
在執(zhí)行renderCanvasDemo渲染時進行判斷戏阅,如果該節(jié)點的狀態(tài)是進行中,那么將其坐標傳入canvasAnimation函數(shù)中啤它,這樣的話奕筐,第三個需求就算是完成了
代碼如下

let timer = null
let AnimationFrame

function renderCanvasDemo(data) {

    if (data.length > 0) {
        renderCircle(100, 150, "#FFA500", { text: "開始", x: 100, y: 150 })

        drawCanvasArr.push({ cb: renderCircle, args: [100, 150, "#FFA500", { text: "開始", x: 100, y: 150 }] })
        let len = data.length
        let pre = { x: 150, y: 100 }//保存開始節(jié)點的最右側(cè)點的坐標
        if (timer) {
            cancelAnimationFrame(AnimationFrame)
            clearInterval(timer)
            clearCanvas(context, c_ani)
        }
        for (let i = 0; i < len; i++) {
            let x = pre.x, y = 150;
            renderLine({ x, y }, { x: x + 100, y: y }, "gray")

            let f_color = data[i].status > 0 ? "green" : "#FFA500"
            renderRect(x + 100, y - 50, f_color, { text: data[i].nodeName, x: x + 200, y, fillColor: '#000' })
            if (data[i].status == "0") {
                AnimationFrame = requestAnimationFrame(canvasAnimation(x + 100, y - 50, "rgba(255,0,0,0.2)", true))
            }

            pre.x += 300;
            drawCanvasArr.push(
                { cb: renderLine, args: [{ x, y }, { x: x + 100, y }, "gray"] },
                {
                    cb: renderRect, args: [x + 100, 100, f_color, { text: data[i].nodeName, x: x + 200, y, fillColor: '#000' }],
                    nodeInfo: { id: data[i].nodeId, x: x + 100, y: 100 }
                }
            )
        }
        let x = pre.x
        renderLine({ x, y: 150 }, { x: x + 100, y: 150 }, "gray")
        renderCircle(x + 150, 150, "#FFA500", { text: "結(jié)束", x: x + 150, y: 150 })

        drawCanvasArr.push(
            { cb: renderLine, args: [{ x, y: 150 }, { x: x + 100, y: 150 }, "gray"] },
            { cb: renderCircle, args: [x + 150, 150, "#FFA500", { text: "結(jié)束", x: x + 150, y: 150 }] }
        )
    } else {
        console.error("錯誤處理或者給用戶提示")
    }
}

效果圖如下


image.png

但是舱痘,這個時候我們發(fā)現(xiàn),交互消失了离赫, 不過問題不大芭逝,因為我們剛剛通過浮動把我們作為背景板的canvas給遮住了,我們只需要將監(jiān)聽的DOM節(jié)點切換為最外層的DOM即可
完整代碼見github

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末笆怠,一起剝皮案震驚了整個濱河市铝耻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蹬刷,老刑警劉巖瓢捉,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異办成,居然都是意外死亡泡态,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門迂卢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來某弦,“玉大人,你說我怎么就攤上這事而克“凶常” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵员萍,是天一觀的道長腾降。 經(jīng)常有香客問我,道長碎绎,這世上最難降的妖魔是什么螃壤? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮筋帖,結(jié)果婚禮上奸晴,老公的妹妹穿的比我還像新娘。我一直安慰自己日麸,他們只是感情好寄啼,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著代箭,像睡著了一般辕录。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上梢卸,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機與錄音副女,去河邊找鬼蛤高。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的戴陡。 我是一名探鬼主播塞绿,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼恤批!你這毒婦竟也來了异吻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤喜庞,失蹤者是張志新(化名)和其女友劉穎诀浪,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體延都,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡雷猪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了晰房。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片求摇。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖殊者,靈堂內(nèi)的尸體忽然破棺而出与境,到底是詐尸還是另有隱情,我是刑警寧澤猖吴,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布摔刁,位于F島的核電站,受9級特大地震影響距误,放射性物質(zhì)發(fā)生泄漏簸搞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一准潭、第九天 我趴在偏房一處隱蔽的房頂上張望趁俊。 院中可真熱鬧,春花似錦刑然、人聲如沸寺擂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怔软。三九已至,卻和暖如春择镇,著一層夾襖步出監(jiān)牢的瞬間挡逼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工腻豌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留家坎,地道東北人嘱能。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像虱疏,于是被迫代替她去往敵國和親惹骂。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355