Canvas钢琴琴键弹起来

举报
空城机 发表于 2021/09/22 15:21:32 2021/09/22
【摘要】 使用Canvas绘制88键的钢琴琴键,并且每个琴键都有自己各自的音调,使用鼠标点击或绑定的键盘按键可以进行弹奏

钢琴键盘

最近很喜欢挺‘JoJo黄金之风’中的那一段钢琴处刑曲,所以准备自己写一个钢琴的键盘,也算是自己重新练习一下canvas

音乐地址:https://y.qq.com/n/ryqq/player

其实这里是承接上一篇文章《简单的前端项目配置》继续走下去的,不过其实不去看也没有关系,毕竟主要还是以写canvas为主。

<canvas> 看起来和 <img> 元素很相像,唯一的不同就是它并没有 src 和 alt 属性。实际上,<canvas> 标签只有两个属性 ——  widthheight。这些都是可选的,并且同样利用 DOM properties 来设置。当没有设置宽度和高度的时候,canvas会初始化宽度为300像素和高度为150像素。

琴键分析

在开始动手制作之前,先要规划一下

首先我去看了一下钢琴的琴键。钢琴的琴键分为黑白两种,白键一共有52个,黑键一共有36个。 黑键比白键略短,但是基本和白键都是靠顶边对齐的。 白键是平均填满了整个键盘,黑键是有规律分布,虽然一般看起来没有规律

image.png



Html

html当中写入canvas标签,可以提前设置好宽高,也可以在JavaScript中设置(我是写在TypeScript中的,用TS来替代JS,所以和普通JavaScript写法略有不同)。

<canvas id="mycanvas" >
   您的浏览器不支持 HTML5 canvas 标签, 建议换一个
</canvas>

在TS文件中编写:使用document.body的宽度来为canvas设置宽度以求做到自适应,这里需要注意的是,很多人都习惯自适应写成100%,但是在canvas当中,这种做法是错误的,这回导致之后绘制的图形产生形变。 canvas的大小也不能设置在style当中。

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;


TS

之前已经将canvas基本大小设定好了,接下来可以计算一下黑键和白键的大小了

我是以高度是宽度的5.5倍来设置黑白键的,所以只需要计算出宽度即可。

下面bw是黑键的宽度,ww是白键的宽度

let bw = width * 0.015, ww = width / 52;

不管黑键还是白键,都可以设置一个同一的公共类KeysSize,然后创建黑键和白键独立的类BlackKeysWhiteKeys

钢琴按键需要的属性有宽度、高度、类别和距离左边的距离
在钢琴按键中需要有pressDown琴键按下方法以及draw绘制琴键方法。
这里使用了canvas中的绘制矩形fillRect方法,这里绘制出来的填充矩形是黑色的,那就当作正常的黑键即可。 黑键直接使用extends继承KeysSize类即可。

不过白键就要对其中的一些方法进行重写,并且添加一些其他的方法

// 钢琴按键
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(){ };
    draw(left: number) {
        this.left = left;
        ctx.fillRect(left, 0, this.width, this.height);
    }
}

在白键当中,不能使用fillRect方法去绘制一个填充的矩形,所以我使用stroke绘制了一个有圆角的白键。 这里面使用了lineTo和arcTo这些绘制线的方法。绘制的白键边框线颜色可以使用strokeStyle进行更改

draw(left: number, backgroundColor: string = '#333') { 
    this.left = left;
    if (typeof this.width == 'number'){
        ctx.lineWidth = 0.5;  // 边框粗细
        ctx.strokeStyle = backgroundColor;  // 修改边线颜色
        this.rectdraw(left);  // 绘制白键
    }
};
rectdraw(start:number) {
    ctx.beginPath();
    ctx.moveTo(start, 0);
    ctx.lineTo(start, this.height - 8);
    ctx.arcTo(start, this.height, start + this.width, this.height, 8);  // 绘制圆角
    ctx.lineTo(start + this.width - 16, this.height);
    ctx.arcTo(start + this.width, this.height, start + this.width, 0, 8);
    ctx.lineTo(start + this.width, 0); 

    ctx.stroke();
};

之后可以生成黑键和白键的实例来填充在canvas界面上了。
白键的生成比较简单:直接排列生成52个即可

for (let i = 0; i < 52; i++) {
    let wkey = new WhiteKeys(ww, 'white');
    wkey.draw(i*ww);
}

而黑键的生成就需要寻找到规律,黑键除了第一个之外,后面的都是2、3、2、3、2、3这样的间隔排列。
所以36个黑键是如下绘制的,这里使用的ww是白键的宽度,bw是黑键的宽度。这一点在上面也提到过。

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);
    } 
}

当前绘制出的效果:

image.png


