Canvas
元素说明
Canvas 是 HTML5 中出现的新标签,对于大部分浏览器都是可用的。
Canvas API 提供了一个通过 JavaScript 和 HTML 的<canvas>元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。
Canvas API 主要聚焦于 2D 图形。同样使用<canvas>元素的 WebGL API 则用于绘制硬件加速的 2D 和 3D 图形。
对于前端开发者而言,canvas 也是一个经常会接触和使用到的元素,canvas 就像一块画布,能够让开发者将自己想要绘制的画面在 canvas 上展现出来。对于某些样式可能使用其他元素进行展示会很复杂,但是使用 canvas 画布就没有这种麻烦,当然 canvas 使用添加和移除元素往往需要擦掉绘图并重新绘制。
Canvas 的 MDN 地址:https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Basic_usage
注意:
canvas元素看起来和 img 元素很像,但其实canvas元素只有 width 和 height 宽高这两个属性
canvas上的内容都是通过开发者使用 JavaScript 脚本进行绘制的
当没有设置宽度和高度的时候,canvas会初始化宽度为 300 像素和高度为 150 像素
基础使用
<canvas>标签在使用时需要添加结束标签</canvas>
<canvas> 元素会创造出一个固定大小的画布,在 JavaScript 中可用通过 Dom 的节点获取到 Canvas 元素,并且通过getContext()方法来获得渲染上下文和绘画功能
示例:
<canvas id="canvas"></canvas>
<script> var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d');</script>
复制代码
Canvas 的使用比较常见,当你在页面上看到图表时,打开 F12 控制台进行查看,往往会发现其可能是使用 canvas 绘制的,比如 echarts 图表就是在 Canvas 画布上展现。 还有平时玩的网页类游戏,通常查看后也会发现主体是在 Canvas 上面。
如果有学过板绘的朋友,可能就会发现 JavaScript 写 Canvas 其实就和画板绘类似。
Canvas 的功能其实有很多,学习总归还是要运用到实际的例子上面。下面主要还是以写例子来展示吧(以下两个例子都是使用 TypeScript 进行编写的)。
我自己也将写的项目提交到码云上面了,地址:https://gitee.com/wzckongchengji/node_study/tree/master/pianoDemo
里面的 Exlmplicit.ts 是我抽奖的核心代码,paino.ts 是我钢琴的核心代码
看看效果先
转盘抽奖:
钢琴弹奏:
转盘示例
上面的抽奖转盘界面有的朋友会觉得熟悉,这是来源于我在掘金一直没有抽中过的非酋抽奖转盘。
为了安抚自己的幼小心灵,我用 Canvas 制作了一个“幸运”抽奖的转盘。 百分之百比中!!!
在 Canvas 上进行绘制物体时,不用急着先动手,先对需要绘制的界面进行分析
界面分析
圆点
在抽奖转盘上面,能够看到界面边框上有一圈小圆点,每一条边上都有 5 个,并且颜色不同。不过总体的大小是不变的,所以我创造了一个Circular类,在类中编写一个drawCir绘制圆点的方法。
class Circular { color:string; constructor(color: string) { this.color = color; }; // 绘制圆点 drawCir(left:number, top: number, fillFlag: boolean = true, color?: string) { ctx.beginPath(); ctx.save(); if (color) { this.color = color; } ctx.strokeStyle = this.color; ctx.fillStyle = this.color; ctx.arc(left, top, 5, 0, 2 * Math.PI); if (fillFlag) { ctx.fill(); } ctx.stroke(); ctx.restore(); }}
复制代码
drawCir方法 (这个类写的有点早了,其实 left 和 top 可以使用之后的 xy 来代替的),在方法中存在一些参数
圆角矩形
首先分析一下抽奖界面中存在最多的图形元素,会发现是圆角的矩形,不管是抽奖转盘的背景还是内部奖品存放的九宫格都是圆角的矩形。
如果一个个进行编写就会很冗余,所以可以封装一个编写圆角矩形的方法。
可以先定义一个坐标轴的 xy 类型:
type xy = { x: number, y: number}
复制代码
封装方法时需要考虑圆角矩形需要的参数:
在方法内可以使用beginPath(),save(),restore()等方法来使得绘制圆角矩形后不影响之后的绘制。
这三个方法合用,能够将 canvas 恢复到你想记录下来的保存状态。
这里的圆角和边线使用moveTo、lineTo、arcTo 来绘制完成
// 绘制圆角矩形的方法 参数:startPos 起点位置, width 矩形宽度 height 矩形高度 border 圆角弧度 color 颜色 function drawCRect(startPos: xy, width:number, height:number, border: number, color?: string) { ctx.beginPath(); ctx.save(); if (color) { ctx.strokeStyle = color; ctx.fillStyle = color; } ctx.moveTo(startPos.x + border, startPos.y); ctx.lineTo(startPos.x + width - border, startPos.y); ctx.arcTo(startPos.x + width, startPos.y, startPos.x + width, startPos.y + border, border); ctx.lineTo(startPos.x + width, startPos.y + height - border); ctx.arcTo(startPos.x + width, startPos.y + height, startPos.x + width - border, startPos.y + height, border); ctx.lineTo(startPos.x + border, startPos.y + height); ctx.arcTo(startPos.x, startPos.y + height, startPos.x, startPos.y - border, border); ctx.lineTo(startPos.x, startPos.y + border); ctx.arcTo(startPos.x, startPos.y, startPos.x + border, startPos.y, border); ctx.fill(); ctx.stroke(); ctx.restore();}
复制代码
这些基础的类和方法创建好了之后,就要通过它们去先将面板绘制出来了。
首先通过 drawCRect 方法绘制矩形背景,在这里使用了 Canvas 的 shadow 阴影方法(我觉得使用不如 CSS 中的要方便)
drawCRect(startPos, 484, 394, 12, '#fadd95');ctx.shadowColor = "#FBCA78";ctx.shadowBlur = 15;drawCRect({ x: 10, y: 10 }, 466, 376, 12, '#fadd95');
复制代码
然后根据不同位置,在边缘使用 drawCir 方法绘制圆点,以下仅以绘制顶部圆点的代码为例子:
let cir01 = new Circular('#e37815');// 顶部圆点cir01.drawCir(12, 13)cir01.drawCir(118.5, 13, false, '#fff7e8');cir01.drawCir(243, 13)cir01.drawCir(357.5, 13, false)cir01.drawCir(474, 13, true, '#e37815');
复制代码
绘制九宫格的抽奖面板其实也和绘制背景方法相同,使用 drawCRect 方法。
文字
对于最中间的抽奖面板,上面的文字使用 fillText、fillStyle、font 等 Canvas 绘制字体方式来进行。
CanvasRenderingContext2D.fillText() 是 Canvas 2D API 在 (x, y)位置填充文本的方法。如果选项的第四个参数提供了最大宽度,文本会进行缩放以适应最大宽度。
// 抽奖drawCRect({ x: 179, y: 148 }, 128, 98, 4, '#ffe6a6');ctx.save();ctx.shadowColor = "#FBCA78";ctx.shadowBlur = 20;drawCRect({ x: 189, y: 158 }, 108, 78, 4, '#FADA90');ctx.shadowBlur = 0;ctx.fillStyle = "#A74B00";ctx.font = "bold 32px Microsoft YaHei";ctx.fillText('抽奖', 210, 200);ctx.fillStyle = "#d25f00"ctx.font = "14px Microsoft YaHei";ctx.fillText('200矿石/次', 205, 225);ctx.restore();
复制代码
奖励
现在基础面板已经绘制完成了,但是还缺少了重要的奖励呀,所以下面还需要创建一个奖励类 Reward
Reward:
// 不同的奖励class Reward { left:number = 128; top:number = 98; content: string = ''; bgColor:string = '#FDF3F3'; constructor() { } drawReward(content:string, left: number, top: number, bgColor:string = '#fdf3f3') { ctx.beginPath(); this.left = left; this.top = top; this.content = content; this.bgColor = bgColor; // console.log(reImg[0]); drawCRect({ x: left, y: top }, 128, 98, 4, bgColor); ctx.drawImage(reImg[0], left + 44, top + 20, 40, 40); ctx.fillStyle = "#d25f00" ctx.font = "14px Microsoft YaHei"; ctx.fillText(content, left + 24, top + 80); }}
复制代码
在上面的奖励类当中,有一个新并且也是重要支持的方法 -- drawImage 方法 。此方法是能够将图片绘制到 Canvas 上的方法。
绘制时可以先创建一个 img 标签,内部存放奖励图片。reImg 是标签的集合,这里为了百分之百抽中,就只用一张奖励图片
let reImg:any[] = [ document.getElementById('bg')]
复制代码
因为需要有 8 个奖励格子,所以循环遍历格子位置,绘制出奖励
// 绘制八个奖励function setReward() { for(let i = 0; i < 8; i++) { let rewardCon = new Reward(); rewardCon.drawReward('乐高海洋巨轮', pos[i].x, pos[i].y); rewardArr.push(rewardCon); }}
复制代码
这样,抽奖转盘的面板就完整的出现了
现在需要点击抽奖按钮,然后开始抽奖了。以上都是静态的画布,现在需要让 Canvas 开始动起来了。
因为 Canvas 上面是不分的,只是一个元素,所以鼠标点击抽奖事件就需要通过鼠标的按钮位置来进行判断。
因为这也是可能会多次用到的方法,所以定义了一个 getPointXY 方法,这能够拿到鼠标点击的 x、y 位置。
// 获取点击位置function getPointXY(e: any):xy { let x = e.offsetX; let y = e.offsetY; return { x: x, y: y };}
复制代码
并且点击时需要将画布清空,并且重新绘制,这一点在本文最开始的元素说明中也有提到过。
转盘的抽奖旋转通过 setInterval 来实现
以下是点击抽奖事件的触发方法
EXcanvas.addEventListener("click", (e:any)=>{ if (running) return ; let pxy = getPointXY(e), nums:number; if (pxy.x >= 179 && pxy.x <= 307 && pxy.y >= 148 && pxy.y <= 256) { nums = luckDraw(); } // 重新点击时将上一次的清空 if (count > 0) { c = (count - 1) % 8; rewardArr[c].drawReward('乐高海洋巨轮', pos[c].x, pos[c].y); count = 0; } let interIndex = setInterval(()=> { running = true; // 将上一次循环到的奖励背景复原 if (count > 0) { c = (count - 1) % 8; rewardArr[c].drawReward('乐高海洋巨轮', pos[c].x, pos[c].y); } // 改变当前奖励背景 c = count % 8; rewardArr[c].drawReward('乐高海洋巨轮', pos[c].x, pos[c].y, '#FFCF8B'); count ++; if (count == (16 + nums)) { clearInterval(interIndex) running = false; } }, 100)});
复制代码
以上基本就是想要做出一个 Canvas 绘制的抽奖面板的流程,这样做可以减少利用元素标签来改变抽奖结果(虽然也没什么用)
钢琴示例
之前很喜欢挺‘JoJo 黄金之风’中的那一段钢琴处刑曲,所以准备自己写一个钢琴的键盘,也算是自己练习一下Canvas。
音乐地址:y.qq.com/n/ryqq/play…
和转盘一样,在编写之前首先进行界面分析
界面分析
看了一下钢琴的琴键。钢琴的琴键分为黑白两种,白键一共有 52 个,黑键一共有 36 个。 黑键比白键略短,但是基本和白键都是靠顶边对齐的。 白键是平均填满了整个键盘,黑键是有规律分布(虽然黑键一眼看去没有什么规律)
钢琴琴键键盘比转盘界面简洁。
琴键分区
钢琴的琴键是存在分区的,根据 do、re、mi、fa、sol、la、si 来进行分类。所以先准备好琴键的分区
// 钢琴琴键分区let keysOrder:string[] = [ 'C', 'D', 'E', 'F', 'G', 'A', 'B' ]
复制代码
琴键
可以创建一个钢琴琴键类,这是之后出现的黑键和白键类的基类。 在这个基类中,可以拟定黑键和白键都需要共有的属性或方法,相当于打一次草稿。 比如说键的宽高、类别、位置通过 left 距离左边的像素来决定,还有琴键按下的方法、绘制琴键的方法。
在钢琴按键中需要有pressDown琴键按下方法以及draw绘制琴键方法。 这里使用了canvas中的绘制矩形fillRect方法,这里绘制出来的填充矩形是黑色的,那就当作正常的黑键即可。
// 钢琴按键class KeysSize { width: number; //宽 height: number; // 高 type: string|undefined; // 按键类别 left: number = 0; constructor(width:number, type:string) { this.width = width; this.height = width * 5.5; this.type = type; }; // 按下事件 pressDown(index:number){ this.changeColor(); }; // 点击时改变颜色 changeColor() {}; // 绘制按键 draw(left: number) { this.left = left; ctx.fillRect(left, 0, this.width, this.height); }}
复制代码
黑键和白键
黑键类和白键类都继承了上面的琴键类,并且其中的方法都各有不同,以下只展示黑键的类 BlackKeys。
在白键当中,不能使用fillRect方法去绘制一个填充的矩形,所以我使用stroke绘制了一个有圆角的白键。 这里面使用了 lineTo 和 arcTo 这些绘制线的方法。绘制的白键边框线颜色可以使用 strokeStyle 进行更改。
// 黑键class BlackKeys extends KeysSize{ constructor(width:number, type:string) { super(width, type); } // 按下按键 pressDown(index:number) { super.pressDown(index); this.toneCheck(index); }; // 点击时改变颜色 changeColor() { ctx.save(); ctx.fillStyle = "#EED143"; this.draw(this.left); ctx.restore(); }; // 判断是什么音调 toneCheck(index:number) { let AG:number = (index%5), numb:string|number = ''; if (index == 0) { numb = '8' } else { numb = Math.floor((index - 1) / 5) + 1; if (numb == 1){ numb = '' } } let volumeName = numb + keysOrder2[AG]; playVolume(volumeName) }}
复制代码
界面
首先先绘制基本的 Canvas
const canvas:any = document.getElementById('mycanvas');const ctx:any = canvas.getContext('2d');let width:number = document.body.clientWidth - 20;// canvas不能设置成百分比,不能在style里设置宽高canvas.width = width;canvas.height = 500;
复制代码
注意:使用 document.body 的宽度来为 canvas 设置宽度以求做到自适应,这里需要注意的是,很多人都习惯自适应写成 100%,但是在 canvas 当中,这种做法是错误的,这回导致之后绘制的图形产生形变。
之前已经将 canvas 基本大小设定好了,接下来可以计算一下黑键和白键的大小了
我是以高度是宽度的 5.5 倍来设置黑白键的,所以只需要计算出宽度即可。
下面 bw 是黑键的宽度,ww 是白键的宽度
let bw = width * 0.015, ww = width / 52;
复制代码
之后可以生成黑键和白键的实例来填充在 Canvas 界面上了。 白键的生成比较简单:直接排列生成 52 个即可。
而黑键的生成就需要寻找到规律,黑键除了第一个之外,后面的都是 2、3、2、3、2、3 这样的间隔排列。 所以 36 个黑键是如下绘制的,这里使用的 ww 是白键的宽度,bw 是黑键的宽度。这一点在上面也提到过。
// 白键绘制for (let i = 0; i < 52; i++) { let wkey = new WhiteKeys(ww, 'white'); wkey.draw(i*ww);}// 黑键绘制for (let i:number = 1; i <= 36; i++) { let bkey = new BlackKeys(bw, 'black'); let nindex:number = Math.floor(i/5); if (i == 1) { bkey.draw(ww - bw/2); } else if (i%5 == 2) { bkey.draw(ww*(2 + nindex*7) + ww - bw/2); } else if (i%5 == 3) { bkey.draw(ww*(3 + nindex*7) + ww - bw/2); } else if (i%5 == 4) { bkey.draw(ww*(5 + nindex*7) + ww - bw/2); } else if (i%5 == 0) { bkey.draw(ww*(6 + nindex*7) + ww - bw/2); } else if (i%5 == 1) { bkey.draw(ww*(7 + (nindex - 1)*7) + ww - bw/2); } }
复制代码
现在键盘界面就这样出现了:
界面鼠标点击
目前的效果当中,我还添加鼠标点击事件,按下白键,白键变色,鼠标抬起,颜色变回白色。这个效果中,我对 Canvas 的鼠标点击事件是使用了addEventListener去绑定 mousedown 和 mouseup 两种方法,然后根据在 Canvas 上点击的位置,来判断到底是点击了什么按键。
首先对鼠标点击位置的(x, y)进行判断,下面由于代码过长,就不准备粘贴了。大家如果想看可以去上面 Gitee 上 paino.ts 中查看。 并且这里也是需要重绘键盘界面的。
这里的鼠标点击判断其实就是和之前转盘抽奖点击类似了
琴音
现在钢琴可以点击按下了,所以钢琴怎么能够没有声音呢!
每一个按键都要有对应的声音,一共 88 个琴键,就要有 88 中音效。
说实话,找这些音效资源花费了我很久的时间......
最后经过一系列的寻找,还是被我找到了
地址就在上面的项目中, https://gitee.com/wzckongchengji/node_study/tree/master/nodeDemoIO/music
注意: 我使用 audio 标签去播放音乐,由于音频的资源比较大,所以我就不存放在前端项目中了,把这些音频资源放在我平时练习的 node 里,使用 node 来给出 url 去调用音频。
node 运行命令: nodemon IO.js
这样测试一下,在网页中输入 URL: http://localhost:3001/music/8A.mp3 。 发现能够访问到音频了
之后就可以在钢琴按下事件中调用音频调用方法了,注意这里需要根据不同的 index 去判断调用不同琴键的音效。
当然了,这些判断需要对钢琴有一定了解,钢琴中是存在不同分区的。下图很重要,这也是我在编写时的重要依据:
可以看出,钢琴琴键排布具有如下特点。
1、共有 52 个白键和 36 个黑键。
2、黑键的长度和宽度均小于白键。
3、每个黑键都位于两个白键中间(但不一定是正中间)。
4、琴键分为若干组,每组有 12 个琴键(7 个白键和 5 个黑键)。
5、最左边的组只有 3 个琴键(2 个白键和 1 个黑键),最后边的组只有 1 个琴键(1 个白键),这两个组都是不完整的组。
每组的这 12 个琴键中,7 个白键从左向右依次为 do、re、mi、fa、sol、la、si,5 个黑键从左向右依次为升 do(降 re)、升 re(降 mi)、升 fa(降 sol)、升 sol(降 la)、升 la(降 si)。
图中的那些汉字是每组的名称(从左向右依次为大字二组、大字一组、大字组、小字组、小字一组、小字二组、小字三组、小字四组、小字五组,其中大字二组和小字五组是不完全音组)
经过这一系列的操作之后,通过鼠标点击琴键就能发出不同的音效声音了。
键盘按键绑定
做到这里,突然觉得还是有些美中不足。 光是使用鼠标点击好像有些过于单调了,那么让打键盘变成弹钢琴吧
将键盘上的按键对应钢琴的琴键匹配绑定起来。
这里的内容我主要写在keyWord.ts当中了,使用 export 抛出
使用 Map 定义的keyWord来存放对应的联系
按键的关系:https://gitee.com/wzckongchengji/node_study/blob/master/pianoDemo/TS/keyWords.ts
通过回调函数来设置电脑键盘的按下与抬起事件对应的操作,这里设置的依据是根据键盘按下的键值,每个按键按下都会有不同的键值:
编写一个键盘点击方法:keyWordDown
// 监听键盘点击function keyWordDown(callback:Function, callback2:Function) { document.onkeydown = (event)=>{ let e = event || window.event; callback(e.keyCode) } document.onkeyup = ()=>{ callback2(); } }
复制代码
通过传入的回调方法来使用:
function matchKey(keyCode: number) { let res:keyObj = keyWord.get(keyCode) as keyObj; if (!res) return ; if (res?.type == 0) { wArr[res.index].pressDown(res.index); } else { bArr[res.index].pressDown(res.index); }}// 监听电脑键盘按下keyWordDown(matchKey, ()=>{ ctx.clearRect(0,0, width, 500); //清理画布 drawAll(); // 重绘键盘 });
复制代码
这样,点击电脑的键盘,就能够对应的响起不同的琴键声音了。
总结
Canvas 制作的两个小实例就这样结束了,虽然也是花了我一段时间去做的,不过总体还是好的,对于 Canvas 绘制方法又更深入了解了一遍。所以这里建议学习 Canvas 的过程当中,不断去做练习进行巩固。
评论