整合营销服务商

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

免费咨询热线:

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

了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最好的免费响应式HTML5网页模板供大家学习。

为什么HTML5, Bootstrap和CSS3的网页模板资源如此受欢迎?

1. 作为一种全新的语言,HTML5支持所有浏览器兼容的浏览器,是创建优秀网站的最新标记语言。由于HTML5语言的日益普及,所以HTML5网站模板也很受欢迎。

2. CSS3是CSS语言的最新版本,用于提供最佳的样式网站,如无限的颜色组合,很棒的字体样式,字体选择等等。总的来说, CSS3语言使您的网站美丽而时尚。

3. Bootstrap已经成为用户界面开发人员最喜欢的前端框架之一,其优势在于其开源的可用性。 它自己修改后的书面CSS为UI开发人员节省了大量时间。 此外,Bootstrap具有一些创新功能,如移动友好型,SAAS,干净轻便的代码,跨浏览器兼容性等等,使得大多数设计人员使用此框架可以用较少的时间和精力创建响应式网站。

5个最好的免费响应式HTML5网页模板 -- 2018

1. Boxus - 软件公司和网页设计公司的创意网站模板

开发技术:HTML 5, CSS 3, JS, jQuery

网站特色:

l 创意机构模板

l 粘性的导航条

l 谷歌地图

l 社交媒体图标

l 色彩斑斓的接口

l 字体图标

l 明亮的配色方案

Boxus是一个充满创意和活力的免费HTML5软件公司和网页设计公司的创意网站模板。其独特的布局以及响应速度非常出色。 最重要的是,它提供了最新的JavaScript插件,使模板更加高效和强大。 你要知道,一个具有启发性的令人惊叹的免费HTML5网页模板可以大大减少耗时并提高生产力。

2. AweSplash - 免费的HTML闪屏页面

开发技术:HTML 5, CSS 3, JS, jQuery

网站特色:

l 滑块

l 响应式视网膜菜单

l 幽灵按钮

l SEO友好

l 设备响应

l jQuery&Javascript插件

l YouTube和Vimeo Player插件

AweSplash非常适合作为欢迎页面或任何其他着陆页来推出新产品或宣布即将举办的活动。它的主要吸引力是它的4个不同的演示页面。幽灵按钮可让您链接到即将推出的产品。使用名为Animate Headline的Javascript插件,页面变得更加美观。在这个免费HTML5启动画面模板的演示中,你可以看到带有美丽背景滑动图像的页面。

3. Beverages - 餐厅类Bootstrap响应式网页模板

开发技术:HTML 5, CSS 3, JS, Bootstrap

网站特色:

l 完全响应

l 支持自定义

l 使用有效的HTML5和CSS3代码构建

l 使用Google网络字体

l Bootstrap框架

Beverages是100%响应式餐厅主题网站模板,适用于任何食品和饮料网站的设计。兼容所有设备,显示在所有屏幕尺寸上。它完全建立在Bootstrap框架中,HTML5,CSS3和JQuery.你可以轻松的将这个模板与任何其他类型的生意相结合。

4. TravelAir - 旅游观光HTML网站模板

开发技术:HTML 5, CSS 3, JS, jQuery

网站特色:

l Bootstrap 4

l HTML5和CSS3

l 粘滞标题

l 跨浏览器兼容性

l Google字体

TravelAir拥有独特而富有创意的主页设计,其现代风格的设计布局。 主页上有一个带有标题文字的猫头鹰旋转木马滑块。此外,有jQuery UI日历的旅行预订表格。在主页有旅游套餐,最热门的目的地和关于您的公司的部分,让网站访问者和专业外观的网站印象深刻。

5. Jessica- 营养师网站模板

开发技术:HTML 5, CSS 3, JS, jQuery

网站特色:

l Bootstrap V3 +

l 极简设计

l HTML5 CSS3

l 谷歌字体(蒙特塞拉特)下载

l 风格指南(开发人员用途和模板设计指南)

