【canvas系列】canvas实现“ 简单的Amaziograph效果”--画对称图【强迫症福利】
标题很难引人入胜,先放个效果图好了
如果图片吸引不了你,那我觉得也就没啥看的了。
demo链接: https://win7killer.github.io/demo_set/html_demo/canvas/can_demo/draw_roll_2.html
*************************************************
上次“雷达图效果”文章很荣幸,被“某天头条”抓数据抓去了,不开心的是demo链接等所有链接都干掉了~~~ blabla,连个名字都木有。
想看的再看下: http://www.cnblogs.com/ufex/p/6655336.html
*************************************************
创意来源
之前看到的gif效果,为了这个文章又去找了一下。貌似是ipad的app “Amaziograph”。看起来真的很爽,很美
配上我自己画的图先:
手残不会画画,各位见笑。(手机上浏览器画的哦)
DEMO讲解
1.效果分析
a.参考线坐标轴 -- 为了简单控制参考线显示隐藏,单独一个canvas来搞,也不用每次重绘
b.绘画主体 -- 绘画效果(canvas画线);对称效果(canvas旋转)
c.配置区 -- 简单dom
简单来看,很容易实现嘛
2.开搞
1> 坐标系统
其实就是画几条线,但是要均分角度。一种方法是,计算出各个点,然后从中心点发散去画线;另一种是,一边旋转canvas,一边画圆心到统一坐标的线。由于绘画是需用到canvas旋转,所以这里统一使用旋转来处理。
那么,就需要先来处理canvas旋转
1 function drawRotate(deg, fn, _ctx) { 2 _ctx = _ctx || ctx 3 _ctx.save(); 4 _ctx.translate(_ctx.canvas.width / 2, _ctx.canvas.height / 2); 5 _ctx.rotate(deg); 6 fn && fn(_ctx); 7 _ctx.restore(); 8 }
当然,这个是我尝试多次之后写好的方法。
1、存储ctx状态到栈,
2、移动旋转点(canvas坐标原点)到canvas中心,
3、旋转指定角度,
4、执行绘制函数fn,
5、从栈里边取回ctx的状态(包含但不仅包含 fillStyle、strokenStyle、translate等等),这里主要处理的是translate,因为我们下次用到坐标会受影响,所以要让canva坐标原点回到原来的位置。
其实这里translate还是比较抽象比较绕的。。。可能我比较迟缓
然后,是绘制参考线坐标
1 function baseLine() { 2 ctx_role.clearRect(0, 0, ctx_role.canvas.width, ctx_role.canvas.height); 3 var deg = 360 / pieace; 4 console.log(deg); 5 ctx_role.lineWidth = 1; 6 ctx_role.strokeStyle = 'rgba(0,0,0,.5)'; 7 for (var i = 0, l = pieace; i < l; i++) { 8 drawRotate(i * deg / 180 * Math.PI, function(ctx_role) { 9 draw({ 10 bx: can_role.width / 2, 11 by: can_role.width / 2, 12 ex: can_role.width / 2 + can_role.width, 13 ey: can_role.width / 2 14 }, ctx_role); 15 }, ctx_role); 16 } 17 }
1 function draw(option, _ctx) { 2 _ctx = _ctx || ctx; 3 _ctx.beginPath(); 4 _ctx.moveTo(option.bx - _ctx.canvas.width / 2, option.by - _ctx.canvas.height / 2); 5 _ctx.lineTo(option.ex - _ctx.canvas.width / 2, option.ey - _ctx.canvas.height / 2); 6 _ctx.stroke(); 7 }
这样,就绘制完成参考线。
2>绘画主体
首先处理一般的画线。跟拖拽效果类似,在move过冲中一直画线链接两个点。对拖拽不了解的可以去了解下,直接上代码
1 function bindPc() { 2 can.onmousedown = function(e) { 3 if (e.button != 0) { 4 return false; 5 } 6 7 var op = {}; 8 op.ex = op.bx = e.clientX - can.parentElement.offsetLeft + window.scrollX; 9 op.ey = op.by = e.clientY - can.parentElement.offsetTop + window.scrollY; 10 drawFn(op); 11 document.onmousemove = function(e) { 12 document.body.style.cursor = 'pointer'; 13 op.bx = op.ex; 14 op.by = op.ey; 15 op.ex = e.clientX - can.parentElement.offsetLeft + window.scrollX; 16 op.ey = e.clientY - can.parentElement.offsetTop + window.scrollY; 17 drawFn(op); 18 }; 19 document.onmouseup = function() { 20 document.body.style.cursor = 'default'; 21 document.onmouseup = document.onmousemove = null; 22 }; 23 }; 24 }
1 function drawFn(op) { 2 var deg = Math.floor(360 / pieace); 3 for (var i = 0, l = 360; i < l; i += deg) { 4 drawRotate(i / 180 * Math.PI, function(ctx) { 5 draw(op); 6 }); 7 } 8 }
需要注意,e.button 用来判断是鼠标哪个键,0是左键
这里又用到了前边的drawRotate 和 draw。
************************************
至此,应该可以画出对称的线条了。
以下就是锦上添花的事情了
************************************
增加移动端的绘制支持(惭愧,没怎么写过移动端,欢迎多指教)
1 function bindWp() { 2 can.addEventListener('touchstart', function(e) { 3 op = can.op = {}; 4 op.ex = op.bx = e.touches[0].clientX - can.parentElement.offsetLeft + window.scrollX; 5 op.ey = op.by = e.touches[0].clientY - can.parentElement.offsetTop + window.scrollY; 6 drawFn(op); 7 can.addEventListener('touchmove', touchMoveFn); 8 can.addEventListener('touchend', touchEndFn); 9 }); 10 11 function touchEndFn() { 12 document.body.style.cursor = 'default'; 13 can.removeEventListener('touchmove', touchMoveFn); 14 can.removeEventListener('touchend', touchEndFn); 15 } 16 17 function touchMoveFn(e) { 18 op = can.op; 19 document.body.style.cursor = 'pointer'; 20 op.bx = op.ex; 21 op.by = op.ey; 22 op.ex = e.touches[0].clientX - can.parentElement.offsetLeft + window.scrollX; 23 op.ey = e.touches[0].clientY - can.parentElement.offsetTop + window.scrollY; 24 drawFn(op); 25 return false; 26 } 27 }
3>设置等
这里dom比较简单,就略过了。只说一项,下载canvas图片到本地
最简单的,右键保存图片到本地,但是你肯定会骂我傻,谁不知道这操作啊;那么就来稍微装X一下吧
线上代码
1 function download() { 2 var data = can.toDataURL('image/png', 0.8); 3 var $a = document.createElement('a'); 4 $a.download = imgName.value || 'default.png'; 5 $a.target = '_blank'; 6 $a.href = data; 7 $a.click(); 8 }
(写这个博客的时候,返现自己把这个方法写麻烦了,绕远了。/手动尴尬,这里直接改了)
关键点在于 a.download属性,这个是把文件下载到本地的关键哦,然后要把canvas转成base64(canvas.toDataUrl方法,不清楚的可以去去了解下,这里不再赘述)
******************************************************
最后,附上完整代码(可能会和上边的有点出如,还在调整)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | <! DOCTYPE html> < html lang="zh"> < head > < meta charset="UTF-8"> < meta name="viewport" content="width=device-width, initial-scale=1.0"> < meta http-equiv="X-UA-Compatible" content="ie=edge"> < mtea author="win7killer@163.com"></ mtea > < title >Document</ title > < style > * { margin: 0; padding: 0; } p { line-height: 15px; font-size: 12px; } @media screen and (max-width: 768px) { .wrapper { width: auto; position: relative; overflow: hidden; } } @media screen and (min-width: 769px) { .wrapper { width: 600px; height: 600px; margin: 100px auto 0; position: relative; overflow: hidden; } #panel_box { position: fixed; top: 20px; right: 20px; width: 200px; } } canvas { background: #fafafa; display: block; } #can_role { background: none; position: absolute; top: 0px; left: 0px; pointer-events: none; } #panel_box { padding: 10px; margin-top: 10px; border: 1px solid rgba(10, 10, 10, .7); box-shadow: 10px 7px 10px #999; z-index: 100; } input { width: 80px; margin-left: 20px; } label { text-align: justify; } </ style > </ head > < body > < div class="wrapper" id="wrapper"> < canvas id="can_role"></ canvas > < canvas id="can"></ canvas > </ div > < div id="panel_box"> < p > < label >画笔颜色< input id="color_val" type="color" value="#0099ff"/></ label > </ p > < p > < label >画笔宽度< input type="number" id="line_width_val" min="1" max="20" value="2"/></ label > </ p > < p > < label >扇形份数< input type="number" id="pieaceNum" min="1" max="200" value="12"/></ label > </ p > < p > < label >参考线< input type="checkbox" id="onOff" checked="checked"/></ label > </ p > < p class="img_name_box"> < label >图片名称< input type="text" id="imgName" placeholder="ex:test.png"></ label > </ p > < p > < a href="javascript:;" id="save_btn" target="">下载到本地</ a > </ p > </ div > < script > var pieace = 6; var ctx = can.getContext('2d'); var ctx_role = can_role.getContext('2d'); can.width = can.height = can_role.width = can_role.height = window.screen.width > 768 ? 600 : window.screen.width; ctx_role.lineJoin = ctx.lineJoin = "round"; ctx_role.lineCap = ctx.lineCap = "round"; function drawFn(op) { var deg = Math.floor(360 / pieace); for (var i = 0, l = 360; i & l t; l; i += deg) { drawRotate(i / 180 * Math.PI, function(ctx) { draw(op); }); } } function draw(option, _ctx) { _ctx = _ctx || ctx; _ctx.beginPath(); _ctx.moveTo(option.bx - _ctx.canvas.width / 2, option.by - _ctx.canvas.height / 2); _ctx.lineTo(option.ex - _ctx.canvas.width / 2, option.ey - _ctx.canvas.height / 2); _ctx.stroke(); } function drawRotate(deg, fn, _ctx) { _ctx = _ctx || ctx _ctx.save(); _ctx.translate(_ctx.canvas.width / 2, _ctx.canvas.height / 2); _ctx.rotate(deg); fn && fn(_ctx); _ctx.restore(); } function baseLine() { ctx_role.clearRect(0, 0, ctx_role.canvas.width, ctx_role.canvas.height); var deg = 360 / pieace; ctx_role.lineWidth = 1; ctx_role.strokeStyle = 'rgba(0,0,0,.5)'; for (var i = 0, l = pieace; i < l; i++) { drawRotate(i * deg / 180 * Math.PI, function(ctx_role) { draw({ bx: can_role.width / 2, by: can_role.width / 2, ex: can_role.width / 2 + can_role.width, ey: can_role.width / 2 }, ctx_role); }, ctx_role); } } function download() { var data = can.toDataURL('image/png', 0.8); var $a = document.createElement('a'); $a.download = imgName.value || 'default.png'; $a.target = '_blank'; $a.href = data; $a.click(); // if (typeof MouseEvent === 'function') { // var evt = new MouseEvent('click', { // view: window, // bubbles: true, // cancelable: false // }); // $a.dispatchEvent(evt); // } } function bindPc() { can.onmousedown = function(e) { if (e.button != 0) { return false; } var op = {}; op.ex = op.bx = e.clientX - can.parentElement.offsetLeft + window.scrollX; op.ey = op.by = e.clientY - can.parentElement.offsetTop + window.scrollY; drawFn(op); document.onmousemove = function(e) { document.body.style.cursor = 'pointer'; op.bx = op.ex; op.by = op.ey; op.ex = e.clientX - can.parentElement.offsetLeft + window.scrollX; op.ey = e.clientY - can.parentElement.offsetTop + window.scrollY; drawFn(op); }; document.onmouseup = function() { document.body.style.cursor = 'default'; document.onmouseup = document.onmousemove = null; }; }; } function bindWp() { can.addEventListener('touchstart', function(e) { op = can.op = {}; op.ex = op.bx = e.touches[0].clientX - can.parentElement.offsetLeft + window.scrollX; op.ey = op.by = e.touches[0].clientY - can.parentElement.offsetTop + window.scrollY; drawFn(op); can.addEventListener('touchmove', touchMoveFn); can.addEventListener('touchend', touchEndFn); }); function touchEndFn() { document.body.style.cursor = 'default'; can.removeEventListener('touchmove', touchMoveFn); can.removeEventListener('touchend', touchEndFn); } function touchMoveFn(e) { op = can.op; document.body.style.cursor = 'pointer'; op.bx = op.ex; op.by = op.ey; op.ex = e.touches[0].clientX - can.parentElement.offsetLeft + window.scrollX; op.ey = e.touches[0].clientY - can.parentElement.offsetTop + window.scrollY; drawFn(op); return false; } } function bindSets() { color_val.onchange = function() { ctx.strokeStyle = color_val.value; } line_width_val.onchange = function() { ctx.lineWidth = line_width_val.value; } pieaceNum.onchange = function() { ctx.clearRect(0, 0, can.width, can.height); reset(); } onOff.onchange = function() { if (this.checked == true) { can_role.style.display = 'block'; } else { can_role.style.display = 'none'; } } } function bind() { bindPc(); bindWp(); bindSets(); save_btn.onclick = download; } function reset() { pieace = pieaceNum.value; ctx.strokeStyle = 'rgba(100,100,100,.7)'; baseLine(); ctx.lineWidth = line_width_val.value; ctx.strokeStyle = color_val.value; } function init() { reset(); bind(); } init(); </script> </ body > </ html > |
**************偷偷留个名字,防抓 博客园-fe-bean***************
涉及姿势点总结
1.canvas_translate
2.canvas_rotate
3.canvas_toDataUrl
4.a.download && base64
其余的想起来再添加吧
最后,欢迎大家多提意见、交流,点赞转载那就更棒了。
再丢一张图
下期再见咯~~~
**************** 少侠留步,能看到这里的,我要给你们一个奖励 ***************
这个demo是可以在移动端玩的,意味着有电容笔的亲,可以爽啊~(个别浏览器脑残会左右来回跑~~)
没有电容笔的亲,肯定是大多数,我们一样能玩啊!!!
叫你们快速做一款电容笔(当然没那么好用)
1.找一只木质铅笔
2.削出铅笔头
3.把铅笔头斜着磨平,如图
4.用磨平这一侧去电容屏上画(开始吧)
我上边那张图就是拿铅笔画的~~~
************************************
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 深入理解 Mybatis 分库分表执行原理
· 如何打造一个高并发系统?
· .NET Core GC压缩(compact_phase)底层原理浅谈
· 现代计算机视觉入门之:什么是图片特征编码
· .NET 9 new features-C#13新的锁类型和语义
· Spring AI + Ollama 实现 deepseek-r1 的API服务和调用
· 《HelloGitHub》第 106 期
· 数据库服务器 SQL Server 版本升级公告
· 深入理解Mybatis分库分表执行原理
· 使用 Dify + LLM 构建精确任务处理应用