过年了!开发一套纸笔系统,随时随地在线写春联

2,120 阅读2分钟

PK创意闹新春,我正在参加「春节创意投稿大赛」,详情请看:春节创意投稿大赛

前言

快过年了,辞旧迎新,无论是城市还是农村,家家户户都要贴春联。手写村联更是别有趣味,传统的纸笔必不能随身携带想写就写,鉴于此开发一套在线纸笔系统,可以随时随地掏出手机就能写春联,写的好的可以下载存到本地,甚至可以到打印社直接的打印出来。

效果展示

d721e2a3066a0071f06a53ecfc04d2fd.gif

在线体验

作品展示

微信图片_20220106101600.jpg

未标题-1.png

需求分析

  1. 字体有粗细,写的块则细,写的慢则粗
  2. 字迹不能有断墨
  3. 字体粗细变化要平滑
  4. 可以动态调整字数
  5. 可以将写好的春联保存成图片

代码开发

使用canvas作为春联主体画布,通过鼠标或触摸事件在canvas上画曲线以写出春联,通过监听鼠标或触摸滑动的速度控制曲线的粗细以达到类似毛笔写春联的效果。

春联布局

canvas作为春联画布主体,另外设置三个按钮分别提供重置画布、修改春联字数、下载已写好的春联图片到本地。

<div id="div" style="position: relative">
    <canvas id="canvas">你的浏览器不支持canvas</canvas>
    <div id="controller">
        <div id="clear_btn" class="op_btn" onclick ="changeNum()"> 字数</div>
        <div id="save_btn" onclick="downloadImage()" class="op_btn">保存</div>
        <div id="back_btn" class="op_btn" onclick="onReset()">重置</div>
        <div class="clearfix"></div>
    </div>
</div>

初始化春联

初始化春联canvas默认宽度为300,每个字为正方形即300 X 300,春联默认字数为7,春联上线留白50px, 春联的高度为:宽度 X 字数 + 100,代码如下。

var context;
var canvas;
var canvasWidth
var canvasHeight
//判断是否按下鼠标
var isMouseDown = false;
//记录上一次鼠标所在位置
var lasloc = {x: 0, y: 0};
var lasTimeStamp = 0;
var laslinewidth = -1;
var strokeColor = "black";
var num = 7;
$(function(){
    canvasWidth = 300;
    canvasHeight = canvasWidth * num + 100;
    canvas = document.getElementById("canvas");
    context = canvas.getContext("2d");
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
    drawBackGround();
})

为canvas填充红色背景,在每个字体位置标识即高5px宽10px黑色背景的div,代码如下。

//设置背景及字体位置标识
function drawBackGround(){
    context.fillStyle = "red";
    context.fillRect(0,0,canvasWidth,canvasHeight);
    $(".tag").remove();
    var y = 50;
    for (var i = 0; i <= num; i++) {
        //具体的绘制,鼠标按下之后
        var divElement = document.createElement("div");
        divElement.style.background = "black";
        divElement.style.position = "absolute";
        divElement.style.top = y + "px";
        divElement.style.left = "0px";
        divElement.style.width = "10px";
        divElement.style.height = "5px";
        divElement.setAttribute("class","tag")
        $("#div").append(divElement);
        y += canvasWidth;
    }
}

字体粗细设置

设置字体线宽最大20,最小1防止断墨,速度最大2最小0.1,通过速度动态计算线宽,为使字体线宽过渡平滑,每次只修改线宽的1/3,代码如下。

//笔画速度越快,笔越细,反之越粗!
var maxlinewidth = 20;
var minlinewidth = 1;
var maxlinespeed = 2;
var minlinespeed = 0.1;
//
function CalClientWidth(t, s) {
    var v = s / t;
    var ResultLineWidth;
    //处理速度很慢和很快的情况
    if (v <= minlinespeed)
        ResultLineWidth = maxlinewidth;
    else if (v >= maxlinespeed)
        ResultLineWidth = minlinewidth;
    else
        ResultLineWidth = maxlinewidth - (v - minlinespeed) / (maxlinespeed - minlinespeed) * (maxlinewidth - minlinewidth);
    if (laslinewidth == -1)
        return ResultLineWidth;
    return laslinewidth * 2 / 3 + ResultLineWidth * 1 / 3;
}

设置鼠标和触摸监听

值得注意的是,春联画布比较长,为保证书写的位置与落笔位置相匹配,在计算纵坐标时需要减去滚动条的滚动距离,代码如下。

//鼠标按下
canvas.onmousedown = function (e) {
    //阻止默认事件响应
    e.preventDefault();
    beginStock({x: e.clientX, y: e.clientY});
};
//鼠标松开
canvas.onmouseup = function (e) {
    e.preventDefault();
    endStock();
};
//鼠标移出指定对象时发生
canvas.onmouseout = function (e) {
    e.preventDefault();
    endStock();
};
//鼠标移动过程中
canvas.onmousemove = function (e) {
    e.preventDefault();
    if (isMouseDown) {
        moveStock({x: e.clientX, y: e.clientY});
    }
};
//移动端(触碰相关的事件)
canvas.addEventListener('touchstart', function (e) {
    e.preventDefault();
    //触碰事件,也可能是多点触碰,就是第一个
    let touch = e.touches[0];
    beginStock({x: touch.pageX, y: touch.pageY});
});
canvas.addEventListener('touchmove', function (e) {
    e.preventDefault();
    if (isMouseDown) {
        let touch = e.touches[0];
        moveStock({x: touch.pageX, y: touch.pageY});
    }
});
canvas.addEventListener('touchend', function (e) {
    e.preventDefault();
    endStock();
});

