canvas绘图:圆环形日期选择器

需要做一个canvas的绘图,四个圆环组成的一个日期选择器,点击选择器上的日期后,跳出一个js窗口显示日期。小E不会做,求大神指导,附照片和要求,谢谢!图片描述

阅读 11.1k
5 个回答

你可以参考一下canvas时钟的实现方法。可以有些启发吧。基本思路是利用rotate,可以简单一些。自己算sin也行,就是稍微麻烦点。
这个东西倒是挺有创意的,有空我也弄一个,嘿嘿

实现了一个:几个问题,字没有正过来显示,如果要正过来显示得用sin去算;颜色搞了很久,最后用hsl搞了;点击不去做了,不想算鼠标位置,如果是我的话,我会考虑用svg实现事件

clipboard.png

var myCanvas = document.getElementById('myCanvas');

myCanvas.width = 500;
myCanvas.height = 500;
var r0 = 240;
var r1 = 190;
var r2 = 150;
var r3 = 120;


var ctx = myCanvas.getContext('2d');

//generate color
var color = [];
for(var i=1;i<=24;i++){
    var p = 30+(70/24)*i;
    color.push('hsl(170,' + p + '%,' + p + '%)');
}


ctx.translate(250,250);

drawSector(4,r0,[2011,2012,2009,2010]);
drawSector(12,r1,[1,2,3,4,5,6,7,8,9,10,11,12]);
drawSector(7,r2,['Mon','Tue','Wed','Thu','Fri','Sat','Sun']);
drawSector(24,r3,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]);

//draw white space center
ctx.save();
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(0,0,80,0,2*Math.PI);
ctx.fill()

ctx.restore();


function drawSector (split,r,text) {
    // body...
    ctx.save();


    ctx.strokeStyle = '#fff';
    for(i=1;i<=split;i++){
        ctx.fillStyle = color[i];

        ctx.beginPath();
        ctx.moveTo(0,0);
        ctx.lineTo(r,0);
        ctx.arc(0,0,r,0,2*Math.PI/split);
        ctx.fill();
        ctx.stroke();

        if(text){
            ctx.rotate(Math.PI/split);
            ctx.save();
            ctx.fillStyle = "#000";
            ctx.fillText(text[i-1],r-25,0);
            ctx.restore();
            ctx.rotate(Math.PI/split);
        }
        else{
            ctx.rotate(2*Math.PI/split);
        }

    }

    ctx.restore();
}

既然canvas的答案都有了正好最近在学习svg我就来写一个svg的答案吧。

SVG 如何画任意角度的圆弧线

将这个问题分解出来就是我们要画一个一个的弧块,所以第一步我们需要了解"如何使用SVG画弧线"。关于 SVG 的Path参数了解大家可以去参考一下 张鑫旭老师的博文:《深度掌握SVG路径path的贝塞尔曲线指令》。不过我们需要的 arc 命令并没有给出,这里我就稍作说明一下:

使用

命令 命令参数 参数说明
A rx 弧线所在椭圆的长轴半径
ry 弧线所在椭圆的短轴半径
x-axis-rotation 弧线与 x 轴的旋转角度
large-arc-flag 两个值:0为小角度弧线,1为大角度弧线
sweep-flag 两个值:0为逆时针,1为顺时针
x 弧线终点的 x 坐标
y 弧线终点的 y 坐标

也就是说画一段弧线你必须给定:

  1. 弧线的起始和终点坐标
  2. 弧线所在椭圆的长短轴半径
  3. 弧线与 x 轴的夹角(即弧线所在椭圆与 x 轴的夹角)
  4. 是大角度弧线还是小角度弧线
  5. 圆弧是顺时针还是逆时针的

总共 7 个参数,怪复杂的。其它参数都比较好理解,就是large-arc-flag这个参数似乎不太明白的样子,这里我引用一张 MDN 文档 中的图片给大家做一下参考:
SVGArcs_Flags.png

实在是不太明白也没有关系,反正就 4 种情况,大家试试也就试出来了。针对这个问题的情况下,因为我们画的是圆弧,所以椭圆直接变成了圆,2 - 5 这 4 条参数都能解决了,剩下的是我们只需要知道弧的起点和终点就好了。这个根据圆弧的角度我们也是可以利用公式计算出来的。这里我画了一个示意图给大家参考一下:
终点计算

