SVG绘制环图

上篇<原生Canvas绘制饼图>介绍了如何使用Canvas来绘制环图,这篇用SVG标签来实现一下。

SVG环图效果

上面是完整效果图,下面来看看具体实现。

使用的SVG元素

  • <svg>:SVG代码的开始标签,相当于创建一个画布。在svg标签里,插入其它SVG元素,来进行绘图;
  • <path>: path元素用于定义一个路径,使用path标签来绘制每项数据的环图份额;
  • <polyline>:polyline元素用于定义一条曲线,使用polyline绘制每项数据的线条;
  • <text>: text元素用于定义文本,使用text显示数据的文本信息。

以上是会用到的几个SVG标签,详细说明可以看看菜鸟教程SVG或者MDN的SVG教程

<svg>标签创建画布

SVG是一种用来描述二维矢量图形的XML标记语言,所以SVG标签不能使用document.createElement直接创建(浏览器无法识别)。需要使用document.createElementNS创建一个具有指定的命名空间URI和限定名称的元素,SVG的命名空间是'http://www.w3.org/2000/svg'。这里将创建SVG标签操作写在了createSvgTag函数里。下面先新建一个svg元素:

/**
 * 将创建环图的所有操作写在drawPie函数内,配置一些默认参数
 * @param  {Element} element [插入SVG的元素,缺省直接插入到body]
 * @param  {Number}  R       [外弧起终点计算半径]
 * @param  {Number}  r       [内弧起终点计算半径]
 * @param  {Number}  width   [画布宽度]
 * @param  {Number}  height  [画布高度]
 * @param  {Array}   data    [图表数据]
 */
function drawPie({element, R = 140, r = 100,width = 450,height = 400,data = []} = {}) {

    let w = width;
    let h = height; //将width、height赋值给w、h
    let origin = [w / 2, h / 2]; //以画布的中心点,作为环图的原点
    
    //创建一个svgs标签
    let svg =  createSvgTag('svg', {
        'width': w + 'px',
        'height': h + 'px',
        'viewBox': `0 0 ${w} ${h}`,     
    }); 
    
    (element && element.appendChild) ? element.appendChild(svg) : document.body.appendChild(svg);//插入到DOM
    /**
     * 将创建SVG标签写成一个函数
     * @param  {tring} tagName    [标签名]
     * @param  {Object} attributes [标签属性]
     * @return {Element} svg标签
     */
    function createSvgTag (tagName, attributes) {
        let tag = document.createElementNS('http://www.w3.org/2000/svg', tagName)
        for (let attr in attributes) {
            tag.setAttribute(attr, attributes[attr])
        }
        return tag;
    }
    
})
//调用
drawPie({
    data: [{
        cost: 4.94,
        category: '通讯',
        color: "#e95e45",
    }, {
        cost: 4.78,
        category: '服装美容',
        color: "#20b6ab",
    }, {
        cost: 4.00,
        category: '交通出行',
        color: "#ef7340",
    }, {
        cost: 3.00,
        category: '饮食',
        color: "#eeb328",
    }, {
        cost: 49.40,
        category: '其他',
        color: "#f79954",
    }, {
        cost: 28.77,
        category: '生活日用',
        color: "#00a294",
    }]
})

<path>绘制每项数据的环图份额

