百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

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

toyiye 2024-07-06 23:31 13 浏览 0 评论

示例简介

用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有跨域问题,所以需要发布到服务器上,才能正常使用;

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码