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 的过程当中,不断去做练习进行巩固。
评论