<path>元素的属性d用于定义路径,属性d实际上是一个字符串,包含了一系列路径描述。这些路径由下面这些指令组成:Moveto,Lineto,Curveto,Arcto,ClosePath。
我们会用到的指令有:

  • Moveto(移动画笔到起始点),语法:'M x,y' 在这里x和y是绝对坐标,分别代表水平坐标和垂直坐标;
  • Lineto(绘制直线),语法:'L x, y' 在这里x和y是绝对坐标,表示直线的结束点坐标;
  • Arcto(绘制弧曲线路径),语法:'A rx,ry xAxisRotate LargeArcFlag,SweepFlag x,y',rx和ry分别是x和y方向的半径(绘制圆弧时,rx和ry相等);LargeArcFlag的值确定是要画小弧或大弧,0表示画小弧(即画两点之间弧长最小的弧),1表示画大弧(当弧度大于Math.PI时需要画大弧);SweepFlag用来确定画弧的方向,0逆时针方向,1顺时针方向;x和y是目的地的坐标;
  • ClosePath(闭合路径),语法是'Z'或'z';
    详情MDN path元素d属性
    我们需要用path绘制如下的路径:
    图1

    如图:份额的绘制是先使用M命令移动到P0,L命令绘制一条直线到P1,A命令从P1画弧到P2,L命令从P2绘制一条直线到P3,A命令从P3绘制一条弧线到P0,最后Z命令关闭路径。然后我们只要填充这个路径就可以完成每项份额绘制了。这里我们需要求出4个点的绝对坐标,如何计算这四个坐标?
    图2

    如图,通过三角函数,我们就可以计算出每个点的位置。我们已知原点O坐标(画布中点)、外环半径R和内环半径r(我们自己给定);再通过计算出每项数据的弧度占比,我们就可以得到每项数据的起始弧度(即上一项的结束弧度,第一项为0)和结束弧度(起点+项数据的弧度占比)。x值为原点x+sin(angel)半径,y值为原点y-cos(angel)半径
    这里将计算点坐标的运算写在evaluateXY函数中,如下:
/**
 * 计算Xy坐标
 * @param  {[type]} r      [半径]
 * @param  {[type]} angel  [角度]
 * @param  {[type]} origin [原点坐标]
 * @return {[Array]} 坐标
 */
function evaluateXY (r, angel, origin) {
    return [origin[0] + Math.sin(angel) * r, origin[0] - Math.cos(angel) * r]                                                                                  
}

接下来,我们遍历数据,计算出每项数据的四个点:

//遍历计算每项数据
for(let v of data) {
    let itemData = Object.assign({}, v);//copy一遍,不直接修改原数据
    eAngel = sAngel + v.cost / total * 2 * Math.PI; //计算结束弧度
    itemData.arclineStarts = [
        evaluateXY(r, sAngel, origin), //计算P0坐标
        evaluateXY(R, sAngel, origin), //计算P1坐标 
        evaluateXY(R, eAngel, origin), //计算P2坐标 
        evaluateXY(r, eAngel, origin)  //计算P3坐标
        ];

    itemData.LargeArcFlag = (eAngel - sAngel) > Math.PI ? '1' : '0';//大于Math.PI需要画大弧,否则画小弧
    drawData.push(itemData);//保存到drawData数组中,绘制时遍历
    sAngel = eAngel;//将下一项数据的起始弧度设置为当前项的结束弧度
}

下面,遍历drawData,绘制环图:

//遍历计算每项数据,进行绘制
for(let v of drawData) {
    let P = v.arclineStarts;
    let path = createSvgTag('path', {
        'fill': v.color, //设置填充色
        'stroke': 'black',
        'stroke-width': '0', //画笔大小为零
        /**
         * d属性设置路径字符串
         * M ${P[0][0]} ${P[0][1]} 移动画笔到P0点
         * L ${P[1][0]} ${P[1][1]} 绘制一条直线到P1
         * A ${R} ${R} 0 ${v.LargeArcFlag} 1 ${P[2][0]} ${P[2][1]} 绘制外环弧到P2,R为外半径,v.LargeArcFlag画大弧还是小弧
         * L ${P[3][0]} ${P[3][1]} 绘制一条直线到P3
         * A ${r} ${r}  0 ${v.LargeArcFlag} 0 ${P[0][0]} ${P[0][1]} 绘制内环弧到P0点
         * z 关闭路径
         */
        'd': `M ${P[0][0]} ${P[0][1]} L ${P[1][0]} ${P[1][1]} A ${R} ${R} 0 ${v.LargeArcFlag} 1 ${P[2][0]} ${P[2][1]} L ${P[3][0]} ${P[3][1]} A ${r} ${r}  0 ${v.LargeArcFlag} 0 ${P[0][0]} ${P[0][1]} z`
    })
    svg.appendChild(path); //添加到画布中
} 

