Canvas 是 HTML5 中新出的一个元素,开发者能够在上面绘制一系列图形canvas
canvas的画布是一个矩形区域,能够控制其每一像素api
canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法浏览器
Canvas提供的功能更适合像素处理,动态渲染和大数据量绘制bash
<canvas>
元素创造了一个固定大小的画布,它公开了一个或多个渲染上下文,其能够用来绘制和处理要展现的内容。markdown
<canvas id="chart1" width="600" height="600"></canvas> const chart1 = document.getElementById("chart1") let ctx = chart1.getContext('2d')复制代码
beginPath()
新建一条路径,生成以后,图形绘制命令被指向到路径上生成路径。app
closePath()
闭合路径以后图形绘制命令又从新指向到上下文中。函数
stroke()
经过线条来绘制图形轮廓。大数据
fill()
经过填充路径的内容区域生成实心的图形。this
lineTo(x, y)
绘制一条从当前位置到指定x以及y位置的直线spa
arc(x, y, radius, startAngle, endAngle, anticlockwise)
画一个以(x,y)为圆心的以radius为半径的圆弧(圆),从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针)来生成
fillStyle = color
设置图形的填充颜色。
strokeStyle = color
设置图形轮廓的颜色。
createLinearGradient(x1, y1, x2, y2)
createLinearGradient 方法接受 4 个参数,表示渐变的起点 (x1,y1) 与终点 (x2,y2)
// 三角 ctx.beginPath(); ctx.moveTo(10, 10); ctx.lineTo(50, 10); ctx.lineTo(50, 50); ctx.lineTo(10, 10); ctx.strokeStyle = "#b18ea6" ctx.lineWidth = 2 ctx.stroke(); ctx.closePath() // 矩形 ctx.fillStyle = "#00f" ctx.fillRect(70, 10, 40 ,40) // 扇形 ctx.beginPath(); ctx.moveTo(130, 10) ctx.arc(130, 10, 40, Math.PI / 4, Math.PI / 2, false) let grd = ctx.createLinearGradient(130, 10, 150, 50); grd.addColorStop(0, "#f00") grd.addColorStop(1, "#0ff") ctx.fillStyle = grd ctx.fill()复制代码
fillText(text, x, y [, maxWidth])
在指定的(x,y)位置填充指定的文本,绘制的最大宽度是可选的.
strokeText(text, x, y [, maxWidth])
在指定的(x,y)位置绘制文本边框,绘制的最大宽度是可选的.
ctx.font = "32px serif"; ctx.textAlign = "center" ctx.fillText("canvas", 100, 100); ctx.strokeText("canvas", 100, 130);复制代码
绘制title,使用绘制文字api
ctx.font = "28px serif"; ctx.textAlign = "center" ctx.fillText(option.title, 300, 30);复制代码
绘制图例,使用绘制文字和矩形的方法
for(..data..) { ctx.fillRect (preLegend, 50, 30, 20) preLegend += 40 // 偏移量 ctx.font = "20px serif"; ctx.fillText(item.name, preLegend, 78); preLegend += ctx.measureText(item.name).width + 16 // measureText返回传入字符串的渲染尺寸 }复制代码
使用上述绘制扇形的方法绘制饼图中每一个元素的比例
option.data.map((item, index) => { ctx.beginPath() ctx.moveTo(300, 300) let start = Math.PI * 2 * preArc // preArc角度偏移量 let end = Math.PI * 2 * (preArc + item.num / total) ctx.arc(300, 300, 200, start, end, false) ctx.fillStyle = defaultColor[index] ctx.fill() preArc += item.num / total })复制代码
为每一个扇形元素添加名称和数量,链接圆心和对应扇形圆弧中心并延长,绘制元素名称和数量便可
for(..data..) { ctx.font = "16px serif"; let lineEndx = Math.cos(start + (end - start) / 2) * 230 + 300 // 三角函数计算下坐标,每一个扇形圆弧中间 let lineEndy = Math.sin(start + (end - start) / 2) * 230 + 300 // 中轴右边延线向右,左边延线向左 let lineEndx2 = lineEndx > 300 ? lineEndx + ctx.measureText(`${item.name}: ${item.num}`).width : lineEndx - ctx.measureText(`${item.name}: ${item.num}`).width ctx.moveTo(300, 300) ctx.lineTo(lineEndx, lineEndy) ctx.lineTo(lineEndx2, lineEndy) ctx.strokeStyle = defaultColor[index] ctx.stroke() ctx.closePath() ctx.fillText(`${item.name}: ${item.num}`, lineEndx2, lineEndy) }复制代码
绘制坐标轴线,两条垂直线段
ctx.strokeStyle = '#ccc' ctx.beginPath() ctx.moveTo(50, 100) ctx.lineTo(50, 350) ctx.lineTo(550, 350) ctx.stroke() ctx.closePath()复制代码
绘制柱状图,计算坐标,渲染对应高度的矩形便可
option.data.map((item, index) => { let x = 80 + index * 60 let y = 350 - item.num/total * 500 * 2 let w = 30 let h = item.num/total * 500 * 2 ctx.fillStyle = defaultColor[index] ctx.fillRect(x,y,w,h) })复制代码
绘制折线,使用画线api链接每一个矩形上边中线便可,这里使用了渐变效果
if(posList.length > 1) { ctx.beginPath() ctx.moveTo(posList[index - 1].x + w / 2, posList[index - 1].y) ctx.lineTo(x + w / 2, y) let grd = ctx.createLinearGradient(posList[index - 1].x + w / 2, posList[index - 1].y, x + w / 2, y); grd.addColorStop(0,defaultColor[index - 1]) grd.addColorStop(1,defaultColor[index]) ctx.strokeStyle = grd ctx.lineWidth = 3 ctx.stroke() ctx.closePath() } 复制代码
绘制坐标轴和元素对应的坐标,以每一个元素柱状的高度画虚线至y轴,并绘制数值;元素下方绘制元素名称
ctx.beginPath() ctx.moveTo(x + w / 2, y) ctx.lineTo(50, y) ctx.setLineDash([4]) // 使用虚线 ctx.strokeStyle = defaultColor[index] ctx.lineWidth = 0.5 ctx.stroke() ctx.closePath() ctx.font = "20px serif"; ctx.fillText(item.num, 35, y + 8); ctx.font = "20px serif"; ctx.fillText(item.name, x + w / 2, 370);复制代码
后台系统中不少都有接入水印,这里使用canvas实践一次
rotate
方法,它用于以原点为中心旋转 canvas。
rotate(angle)
这个方法只接受一个参数:旋转的角度(angle),它是顺时针方向的,以弧度为单位的值。
首先绘制一个水印模板
let watermarkItem = document.createElement("canvas") watermarkItem.id = "watermark-item" watermarkItem.width = 160 watermarkItem.height = 160 document.body.appendChild(watermarkItem) let ctxitem = watermarkItem.getContext('2d') ctxitem.font = "28px serif" ctxitem.textAlign = "center" ctxitem.save() ctxitem.rotate(-Math.PI/4) ctxitem.fillText("canvas", 0, 110) ctxitem.restore() 复制代码
以该模板渲染整个浏览器窗口便可
createPattern()
方法在指定的方向内重复指定的元素。
元素能够是图片、视频,或者其余 <canvas> 元素。
被重复的元素可用于绘制/填充矩形、圆形或线条等等。
let pat = ctx.createPattern(item, "repeat") ctx.fillStyle = pat ctx.fillRect(0, 0, watermark.width, watermark.height)复制代码
接下来隐藏第一个模板,并监听浏览器窗口的变化,窗口变化时从新渲染水印覆盖窗口
window.onload = createWatermark window.onresize = drawWatermark function createWatermark() { let watermarkItem = document.createElement("canvas") let watermark = document.createElement("canvas") watermarkItem.id = "watermark-item" watermarkItem.width = 160 watermarkItem.height = 160 watermarkItem.style.display = "none" watermark.id = "watermark" watermark.width = window.innerWidth watermark.height = window.innerHeight document.body.appendChild(watermark) document.body.appendChild(watermarkItem) drawWatermark() } function drawWatermark () { let item = document.getElementById("watermark-item") let watermark = document.getElementById("watermark") watermark.id = "watermark" watermark.width = window.innerWidth watermark.height = window.innerHeight let ctxitem = item.getContext('2d') let ctx = watermark.getContext('2d') ctxitem.font = "28px serif" ctxitem.textAlign = "center" ctxitem.save() ctxitem.rotate(-Math.PI/4) ctxitem.fillText("canvas", 0, 110) ctxitem.restore() let pat = ctx.createPattern(item, "repeat") ctx.fillStyle = pat ctx.fillRect(0, 0, watermark.width, watermark.height) }复制代码
这里使用canvas绘制一棵树,并具有拖拽、增删改等功能
首先遍历树的节点,绘制树的节点
这里对每层节点都设置不一样的高度;对每一个节点计算应占有的x轴空间,子节点平分父节点所占空间
递归遍历中 datalist.map((item, index) => { if(p.children) { // x1,x2为占用x轴空间坐标 item.x1 = p.x1 + (p.x2 - p.x1) * index / p.children.length item.x2 = p.x1 + (p.x2 - p.x1) * (index + 1) / p.children.length item.x = item.x1 + (item.x2 - item.x1) / 2 item.y = item.level * 100 } else { item.x = 375 item.y = item.level * 100 item.x1 = 0 item.x2 = 800 } ctx.fillStyle = defaultColor[item.level] ctx.fillRect(item.x, item.y, 50, 25) }复制代码
而后链接子节点与父节点之间,并填写每一个节点的信息
ctx.fillStyle = "#000" ctx.font = "20px serif"; ctx.textAlign = "center" ctx.fillText(`${item.name}${item.num}`, ix + 25, iy + 20); ctx.beginPath() ctx.moveTo(item.x + 25, item.y) ctx.lineTo(p.x + 25, p.y + 25) ctx.stroke() ctx.closePath()复制代码
接下来完成拖拽功能,首先绑定事件,并得到mousedown事件点击在canvas画板上的坐标,判断是否点击坐标在树的节点上,点击的是哪一个节点
若是点击在节点上,则监听mousemove事件,让节点跟随鼠标移动(经过计算新的节点坐标,并重绘)
监听到mouseup事件,即表明拖拽事件结束,清除掉mouseover事件便可
let move = (e) => { ctx.clearRect(0,0,800,1000) this.drawTree(this.datalist, {}, ctx, { id, curX: e.clientX - chart3.getBoundingClientRect().left, curY: e.clientY - chart3.getBoundingClientRect().top }) } chart3.addEventListener('mousedown', (e) => { // 计算点击在canvas上的坐标 curX = e.clientX - chart3.getBoundingClientRect().left curY = e.clientY - chart3.getBoundingClientRect().top this.rectList.forEach(item => { // 判断点击在矩形上 if(curX >= item.x1 && curX <= item.x2 && curY >= item.y1 && curY <= item.y2) { id = item.id this.current = item chart3.addEventListener('mousemove', move, false) } }) }, false) chart3.addEventListener('mouseup', (e) => { chart3.removeEventListener('mousemove', move) }, false) // 重绘节点 if(item.curX) { ctx.fillRect(item.curX, item.curY, 50, 25) } else { ctx.fillRect(item.x, item.y, 50, 25) } ctx.fillStyle = "#000" ctx.font = "20px serif"; ctx.textAlign = "center" let ix = item.curX ? item.curX : item.x let iy = item.curY ? item.curY : item.y let px = p.curX ? p.curX : p.x let py = p.curY ? p.curY : p.y ctx.fillText(`${item.name}:${item.num}`, ix + 25, iy + 20); // 从新连线 ctx.beginPath() ctx.moveTo(ix + 25, iy) ctx.lineTo(px + 25, py + 25)复制代码
接下来为节点添加增删改的功能,右键点击节点时弹出操做菜单
// 清除默认右键事件 document.oncontextmenu = function(e){ e.preventDefault(); }; chart3.addEventListener('mousedown', (e) => { 。。。。判断点击在某个节点上 if(e.button ==2){ // 右键点击 this.outerVisible = true // 弹出菜单 return } })复制代码
这里使用element简单作一个表单
删除操做:
在遍历节点的时候,判断节点的id和须要删除的节点的id一致时,将该节点置空,并从新渲染便可
修改操做:
在遍历节点的时候,判断节点的id和须要修改的节点的id一致时,将该节点的数据设置为新的,并从新渲染
新增节点:
在遍历节点的时候,判断节点的id和须要新增子节点的id一致时,在该节点树children中新增对应的数据,并从新渲染
// 修改节点 edit(datalist) { datalist.map((item, index) => { if(item.id == this.current.id) { item.num = this.num item.name = this.name const chart3 = document.getElementById("chart3") let ctx = chart3.getContext('2d') // 从新渲染 ctx.clearRect(0,0,800,1000) this.drawTree(this.datalist, {}, ctx) return } if(item.children) { this.edit(item.children) } }) }复制代码
源代码
<template> <div class="hello"> <canvas id="chart1" width="600" height="600"></canvas> <canvas id="chart2" width="600" height="600"></canvas> <canvas id="chart3" width="800" height="1000"></canvas> <el-dialog title="操做" :visible.sync="outerVisible"> <el-button-group> <div>id: {{current.id}}</div> <el-button type="primary" @click="oprNode(1)">增长节点</el-button> <el-button type="primary" @click="deleteNode(datalist, current.id)">删除节点</el-button> <el-button type="primary" @click="oprNode(2)">修改节点</el-button> </el-button-group> <el-dialog width="30%" title="" :visible.sync="innerVisible" append-to-body> <el-form ref="form" label-width="80px"> <el-form-item label="id"> <el-input v-model="id"></el-input> </el-form-item> <el-form-item label="name"> <el-input v-model="name"></el-input> </el-form-item> <el-form-item label="num"> <el-input v-model="num"></el-input> </el-form-item> <el-button @click="submit">提交</el-button> </el-form> </el-dialog> </el-dialog> </div> </template> <script> // import * as tf from '@tensorflow/tfjs'; export default { name: 'Tensor', data() { return { rectList: [], outerVisible: false, innerVisible: false, current: { id: '' }, type: '', id: '', num: '', name: '', datalist: [{ name: "A", num: 400, level: 1, id: 1, children: [ { name: "B", num: 300, level: 2, id: 2, children: [ { name: "D", num: 150, level: 3, id: 3, children: [ { name: "I", num: 70, level: 4, id: 4, }, { name: "J", num: 80, level: 4, id: 5, }, ] }, { name: "E", num: 50, level: 3, id: 6, }, { name: "F", num: 100, level: 3, id: 7, }, ] }, { name: "C", num: 100, level: 2, id: 8, children: [ { name: "G", num: 20, level: 3, id: 9 }, { name: "H", num: 80, level: 3, id: 10 }, ] }, ] }] } }, mounted() { this.init() this.initTree() document.oncontextmenu = function(e){ e.preventDefault(); }; }, methods: { init() { // const model = tf.sequential(); // model.add(tf.layers.dense({units: 1, inputShape: [1]})); // model.compile({loss: 'meanSquaredError', optimizer: 'sgd'}); // const xs = tf.tensor2d([1, 2, 3, 4], [4, 1]); // const ys = tf.tensor2d([1, 3, 5, 7], [4, 1]); // model.fit(xs, ys).then(() => { // model.predict(tf.tensor2d([5], [1, 1])).print(); // }); let data = [{ name: 'A', num: 80, },{ name: 'B', num: 60, },{ name: 'C', num: 20, },{ name: 'D', num: 40, },{ name: 'E', num: 30, },{ name: 'F', num: 70, },{ name: 'G', num: 60, },{ name: 'H', num: 40, }] const chart1 = document.getElementById("chart1") const chart2 = document.getElementById("chart2") this.createChart(chart1, { title: "canvas饼图:各阶段比例", type: "pie", data: data }) this.createChart(chart2, { title: "canvas折线柱状图", type: "bar", data: data }) }, createChart(canvas, option) { const defaultColor = ["#ff8080", "#b6ffea", "#fb0091", "#fddede", "#a0855b", "#b18ea6", "#ffc5a1", "#08ffc8"] let ctx = canvas.getContext('2d') ctx.font = "28px serif"; ctx.textAlign = "center" ctx.fillText(option.title, 300, 30); let total = option.data.reduce((pre, a) => { return pre + a.num }, 0) let preArc = 0 let preLegend = 40 // 饼图 if(option.type === "pie") { ctx.arc(300,300,210,0,Math.PI*2,false) ctx.strokeStyle = '#ccc' ctx.stroke() ctx.closePath() option.data.map((item, index) => { ctx.beginPath() ctx.moveTo(300, 300) let start = Math.PI * 2 * preArc let end = Math.PI * 2 * (preArc + item.num / total) ctx.arc(300, 300, 200, start, end, false) ctx.fillStyle = defaultColor[index] ctx.fill() ctx.font = "16px serif"; let lineEndx = Math.cos(start + (end - start) / 2) * 230 + 300 let lineEndy = Math.sin(start + (end - start) / 2) * 230 + 300 let lineEndx2 = lineEndx > 300 ? lineEndx + ctx.measureText(`${item.name}: ${item.num}`).width : lineEndx - ctx.measureText(`${item.name}: ${item.num}`).width ctx.moveTo(300, 300) ctx.lineTo(lineEndx, lineEndy) ctx.lineTo(lineEndx2, lineEndy) ctx.strokeStyle = defaultColor[index] ctx.stroke() ctx.closePath() ctx.fillText(`${item.name}: ${item.num}`, lineEndx2, lineEndy); // 图例 ctx.fillRect (preLegend, 60, 30, 20) preLegend += 40 ctx.font = "20px serif"; ctx.fillText(item.name, preLegend, 78); preLegend += ctx.measureText(item.name).width + 16 preArc += item.num / total }) } else if(option.type === "bar"){ ctx.strokeStyle = '#ccc' ctx.beginPath() ctx.moveTo(50, 100) ctx.lineTo(50, 350) ctx.lineTo(550, 350) ctx.stroke() ctx.closePath() let posList = [] preArc = 0 preLegend = 40 option.data.map((item, index) => { let x = 80 + index * 60 let y = 350 - item.num/total * 500 * 2 let w = 30 let h = item.num/total * 500 * 2 ctx.fillStyle = defaultColor[index] ctx.fillRect(x,y,w,h) // 坐标轴 ctx.beginPath() ctx.moveTo(x + w / 2, y) ctx.lineTo(50, y) ctx.setLineDash([4]) ctx.strokeStyle = defaultColor[index] ctx.lineWidth = 0.5 ctx.stroke() ctx.closePath() ctx.font = "20px serif"; ctx.fillText(item.num, 35, y + 8); ctx.font = "20px serif"; ctx.fillText(item.name, x + w / 2, 370); // 图例 ctx.fillRect (preLegend, 60, 30, 20) preLegend += 40 ctx.font = "20px serif"; ctx.fillText(item.name, preLegend, 78); preLegend += ctx.measureText(item.name).width + 16 preArc += item.num / total posList.push({x, y}) // 折线 if(posList.length > 1) { ctx.beginPath() ctx.moveTo(posList[index - 1].x + w / 2, posList[index - 1].y) ctx.lineTo(x + w / 2, y) let grd = ctx.createLinearGradient(posList[index - 1].x + w / 2, posList[index - 1].y, x + w / 2, y); grd.addColorStop(0,defaultColor[index - 1]) grd.addColorStop(1,defaultColor[index]) ctx.strokeStyle = grd ctx.lineWidth = 3 ctx.setLineDash([]) ctx.lineCap = "round" ctx.lineJoin = "round" ctx.stroke() ctx.closePath() } }) } }, oprNode(type) { this.outerVisible = false this.innerVisible = true this.type = type }, submit() { if(this.type == 1) { this.add(this.datalist) } else { this.edit(this.datalist) } }, add(datalist) { datalist.map((item, index) => { if(item.id == this.current.id) { if(!item.children) { item.children = [{ num: this.num, id: this.id, name: this.name, level: item.level + 1 }] } else { item.children.push({ num: this.num, id: this.id, name: this.name, level: item.level + 1 }) } const chart3 = document.getElementById("chart3") let ctx = chart3.getContext('2d') ctx.clearRect(0,0,800,1000) this.drawTree(this.datalist, {}, ctx) return } if(item.children) { this.add(item.children) } }) }, edit(datalist) { datalist.map((item, index) => { if(item.id == this.current.id) { item.num = this.num item.id = this.id item.name = this.name const chart3 = document.getElementById("chart3") let ctx = chart3.getContext('2d') ctx.clearRect(0,0,800,1000) this.drawTree(this.datalist, {}, ctx) return } if(item.children) { this.edit(item.children) } }) }, initTree() { const chart3 = document.getElementById("chart3") let ctx = chart3.getContext('2d') let id = '' let curX let curY let move = (e) => { ctx.clearRect(0,0,800,1000) this.drawTree(this.datalist, {}, ctx, { id, curX: e.clientX - chart3.getBoundingClientRect().left, curY: e.clientY - chart3.getBoundingClientRect().top }) } chart3.addEventListener('mousedown', (e) => { curX = e.clientX - chart3.getBoundingClientRect().left curY = e.clientY - chart3.getBoundingClientRect().top this.rectList.forEach(item => { // 点击在矩形上 if(curX >= item.x1 && curX <= item.x2 && curY >= item.y1 && curY <= item.y2) { id = item.id this.current = item if(e.button ==2){ this.outerVisible = true return } chart3.addEventListener('mousemove', move, false) } }) }, false) chart3.addEventListener('mouseup', (e) => { chart3.removeEventListener('mousemove', move) }, false) this.drawTree(this.datalist, {}, ctx) }, deleteNode(data, id) { console.log('data',data) this.outerVisible = false if(id == 1) return let flag = 0 data.map((item, index) => { if(item.children) item.children.map((t, index) => { if(t.id == id) { flag = 1 item.children.splice(index ,1) const chart3 = document.getElementById("chart3") let ctx = chart3.getContext('2d') ctx.clearRect(0,0,800,1000) this.drawTree(this.datalist, {}, ctx) return } }) if(item.children && flag == 0) { this.deleteNode(item.children, id) } }) }, drawTree(datalist, p, ctx, move) { console.log('datalist',datalist) const defaultColor = ["#ff8080", "#b6ffea", "#fb0091", "#fddede", "#a0855b", "#b18ea6", "#ffc5a1", "#08ffc8"] if(!p.x) this.rectList = [] datalist.map((item, index) => { if(move && move.id == item.id) { item.curX = move.curX item.curY = move.curY } if(p.children) { item.x1 = p.x1 + (p.x2 - p.x1) * index / p.children.length item.x2 = p.x1 + (p.x2 - p.x1) * (index + 1) / p.children.length item.x = item.x1 + (item.x2 - item.x1) / 2 item.y = item.level * 100 } else { item.x = 375 item.y = item.level * 100 item.x1 = 0 item.x2 = 800 } ctx.fillStyle = defaultColor[item.level] if(item.curX) { ctx.fillRect(item.curX, item.curY, 50, 25) } else { ctx.fillRect(item.x, item.y, 50, 25) } ctx.fillStyle = "#000" ctx.font = "20px serif"; ctx.textAlign = "center" let ix = item.curX ? item.curX : item.x let iy = item.curY ? item.curY : item.y let px = p.curX ? p.curX : p.x let py = p.curY ? p.curY : p.y ctx.fillText(`${item.name}:${item.num}`, ix + 25, iy + 20); ctx.beginPath() ctx.moveTo(ix + 25, iy) ctx.lineTo(px + 25, py + 25) ctx.stroke() ctx.closePath() if(item.children) { this.drawTree(item.children, item, ctx, move) } let obj = { id: item.id, x1: item.x, x2: item.x + 50, y1: item.y, y2: item.y + 25 } if(item.curX) { obj.x1 = item.curX obj.x2 = item.curX + 50 obj.y1 = item.curY obj.y2 = item.curY + 25 } this.rectList.push(obj) }) } } } </script> <style scoped> #chart1 { border: 1px solid #eee; } #chart2 { border: 1px solid #eee; } #chart3 { border: 1px solid #eee; } </style> 复制代码