function beginStock(point) {
    isMouseDown = true;
    lasloc = windowToCanvas(point.x, point.y - Math.max($("body").scrollTop(),document.documentElement.scrollTop));
    lasTimeStamp = new Date().getTime();
}

function endStock(point) {
    isMouseDown = false;
}

function moveStock(point) {
    var curloc = windowToCanvas(point.x, point.y - Math.max($("body").scrollTop(),document.documentElement.scrollTop));
    var curTimeStamp = new Date().getTime();

    var s = calDistance(curloc, lasloc);
    var t = curTimeStamp - lasTimeStamp;

    var lineWidth = CalClientWidth(t, s);

    //具体的绘制,鼠标按下之后
    context.beginPath();
    context.moveTo(lasloc.x, lasloc.y);
    context.lineTo(curloc.x, curloc.y);
    context.strokeStyle = strokeColor;
    context.lineWidth = lineWidth;
    //设置线条的帽子,是线条平滑
    context.lineCap = "round";
    context.lineJoin = "round";
    context.stroke();
    lasloc = curloc;
    lasTimeStamp = curTimeStamp;
    laslinewidth = lineWidth;
}

//屏幕坐标点转化为canvas画布坐标,定位canvas画布上的坐标
function windowToCanvas(x, y) {
    //包含canvas距离画布的上和左边距
    var bbox = canvas.getBoundingClientRect();
    return {x: Math.round(x - bbox.left), y: Math.round(y - bbox.top)}
}

//通过两点计算出两点之间距离
function calDistance(loc1, loc2) {
    return Math.sqrt((loc1.x - loc2.x) * (loc1.x - loc2.x) + (loc1.y - loc2.y) * (loc1.y - loc2.y));
}

设置功能按钮

逻辑比较简单,不做说明。

function downloadImage(){
    var image = canvas.toDataURL("image/png");
    var linkElement = document.createElement("a");
    linkElement.setAttribute('href',image);
    linkElement.setAttribute('downLoad','春联');
    linkElement.click();
    drawBackGround();
}

function changeNum(){
    var s = prompt("请填写春联字数");
    if(!s.match(/\d+/) || new Number(s) < 1){
        alert("请填写正确数字")
        return;
    }
    num = new Number(s);
    canvasHeight = canvasWidth * num + 100;
    canvas.height = canvasHeight;
    drawBackGround();
}

function onReset() {
    drawBackGround();
}

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
    <script type="text/javascript" src="${rc.contextPath}/static/js/jquery.min.js"></script>
</head>
<body>
<div id="div" style="position: relative">
    <canvas id="canvas">你的浏览器不支持canvas</canvas>
    <div id="controller">
        <div id="clear_btn" class="op_btn" onclick ="changeNum()"> 字数</div>
        <div id="save_btn" onclick="downloadImage()" class="op_btn">保存</div>
        <div id="back_btn" class="op_btn" onclick="onReset()">重置</div>
        <div class="clearfix"></div>
    </div>