目前的效果当中,我还添加鼠标点击白键事件,按下白键,白键变色,鼠标抬起,颜色变回白色。
这个效果中,我对canvas的鼠标点击事件是使用了addEventListener去绑定mousedown和mouseup两种方法,然后根据在canvas上点击的位置,来判断到底是点击了什么按键。

至于可能遇到的画布上按键颜色重置,就是直接重绘了键盘出来的。

当前的效果:

223.gif



再起

不过光是点击白键还不够,钢琴也是需要点击黑键的,并且最重要的是钢琴怎么能够没有声音呢!

项目Gitee的地址https://gitee.com/wzckongchengji/node_study/tree/master/pianoDemo



因为之前已经写过白键的点击方法,所以本次就用只需要稍作改变,即可完成黑键的点击。

首先对鼠标点击位置的(x, y)进行判断。大家如果想看可以去上面Gitee上main.ts中查看

// 判断点击的位置是否在按键上
function isTrueKey(xy: xy) {
    // 如果在键盘外
    if (xy.y > ww * 5.5) { return false; }
    // 判断是否在黑键上
    if (xy.y <= bw * 5.5) {
        for (let index in bArr  ) {
            let item = bArr[index];
            if(item.left <= xy.x && item.left + bw > xy.x) {
                item.pressDown(parseInt(index));
                break;
            } 
            if (index == '35') {
                whiteKeyClick(xy);
            }
        }
    } else {
        whiteKeyClick(xy)
    }
}

因为是在TypeScript中编写的,所以会有类型定义,xy是之前定义的坐标类型

type xy = {
    x: number,
    y: number
}

通过这一步,能够做到不同类别的按键判断。

其中的whiteKeyClick是白键按下的方法,白键和黑键按下在类中都有pressDown按下事件,各自会重写一下此方法

image.png

当前的效果是鼠标点击黑键和白键都会变色

80901.gif



钢琴音效

钢琴如果没有琴键的声音又怎么能称为钢琴呢,所以每一个按键都要有对应的声音,一共88个琴键,就要有88中音效。

说实话,找这些音效资源花费了我很久的时间…

image.png

最后经过一系列的寻找,还是被我找到了

地址就在上面的项目中, https://gitee.com/wzckongchengji/node_study/tree/master/nodeDemoIO/music

注意: 我使用audio标签去播放音乐,由于音频的资源比较大,所以我就不存放在前端项目中了,把这些音频资源放在我平时练习的node里,使用node来给出url去调用音频。

node运行命令: nodemon IO.js

image.png

这样测试一下,在网页中输入URL: http://localhost:3001/music/8A.mp3 。 发现能够访问到音频了

image.png

之后就可以在钢琴按下事件中调用音频调用方法了,注意这里需要根据不同的index去判断调用不同琴键的音效。

下面是白键的音调判断方法:

image.png

黑键判断:

image.png


当然了,这些判断需要对钢琴有一定了解,钢琴中是存在不同分区的。下图很重要,这也是我在编写时的重要依据:

可以看出,钢琴琴键排布具有如下特点。

  • 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)。

图中的那些汉字是每组的名称(从左向右依次为大字二组、大字一组、大字组、小字组、小字一组、小字二组、小字三组、小字四组、小字五组,其中大字二组和小字五组是不完全音组)

image.png

经过这一系列的操作之后,点击琴键就能发出不同的音效声音了。



键盘绑定

做到这里,突然觉得还是有些美中不足。 光是使用鼠标点击好像有些过于单调了,那么让打键盘变成弹钢琴

将键盘上的按键对应钢琴的琴键匹配绑定起来。

这里的内容我主要写在keyWord.ts当中了,使用export抛出

使用Map定义的keyWord来存放对应的联系

interface keyObj {
    keyname: string;
    type: number;  // 0:白键  1:黑键
    index: number;   // 对应的黑键和白键index
    represent: string
}
let keyWord:Map<string|number, keyObj> = new Map();

keyWord中的内容:

image.png

通过回调函数来设置电脑键盘的按下与抬起事件对应的操作,这里设置的依据是根据键盘按下的键值,每个按键按下都会有不同的键值:

image.png

// 监听键盘点击
function keyWordDown(callback:Function, callback2:Function) {    
    document.onkeydown = (event)=>{ 
        let e = event || window.event;
        callback(e.keyCode)
    }    
    document.onkeyup = ()=>{ 
        callback2();
    }   
}

export { keyWord, keyWordDown, keyObj }

之后回到main.ts对应的回调方法写入即可。

// 匹配按下的电脑键盘和对应的琴键
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();  // 重绘键盘 
});


完成

当然下面的GIF中是没有声音的,挺可惜的,毕竟重点就本文在于钢琴能响了

键盘弹奏:

80901.gif

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。