到这里,就已经绘制出如下环图了:

图3

记得还需要把相关变量先声明一下。

<polyline>、<text>绘制线条、文字

下面我们需要绘制线条和文字。

<polyline>绘制线条需要的数据

  • 起点坐标:这里设置起点为每项数据的弧线中间位置,通过计算中间位置对应的弧度,就可以通过三角函数计算出这个点坐标;
  • 线束点坐标:当线条起点在右侧时,线束点就是垂直平行起点图表最右侧位置;当线条起点在左侧时,线束点就是垂直平行起点图表最右左位置;假设起点为[sx,sy],右左结束点应该就是[w,sy]、[0,sy],w为图表宽度;
  • 折点:需要处理数据会挤在一起的情况,就会改变结束点坐标的y值,当起点和结束点y值不相等时,就需要设置折点。

<text>绘制文字:调整过后的线束点,就是文字的位置。
以下是完整代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>svg-pie</title>
</head>
<body>
    <div id="svgWrap" class="svg-wrap"></div>
    <script>
        /**
         * 将创建环图的所有操作写在drawPie函数内,配置一些默认参数
         * @param  {Element} element [插入SVG的元素,缺省直接插入到body]
         * @param  {Number}  R       [外弧起终点计算半径]
         * @param  {Number}  r       [内弧起终点计算半径]
         * @param  {Number}  width   [画布宽度]
         * @param  {Number}  height  [画布高度]
         * @param  {Array}   data    [图表数据]
         */
        function drawPie({element, R = 140, r = 100,width = 450,height = 400,data = []} = {}) {

            let w = width;
            let h = height; //将width、height赋值给w、h
            let origin = [w / 2, h / 2]; //原点坐标
            let drawData = [];//保存遍历后可直接绘制的数据
            let sAngel = 0;//保存每项数据的起始点角度
            let eAngel = sAngel;//保存每项数据的结束角点度
            let cAngel ;//保存每项数据的中点角度
            let leftPoints = []; //保存在左边的点
            let rightPoints= []; //保存在右边的点,分出左右是为了计算两点垂直间距是否靠太近
            let tAngel = Math.PI * 2; 
            let minPadding = 40; //设置数据项两点最小间距
            //total保存总花费,用于计算数据项占比
            let total = data.reduce(function(v, item) {
                return v + item.cost;
            }, 0)

            //创建svg标签,设置画布
            let svg =  createSvgTag('svg', {
                'width': w + 'px',
                'height': h + 'px',
                'viewBox': `0 0 ${w} ${h}`,     
            });

            //遍历计算每项数据,生成绘制数据
            for(let v of data) {
                let itemData = Object.assign({}, v);//copy一遍,不直接修改原数据
                let isLeft = false; 
                eAngel = sAngel + v.cost / total * tAngel;//计算结束弧度
                itemData.arclineStarts = [
                    evaluateXY(r, sAngel, origin), //计算P0坐标
                    evaluateXY(R, sAngel, origin), //计算P1坐标 
                    evaluateXY(R, eAngel, origin), //计算P2坐标 
                    evaluateXY(r, eAngel, origin)  //计算P3坐标
                    ];
                //大于Math.PI需要画大弧,否则画小弧
                itemData.LargeArcFlag = (eAngel - sAngel) > Math.PI ? '1' : '0'; 
                //计算线条起始点公位置
                itemData.lineStart = evaluateXY(R, sAngel + (eAngel - sAngel)/2, origin);
                //线条起点x值小于原点x值,在左侧,否则在右侧
                itemData.isLeft = isLeft = itemData.lineStart[0]  < origin[0];
                //根据线条起点左右,设置结束点
                itemData.lineEnd = [(isLeft ? 0 : w), itemData.lineStart[1]];
                //线条起点y值小于原点y值,在上部,否则在下部,用于确实过挤往上/下移动
                itemData.top = itemData.lineStart[1] < origin[1];
                //根据线条起点左右,添加到leftPoints/rightPoints,用于处理过挤
                isLeft? leftPoints.push(itemData) : rightPoints.push(itemData); 
                drawData.push(itemData)//保存到drawData数据中,绘制时遍历
                sAngel = eAngel;//将下一项数据的起始弧度设置为当前项的结束弧度
            }

           makeUseable(rightPoints); //处理右边挤在一起的情况
           makeUseable(leftPoints.reverse(), true); //处理左边挤在一起的情况,为什么要翻转一下,看makeUseable函数

            //遍历drawData开始绘制
            for(let v of drawData) {
                let P = v.arclineStarts;//将path路四个点变量,赋值给变量p

                //创建path标签(份额)
                let path = createSvgTag('path', {
                    'fill': v.color, //设置填充色
                    'stroke': 'black',
                    'stroke-width': '0', //画笔大小为零
                    /**
                     * d属性设置路径字符串
                     * M ${P[0][0]} ${P[0][1]} 移动画笔到P0点
                     * L ${P[1][0]} ${P[1][1]} 绘制一条直线到P1
                     * A ${R} ${R} 0 ${v.LargeArcFlag} 1 ${P[2][0]} ${P[2][1]} 绘制外环弧到P2,R为外半径,v.LargeArcFlag画大弧还是小弧
                     * L ${P[3][0]} ${P[3][1]} 绘制一条直线到P3
                     * A ${r} ${r}  0 ${v.LargeArcFlag} 0 ${P[0][0]} ${P[0][1]} 绘制内环弧到P0点
                     * z 关闭路径
                     */
                    'd': `M ${P[0][0]} ${P[0][1]} L ${P[1][0]} ${P[1][1]} A ${R} ${R} 0 ${v.LargeArcFlag} 1 ${P[2][0]} ${P[2][1]} L ${P[3][0]} ${P[3][1]} A ${r} ${r}  0 ${v.LargeArcFlag} 0 ${P[0][0]} ${P[0][1]} z`
                })
                //设置线条点
                let linePoints = v.lineStart[0] + ' ' + v.lineStart[1]; //设置起点
                //如果有折点,添加折点
                v.turingPoints && (linePoints += ',' + v.turingPoints[0] + ' ' + v.turingPoints[1]);
                //设置结束点
                linePoints += ',' + v.lineEnd[0] + ' ' + v.lineEnd[1];
                //创建polyline标签(线条)
                let polyline = createSvgTag('polyline', {
                    points: linePoints,
                    style: `fill:none;stroke:${v.color};stroke-width:.5`
                })
                //创建text标签,显示花费
                let cost = createSvgTag("text", {
                    'x':  v.lineEnd[0],
                    'y':  v.lineEnd[1],
                    'dy': -2,
                    style: `fill:${v.color};font-size:12px;text-anchor: ${v.isLeft? 'start':'end'};`
                })
                cost.innerHTML = v.cost;
                //创建text标签,显示花费分类
                let category = createSvgTag("text", {
                    'x':  v.lineEnd[0],
                    'y':  v.lineEnd[1],
                    'dy': 14,
                    style: `fill:${v.color};font-size:12px;text-anchor: ${v.isLeft? 'start':'end'};`
                })
                category.innerHTML = v.category;

                svg.appendChild(path);  //path(份额)添加到画布中            
                svg.appendChild(polyline);//polyline(线条)添加到画布中  
                svg.appendChild(cost);//text(花费)添加到画布中  
                svg.appendChild(category);//text(花费分类)添加到画布中  
            } 

            

            (element && element.appendChild) ? element.appendChild(svg) : document.body.appendChild(svg); //插入到DOM
            
            return svg;

            /**
             * 计算Xy坐标
             * @param  {[type]} r      [半径]
             * @param  {[type]} angel  [角度]
             * @param  {[type]} origin [原点坐标]
             * @return {[Array]} 坐标
             */
            function evaluateXY (r, angel, origin) {
                return [origin[0] + Math.sin(angel) * r, origin[0] - Math.cos(angel) * r]                                                                                  
            }
            /**
             * 将创建SVG标签写成一个函数
             * @param  {tring} tagName    [标签名]
             * @param  {Object} attributes [标签属性]
             * @return {Element} svg标签
             */
            function createSvgTag (tagName, attributes) {
                let tag = document.createElementNS('http://www.w3.org/2000/svg', tagName)
                for (let attr in attributes) {
                    tag.setAttribute(attr, attributes[attr])
                }
                return tag;
            }

            function isUseable(arr) { // 判断是否会有数据挤在一起(两点最小间距是否都大于等于minPadding)
                if (arr.length <= 1)
                    return true;
                
                return arr.every(function(p, index, arr) {
                    if (index === arr.length-1) {
                        //因为是当前项和下一项比较,所以index === arr.length-1直接返回true
                        return true;
                    } else {
                        /**
                         * 判断当前数据项结束点:p.lineEnd[1]
                         * 和下一数据项结束点垂直间距是否大于等于最小间距:minPadding
                         * 只有数据线条结束点垂直间距大于等于最小间距,才会返回true
                         */
                        return arr[index + 1].lineEnd[1]  - p.lineEnd[1] >= minPadding;
                    }
                })
            }

            function makeUseable(arr, left) {// 处理挤在一起的情况 
                let diff, turingAngel, x, maths = Math.sin,diffH, l;

                /**
                 * 这里的思路是
                 * 如果数据是非可用的(会挤在一起,isUseable判断)
                 * 就一直循环移动数据,直至可用
                 * 可能会有更好的算法,我这鱼木脑袋只想到这种的
                 * 欢迎大家提供更好的思路或算法
                 */
                while (!isUseable(arr)) { //每次循环处理一次,直至数据不会挤在一起

                    for (let i = 0, len = arr.length - 1; i < len; i++) { //遍历数组

                        diff = arr[i + 1].lineEnd[1] - arr[i].lineEnd[1]; //计算两点垂直间距

                        if (diff < minPadding) { //小于最小间距,表示会挤到一起

                            if (arr[i].top && arr[i + 1].top) { //是在上部的点,向上移动

                                /**
                                 * 判断当前的点是否还可以向上移动
                                 * 上方第一个点最往上只可以移动到y值为0
                                 * 之后依次最往上只能移动动y值为:i * minPadding 
                                 * 所以下面判断应该是:arr[i].lineEnd[1] - (minPadding - diff) > i * minPadding
                                 */
                                /**
                                 * 上面左边leftPoints的点需要翻转一下的原因是
                                 * 左边leftPoints的点最上面的点是排在最后的
                                 */
                                if (arr[i].lineEnd[1] - (minPadding - diff) > 0 && arr[i].lineEnd[1] > i * minPadding) {
                                    //当前点还能向上移动
                                    //向上移动到不挤(满足最小间距)
                                    arr[i].lineEnd[1] = arr[i].lineEnd[1] - (minPadding - diff);
                                } else {
                                    //当前点不向上移动到满足最小间距的位置
                                    //先把当前点移动到能够移动的最上位置
                                    arr[i].lineEnd[1] = i * minPadding;
                                    //再把下个点移动,使满足最小间距
                                    arr[i + 1].lineEnd[1] = arr[i + 1].lineEnd[1] + (minPadding - diff);
                                }

                            } else {
                                //是在下部的点,向下移动
                                /**
                                 * 判断当前点的下个点是否还可以向下移动
                                 * 下方最后一个点最往下只可以移动到y值为h,即图表高度
                                 * 之前的点依次最往下只能移动动y值为:h - (len - i - 1) * minPadding
                                 * 所以下面判断应该是:arr[i + 1].lineEnd[1] + (minPadding - diff) < h - (len - i - 1) * minPadding
                                 */
                                if (arr[i + 1].lineEnd[1] + (minPadding - diff) < h && arr[i + 1].lineEnd[1] < h - (len - i - 1) * minPadding) {
                                     //当前点的下个点还能向下移动
                                    //当前点的下个点向下移动到不挤(满足最小间距)
                                    arr[i + 1].lineEnd[1] = arr[i + 1].lineEnd[1] + (minPadding - diff)
                                } else {
                                    //当前点的下个点不能向下移动
                                    //先把当前点的下个点向下移动能够移动的最下位置
                                    arr[i + 1].lineEnd[1] = h - (len - i - 1) * minPadding;
                                    //再把当前点移动,使满足最小间距
                                    arr[i].lineEnd[1] = arr[i].lineEnd[1] - (minPadding - diff);
                                }
                            }

                            break; //每次移动完成直接退出循环,判断一次是否已经不挤
                        }
                    }
                }


                /**
                 * 遍历已经可用的数据 
                 * 起点和结束点不在同一水平线上
                 * 需要设置折点
                 * 设置折点为线束点水平距离40像素的位置
                 */
                for (let i = 0, len = arr.length; i < len; i++) { 
                    if (arr[i].lineStart[1] !== arr[i].lineEnd[1]) { 
                        arr[i].turingPoints = [arr[i].lineEnd[0] + (left ? 40 : -40) , arr[i].lineEnd[1]];
                    }
                }
            }
        }
        drawPie({
            element: document.getElementById('svgWrap'),
            data: [{
                cost: 4.94,
                category: '通讯',
                color: "#e95e45",
            }, {
                cost: 4.78,
                category: '服装美容',
                color: "#20b6ab",
            }, {
                cost: 4.00,
                category: '交通出行',
                color: "#ef7340",
            }, {
                cost: 3.00,
                category: '饮食',
                color: "#eeb328",
            }, {
                cost: 49.40,
                category: '其他',
                color: "#f79954",
            }, {
                cost: 28.77,
                category: '生活日用',
                color: "#00a294",
            }]
        })
    </script>