作为营养师网站模板,Jessica采用了极简风格的网页设计,颜色搭配非常美观,图片看起来让人很有食欲。营养网站模板对健康,健身,美体,美食,美容,饮食,减肥教练,女教练,女性饮食等主题都是新鲜而具有吸引力的。

3个最好的免费Bootstrap网页模板 -- 2018

1. Vex - 免费Bootstrap 4着陆页模板

开发技术:HTML 5, CSS 3, Bootstrap 4 alpha.5, JS, jQuery

网页特色:

l 视差背景效果

l 电子邮件订阅选项

l 页脚菜单

l Bootstrap 4框架

l 友好的用户界面

Vex由最近发布的Bootstrap 4 CSS框架构建而成,非常灵敏。由于Bootstrap 4为开发人员和用户提供了更多的舒适性和灵活性,Vex模板在小屏幕上可以发挥出色的效果。

2. Conceit - 企业类Bootstrap响应式Web模板

开发技术:Bootstrap framework, HTML5, CSS3, JQuery

网页特色:

l 100%响应Bootstrap滑块

l 基于Font Awesome的图标

l HTML5和CSS3

l Google字体

l Bootstrap框架

l 图像转换效果

Conceit是一个现代主题多页多用途商业和企业相关高利用率网站模板,支持用户构建自己的创意网站。这个模板提供了很多实用的页面包括关于页面,联系页面,404页面,最新博客等。这个模板的设计是完全基于Bootstrap框架,HTML5,CSS3和JQuery构建的100%响应式跨浏览器模板。

3. Asentus - 免费的响应式引导页HTML5模板

开发技术:HTML 5, CSS 3, Bootstrap 3, JS, jQuery

网页特色:

l 粘滞菜单栏

l 滑动标题背景

l 幽灵按钮

l HTML5 / CSS3

如果你想要轻量级,灵活且易于定制,免费供商业和个人使用的企业代理网站模板; Asentus正是你想要的。这是一个免费的自适应引导企业代理机构的HTML5模板。 超级干净,优雅的风格。

1. Garage - 免费的HTML5 CSS3 Bootstrap响应式网页模板

开发技术:HTML 5, CSS 3, Bootstrap 3, JS, jQuery

网页特色:

l 视差效应

l W3C有效标记

l 平滑过渡效果

l 跨浏览器支持

l 100%响应式布局

l 100%的搜索引擎友好

Garage是由webdomus开发团队开发的完全特殊的创意模板,特别适用于古董或经典汽车展示。这个多页面的HTML5 CSS3 Bootstrap响应模板有相关章节,可以满足客户的需求。

2. Graffiti Artist - 免费的涂鸦艺术类CSS网页模板

开发技术:HTML 5, CSS 3,

网站特色:

l 便捷的网页编辑入口

l 丰富的教程

l 设计工具

Graffiti是一个适于涂鸦艺术家,街头摄影师和创意专业人士的CSS网页模板。艺术作品和创意项目在模板正面和中心位置展示,非常吸引人。引人注目的黑白媒体以及视差滚动为丰富多彩的独特风格提供了完美的背景。

总结:

这些免费的HTML网站模板对网页设计师和开发甚至初学者都很有用,他们不需要花费过多的精力就可以自己创建的个人网站。如果你想把握2018年最新最好的免费响应式HTML5, Bootstrap, CSS网页设计,不妨将上面的网页模板下载下来自己研究,激发自己的创作灵感。

如果你不会任何开发语言但也想同样拥有自己的网站,推荐你借助原型设计工具,例如国产的Mockplus快速完成网页模板设计。如果想像这些优秀的模板那样,直接下载套用也是可以的。除软件内置的丰富网页模板,Mockplus官网上也提供了很多优秀的真实网页模板。直接下载原文件,在Mockplus桌面端打开即可开始设计。只需要通过Mockplus的图片组件导入自己的图片和自定义组件,就可以快速的完成一个中低保真的HTML5网页原型设计。

鸟网作者:bxj990915

出自蜂鸟网-技巧讨论,原贴链接:https://m.fengniao.com/thread/10909935.html

光绘之习拍

欢迎关注 蜂鸟网微信公众号:fengniaoweixin