也就是说假设已知圆心坐标和圆心半径,逆时针方向角度为正值。则圆弧α的起点 A 和终点 B 的坐标我们都能知道了。所以控制一段圆弧通用的指令应该是:

M Xo, Yo-r A r r 0 [1|0]** 0 Xo-r*sinα, Yo-r*cosα
注: ** 当弧角度 小于180° 时使用小角度弧线,当弧角度 大于180° 时使用大角度弧线

举个例子,假设圆心坐标为 (100,100),半径为 50,则我们画一个 30° 角的圆弧为:
http://jsfiddle.net/hvaomwe0/

我们可以将其化作一个 JS 函数以便动态创建:
http://jsfiddle.net/wwrxwLuc/2/

如何处理弧线标注位置

SVG本身是有marker用来指定其他元素用来做标注的,不过用起来稍微麻烦最终还是需要用到text标签,所以我就直接用text标签来做了。

text标签需要指定标签左下角的 (x,y) 坐标来确定标签的位置,为了达到好看的效果,通过计算弧中点的坐标将其旋转到其切线方向会达到很好看的效果。弧重点的坐标利用上一步中求终点的方法可以非常简单拿到。而旋转到切线方向其实就是将文字旋转弧线的角度。

text也是支持 transform属性 的,和平常在 CSS 中一样也是支持 rotate 等一些常用的变换的。但这里需要注意的是,默认的rotate并不是以文字的左下角做旋转,所以我们要在旋转角度后面定义旋转中心坐标,也就是 transform = "rotate(α x y)"

这样做完之后有个未完成的地方在于由于不是按照文字中心做的运算,所以你必须左下移动文字宽高的一半才能到达中心。我将这一步的过程封装成了svg.prototype.appendCircleArcText方法做了一个DEMO:
http://jsfiddle.net/wwrxwLuc/3/

如何处理渐变色

这个问题我不是很清楚标准的解决办法,但是我的第一反应是利用 alpha通道 来做。利用透明通道能过非常简单的给出一个颜色的渐变出来。但是透明通道有个我们不需要的功能就是透明效果,所以需要将透明过的颜色处理成普通颜色。这一步过程其实非常简单了,代码就是以下这样的:

function gradientColor(len, color) {
    color = color || [147, 112, 219];
    var delta = 0.8 / len;
    function colorTransfer(c, o) {
        var bg = [255,255,255];
        return '#'+c.map(function(t,i) {return parseInt((1-o)*bg[i] + o*t).toString(16)}).join('');
    }
    return new Array(len).join(',').split(',').map(function(o,i) {
        return colorTransfer(color, 1 - i * delta);
    })
}

比较温馨的做法是透明度并不是从 100% -> 0%,而是预留了 20% 的基值。

最终效果

处理完以上三个关键的问题之后其实这道题的代码已经出来了。由于要增加点击事件,我使用g标签将同一个圆弧和其对应的text标签包起来做成一个group,而后对每一个组增加了点击事件。由于 SVG 实际上可以看成一个一个的 DOM 标签,所以点击事件处理起来也是非常的得心应手的。最后附上我的最终代码和效果:

http://jsfiddle.net/wwrxwLuc/5/embedded/result/

我来提供一个用DIV + CSS实现的版本,效果截图如下。

Date Picker

难点在于用DIV实现圆弧,这里使用了SASS以及Compass(主要使用其中的tan()sin()函数)。

圆弧效果参见:http://codepen.io/yuezk/pen/gbMEwy
具体做法是用边框实现三角形,然后用父级元素将上方的两个角覆盖掉。

最终效果参见:http://codepen.io/yuezk/pen/pvbGrX

不足之处在于:

  • 个别层之间有轻微重叠,可能会影响到点击事件。
  • 单圈的项目数不能少于3个(2个或1个的情况未做处理)

其实一个圆环是一个圆形,然后小圆覆盖在上面,根据鼠标点击的位置去判断点了哪里,我是这么想的

小E呢通过大神的指导,和询问了一些同学之后终于做出来了,所以呢,我把我现在的成品拿出来~~,谢谢P_chou和Fackship两位大神的帮助!!!



