整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

canvas实现涂鸦效果(颜色、背景图、橡皮擦、历史

canvas实现涂鸦效果(颜色、背景图、橡皮擦、历史记录、清屏等)

例简介

用canvas实现涂鸦效果,包括更换笔触大小颜色、换背景图、橡皮檫、历史记录、清屏等功能,并能保存涂鸦图片到本地。

编写静态页面

html代码和css样式如下图,这一块比较简单,也不是本文重点,可自行查看。

<!DOCTYPE html>
<html lang="zh">


<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,Chrome=1" />
    <title>涂鸦</title>
    <link rel="shortcut icon" href="#" />
    <link rel="stylesheet" type="text/css" href="css/base.css">
    <link rel="stylesheet" type="text/css" href="css/handWriting.css">
</head>


<body>
    <div class="wrapper">
        <canvas class="offCanvas"></canvas>
        <canvas class="canvas"></canvas>
    </div>
    <div class="footer">
        <div class="control-button">
            <div class="item colorButton"><img src="images/colors.png" alt=""><span>黑色</span></div>
            <div class="item sizeButton"><img src="images/size.png" alt=""><span>中笔</span></div>
            <div class="item bgButton"><img src="images/bg.png" alt=""><span>背景</span></div>
            <div class="item rubberButton"><img src="images/rubber.png" alt=""><span>擦掉</span></div>
            <div class="item historyButton"><img src="images/history.png" alt=""><span>历史</span></div>
            <div class="item clearButton"><img src="images/clear.png" alt=""><span>清屏</span></div>
            <div class="item saveButton"><img src="images/save.png" alt=""><span>保存</span></div>
        </div>
        <div class="pop-up colors-panel">
            <div class="title">笔触颜色<span class="closeBtn"></span></div>
            <div class="colors">
                <div class="lineColors">
                    <div><span class="red" data-text="红色" data-color="#ff0000"></span></div>
                    <div><span class="blue" data-text="蓝色" data-color="#0000ff"></span></div>
                    <div><span class="green" data-text="绿色" data-color="#00ff00"></span></div>
                    <div><span class="black" data-text="黑色" data-color="#000000"></span></div>
                    <div><span class="orange" data-text="橙色" data-color="#ff6302"></span></div>
                </div>
                <div class="lineColors">
                    <div><span class="red" data-text="红色" data-color="#ff0000"></span></div>
                    <div><span class="blue" data-text="蓝色" data-color="#0000ff"></span></div>
                    <div><span class="green" data-text="绿色" data-color="#00ff00"></span></div>
                    <div><span class="black" data-text="黑色" data-color="#000000"></span></div>
                    <div><span class="orange" data-text="橙色" data-color="#ff6302"></span></div>
                </div>
            </div>
        </div>
        <div class="pop-up size-panel">
            <div class="title">笔触大小<span class="closeBtn"></span></div>
            <div class="sizes">
                <div class="lineSizes"><span data-lineWidth="10" data-text="大笔" class="big"></span></div>
                <div class="lineSizes"><span data-lineWidth="30" data-text="中笔" class="middle"></span></div>
                <div class="lineSizes"><span data-lineWidth="50" data-text="小笔" class="small"></span></div>
            </div>
        </div>
        <div class="pop-up bg-panel">
            <div class="title">推荐背景<span class="closeBtn"></span></div>
            <div class="list">
                <img src="images/white.jpg" alt="" />
                <img src="images/white.jpg" alt="" />
                <img src="images/white.jpg" alt="" />
                <img src="images/white.jpg" alt="" />
                <img src="images/white.jpg" alt="" />
            </div>
        </div>
        <!-- 添加橡皮檫列表和历史记录列表样式 -->
        <div class="pop-up rubber-panel">
            <div class="title">橡皮檫大小<span class="closeBtn"></span></div>
            <div class="rubbers">
                <div class="first">大小:</div>
                <div class="second"><input type="range" min="1" max="50" value="25" step="1" name="大小" /></div>
                <div class="last"><span class="rubberSize">25</span>像素</div>
            </div>
        </div>
        <div class="pop-up history-panel">
            <div class="title">历史记录<span class="closeBtn"></span></div>
            <div class="history">
                <div class="lineBox"></div>
            </div>
        </div>
    </div>
    <div class="offImgs" style="display: none;"></div>
    <script src="js/jquery.min.js"></script>
    <script src="js/handWriting.js"></script>
</body>


</html>
html,
body,
.wrapper {
    height: 100%
}


.wrapper {
    position: relative;
    padding-bottom: 60px;
    box-sizing: border-box
}


.wrapper .offCanvas,
.wrapper .canvas {
    position: absolute;
    top: 0;
    left: 0
}


.footer {
    position: absolute;
    bottom: 0;
    width: 100%;
    height: 60px;
    background-color: #fff;
    box-shadow: 0 0 10px 3px #e2e2e2;
    overflow: hidden;
}


.footer .control-button {
    display: flex;
    height: 100%
}


.control-button .item {
    flex: 1;
    text-align: center
}


.control-button .item img {
    width: 22px;
    height: 22px;
    margin: 8px auto 5px;
    display: block;
}


.control-button .item span {
    color: #2e344a;
    font-size: 12px
}






/*后面添加*/
/*笔触设置*/
.footer .pop-up{display:none;height:130px;padding:0 15px}
.pop-up .title{font-size:14px;color:#eb4985;margin:10px 0 15px;text-align:center}
.pop-up .title .closeBtn{background:url("../images/close.png") no-repeat;background-size:100%;width:20px;height:20px;float:right}
.pop-up .colors{overflow:hidden}
.pop-up .lineColors div{width:20%;float:left;margin:6px 0}
.pop-up .lineColors span{display:block;width:28px;height:28px;margin:auto;border-radius:50%}
.pop-up .lineColors span.red{background-color:#f00}
.pop-up .lineColors span.blue{background-color:#00f}
.pop-up .lineColors span.green{background-color:#0f0}
.pop-up .lineColors span.black{background-color:#000}
.pop-up .lineColors span.orange{background-color:#ff6302}
.pop-up .sizes{margin-top:20px}
.pop-up .sizes .lineSizes{height:30px;cursor:pointer}
.pop-up .sizes .big{display:block;height:10px;width:100%;background-color:#eb4985;border-radius:3px}
.pop-up .sizes .middle{display:block;height:6px;width:100%;background-color:#eb4985;border-radius:3px}
.pop-up .sizes .small{display:block;height:3px;width:100%;background-color:#eb4985;border-radius:3px}
.pop-up .list{height:80px;line-height:80px}
.pop-up .list img{width:20%;float:left;padding:5px;box-sizing:border-box}
/*橡皮檫样式*/
.rubbers {
    display: flex;
    color: #2e344a;
    font-size: 14px;
    margin-top: 40px;
}
.rubbers div {
    flex: 1;
}
.rubbers .second {
    flex: 5;
}
.rubbers .second input { /*滑动条的样式*/
    width: 100%;
    -webkit-appearance: none;
    height: 3px;
    border-radius: 5px;
    vertical-align: super;
    background-color: #2e344a;
}
.rubbers .second input::-webkit-slider-thumb { /*滑动条的样式*/
    -webkit-appearance: none;
    height: 25px;
    width: 25px;
    background-color: #eb4985;
    border-radius: 50%;
}
.rubbers .last {
    text-align: right;
}




.history-panel .history {
    overflow-x: scroll;
    -webkit-overflow-scrolling: touch;
}
.history-panel .lineBox img {
    width: 70px;
    height: 70px;
    border: 1px solid #2e344a;
    margin-right: 8px;
}

实现原理

$(function() {
    var offCanvas=$('.offCanvas')[0]; // 用于更换背景图
    var offCtx=offCanvas.getContext('2d');
    var canvas=$('.canvas')[0]; // 用于涂鸦
    var ctx=canvas.getContext('2d');


    var lastCoordinate=null; // 前一个坐标
    var lastTimestamp=0; // 前一个时间戳
    var lastLineWidth=-1; // 用于线光滑过度
    var point=null; // 存储鼠标或触发坐标
    var sizeWidth=30; // 中笔触计算值
    var strokeColor='#000'; // 笔触颜色默认黑色
    var imgSrc=null; // 背景图片地址
    var imgArray=[]; // 存储背景图和涂鸦图
    var rubberSize=25; // 存储橡皮檫大小
    var flag=true; // 用于判断涂鸦还是擦除
    var footerHeight=$('.footer').height(); // 获取底部高度


    offCanvas.width=$(window).width();
    offCanvas.height=$(window).height() - footerHeight;
    canvas.width=$(window).width();
    canvas.height=$(window).height() - footerHeight;


    // 选择颜色
    $('.lineColors span').click(function() {
        strokeColor=$(this).attr('data-color'); // 获取颜色值,用于替换笔触颜色
        var colorName=$(this).attr('data-text'); // 获取颜色文字,用于替换操作栏文字
        $('.colorButton span').html(colorName); // 替换操作栏文字


        animatePanel('.colors-panel', '-130px', '.control-button', '60px'); // 收起颜色列表显示操作栏
    });
    // 选择大小
    $('.lineSizes span').click(function() {
        sizeWidth=$(this).attr('data-lineWidth'); // 获取大小值,用于计算笔触大小
        var sizeName=$(this).attr('data-text'); // 获取大小文字,用于替换操作栏文字
        $('.sizeButton span').html(sizeName); // 替换操作栏文字


        animatePanel('.size-panel', '-130px', '.control-button', '60px'); // 收起大小列表显示操作栏
    });    
    // canvas触摸事件
    $('.canvas').on('touchstart', function(event) {
        point={ x: event.originalEvent.targetTouches[0].clientX, y: event.originalEvent.targetTouches[0].clientY };
        lastCoordinate=windowToCanvas(point.x, point.y);
        lastTimestamp=new Date().getTime();
    });
    $('.canvas').on('touchmove', function(event) {
        point={ x: event.originalEvent.targetTouches[0].clientX, y: event.originalEvent.targetTouches[0].clientY };
        var curCoordinate=windowToCanvas(point.x, point.y);        


        if (flag) { // 涂鸦
            var curTimestamp=new Date().getTime();
            var s=calcDistance(lastCoordinate, curCoordinate); // 计算两点之间的距离         
            var t=curTimestamp - lastTimestamp; // 计算两点之间的时间差
            var curLineWidth=caleLineWidth(s, t, sizeWidth);


            drawLine(ctx, lastCoordinate.x, lastCoordinate.y, curCoordinate.x, curCoordinate.y, curLineWidth, strokeColor);


            lastCoordinate=curCoordinate; // 现在坐标替换前一个坐标
            lastTimestamp=curTimestamp;
            lastLineWidth=curLineWidth;
        } else { // 擦掉
            ctx.save();
            ctx.beginPath();
            ctx.arc(curCoordinate.x, curCoordinate.y, rubberSize/2, 0, Math.PI * 2);
            ctx.clip();
            ctx.clearRect(curCoordinate.x - rubberSize/2, curCoordinate.y - rubberSize/2, rubberSize, rubberSize); // 清除涂鸦画布内容
            ctx.restore();
        }
    });
    $('.canvas').on('touchend', function() {
        var imageSrc=canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream'); // 画布转换为图片地址
        $('.lineBox').append('<img src="' + imageSrc + '" />');
        var boxWidth=$('.lineBox img').length * 80; // 80为图片宽度(72)+间隔(8)
        $('.lineBox').css({ // 设置lineBox宽度
            width: boxWidth + 'px'
        });
    });


    // 根据不同速度计算线的宽度函数
    function caleLineWidth(s, t, brushWidth) {
        var v=s / t; // 获取速度
        // 声明最大最小速度和最大最小边界
        var maxVelocity=10,
            minVelocity=0.1,
            maxLineWidth=Math.min(30, canvas.width / brushWidth), // 避免手机端线条太粗
            minLineWidth=1,
            resultLineWidth; // 用于返回的线宽度


        if (v <=minVelocity) {
            resultLineWidth=maxLineWidth;
        } else if (v >=maxVelocity) {
            resultLineWidth=minLineWidth;
        } else {
            resultLineWidth=maxLineWidth - (v - minVelocity) / (maxVelocity - minVelocity) * (maxLineWidth - minLineWidth);
        }
        if (lastLineWidth==-1) { // 开始时候
            return resultLineWidth;
        } else {
            return resultLineWidth * 2 / 3 + lastLineWidth * 1 / 3; // lastLineWidth占得比重越大越平滑
        }
    }
    // 计算两点之间的距离函数
    function calcDistance(lastCoordinate, curCoordinate) {
        var distance=Math.sqrt(Math.pow(curCoordinate.x - lastCoordinate.x, 2) + Math.pow(curCoordinate.y - lastCoordinate.y, 2));
        return distance;
    }
    // 坐标转换
    function windowToCanvas(x, y) {
        var bbox=canvas.getBoundingClientRect();
        return { x: x - bbox.left, y: y - bbox.top };
    }
    // 绘制直线
    function drawLine(context, x1, y1, x2, y2, /*optional*/ lineWidth, /*optional*/ strokeColor) {
        context.beginPath();
        context.lineTo(x1, y1);
        context.lineTo(x2, y2);


        context.lineWidth=lineWidth;
        context.lineCap='round'; // 线与线交合不会产生空隙
        context.lineJoin='round';
        context.strokeStyle=strokeColor; // 默认笔触黑色


        context.stroke();
    }
    // 选择背景
    $('.bg-panel img').click(function() {
        imgSrc=$(this).attr('src'); // 获取图片src
        drawImg(imgSrc); // 画图


        animatePanel('.bg-panel', '-130px', '.control-button', '60px');
    });
    // 绘制图像到画布
    function drawImg(changeValue) {
        offCtx.clearRect(0, 0, canvas.width, canvas.height); // 先清除画布
        var changeImg=new Image();
        // changeImg.crossOrigin='Anonymous';
        changeImg.src=changeValue;
        changeImg.onload=function() {
            offCtx.drawImage(changeImg, 0, 0, canvas.width, canvas.height);
        };
    }
    // 清屏
    $('.clearButton').click(function() {
        ctx.clearRect(0, 0, canvas.width, canvas.height); // 清除涂鸦画布内容
        offCtx.clearRect(0, 0, canvas.width, canvas.height); // 清除背景图画布内容
    });
    // 保存涂鸦效果
    $('.saveButton').click(function() {
        // toDataURL兼容大部分浏览器,缺点就是保存的文件没有后缀名
        if (imgSrc) { // 存在背景图才执行
            imgArray.push(offCanvas.toDataURL('image/png').replace('image/png', 'image/octet-stream'));
        }
        imgArray.push(canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream'));


        compositeGraph(imgArray);
    });
    /**
     * [离屏合成图]
     * @param  {[type]} imgArray   [背景图画布和涂鸦画布的地址数组]
     */
    function compositeGraph(imgArray) {
        // 下载后的文件名
        var filename='canvas_' + (new Date()).getTime() + '.png';


        var compositeCanvas=document.createElement('canvas');
        compositeCanvas.width=canvas.width;
        compositeCanvas.height=canvas.height;
        var compositeCtx=compositeCanvas.getContext('2d');
        $.each(imgArray, function(index, val) {
            $('.offImgs').append('<img src="' + val + '" />'); // 增加img元素用于获取合成
        });
        $.each($('.offImgs img'), function(index, val) {
            val.onload=function() {
                compositeCtx.drawImage(val, 0, 0); // 循环绘制图片到离屏画布
            };
        });
        var timer=setTimeout(function() {
            var compositeImg=compositeCanvas.toDataURL('image/png').replace('image/png', 'image/octet-stream');
            saveFile(compositeImg, filename);
            timer=null; // 注销定时器
        }, 50);
    }
    /**
     * 模拟鼠标点击事件进行保存
     * @param  {String} data     要保存到本地的图片数据
     * @param  {String} filename 文件名
     */
    function saveFile(data, filename) {
        var saveLink=document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
        saveLink.href=data;
        saveLink.download=filename; // download只兼容chrome和firefox,需要兼容全部浏览器,只能用服务器保存


        var event=document.createEvent('MouseEvents');
        event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
        saveLink.dispatchEvent(event);
    }
    // 点击颜色按钮弹出颜色列表
    $('.colorButton').click(function() {
        animatePanel('.control-button', '-60px', '.colors-panel', '130px');
        flag=true; // 点击颜色时候变为涂鸦状态
    });
    // 点击颜色列表的关闭按钮
    $('.colors-panel .closeBtn').click(function() {
        animatePanel('.colors-panel', '-130px', '.control-button', '60px');
    });
    // 点击大小按钮弹出大小列表
    $('.sizeButton').click(function() {
        animatePanel('.control-button', '-60px', '.size-panel', '130px');
        flag=true; // 点击大小时候变为涂鸦状态
    });
    // 点击大小列表的关闭按钮
    $('.size-panel .closeBtn').click(function() {
        animatePanel('.size-panel', '-130px', '.control-button', '60px');
    });
    // 点击背景按钮弹出背景列表
    $('.bgButton').click(function() {
        animatePanel('.control-button', '-60px', '.bg-panel', '130px');
    });
    // 点击背景列表的关闭按钮
    $('.bg-panel .closeBtn').click(function() {
        animatePanel('.bg-panel', '-130px', '.control-button', '60px');
    });
    // 点击擦掉按钮弹出橡皮檫大小列表
    $('.rubberButton').click(function() {
        animatePanel('.control-button', '-60px', '.rubber-panel', '130px');
        flag=false; // 点击擦掉时候变为橡皮檫状态
    });
    // 点击橡皮檫大小列表的关闭按钮
    $('.rubber-panel .closeBtn').click(function() {
        animatePanel('.rubber-panel', '-130px', '.control-button', '60px');
    });
    // 拖动滑动条获取数值
    $('.rubbers .second input').on('touchmove', function() {
        rubberSize=$(this)[0].value;
        $('.rubberSize').html(rubberSize);
    });
    // 点击历史按钮弹出历史记录列表
    $('.historyButton').click(function() {
        animatePanel('.control-button', '-60px', '.history-panel', '130px');
    });
    // 点击历史记录列表的关闭按钮
    $('.history-panel .closeBtn').click(function() {
        animatePanel('.history-panel', '-130px', '.control-button', '60px');
    });
    // 点击历史记录图片绘制到画布
    $('.lineBox').on('click', 'img', function() { // 事件委托
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage($(this)[0], 0, 0, canvas.width, canvas.height); // 绘制点击的图片到画布
    });






    // 底部操作栏和弹出框交互函数
    function animatePanel(fName, fHeight, sName, sHeight) {
        $(fName).slideUp(300);
        $('.footer').animate({'bottom': fHeight}, 300);
        var timer=setTimeout(function() {
            $(sName).slideDown(500);
            $('.footer').animate({'bottom': 0, 'height': sHeight}, 500);
            timer=null;
        }, 0);
    }
    // 阻止手机滑动时拖动页面
    $('.wrapper').on('touchmove', function(event) {
        event.preventDefault();
    });
});

声明变量和初始化数据,具体用途说明都已经有备注,主要分析重点:

1、offCanvas用于更换背景图的画布,所以宽高跟涂鸦画布(canvas)一致,默认空白;

2、背景图画布(offCanvas)和涂鸦画布(canvas)的高度都需要减去footerHeight,避免被底部操作栏遮住;

3、imgSrc设置背景图片地址,也用于判断是否有背景图;

4、imgArray存储背景图和涂鸦图,用于循环添加到元素img;

5、rubberSize设置橡皮檫默认大小,该值跟html中input[type="range"]的value值一致,后面用于计算清除区域;

6、flag用于判断是涂鸦还是擦除(true为涂鸦,false为擦除);

7、strokeColor设置默认笔触的颜色,跟首页导航栏底部显示的文字对应;

8、imgSrc存储背景图片地址,用于绘制图片到画布。


分析基本函数(重点):

1、caleLineWidth根据不同速度计算线的宽度函数,因为涂鸦过程速度快慢会影响线的宽度,为了更逼真,增加该函数,可根据实际情况对里面数据进行修改;

2、calcDistance计算两点之间的距离函数,这个是用于caleLineWidth(距离/时间),一个简单的两点计算公式(两边长平方后相加再开方);

3、windowToCanvas坐标转换函数,屏幕坐标转换为在画布上面的坐标,不然画出来的线会有偏移;其实该实例是满屏(width: 100%),是可以不用转换,主要是为了给不是满屏时候用的。

// 根据不同速度计算线的宽度函数
function caleLineWidth(s, t, brushWidth) {
    var v=s / t; // 获取速度
    // 声明最大最小速度和最大最小边界
    var maxVelocity=10,
        minVelocity=0.1,
        maxLineWidth=Math.min(30, canvas.width / brushWidth), // 避免手机端线条太粗
        minLineWidth=1,
        resultLineWidth; // 用于返回的线宽度
    if (v <=minVelocity) { resultLineWidth=maxLineWidth; } else if (v >=maxVelocity) {
        resultLineWidth=minLineWidth;
    } else {
        resultLineWidth=maxLineWidth - (v - minVelocity) / (maxVelocity - minVelocity) * (maxLineWidth - minLineWidth);
    }
    if (lastLineWidth==-1) { // 开始时候
        return resultLineWidth;
    } else {
        return resultLineWidth * 2 / 3 + lastLineWidth * 1 / 3; // lastLineWidth占得比重越大越平滑
    }
}
// 计算两点之间的距离函数
function calcDistance(lastCoordinate, curCoordinate) {
    var distance=Math.sqrt(Math.pow(curCoordinate.x - lastCoordinate.x, 2) + Math.pow(curCoordinate.y - lastCoordinate.y, 2));
    return distance;
}
// 坐标转换
function windowToCanvas(x, y) {
    var bbox=canvas.getBoundingClientRect();
    return { x: x - bbox.left, y: y - bbox.top };
}

4、函数saveFile,因为canvas没办法直接保存为图片,所以下面的代码利用了模拟鼠标点击事件进行保存,且能自定义文件名;

/**
 * 模拟鼠标点击事件进行保存
 * @param  {String} data     要保存到本地的图片数据
 * @param  {String} filename 文件名
 */
function saveFile(data, filename) {
    var saveLink=document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
    saveLink.href=data;
    saveLink.download=filename; // download只兼容chrome和firefox,需要兼容全部浏览器,只能用服务器保存


    var event=document.createEvent('MouseEvents');
    event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
    saveLink.dispatchEvent(event);
}

5、添加阻止拖动函数,详细代码如下图;因为H5在手机滑动页面时候,页面会被拖动,导致跟手指涂鸦冲突,体验不好,所以需要增加该函数,阻止页面被拖动;

// 阻止手机滑动时拖动页面
$('.wrapper').on('touchmove', function(event) {
    event.preventDefault();
});

6、添加底部按钮和弹出列表的交互效果(代码如下图),为了提高交互体验,使用了animate、setTimeout和slideUp\Down,并写成一个函数,便于多处调用,代码主要意思就是先向下隐藏设置的栏目然后再向上显示需要的栏目;

// 底部操作栏和弹出框交互函数
function animatePanel(fName, fHeight, sName, sHeight) {
    $(fName).slideUp(300);
    $('.footer').animate({ 'bottom': fHeight }, 300);
    var timer=setTimeout(function() {
        $(sName).slideDown(500);
        $('.footer').animate({ 'bottom': 0, 'height': sHeight }, 500);
        timer=null;
    }, 0);
}


原理过程分析:

1、涂鸦实现过程,简单说就是记录触摸时的坐标和滑动时的坐标,然后利用这两个坐标进行画线,从而实现涂鸦效果,详细分析如下:

第一步,触摸时候记录触摸时坐标并转换为(windowToCanvas函数)canvas的坐标(lastCoordinate),并且保存当前的时间戳(lastTimestamp);

第二步,滑动时记录滑动到的坐标并转换为canvas坐标(curCoordinate),并且保存当前的时间戳(curTimestamp);再把lastCoordinate作为开始坐标,curCoordinate作为结束坐标进行画线(drawLine函数),并把curCoordinate赋值给lastCoordinate,curTimestamp赋值给lastTimestamp;所以滑动时候,都是起始点--第一点--第二点--...--最后结束的点,这样两点两点画线,从而产生滑动过程中的一条线,比较符合实际情况,直接计算起始点--结束点的线是不符合实际情况;最后为了符合慢的时候笔触比较大,快的时候笔触比较小,利用了函数curLineWidth进行即时计算,里面的数值可以自己根据实际情况调节。

2、清屏功能比较简单,原理就是点击清屏按钮(clearButton)时候,清除(clearRect)掉涂鸦画布(ctx)和背景图画布(offCtx)的内容;

3、保存功能实现过程,简单说就是把涂鸦画布和背景图画布的内容合成到另一个画布,然后把该画布内容保存成图片到本地,详细分析如下(点击保存按钮(saveButton)时候):

首先把涂鸦画布和背景图画布的内容转换成图片存储到数组imgArray;

然后把imgArray传值给函数compositeGraph,该函数首先把数组imgArray内容循环转化成html中元素img的内容(该内容是隐藏的),然后循环该元素img轮流绘画到离屏画布上面,最后把该离屏画布转化成图片并利用函数saveFile保存成图片到本地。

注意:

图片需要使用onload(val.onload),不然图片未准备就执行,会显示空白;

转换离屏画布为图片和执行函数saveFile需要使用定时器,不然也会导致保存空白图片。

4、橡皮檫功能实现过程,简单说就是获取滑动过程中的坐标点,然后利用clearRect清除坐标点周围的涂鸦内容;详细分析如下(红色框部分):

跟基本功能代码加了flag区分,else部分为擦掉功能实现代码;

利用了画布清除功能ctx.clearRect对滑动坐标点周围矩形进行清除,因为坐标点是圆心,所以清除的起始坐标(curCoordinate)需要滑动坐标点减去半径(rubberSize/2);

直接用矩形擦除,过程会有锯齿,为了达到更好效果,特意在上面加了圆形(ctx.arc)剪切(ctx.clip),使擦除效果比较光滑。

5、历史记录功能实现过程,简单说就是手指离开屏幕时候,把当前画布内容转化为图片地址,然后新增元素img(src为该图片地址)插入历史记录列表;详细分析如下(手指离开屏幕时候(touchend)):

把当前画布内容转化为图片地址,然后新增元素img(src为该图片地址)插入历史记录列表;

当操作次数多了之后,历史记录上的图片会一直增加,为了让超过一屛的图片能正常滑动显示,所以需要实时计算全部图片的宽度(+间隔)的值boxWidth,然后赋值lineBox。

6、实现可更换笔触颜色和大小的功能,详细代码如下图:

查看代码可看出滑动过程中会调用函数drawLine,且函数有参数curLineWidth(笔触大小)和strokeColor(笔触颜色),所以只需要选择颜色和大小的时候,替换这两个参数的值就可以实现功能,选择对应的值就是(sizeWidth(用于计算)和strokeColor);

为了便于用户知道选择了什么颜色和那个大小,也实现了选择回填;

最后实现了点击后关闭弹出框显示操作栏。

7、实现可更换背景图功能,详细代码如下图:

实现原理就是利用点击背景图列表的图片获取src,然后使用canvas的drawImage功能,把图片绘画到offCtx(该画布是处于涂鸦画布的下面),从而实现更换背景图功能;

Tips:背景图列表的图片可改为缩略图加上描述名称有利于体验;绘画新背景之前一定要清除画布(clearRect),不然性能会有问题。


注意事项

1、由于该实例是用于手机端,所以使用触摸事件,如果要用于PC端,改为点击事件即可,但要注意增加判断点击后才能涂鸦,不然会导致未点击就能涂鸦;

2、toDataURL有跨域问题,所以需要发布到服务器上,才能正常使用;

lt;meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

<html>

<body>

<canvas id='canvas'></canvas>

<div id="write"></div>

</body>

</html>

<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>

<script>

//画布

var c=document.getElementById('canvas');

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

//画布尺寸

var h=650;

var w=800;

c.width=w;

c.height=h;

//背景数量

var num=100;

//大红尺寸

var gh=h/num;

var gw=w/num;

//小绿尺寸

var gamex=40;

var gamey=30;

//背景布置

function bk(){

for(var x=0;x<num;x++){

for(var y=0;y<num;y++){

ctx.fillStyle="green";

ctx.fillRect(10*(x-1)+x,10*(y-1)+y,gw,gh);

}

}

}

//定义小绿数量

var sts=new Array();

//定义小红数量

var drs=new Array();

//小红小绿范围内相遇删除对方

function dels(){

if(sts.length>0){

for(var i=0;i<sts.length;i++){

//范围大小

var a=sts[i]['x']+20;

var b=sts[i]['x']-20;

var aa=sts[i]['y']+20;

var bb=sts[i]['y']-20;

$('#write').text(a+'---'+b);

for(var s=0;s<drs.length;s++){

//判断是否在范围内

if((drs[s]['x']<a && drs[s]['x']>b) && (drs[s]['y']<aa && drs[s]['y']>bb)){

//sts.splice(i,1);//删除小绿

drs.splice(s,1);//删除小红

}

}

}

}

}

//小红数量循环

function drss(){

var dr=new Array();

dr['x']=Math.floor((Math.random()*w)+1);

dr['y']=0;

//dr['y']=Math.floor((Math.random()*h)+1);

drs.push(dr);

if(drs.length>0){

for(var i=0;i<drs.length;i++){

if(drs[i]['y']>h){

drs.splice(i,1);

}else{

drs[i]['y']+=1;

}

ctx.fillStyle="orange";

ctx.fillRect(drs[i]['x'],drs[i]['y'],gw,gh);

}

}

}

//小绿数量循环

function st(){

ctx.fillStyle="black";

var st=new Array();

st['x']=10*(gamex-1)+gamex+10;

st['y']=10*(gamey-1)+gamey-10;

sts.push(st);

ctx.fillRect(10*(gamex-1)+gamex+10,10*(gamey-1)+gamey-10,gw,gh);

}

//小绿移动

function stsup(){

if(sts.length>0){

for(var i=0;i<sts.length;i++){

if(sts[i]['y']<0){

sts.splice(i,1);

}else{

sts[i]['y']-=1;

}

ctx.fillStyle="red";

ctx.fillRect(sts[i]['x'],sts[i]['y'],gw,gh);

}

}

}

//大红位置移动

function game(gamex,gamey){

ctx.fillStyle="red";

//ctx.fillRect(10*(gamex-1)+gamex,10*(gamey-1)+gamey,gw,gh);

ctx.fillRect(10*(gamex-1)+gamex,10*(gamey-1)+gamey,20,20);

}

bk();

game(gamex,gamey);

//时间戳

setInterval(function(){

//按键判断

document.onkeydown=function(event){

var e=event || window.event || arguments.callee.caller.arguments[0];

if(e.keyCode=='37'){

gamex=gamex-1;

}

else if(e.keyCode=='38'){

gamey=gamey-1;

}

else if(e.keyCode=='39'){

gamex=gamex+1;

}

else if(e.keyCode=='40'){

gamey=gamey+1;

}

if(e.keyCode=='32'){

st();

}

}

//清空画布

c.width=w;

c.height=h;

bk();

game(gamex,gamey);

drss();

stsup();

dels();

},10);

</script>

布 (canvas) 是 JavaScript 库,用于在网页中创建交互式图形和动画。它提供一个绘图上下文,用于绘制各种图形元素。


画布 API

画布 API 提供以下方法:

  • getContext():获取绘图上下文。
  • fillRect():绘制一个矩形。
  • strokeRect():绘制一个矩形的边框。
  • fillStyle:填充颜色。
  • strokeStyle:边框颜色。

示例

const canvas=document.getElementById("canvas");
const ctx=canvas.getContext("2d");

ctx.fillRect(10 viciss, 10 viciss, 10 viciss, 10 viciss);

图形操作

  • 线条:使用 beginPath()、moveTo() 和 lineTo() 方法绘制线条。
  • 图形:使用 beginPath()、arc() 和 closePath() 方法绘制图形。
  • 文本:使用 fillText() 方法绘制文本。

动画

  • 使用 requestAnimationFrame() 方法在动画帧中更新图形。
  • 使用 transform 属性来改变图形的坐标系。

结论

画布 API 提供了在 JavaScript 中创建交互式图形的强大工具。它适合各种目的,从简单图形到复杂的动画。了解画布 API 可以帮助您在网页上创建令人惊叹的视觉效果。