canvas详解(1)-原理
canvas详解(1)-原理原理canvas本身并不具备绘画能力,它本身只是一个画布,是一个容器。绘图能力是基于html5的getContext("2d")返回的CanvasRenderingContext2D对象来完成的。const canvas = document.getElementById("payAbilityLoginTree");//获取ca
canvas详解(1)-原理
原理
canvas
本身并不具备绘画能力,它本身只是一个画布,是一个容器。绘图能力是基于html5
的getContext("2d")
返回的CanvasRenderingContext2D
对象来完成的。
const canvas = document.getElementById("payAbilityLoginTree");//获取canvas dom对象
const ctx = canvas.getContext('2d'); //获取绘图对象
<canvas id="payAbilityLoginTree" width="1000" height="800"></canvas>
需注意,必须指定canvas画布的大小,canvas本身是受自身的width、height属性来决定是否重绘的,而不是style属性的宽高,只是默认情况下,canvas的宽高跟style属性的宽高一致。
canvas
是一个二维网络,以画布左上角(0,0)为坐标原点,x轴向右延伸,y轴向下延伸。所以canvas
画布中的坐标全为正数,没有负数。
CanvasRenderingContext2D
对象提供了很多绘图方法,我们可以通过这些方法来绘制任何你需要的图形,我就不一一介绍了,大家可以参考HTML5 参考手册。但有几个常用的方法我着重强调一下。
strokeRect(x,y,width,height)
:绘制无填充矩形arc(x,y,r,sAngle,eAngle,counterclockwise)
:绘制圆fillStyle=color|gradient|pattern
:填充绘画的颜色、渐变或模式moveTo(x,y)
:把路径移动到画布中的指定点,不创建线条lineTo(x,y)
:添加一个新点stroke()
:绘制已定义的路径,即线条绘制fill()
:填充当前绘图(主要是颜色填充)drawImage(img,x,y,width,height)
:绘制图像scale(scalewidth,scaleheight)
:缩放当前绘图save()
:保存当前环境的状态。该方法的使用是将之前绘图的属性进行缓存,使之后的绘图能够独立出来
restore()
:返回之前保存过的路径状态和属性。即是消除
save()
的影响,让绘图回到原先的状态。beginPath()
: 开始一条新的路径,该方法将消除方法调用前的绘图影响。通常我们在一个新的绘图前都会使用该方法,目的是杜绝之后的
stroke
或fill
填充当前图形。closePath()
:创建当前点到开始点的路径,即闭合路径,常用在三角形的第3边绘制。isPointInPath(x,y)
:判断指定的点是否在当前路径上。clearRect(x,y,width,height)
:清除画布指定区域的绘图。该方法很重要,在canvas中只有该方法可以清除绘图,在重绘时常常用到。
实例
说了这么多,不如来点实际的,下面讲解一个自定义图形的画法。
1.数据准备
const data = {
desc: "自定义图形", //第一行描述文字
count: 2321341, //第二行描述文字
percent: 60, //圆环中心显示数字
pieData: [{ desc: '圆环', name: "A", value: "0.6" }, { desc: '圆环',name: "B", value: "0.4" }],//环图数据
x: 250, //图形中心点在画布的x轴坐标
y: 250 //图形中心点在画布Y轴坐标
}
constructor(props) {
super(props);
this.state = {
color: ['#bd94ff', '#48eaa7'],
ratio: 1,
tipNode: null,
mousePosition: {
x: 0,
y: 0
}
}
}
static defaultProps = {
canvasId: 'customCanvas',
data: data,
width: 500,
height: 500,
nodeWidth: 300,
nodeHeight: 150,
radius: 65, //nodeWidth*0.25-10
innerRadius: 40, //nodeWidth*0.25-35
}
这里用了react的相关知识,需要有一点react能力的同学才能理解。不懂得同学可以只关注
this.state
和defaultProps
,之后用到的this.props
即是defaultProps
。当然这样理解并不准确,但是在我这个静态的例子中可以暂时这么理解。
2.html准备
<canvas id={this.props.canvasId} width={this.props.width} height={this.props.height}></canvas>
3.初始化画布
const canvas = document.getElementById(this.props.canvasId);
const ctx = canvas.getContext('2d');
let scale = 1;
//let ratio = this.getPixelRatio(ctx); //屏幕像素比,之后讲模糊处理的时候会用到
//let scale = ratio;
//this.setState({ ratio: 1 / ratio });
this.drawNode(ctx, data, data.x, data.y, this.props.nodeWidth, this.props.nodeHeight,
this.props.radius, this.props.innerRadius, this.state.color, scale, null, false, this);
4.画整个图形
/**
* 绘制一个节点
* ctx:上下文
* node:节点数据{desc: string,count:number,percent:number,pieData:[]}
* x:节点中心横坐标
* y:节点中心纵坐标
* width:节点容器宽度
* height:节点容器高度
* radius:外环半径
* innerRadius:内环半径
* scale:缩放比例
* mousePoint:鼠标对象
* isRingRange:鼠标是否在圆环上
* treePage:当前页面对象
*/
drawNode(ctx, node, x, y, width = 300, height = 150,
radius, innerRadius, pieColor, scale = 1, mousePoint = null, isRingRange = false, treePage) {
//绘制节点容器,一个矩形框
ctx.strokeStyle = '#E9E9E9';
ctx.lineWidth = 1.5 * scale;
width = width * scale;
height = height * scale;
ctx.strokeRect(x - width / 2, y - height / 2, width, height);
//绘制第一行显示文本
let fontSize = 12 * scale;
ctx.font = fontSize + "px Arial";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "#9c9c9c";
let textX1 = x - width * 0.25;
let textY1 = y - 14 * scale;
ctx.fillText(node.desc, textX1, textY1);
//绘制第二行显示文本
ctx.font = fontSize + "px Arial";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "#7E317E";
let textX2 = x - width * 0.25;
let textY2 = y + 14 * scale;
ctx.fillText(node.count, textX2, textY2);
//绘制圆环
this.drawRingPie(ctx, node, x + width * 0.25, y, radius * scale, innerRadius * scale,
pieColor, scale, mousePoint, isRingRange, treePage);
}
整个图形被平分为了左右2个部分,左边部分显示文字,右边部分显示圆环,圆环圆心位于右边部分的正中心。图形数据中的x,y坐标表示矩形的中心点。
5.绘制饼图
/**
* 画环形饼图
* ctx:上下文
* node:节点信息
* x:饼图圆心横坐标
* y:饼图圆心纵坐标
* radius:外层圆半径
* innerRadius:内层圆半径
* color:饼图颜色数组
* scale:缩放比例
* mousePoint:鼠标对象
* isRingRange:鼠标是否在圆环上
* treePage:当前页面对象
*/
drawRingPie(ctx, node, x, y, radius, innerRadius, color, scale = 1, mousePoint = null, isRingRange, treePage) {
//画外层圆环
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
let startRadian = 0, endRadian = 0;
for (let i = 0; i < node.pieData.length; i++) {
ctx.beginPath();
//起始点移动到圆心
ctx.moveTo(0, 0);
endRadian += node.pieData[i].value * Math.PI * 2;
//以圆心为起点,0度开始绘制一个圆
ctx.arc(0, 0, radius, startRadian, endRadian, false);
ctx.closePath();
// 填充颜色
ctx.fillStyle = color[i];
ctx.fill();
startRadian = endRadian;
/*
if (mousePoint && ctx.isPointInPath(mousePoint.x, mousePoint.y) && isRingRange) {//鼠标点击了并且在该部分圆环上
ctx.clearRect(-radius, -radius, 2 * radius, 2 * radius);
this.drawDynamicPie(ctx, node, radius, color, i);//重绘圆
let tipNode = {
desc: node.desc,
name: node.pieData[i].name,
value: this.floatMul(node.pieData[i].value, 100) + "%"
}
treePage.setState({ tipNode: tipNode, mousePosition: { x: mousePoint.clientX, y: mousePoint.clientY } });
}
*/
}
/**
* 画内层圆
*/
ctx.beginPath();
//起始点移动到圆心
ctx.moveTo(0, 0);
//以圆心为起点,0度开始绘制一个圆
ctx.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
ctx.closePath();
// 填充颜色
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = "#ffffff";
ctx.stroke();
ctx.fill();
ctx.restore();
/**
* 环心填充文字
*/
let fontSize = 12 * scale;
ctx.font = fontSize + "px Arial";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "#000";
ctx.fillText(node.percent + "%", x, y);
}
canvas并不具备绘画饼图的能力,它只是给了我们画弧的方法,所谓的圆无非是一个
Math.PI * 2
的弧,再被我们填充颜色,就成了一个完整的圆。而环形无非是同心的大圆套小圆,将小圆的颜色填充为白色,那么看上去就只剩下大圆最外层的部分,就成为了环形。外层圆因为又是多个弧形组成的,所以就构成了我们所需要环形图。
完整代码
可参看我的github,https://github.com/windSandEye/custom-tree
import React, { Component } from 'react';
import lodash from 'lodash';
const data = {
desc: "自定义图形", //第一行描述文字
count: 2321341, //第二行描述文字
percent: 60, //圆环中心显示数字
pieData: [{ desc: '圆环', name: "A", value: "0.6" }, { desc: '圆环',name: "B", value: "0.4" }],//环图数据
x: 250, //图形中心点在画布的x轴坐标
y: 250 //图形中心点在画布Y轴坐标
}
class CustomCanvas extends Component {
constructor(props) {
super(props);
this.state = {
color: ['#bd94ff', '#48eaa7'],
ratio: 1,
tipNode: null,
mousePosition: {
x: 0,
y: 0
}
}
}
static defaultProps = {
canvasId: 'customCanvas',
data: data,
width: 500,
height: 500,
nodeWidth: 300,
nodeHeight: 150,
radius: 65, //nodeWidth*0.25-10
innerRadius: 40, //nodeWidth*0.25-35
}
componentDidMount() {
this.redrawTree(this.props.data);
}
componentWillReceiveProps(nextProps) {
if(!lodash.isEqual(nextProps.data,this.props.data)){
this.redrawTree(nextProps.data);
}
}
//重绘
redrawTree(data) {
const canvas = document.getElementById(this.props.canvasId);
const ctx = canvas.getContext('2d');
let ratio = this.getPixelRatio(ctx);
let scale = ratio;
this.setState({ ratio: 1 / ratio });
this.drawNode(ctx, data, data.x, data.y, this.props.nodeWidth, this.props.nodeHeight,
this.props.radius, this.props.innerRadius, this.state.color, scale, null, false, this);
canvas.addEventListener('mousemove', (e) => {
let eventX = e.clientX * ratio - canvas.getBoundingClientRect().left;
let eventY = e.clientY * ratio - canvas.getBoundingClientRect().top;
let mousePoint = { x: eventX, y: eventY, clientX: e.clientX, clientY: e.clientY };
let isRingRange = this.isRingPostion(mousePoint, data, this.props.nodeWidth,
this.props.innerRadius, this.props.radius, scale);
ctx.clearRect(0, 0, canvas.width, canvas.height);
this.drawNode(ctx, data, data.x, data.y, this.props.nodeWidth, this.props.nodeHeight,
this.props.radius, this.props.innerRadius, this.state.color, scale, mousePoint, isRingRange, this);
if (!isRingRange) {
this.setState({ tipNode: null });
}
}, false)
//鼠标滚动时关闭提示
canvas.addEventListener('wheel', (e) => {
this.setState({ tipNode: null });
})
}
/**
* 绘制一个节点
* ctx:上下文
* node:节点数据{desc: string,count:number,percent:number,pieData:[]}
* x:节点中心横坐标
* y:节点中心纵坐标
* width:节点容器宽度
* height:节点容器高度
* radius:外环半径
* innerRadius:内环半径
* scale:缩放比例
* mousePoint:鼠标对象
* isRingRange:鼠标是否在圆环上
* treePage:当前页面对象
*/
drawNode(ctx, node, x, y, width = 300, height = 150,
radius, innerRadius, pieColor, scale = 1, mousePoint = null, isRingRange = false, treePage) {
//绘制节点容器,一个矩形框
ctx.strokeStyle = '#E9E9E9';
ctx.lineWidth = 1.5 * scale;
width = width * scale;
height = height * scale;
ctx.strokeRect(x - width / 2, y - height / 2, width, height);
//绘制第一行显示文本
let fontSize = 12 * scale;
ctx.font = fontSize + "px Arial";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "#9c9c9c";
let textX1 = x - width * 0.25;
let textY1 = y - 14 * scale;
ctx.fillText(node.desc, textX1, textY1);
//绘制第二行显示文本
ctx.font = fontSize + "px Arial";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "#7E317E";
let textX2 = x - width * 0.25;
let textY2 = y + 14 * scale;
ctx.fillText(node.count, textX2, textY2);
//绘制圆环
this.drawRingPie(ctx, node, x + width * 0.25, y, radius * scale, innerRadius * scale,
pieColor, scale, mousePoint, isRingRange, treePage);
}
/**
* 画环形饼图
* ctx:上下文
* node:节点信息
* x:饼图圆心横坐标
* y:饼图圆心纵坐标
* radius:外层圆半径
* innerRadius:内层圆半径
* color:饼图颜色数组
* scale:缩放比例
* mousePoint:鼠标对象
* isRingRange:鼠标是否在圆环上
* treePage:当前页面对象
*/
drawRingPie(ctx, node, x, y, radius, innerRadius, color, scale = 1, mousePoint = null, isRingRange, treePage) {
//画外层圆环
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
let startRadian = 0, endRadian = 0;
for (let i = 0; i < node.pieData.length; i++) {
ctx.beginPath();
//起始点移动到圆心
ctx.moveTo(0, 0);
endRadian += node.pieData[i].value * Math.PI * 2;
//以圆心为起点,0度开始绘制一个圆
ctx.arc(0, 0, radius, startRadian, endRadian, false);
ctx.closePath();
// 填充颜色
ctx.fillStyle = color[i];
ctx.fill();
startRadian = endRadian;
if (mousePoint && ctx.isPointInPath(mousePoint.x, mousePoint.y) && isRingRange) {//鼠标点击了并且在该部分圆环上
ctx.clearRect(-radius, -radius, 2 * radius, 2 * radius);
this.drawDynamicPie(ctx, node, radius, color, i);//重绘圆
let tipNode = {
desc: node.desc,
name: node.pieData[i].name,
value: this.floatMul(node.pieData[i].value, 100) + "%"
}
treePage.setState({ tipNode: tipNode, mousePosition: { x: mousePoint.clientX, y: mousePoint.clientY } });
}
}
/**
* 画内层圆
*/
ctx.beginPath();
//起始点移动到圆心
ctx.moveTo(0, 0);
//以圆心为起点,0度开始绘制一个圆
ctx.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
ctx.closePath();
// 填充颜色
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = "#ffffff";
ctx.stroke();
ctx.fill();
ctx.restore();
/**
* 环心填充文字
*/
let fontSize = 12 * scale;
ctx.font = fontSize + "px Arial";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "#000";
ctx.fillText(node.percent + "%", x, y);
}
//绘制动态圆
drawDynamicPie(ctx, node, radius, color, index) {
let startRadian = 0, endRadian = 0;
for (let i = 0; i < node.pieData.length; i++) {
ctx.beginPath();
//起始点移动到圆心
ctx.moveTo(0, 0);
endRadian += node.pieData[i].value * Math.PI * 2;
//以圆心为起点,0度开始绘制一个圆
if (index == i) {
ctx.arc(0, 0, radius + 5, startRadian, endRadian, false);
} else {
ctx.arc(0, 0, radius, startRadian, endRadian, false);
}
ctx.closePath();
// 填充颜色
ctx.fillStyle = color[i];
ctx.fill();
startRadian = endRadian;
}
}
//点击位置是否在圆环上(数据为列表)
isRingPostion(mousePoint, node, nodeWidth, innerRadius, radius, scale) {
if (!mousePoint) {
return false;
}
nodeWidth = nodeWidth * scale;
innerRadius = innerRadius * scale;
radius = radius * scale;
let eventX = mousePoint.x;
let eventY = mousePoint.y;
//点击位置到圆心的距离,勾股定理计算
let cricleX = node.x + nodeWidth / 4;//圆心x坐标
let cricleY = node.y;
let distanceFromCenter = Math.sqrt(Math.pow(cricleX - eventX, 2)
+ Math.pow(cricleY - eventY, 2))
//是否在圆环上
if (distanceFromCenter > innerRadius && distanceFromCenter < radius) {
return true;
}
return false;
}
//浮点数乘法
floatMul(a, b) {
let m = 0, n = 0, //记录a,b的小数位数
d = a + "", //字符串化
e = b + "";
try {
m = d.split(".")[1].length;
} catch (error) {
console.log(error)
}
try {
n = e.split(".")[1].length;
} catch (error) {
console.log(error)
}
let maxInt = Math.pow(10, m + n); //将数字转换为整数的最大倍数
return Number(d.replace(".", "")) * Number(e.replace(".", "")) / maxInt;
}
//设备像素比
getPixelRatio(context) {
var backingStore = context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
return (window.devicePixelRatio || 1) / backingStore;
};
//获取提示的定位位置
getTipPosition() {
let tipDiv = document.getElementById(`${this.props.treeId}Tip`);
let mousePosition = this.state.mousePosition;
let top1 = mousePosition.y + 12;
let left = mousePosition.x + 12;
if (tipDiv) {
if (mousePosition.x + tipDiv.offsetWidth > window.innerWidth) {
left = mousePosition.x - 12 - tipDiv.offsetWidth;
}
if (mousePosition.y + tipDiv.offsetHeight > window.innerHeight) {
top1 = mousePosition.y - 12 - tipDiv.offsetHeight;
}
}
return { top: top1, left: left }
}
render() {
let position = this.getTipPosition();
let tipClass = {
position: 'fixed',
zIndex: 999,
visibility: this.state.tipNode ? 'visible' : 'hidden',
backgroundColor: '#826d6d',
top: position.top,
left: position.left,
padding: '15px',
color: '#fff',
borderRadius: '5px',
textAlign: 'left'
}
return (
<div style={{ padding: 100 }}>
<canvas id={this.props.canvasId} width={this.props.width} height={this.props.height} style={{zoom:this.state.ratio}}></canvas>
<div style={tipClass} id={`${this.props.treeId}Tip`}>
<div>{this.state.tipNode ? this.state.tipNode.desc : null}</div>
<div>{this.state.tipNode ? this.state.tipNode.name : null} : {this.state.tipNode ? this.state.tipNode.value : null}</div>
</div>
</div>
);
}
}
export default CustomCanvas;
注意:这份完整的代码包含了事件处理,和模糊处理,这两点将会在之后着重讲解。
更多推荐
所有评论(0)