為了更直觀的展示使用canvas開發(fā)向图,本文以繪制一個項目中常見的步驟圖來展示效果粉楚,大致的構(gòu)圖如下
需求如下:
- 根據(jù)實際情況向后端請求數(shù)據(jù)并刷新頁面,本文用setTimeout模擬為6s刷新一次
- 本步驟完成時,本步驟節(jié)點及之前的節(jié)點和線條全部點亮
- 在進行中狀態(tài)的節(jié)點會有動畫效果顯示
- 點擊節(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)的步驟圖算是完工了
第四步 添加交互
這個步驟就稍微有點難度了,因為我們只能在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的時候就可以得到想要的信息了
第五步 添加動畫
這一步才算是真正的看到canvas動畫的尾燈了,不知道大家留意過沒有恢准,一般有canvas動畫場景的魂挂,一般canvas元素不止一個,比如畫圖方面比較出名的process on
這是因為在展示的時候馁筐,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("錯誤處理或者給用戶提示")
}
}
效果圖如下
但是舱痘,這個時候我們發(fā)現(xiàn),交互消失了离赫, 不過問題不大芭逝,因為我們剛剛通過浮動把我們作為背景板的canvas給遮住了,我們只需要將監(jiān)聽的DOM節(jié)點切換為最外層的DOM即可
完整代碼見github