上一篇讲到了把折线图和拐点都画好了,这一篇我们来更进一步,绘制出拐点处的更多信息,价格信息和拼团信息。
效果如下图所示。(不包括下面的日期和刻度线)
一、普通情况下的价格和拼团人数的绘制
很容易就看出,图上第二个拐点不仅圆圈更大,而且价格和拼团人数的样式明显不同。我们先考虑更简单的情况。
首先,思考一个问题,拐点与这些信息的位置有什么关系?根据图得知,价格位于拐点上方不远处,拼团人数则位于价格的上方不远处。因此价格和拼团人数的位置应该与拐点坐标有一定的关系。所以我绘制这两个信息的时候,会把拐点信息先拿到。
然后,根据拐点与这些信息的位置关系,计算出绘制文字的正确位置。虽然在我们习惯的坐标系中,越往上的位置,y坐标更大,但是在canvas中,越往上,y是越小的。所以价格文字的y坐标应该是拐点y坐标减去某个合适的值来得到,拼团人数的y坐标是是拐点y坐标减去一个更大的值。价格和拼团人数的x坐标就暂时为拐点的x坐标。
现在,我们让价格和拼团文字与拐点位置水平居中。当前,我们的文字就写在拐点圆心处,我们很容易就能想到,让文字往左平移文字本身宽度的一半,就可以实现水平居中对齐。那么,怎么知道文字的宽度呢?我们可以在设置好文字的字体样式后,调用context.measureText(text)方法来获取(参数是要测量的文本,返回值是一个对象,里面有一个width属性,值为数字,单位是px)
综上所述,它们的x坐标为为拐点的x坐标减去文字内容宽度的一半,y坐标为拐点的y坐标减去一个合适的常量。做好以上三步后,价格和拼团人数就能正常显示啦。
贴一下我写的相关代码:(获取文字宽度的代码我封装到函数里去了,因为返回值是一个对象,在我封装的函数里,返回的是这个对象里的width属性,这才是我们需要的)
/**
* 计算文字的宽度,返回计算结果
* @param {*} context 上下文对象
* @param {*} text 要计算的文字
*/
function getTextWidth(context, text) {
return context.measureText(text).width;
}
/**
* 绘制普通价格
* @param {*} context 上下文对象
* @param {*} center 拐点信息
*/
function drawNormalPrice(context, center) {
// 写价格
context.beginPath();
context.setFontSize(12);
context.fillStyle = "#333333";
// 写出价格,注意x的偏移量是文字宽度的一半
context.fillText('¥' + center.value, center.x - getTextWidth(context, '¥' + center.value) / 2, center.y - 30 * _this.data.toPx);
context.draw(true);
}
/**
* 绘制普通拼团人数
* @param {*} context 上下文对象
* @param {*} center 拐点信息
*/
function drawNormalTeamNumber(context, center) {
context.beginPath();
context.setFontSize(12);
context.fillStyle = "#999999";
context.fillText('(' + center.teammateNumber + '人团)', center.x - getTextWidth(context, '(' + center.teammateNumber + '人团)') / 2, center.y - 64 * _this.data.toPx);
context.draw(true);
}
二、特殊情况下的价格和拼团人数的绘制
第二个点是特殊点,它的价格和拼团人数有哪些特殊之处呢?首先是位置,它的价格写到了拐点下方,拼团人数写到了拐点右方。然后是字体的颜色,价格变成了白色,并且带有渐变色填充的背景,拼团人数是橙色的。还有字体的大小也略有不同。
我们先来看看,特殊拐点处的拼团价格怎么绘制。
首先,先绘制出这样的一个胶囊形轮廓。流程说起来很简单——“画一条向右的直线,然后画右半边圆弧,再然后画一条向左的直线,最后画一条左半边圆弧”。
但我个人感觉这不太容易,要注意计算,如果算错了一个点的坐标,整个效果就成了车祸现场。我最后得出来的方法是:
①确定好胶囊的宽度w和半圆的半径r。
②找到一个合适的点,调用context.moveTo,移动到这里;
③调用context.lineTo,画条直线到第二个点,第二个点与第一个点的关系是:y坐标不变,x坐标加上胶囊的宽度w;
④找到圆心,调用context.arc来绘制右半边圆。圆心的坐标是:x坐标等于第二个点的x坐标,y坐标等于第二个点的y坐标加上圆弧半径r。注意圆弧的起点和终点,由于我们画的是右半边的圆弧,要记得右边的方向的弧度是0或2π,所以上方的弧度是1.5π,下方的弧度是2.5π,所以我们要绘制的圆弧是顺时针的1.5π到2.5π;(这里我折腾了好久的)
弧度见下图:
⑤调用context.lineTo方法,画直线到第三个点,第三个点的位置是:x坐标与第一个点的x坐标相同,y坐标为第一个点的x坐标加上两边的圆弧半径r;
⑥找到圆心,调用context.arc来绘制右半边圆。圆心的坐标是:x坐标等于第三个点的x坐标,y坐标等于之前的那个圆弧的圆心y坐标。注意圆弧的起点和终点,由于我们画的是左半边的圆弧,所以我们要绘制的圆弧是顺时针的0.5π到1.5π;
做好了以上六步,恭喜你,胶囊形就绘制完毕了。
现在,我们要弄一个渐变色来填充胶囊。渐变色在canvas中如何绘制呢?渐变色也是一种填充,所以是先设置出一种渐变,再作为路径的填充风格。使用context.createLinearGradient()创建渐变对象,参数是渐变的起止位置。然后使用渐变对象的addColorStop(颜色,位置)来设置渐变的过程有哪些颜色,位置的值位于0到1之间,0表示起点,1表示终点。之后,就把context的fillStyle设为这个渐变对象,然后调用context.fill和context.draw(true)即可。
最后,注意一个细节问题,胶囊形的宽度定为多少合适?位置在哪里合适?显然是比文字宽度稍微大一点,并且与白色的文字水平居中对齐合适。要怎么做到这些呢?
第一步,来获取白色文字的宽度。让我非常惊喜的是, 这个API可以告诉我文本的宽度——context.measureText ,参数是要计算宽度的文本,返回值是一个对象,里面有weight属性,数字类型的。注意,要先设置好文字的样式和大小以后再来计算哦。因为样式和文本大小决定了一个字有多宽,所以肯定会影响到文本写出来的宽度。我上次就因为这个,导致做出来的结果总是不如预期效果,难受了好久。
第二步,准备一个常量来存储胶囊宽度。计算好了文字宽度后,胶囊的宽度也就呼之欲出了,我们再次基础上加上一个比较小的值作为胶囊宽度,把它存入常量中。然后把这个常量用到上面那个画胶囊形的公式里去。这样胶囊的宽度就是正好的。
第三步,确定白色文字和胶囊的位置。这个位置肯定与特殊点x坐标有关系。假如白色文字的左边缘x坐标等于特殊点的x坐标,那么文字显然是偏右的,要向左移动(把特殊点的x坐标减去)文字宽度的一半才可以。同理,胶囊形的x坐标也是把特殊点的x坐标减去胶囊宽度的一半。它们的位置显然比特殊点更高,所以y应该是更小,适当减少特殊点坐标y的值,赋值给它们就可以了。把胶囊形底边的y坐标也代入前面画胶囊的公式里去。
做完这三步以后,恭喜你,你已经把拼团人数和价格都画完了。
最后贴一下这一步相关的代码:
/**
* 绘制普通价格
* @param {*} context 上下文对象
* @param {*} center 拐点信息
*/
function drawNormalPrice(context, center) {
// 写价格
context.beginPath();
context.setFontSize(12);
context.fillStyle = "#333333";
// 写出价格,注意x的偏移量是文字宽度的一半
context.fillText('¥' + center.value, center.x - getTextWidth(context, '¥' + center.value) / 2, center.y - 30 * _this.data.toPx);
context.draw(true);
}
//
/**
* 绘制特殊情况的拼团价格
* @param {*} context 上下文对象
* @param {*} center 拐点信息
*/
function drawSpecialPrice(context, center) {
context.beginPath();
context.setFontSize(30 * _this.data.toPx);
// 计算白字的宽度
const textWidth = getTextWidth(context, '¥' + center.value);
// 先画胶囊形状
// 根据白字的宽度,来计算胶囊的宽度,才能确定胶囊的位置和宽度
const capsultWidth = textWidth + 10 * _this.data.toPx;
context.moveTo(center.x - capsultWidth / 2, center.y + 42 * _this.data.toPx);
context.lineTo(center.x + capsultWidth / 2, center.y + 42 * _this.data.toPx);
context.arc(center.x + capsultWidth / 2, center.y + 70 * _this.data.toPx, 28 * _this.data.toPx, 1.5 * Math.PI, 2.5 * Math.PI, false);
context.lineTo(center.x - capsultWidth / 2, center.y + 98 * _this.data.toPx);
context.arc(center.x - capsultWidth / 2, center.y + 70 * _this.data.toPx, 28 * _this.data.toPx, 0.5 * Math.PI, 1.5 * Math.PI, false);
context.closePath();
// 准备一个渐变
const grd = context.createLinearGradient(center.x - (capsultWidth / 2 - 28 * _this.data.toPx), 0, center.x + (capsultWidth / 2 + 28 * _this.data.toPx), 0)
grd.addColorStop(0, '#FE7301');
grd.addColorStop(1, '#FF4800');
context.fillStyle = grd;
// 胶囊填充的是渐变色
context.fill();
context.draw(true);
// 写白色的字
context.beginPath();
context.fillStyle = "#ffffff";
context.fillText('¥' + center.value, center.x - textWidth / 2, center.y + 82 * _this.data.toPx);
context.draw(true);
}
/**
* 绘制普通拼团人数
* @param {*} context 上下文对象
* @param {*} center 拐点信息
*/
function drawNormalTeamNumber(context, center) {
context.beginPath();
context.setFontSize(12);
context.fillStyle = "#999999";
context.fillText('(' + center.teammateNumber + '人团)', center.x - getTextWidth(context, '(' + center.teammateNumber + '人团)') / 2, center.y - 64 * _this.data.toPx);
context.draw(true);
}
/**
* 绘制特殊情况的拼团人数
* @param {*} context 上下文对象
* @param {*} center 拐点信息
* @param {*} color 颜色
*/
function drawSpecialTeamNumber(context, center, color) {
context.beginPath();
context.setFontSize(13);
context.fillStyle = color;
context.fillText('(' + center.teammateNumber + '人团)', center.x + 28, center.y + 5);
context.draw(true);
}