</div>
<script>
    var context;
    var canvas;
    var canvasWidth
    var canvasHeight
    //判断是否按下鼠标
    var isMouseDown = false;
    //记录上一次鼠标所在位置
    var lasloc = {x: 0, y: 0};
    var lasTimeStamp = 0;
    var laslinewidth = -1;
    var strokeColor = "black";
    var num = 7;
    $(function(){
        canvasWidth = 300;
        canvasHeight = canvasWidth * num + 100;
        canvas = document.getElementById("canvas");
        context = canvas.getContext("2d");
        canvas.width = canvasWidth;
        canvas.height = canvasHeight;
        drawBackGround();
        //鼠标按下
        canvas.onmousedown = function (e) {
            //阻止默认事件响应
            e.preventDefault();
            beginStock({x: e.clientX, y: e.clientY});
        };
        //鼠标松开
        canvas.onmouseup = function (e) {
            e.preventDefault();
            endStock();
            //console.log("mouseup");
        };
        //鼠标移出指定对象时发生
        canvas.onmouseout = function (e) {
            e.preventDefault();
            endStock();
        };
        //鼠标移动过程中
        canvas.onmousemove = function (e) {
            e.preventDefault();
            if (isMouseDown) {
                moveStock({x: e.clientX, y: e.clientY});
            }
        };
        //移动端(触碰相关的事件)
        canvas.addEventListener('touchstart', function (e) {
            e.preventDefault();
            //触碰事件,也可能是多点触碰,就是第一个
            let touch = e.touches[0];
            beginStock({x: touch.pageX, y: touch.pageY});
        });
        canvas.addEventListener('touchmove', function (e) {
            e.preventDefault();
            if (isMouseDown) {
                //console.log("mousemove");
                let touch = e.touches[0];
                moveStock({x: touch.pageX, y: touch.pageY});

            }
        });
        canvas.addEventListener('touchend', function (e) {
            e.preventDefault();
            endStock();
        });
    })

    //屏幕坐标点转化为canvas画布坐标,定位canvas画布上的坐标
    function windowToCanvas(x, y) {
        //包含canvas距离画布的上和左边距
        var bbox = canvas.getBoundingClientRect();
        return {x: Math.round(x - bbox.left), y: Math.round(y - bbox.top)}
    }

    //通过两点计算出两点之间距离
    function calDistance(loc1, loc2) {
        return Math.sqrt((loc1.x - loc2.x) * (loc1.x - loc2.x) + (loc1.y - loc2.y) * (loc1.y - loc2.y));
    }

    //笔画速度越快,笔越细,反之越粗!
    var maxlinewidth = 20;
    var minlinewidth = 1;
    var maxlinespeed = 2;
    var minlinespeed = 0.1;

    //计算毛笔宽度
    function CalClientWidth(t, s) {
        var v = s / t;
        var ResultLineWidth;
        //处理速度很慢和很快的情况
        if (v <= minlinespeed)
            ResultLineWidth = maxlinewidth;
        else if (v >= maxlinespeed)

            ResultLineWidth = minlinewidth;
        else
            ResultLineWidth = maxlinewidth - (v - minlinespeed) / (maxlinespeed - minlinespeed) * (maxlinewidth - minlinewidth);
        if (laslinewidth == -1)
            return ResultLineWidth;
        return laslinewidth * 2 / 3 + ResultLineWidth * 1 / 3;
    }

    //设置背景及字体位置标识
    function drawBackGround(){
        context.fillStyle = "red";
        context.fillRect(0,0,canvasWidth,canvasHeight);
        $(".tag").remove();
        var y = 50;
        for (var i = 0; i <= num; i++) {
            //具体的绘制,鼠标按下之后
            var divElement = document.createElement("div");
            divElement.style.background = "black";
            divElement.style.position = "absolute";
            divElement.style.top = y + "px";
            divElement.style.left = "0px";
            divElement.style.width = "10px";
            divElement.style.height = "5px";
            divElement.setAttribute("class","tag")
            $("#div").append(divElement);
            y += canvasWidth;
        }
    }

    function beginStock(point) {
        isMouseDown = true;
        lasloc = windowToCanvas(point.x, point.y - Math.max($("body").scrollTop(),document.documentElement.scrollTop));
        lasTimeStamp = new Date().getTime();
    }

    function endStock(point) {
        isMouseDown = false;
    }

    function moveStock(point) {
        var curloc = windowToCanvas(point.x, point.y - Math.max($("body").scrollTop(),document.documentElement.scrollTop));
        var curTimeStamp = new Date().getTime();

        var s = calDistance(curloc, lasloc);
        var t = curTimeStamp - lasTimeStamp;

        var lineWidth = CalClientWidth(t, s);

        //具体的绘制,鼠标按下之后
        context.beginPath();
        context.moveTo(lasloc.x, lasloc.y);
        context.lineTo(curloc.x, curloc.y);
        context.strokeStyle = strokeColor;
        context.lineWidth = lineWidth;
        //设置线条的帽子,是线条平滑
        context.lineCap = "round";
        context.lineJoin = "round";
        context.stroke();
        lasloc = curloc;
        lasTimeStamp = curTimeStamp;
        laslinewidth = lineWidth;
    }

    function downloadImage(){
        var image = canvas.toDataURL("image/png");
        var linkElement = document.createElement("a");
        linkElement.setAttribute('href',image);
        linkElement.setAttribute('downLoad','春联');
        linkElement.click();
        drawBackGround();
    }

    function changeNum(){
        var s = prompt("请填写春联字数");
        if(!s.match(/\d+/) || new Number(s) < 1){
            alert("请填写正确数字")
            return;
        }
        num = new Number(s);
        canvasHeight = canvasWidth * num + 100;
        canvas.height = canvasHeight;
        drawBackGround();
    }

    function onReset() {
        drawBackGround();
    }
</script>

<style>
    /* CSS Document */

    #canvas {
        display: block;
        margin: 0 auto;
        border: 1px solid #aaa;
    }

    #controller {
        position: fixed;
        width: 200px;
        bottom: 180px;
        right: -90px;
        transform: rotate(90deg);
    }

    .op_btn {
        float: right;
        margin: 10px 0 0 10px;
        border: 1px solid #efefef;
        width: 50px;
        height: 25px;
        line-height: 25px;
        font-size: 12px;
        text-align: center;
        border-radius: 2px;
        cursor: pointer;
        background: #fff;
    }

    .op_btn {
        background-color: red;
        color:#fff;
    }

    .clearfix {
        clear: both;
    }
</style>

</body>
</html>