artwlfeedback.js——仿google搜索结果页的“发送反馈”功能
缘起
不知道大家有没有用过google搜索结果页的“发送反馈”功能(还没有用过的,快去体验一下吧),个人用过后觉得非常酷,特别适合反馈界面视觉问题,于是就有了本文介绍的小作品。
给不能FQ的截张图吧:
效果
不知道大家有没有注意到本页最下面有个“发送反馈”的固定链接,可以点击看看效果。下面是chrome下的效果:
注:需要浏览器支持HTML5
原理
通过查看google搜索结果页反馈时的代码可以看到,是把页面生成了一个canvas,然后在canvas上画矩形来实现的:
所以在不支持canvas的浏览器下,是没有这个效果的。
我的方案是利用html2canvas库把页面内容渲染成一个canvas,然后利用canvas的画图功能来做标记,然后把canvas转换为base64格式的图片用于发送反馈。
代码
代码比较简单,就是先用调用html2canvas库把页面转换成一个canvas,然后把这个canvas添加到页面上,然后再创建一个空白的canvas用于画标记(矩形),添加一个空白的canvas的作用是如果标记有误方便清除。其他的就是canvas的画图的代码,最后当用户点击保存时合并前面创建的两个canvas,并利用canvas的toDataUrl方法把canvas转换为base64格式的png图片输出,后续的操作开发者就可以自己定义了。
JS代码如下(canvas画图的代码有参考 Javascript实现canvas画图功能 一文):
var artwlfeedback = (function(){ var load =function(callback){ html2canvas(document.body, { onrendered: function(canvas) { canvas.id = "artwlfeedback_pagecanvas"; var cv = document.createElement("canvas"); cv.width = canvas.width; cv.height = canvas.height; cv.style.background = "#666"; cv.id = "artwlfeedback_canvas"; document.body.appendChild(cv); document.body.appendChild(canvas); init(callback); } }); } var init = function(callback){ var paint={ init:function(){ this.addDrawTool(); this.load(); this.bind(); }, addDrawTool: function(){ var NewLine = '\n'; var drawToolHtml = ''; drawToolHtml+=' <div id="artwlfeedback_operate">'+NewLine; drawToolHtml+=' <input id="artwlfeedback_clear" type="button" value=" " title="clear"/>'+NewLine; drawToolHtml+=' <input id="artwlfeedback_cancel" type="button" value=" " title="cancel"/>'+NewLine; drawToolHtml+=' <input id="artwlfeedback_save" type="button" value=" " title="save as image" />'+NewLine; drawToolHtml+=' </div>'+NewLine; var drawToolNode = document.createElement("div"); drawToolNode.id = "artwlfeedback_draw_tool"; drawToolNode.className = "artwlfeedback"; drawToolNode.innerHTML = drawToolHtml; document.body.appendChild(drawToolNode); }, load:function(){ this.x=[];//记录鼠标移动是的X坐标 this.y=[];//记录鼠标移动是的Y坐标 this.clickDrag=[]; this.Rectangles = []; this.lock=false;//鼠标移动前,判断鼠标是否按下 this.storageColor="#000000"; this.$=function(id){return typeof id=="string"?document.getElementById(id):id;}; this.canvas=this.$("artwlfeedback_canvas"); this.pageCanvas = this.$("artwlfeedback_pagecanvas"); this.cxt=this.canvas.getContext('2d'); this.cxt.lineJoin = "round";//context.lineJoin - 指定两条线段的连接方式 this.cxt.lineWidth = 2;//线条的宽度 this.iptClear=this.$("artwlfeedback_clear"); this.cancel= this.$("artwlfeedback_cancel"); this.saveAs = this.$("artwlfeedback_save"); this.w=this.pageCanvas.width;//取画布的宽 this.h=this.pageCanvas.height;//取画布的高 this.touch =("createTouch" in document);//判定是否为手持设备 this.StartEvent = this.touch ? "touchstart" : "mousedown";//支持触摸式使用相应的事件替代 this.MoveEvent = this.touch ? "touchmove" : "mousemove"; this.EndEvent = this.touch ? "touchend" : "mouseup"; this.drawTool = this.$("artwlfeedback_draw_tool"); this.callback = callback; }, bind:function(){ var t=this; /*清除画布*/ this.iptClear.onclick=function(){ t.clear(); t.Rectangles.length = []; }; this.cancel.onclick = function(){ t.removeNode(t.pageCanvas); t.removeNode(t.canvas); t.removeNode(t.drawTool); }; /*保存*/ this.saveAs.onclick = function(){ //创建新canvas用于合并pageCanvas和canvas var saveCanvas = document.createElement('canvas'); saveCanvas.width = t.w; saveCanvas.height = t.h; var saveCxt = saveCanvas.getContext('2d'); saveCxt.fillStyle = "#666"; saveCxt.fillRect(0, 0, t.w, t.h); saveCxt.globalAlpha=1; saveCxt.drawImage(t.canvas, 0, 0); saveCxt.globalAlpha=0.5; saveCxt.drawImage(t.pageCanvas, 0, 0); t.removeNode(t.pageCanvas); t.removeNode(t.canvas); t.removeNode(t.drawTool); //输出图片 var imgData = saveCanvas.toDataURL("image/png"); if(t.callback){ callback(imgData); } else { var w=window.open('about:blank','image from canvas'); w.document.write("<img src='"+imgData+"' alt='from canvas'/>"); } }; /*鼠标按下事件,记录鼠标位置,并绘制,解锁lock,打开mousemove事件*/ this.canvas['on'+t.StartEvent]=function(e){ var touch=t.touch ? e.touches[0] : e; var scrollTop = window.pageYOffset|| document.documentElement.scrollTop || document.body.scrollTop; t.movePoint(touch.clientX - touch.target.offsetLeft,touch.clientY - touch.target.offsetTop + scrollTop);//记录鼠标位置 t.lock=true; t.drawTool.style.display = "none"; }; /*鼠标移动事件*/ this.canvas['on'+t.MoveEvent]=function(e){ var touch=t.touch ? e.touches[0] : e; if(t.lock)//t.lock为true则执行 { var _x=touch.clientX - touch.target.offsetLeft;//鼠标在画布上的x坐标,以画布左上角为起点 var scrollTop = window.pageYOffset|| document.documentElement.scrollTop || document.body.scrollTop; var _y=touch.clientY - touch.target.offsetTop + scrollTop;//鼠标在画布上的y坐标,以画布左上角为起点 t.movePoint(_x,_y,true);//记录鼠标位置 t.drawRectangle(); } }; this.canvas['on'+t.EndEvent]=function(e) { /*重置数据*/ t.lock=false; t.Rectangles.push([t.x[0], t.y[0], t.x[t.x.length -1] - t.x[0], t.y[t.y.length -1] - t.y[0]]); t.x=[]; t.y=[]; t.clickDrag=[]; t.drawTool.style.display = "block"; }; }, movePoint:function(x,y,dragging){ /*将鼠标坐标添加到各自对应的数组里*/ this.x.push(x); this.y.push(y); this.clickDrag.push(y); }, drawRectangle: function(){ var width = this.x[this.x.length-1] - this.x[0], height = this.y[this.y.length-1] - this.y[0]; this.clear(); var i = this.Rectangles.length; if(i){ for (i=i-1; i >= 0; i--) { var rectangle = this.Rectangles[i], r_x = rectangle[0], r_y = rectangle[1], r_width = rectangle[2], r_height = rectangle[3]; this.cxt.strokeRect(r_x, r_y, r_width, r_height); // 只勾画出矩形的外框 this.cxt.fillStyle = "#FFFFFF"; this.cxt.fillRect(r_x, r_y, r_width, r_height); // 画出矩形并使用颜色填充矩形区域 }; } this.cxt.strokeRect(this.x[0], this.y[0], width, height); // 只勾画出矩形的外框 this.cxt.fillStyle = "#FFFFFF"; this.cxt.fillRect(this.x[0], this.y[0], width, height); // 画出矩形并使用颜色填充矩形区域 }, clear:function(){ this.cxt.clearRect(0, 0, this.w, this.h);//清除画布,左上角为起点 }, removeNode: function(node){ node.parentNode.removeChild(node); } }; paint.init(); } return { load: load } })();
调用
关于如何调用可参考这里:http://afeedback.duapp.com/
局限
由于html2canvas有跨域限制,所以如果页面用了不同域下的图片(如本文)就不能正常显示。
另外,由于html2canvas是根据HTML代码重新渲染成canvas,而有些css无法识别,会造成页面跟canvas上的不完全一致。
改进空间和后续计划
目前在不支持canvas的浏览器下没有任何效果,这个可改进为传统方式。
html2canvas的库比较大,后续会改进为当用户点击反馈链接时进行异步加载。
当然,大家在体验的过程中如果有什么意见和建议非常欢迎提出,一起完善。