<script> function draw(id) { var myCanvas = document.getElementById('myCanvas'); var r0 = 240; var r1 = 190; var r2 = 150; var r3 = 120; var ctx = myCanvas.getContext('2d'); //定义基础颜色 var color = []; for(var i=1;i<=24;i++){ var p = 30+(70/24)*i; color.push('hsl(170,' + p + '%,' + p + '%)'); } //把坐标轴移动到(250,250)这个点上 ctx.translate(250,250); drawSector(4,r0,[2009,2010,2011,2012]); drawSector(12,r1,[1,2,3,4,5,6,7,8,9,10,11,12]); drawSector(7,r2,['Mon','Tue','Wed','Thu','Fri','Sat','Sun']); drawSector(24,r3,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]); //画白色区域 ctx.save(); ctx.fillStyle = "#fff"; ctx.beginPath(); ctx.arc(0,0,80,0,2*Math.PI); ctx.fill() ctx.restore(); myCanvas.onmousedown=function(e) { var text=[2010,2011,2012,2009]; var text1=[12,11,10,9,8,7,6,5,4,3,2,1]; var text2=['Sun','Sat','Fri','Thu','Wed','Tue','Mon']; var text3=[24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1]; var x=e.clientX-250; var y=250-e.clientY; //当鼠标位置在半径190以外的区域 if((x*x+y*y)>190*190){ var id1=Math.ceil(Math.atan2(y,x)/(Math.PI*2/4)); if(id1<=0){ id1=id1+4;} alert(text[id1-1]+"年")} //当鼠标位置在半径150以外,190以内的区域 if((x*x+y*y)>(150*150)&&(x*x+y*y)<(190*190)){ var id1=Math.ceil(Math.atan2(y,x)/(Math.PI*2/4)); var id2=Math.ceil(Math.atan2(y,x)/(Math.PI*2/12)); if(id1<=0){ id1=id1+4;} if(id2<=0){ id2=id2+12;} alert(text[id1-1]+"年"+text1[id2-1]+"月")} //当鼠标位置在半径120以外,150以内的区域 if((x*x+y*y)>(120*120)&&(x*x+y*y)<(150*150)){ var id1=Math.ceil(Math.atan2(y,x)/(Math.PI*2/4)); var id2=Math.ceil(Math.atan2(y,x)/(Math.PI*2/12)); var id3=Math.ceil(Math.atan2(y,x)/(Math.PI*2/7)); if(id1<=0){ id1=id1+4;} if(id2<=0){ id2=id2+12;} if(id3<=0){ id3=id3+7;} alert(text[id1-1]+"年"+text1[id2-1]+"月"+text2[id3-1]+",")} //当鼠标位置在半径80以外,120以内的区域 if((x*x+y*y)>(80*80)&&(x*x+y*y)<(120*120)){ var id1=Math.ceil(Math.atan2(y,x)/(Math.PI*2/4)); var id2=Math.ceil(Math.atan2(y,x)/(Math.PI*2/12)); var id3=Math.ceil(Math.atan2(y,x)/(Math.PI*2/7)); var id4=Math.ceil(Math.atan2(y,x)/(Math.PI*2/24)); if(id1<=0){ id1=id1+4;} if(id2<=0){ id2=id2+12;} if(id3<=0){ id3=id3+7;} if(id4<=0){ id4=id4+24;} alert(text[id1-1]+"年"+text1[id2-1]+"月"+text2[id3-1]+","+text3[id4-1]+"时")} }; function drawSector (split,r,text) { // body... ctx.save(); ctx.strokeStyle = '#fff'; for(i=1;i<=split;i++){ ctx.fillStyle = color[i]; ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(r,0); ctx.arc(0,0,r,0,2*Math.PI/split); ctx.fill(); ctx.stroke(); if(text){ ctx.rotate(Math.PI/split); ctx.save(); ctx.fillStyle = "#000"; ctx.fillText(text[i-1],r-25,0); ctx.restore(); ctx.rotate(Math.PI/split); } else{ ctx.rotate(2*Math.PI/split); } } ctx.restore(); } } </script> </head> <body onload="draw('myCanvas');"> <canvas id="myCanvas" width = 500 height = 500/> </body> </html>
推荐问题
宣传栏