评论涂鸦
前几天在 Joe(https://ae.js.cn/)网站上留言的时候发现了一个叫“画图模式”的东西,点进去后自动切换文本框到画板了(类似QQ涂鸦,你画我猜那种画板),然后可以在画板上画画,还可以选择画笔粗细、颜色等等,画错了还能撤销各种功能,欸感觉挺有意思的,当时也猜到了应该是用 canvas 做的,不过自己也不太了解这块,但就是感觉挺有意思的,加上我又喜欢魔改 valine 评论,所以立下计划决定给评论系统加上这么一个好玩的功能。
评论涂鸦画板样式
Canvas
说起 html 画图,肯定避不开 html5 的 canvas 技术,canvas 能提供的不仅是画图功能,很多网页游戏也都是基于 canvas 制作的。我们要实现 canvas 画板,首先还得了解 canvas 本身的一些语法 api 之类的东西,然后再思考实现的思路,最后再结合评论系统将功能写出来附加上去测试(关于 canvas 的基础语法可以在 w3school 或者 runoob 教程网站自行查询)
实现思路
简单来说首先要实现的还是画图功能,先创建 canvas 面板,再给面板添加画图触发事件(鼠标按下并移动、松开等),然后添加画板工具事件(画笔颜色、粗细),最后绑定完成画板功能事件(撤销、重做、擦除、清除)
Valine
通过创建 canvas 画板加入到 valine 评论中,需要先定位到 valine.js 中的 </textarea>
后添加元素
<div class="canvas_paint_board" style="display:none">
<div class="paint_tools">
<input id="fill" type="color" title="画笔颜色"> 粗细
<input id="bold" type="number" title="画笔粗细">
<button id="undraw" title="上一步">撤销</button>
<button id="redraw" title="下一步">重做</button>
<button id="eraser" title="橡皮擦">擦除</button>
<button id="clear" title="全部清除">清屏</button>
</div>
<canvas id="canvas"></canvas>
然后先定位到 class="vctrl"
在 vctrl 内部添加画板控制按钮
<span class="painting-btn" title="Canvas 画图面板">涂鸦画板</span>
主要功能
完成以上配置可以看到已经添加的元素及切换功能,然后是一大串的 canvas 结合 valine 评论配置(以下代码格式化带注释,有问题可以留言)
已更新移动端代码支持
定位到 e.nodata.show(),e}}
后添加
var mycanvas = document.getElementById('canvas'), //canvas 元素
ctx = mycanvas.getContext('2d'), //创建 canvas 2d 画板
vedit = document.getElementsByClassName("vedit")[0], //canvas 父元素
veditor = document.getElementById('veditor'), //文本框 元素
eraser = document.getElementById('eraser'), //撤销(橡皮擦)按钮
clear = document.getElementById('clear'), //清屏 按钮
number = document.getElementById('bold'), //粗细 输入框
color = document.getElementById('fill'), //取色框
lineColor = "#eb6844", //默认画笔颜色(6位hex值)
width = 1102, //canvas 默认画板宽度
height = 322, //canvas 默认画板高度
lineBold = 5, //默认画笔粗细(5px)
trigger = false, //默认橡皮擦状态(关闭)
drawCount = 0, //已画图 计数
drawHistory = [], //已画图 数组
//画笔移动函数
move = (down_x, down_y, move_x, move_y) => {
//判断是否启用橡皮擦
if (trigger == true) {
//canvas 擦除开始
ctx.lineTo(down_x, down_y);
ctx.lineTo(move_x, move_y);
ctx.clearRect(move_x, move_y, number.value, number.value)
} else {
//canvas 画图开始
ctx.beginPath();
ctx.lineTo(down_x, down_y);
ctx.lineTo(move_x, move_y);
ctx.stroke()
}
//此函数内记录最后坐标会导致画笔闭合路径无效
},
// 定义画图函数
draw = () => {
//鼠标按下事件
mycanvas.onmousedown = () => {
let down_x = event.offsetX, //按下时 x 坐标
down_y = event.offsetY; //按下时 y 坐标
//鼠标移动事件
mycanvas.onmousemove = () => {
let move_x = event.offsetX, //(按下并)移动时 x 坐标
move_y = event.offsetY; //(按下并)移动时 y 坐标
document.body.style.userSelect="none"; //禁用选中(优化体验)
//画笔移动函数
move(down_x,down_y,move_x,move_y);
//记录最后坐标
down_x = move_x;
down_y = move_y;
//首次移除画板触发断点续连
mycanvas.onmouseup=()=>{
drawdone();
move(down_x,down_y,move_x,move_y)
}
};
mycanvas.onmouseup=()=>{
drawdone(); //修复点击鼠标松开后移动鼠标未解除绑定 bug
}
}
//触摸按下事件
mycanvas.ontouchstart = (ots) => {
ots.preventDefault(); //阻止默认事件
let boundingTopStart = canvas.getBoundingClientRect().top, //触摸时 当前画板相对可视页面顶部距离
boundingLeftStart = canvas.getBoundingClientRect().left,, //触摸时 当前画板相对可视页面侧面距离
down_x = ots.offsetX-boundingLeftStart, //按下时 x 坐标
down_y = ots.offsetY-boundingTopStart; //按下时 y 坐标
//触摸移动事件
mycanvas.ontouchmove = (otm) => {
document.body.style.userSelect="none"; //禁用选中(优化体验)
let boundingTopMove = canvas.getBoundingClientRect().top, //触摸并移动时 当前画板相对可视页面顶部距离
boundingLeftMove = canvas.getBoundingClientRect().left,, //触摸并移动时 当前画板相对可视页面侧面距离
move_x = otm.offsetX-boundingLeftMove, //(触摸并)移动时 x 坐标
move_y = otm.offsetY-boundingTopMove; //(触摸并)移动时 y 坐标
//画笔移动函数
move(down_x,down_y,move_x,move_y);
//记录最后坐标
down_x = move_x;
down_y = move_y;
//首次移除画板触发断点续连
mycanvas.ontouchend=()=>{
drawdone();
move(down_x,down_y,move_x,move_y)
}
}
}
},
//清空已绑定事件函数
unbind = () => {
mycanvas.onmousedown = null;
mycanvas.onmousemove = null;
mycanvas.onmouseup = null; //修复画笔移出画板外再移进画板内画笔断连现象
document.body.style.userSelect=""; //解除禁用选中
mycanvas.ontouchstart = null;
mycanvas.ontouchmove = null;
mycanvas.ontouchend = null;
},
//canvas 画图完成(松开)执行函数
drawdone = () => {
unbind(); //清空已绑定事件
draw(); //执行画图函数
let baseUrl = canvas.toDataURL("image/png"), //获取已画图的 base64 链接
imgDom = '<img id="draw" src="' baseUrl '" />'; //写入链接到 img 标签
veditor.value = imgDom; //将已写入 base64 的 img 标签写入输入框
veditor.focus(); //聚焦输入框
//drawCount ; //直接画图次数 1(drawCount )会导致无法撤销再涂鸦之后无法定位到最新画图记录(index)
drawCount = drawHistory.length 1; //定位到最新涂鸦记录
drawHistory.push(baseUrl) //记录当前涂鸦到已画图数组
},
//清屏函数
clearCanvas = (ctx) => {
ctx.clearRect(0, 0, width, height)
},
//canvas 默认配置
initCanvas = () =>{
canvas.width = width; //canvas 宽度
canvas.height = height; //canvas 高度
canvas.style = "cursor:crosshair"; //canvas 样式
ctx.lineCap = 'round'; //画笔类型(圆)
ctx.lineWidth = lineBold; //画笔粗细
ctx.strokeStyle = lineColor; //画笔颜色
color.value = lineColor; //取色框颜色
number.value = lineBold; //输入框粗细值
};
//判断 canvas 父元素是否存在,是则获取父元素高宽写入 canvas,否则获取默认 canvas 高宽
if(vedit != null){
var cpb = document.getElementsByClassName('canvas_paint_board')[0], //画板父元素
btn = document.getElementsByClassName('painting-btn')[0], //画板切换按钮
btnSwitch = false;
width = vedit.clientWidth - 40; //定位到文本框宽度
//切换点击事件
btn.onclick = () =>{
width = vedit.clientWidth - 40; //修改默认 width 参数后再重复初始化参数(直接 canvas.width 覆盖会造成其他默认配置无效)
initCanvas(); //更新修改后的 canvas 配置
if(btnSwitch == false){
btnSwitch = true;
btn.innerText = "关闭面板";
cpb.style.display = "block";
veditor.style.cssText = "min-height:50px;max-height:0px;"
}else{
btnSwitch = false;
btn.innerText = "涂鸦面板";
cpb.style.display = "none";
veditor.style.cssText = "min-height:;max-height:";
}
//min-height 和 max-height 属性的设置是为了在填充 base64 链接到 valine 文本框时防止字符过长导致的文本框高度问题
}
}
initCanvas(); //初始化 canvas 参数
draw(); //执行画图函数
//颜色 输入框变更时,将变更后的值写入画笔颜色
color.onchange = function() {
ctx.strokeStyle = this.value
};
//粗细 输入框变更时,将变更后的值写入到画笔粗细
number.onchange = function() {
this.value <= 1 ? (this.value = 1, ctx.lineWidth = 1) : ctx.lineWidth = this.value; //判断如果值小于1则强制等于1
};
//擦除 按钮点击时,切换显示状态
eraser.onclick = () = >{
//判断橡皮擦默认状态(trigger)如果已开启则关闭,否则开启
trigger == false ? (trigger = true, eraser.innerText = "取消擦除") : (trigger = false, eraser.innerText = "擦除");
};
//清屏 按钮点击时,执行清屏函数
clear.onclick = () = >{
clearCanvas(ctx);
veditor.value = null; //清空输入框
veditor.focus() //聚焦输入框(过长的 base64 字符会导致清除后还能提交涂鸦到评论)
};
//撤销(上一步)事件点击函数
undraw.onclick = () = >{
drawCount > 0 ? drawCount--:drawCount = 0; //判断画图次数并递减
//判断画图次数,如果已是最后记录则清空并聚焦文本框,重置画图次数
drawCount <= 1 ? (veditor.value = null, veditor.focus()) : false;
let stepback = drawHistory[drawCount - 1], //选择当前涂鸦的前一个涂鸦数组
imgDom = new Image, //新建 image
img = '<img id="draw" src="' stepback '" />'; //写入前一个涂鸦到 img 标签
//判断并插入已写入 src 属性的 image 到文本框并聚焦
stepback != undefined ? (imgDom.src = stepback, veditor.value = img, veditor.focus()) : false;
clearCanvas(ctx); //撤销前执行清屏
//给 image 绑定 load 事件后执行 canvas 自带的 drawImage() 画图函数
imgDom.addEventListener('load', () = >{
ctx.drawImage(imgDom, 0, 0)
})
};
//重做(下一步)事件点击函数(和撤销类似,不再注释)
redraw.onclick = () = >{
drawCount < drawHistory.length ? drawCount :drawCount = drawHistory.length;
let stepfoward = drawHistory[drawCount - 1],
imgDom = new Image,
img = '<img id="draw" src="' stepfoward '" />';
stepfoward != undefined ? (imgDom.src = stepfoward, veditor.value = img, veditor.focus()) : false;
clearCanvas(ctx);
imgDom.addEventListener('load', () = >{
ctx.drawImage(imgDom, 0, 0)
})
};
21.7.18 修复信息
修复点击再松开鼠标时移动鼠标仍可继续绘画 bug