</body>
</html>

也可以查看gitee

总结一下

  • 难点:难的地方就在弧上各点的计算,需要好好再回忆一下数学的三角函数。
  • 对比canvas:canvas只需有一个标签,svg实现就在DOM中增加了一堆标签。这样一来,svg的优势就在于第项都是一个标签,你可以直接针对这个标签要绑定事件和做修改。比如要实现鼠标称到某个项,放大这个项,svg只要给每个path绑定事件,修改当前的这个path就行;而canvas只能在canvase绑定事件,先通过计算鼠标位置来判断移动到了哪个份额上,然后再重绘整个canvas;同样,标签过多也是svg的缺点,我们这点标签其实没什么,一旦标签多起来,肯定是会给浏览器渲染带来负担的。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容

  • 一:canvas简介 1.1什么是canvas? ①:canvas是HTML5提供的一种新标签 ②:HTML5 ...
    GreenHand1阅读 4,657评论 2 32
  • 【Android 自定义View之绘图】 基础图形的绘制 一、Paint与Canvas 绘图需要两个工具,笔和纸。...
    Rtia阅读 11,643评论 5 34
  • 一、canvas简介 1.1 什么是canvas?(了解) 是HTML5提供的一种新标签 Canvas是一个矩形区...
    Looog阅读 3,932评论 3 40
  • 这阵子恰逢高考成绩出来了。掐指一算,原来我高考已经是十年前的事情,恍然隔世啊。从残存的记忆里搜寻了下当时报考志愿的...
    graceyan阅读 191评论 0 0