写点什么

Canvas 制作转盘和钢琴

用户头像
空城机
关注
发布于: 4 小时前
Canvas制作转盘和钢琴

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



发布于: 4 小时前阅读数: 7
用户头像

空城机

关注

曾经沧海难为水,只是当时已惘然 2021.03.22 加入

业余作者,在线水文 主要干前端的活,业余会学学python 欢迎各位关注,互相学习,互相进步

评论

发布
暂无评论
Canvas制作转盘和钢琴