介绍
Open Sheet Music Display
: A MusicXML renderer for the Browser
可以理解成是一个用于加载xml
格式的曲谱,并提供一些个性化支持。因为本质是 HTML 和 JavaScript,所以理论上可以用于所有能加载网页的设备
官网:https://opensheetmusicdisplay.org/
Github:https://github.com/opensheetmusicdisplay/opensheetmusicdisplay
需求
因为需要在移动设备上有加载曲谱的需求,并且需要对曲谱进行一些定制需求,但网上可参考的文档比较少,只能通过issue
来查找相关信息。所以在这记下需求和解决办法,便于他人查找使用
需求一:标记/修改音符颜色
这里列举了一些在研究过程中碰到的问题以及尝试过的方式,要知道正确实现方式请看方法四
方式一:全局渲染
Link: https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/wiki/Exploring-the-Demo
// 小节索引
var noteSubsectionIndex = 0
// 音符索引
var noteIndex = 0
function updateNoteColor() {
// 每小节音符数
var staffEntries = osmd.graphic.measureList[noteSubsectionIndex][0].staffEntries
if (noteIndex >= staffEntries.length) {
noteIndex = 0
noteSubsectionIndex++
staffEntries = osmd.graphic.measureList[noteSubsectionIndex][0].staffEntries
}
staffEntries[noteIndex].graphicalVoiceEntries[0].notes[0].sourceNote.noteheadColor = "#32D74B"
osmd.render()
noteIndex++
}
上面部分内容的含义:
// 小节数 osmd.graphic.measureList
// 第几小节 osmd.graphic.measureList[0] [第1小节]
// 每小节 第几部分 indexosmd.graphic.measureList[0][0] [第1小节第一部分]
// 每小节 第几部分 音符数 osmd.graphic.measureList[1][1].staffEntries [第2小节第2部分所有音符]
这是官方Demo
里的展示,用于修改音符的颜色,适用于初始化曲谱需要修改指定位置的音符的颜色,但不适用于实时修改音符,因为每次都需要重新渲染,对于比较大的曲谱等待时间过长
缺点:每次修改颜色都需要重新渲染
方式二:根据音符坐标绘制线
Link: https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/940
function updateNoteColorWithLine() {
var staffEntries = osmd.graphic.measureList[noteSubsectionIndex][0].staffEntries
if (noteIndex >= staffEntries.length) {
noteIndex = 0
noteSubsectionIndex++
staffEntries = osmd.graphic.measureList[noteSubsectionIndex][0].staffEntries
}
var startNote = staffEntries[noteIndex].graphicalVoiceEntries[0].notes[0]
var start = startNote.PositionAndShape.AbsolutePosition;
console.log(start)
var end = new opensheetmusicdisplay.PointF2D(start.x, start.y)
start.x -= 1
end.x += 1
console.log(start, end)
osmd.Drawer.DrawOverlayLine(start, end, startNote.ParentMusicPage, "#8020a162", 1);
noteIndex++
}
这个方法其实不算是修改音符本身,而是在音符的位置上画一个线,相当于是打上标记,这里注意一点:线的颜色最后两位是透明度的值
缺点:不是作用于音符本身
方式三:根据id获取页面svg标签
Link:: https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/549`
// 小节索引
var noteSubsectionIndex = 0
// 音符索引
var noteIndex = 0
function updateNoteColorWithId() {
var staffEntries = osmd.graphic.measureList[noteSubsectionIndex][0].staffEntries
if (noteIndex >= staffEntries.length) {
noteIndex = 0
noteSubsectionIndex++
staffEntries = osmd.graphic.measureList[noteSubsectionIndex][0].staffEntries
}
var vfnote = staffEntries[noteIndex].graphicalVoiceEntries[0].notes[0].vfnote[0]
var noteId = 'vf-' + vfnote.attrs.id
document.querySelectorAll(`g#${noteId} path`).forEach(item => {
item.setAttribute('fill', '#20a162');
})
noteIndex++
}
这种通过获得当前节点的音符对应的svg
的id
,以实现直接修改音符颜色,这种方式不需要重新渲染。但是有个限制条件:就是只适用于单行曲谱
缺点:只能用于单行曲谱
方式三:根据id获取页面svg标签(改进版)
Link: https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/549
// 小节索引
var noteSubsectionIndex = 0
// 音符索引
var noteIndex = 0
function updateAllNoteColorWithId() {
var measureList = osmd.graphic.measureList[noteSubsectionIndex]
// 所有列表的长度
let listLength = measureList.map(item => {
return item.staffEntries.length
})
console.log(listLength)
// 最大的列表长度
let max_length = Math.max(...listLength)
// 最大列表的索引
let max_index = listLength.indexOf(max_length)
// console.log(max_index)
// 当前小节 每个部分 当前位置的音符
for (i = 0; i < measureList.length; i++) {
let staffEntry = measureList[i].staffEntries[noteIndex]
if (typeof staffEntry != "undefined") {
var vfnote = staffEntry.graphicalVoiceEntries[0].notes[0].vfnote[0]
var noteId = 'vf-' + vfnote.attrs.id
document.querySelectorAll(`g#${noteId} path`).forEach(item => {
item.setAttribute('fill', '#20a162');
})
}
}
console.log("index", noteSubsectionIndex, noteIndex)
// 当前小节已遍历完每个部分的音符(即已经遍历完最长音符列表)
noteIndex++
if (noteIndex >= max_length) {
noteIndex = 0
noteSubsectionIndex++
}
}
这个方法弥补了之前只能作用于当行曲谱的缺陷,考虑到上下曲谱可能会个数不一致,所以使用最大长度作为遍历条件。确保能全部遍历到,但是测试了一些曲谱还是发现了问题:
可以看第二小节的第二个节点出现问题了,本来应该要等下行的到第五个上面才应该变色的,但是实际代码获取的列表并不会这么理想,获取到的只是向下都是个数为5的列表
缺点:如果上下行音符数量不一致,高亮部分会错位
方式四:根据当前 Cursor 来获取对应的 Note
Link: https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/pull/659
function updateNoteColorByCursor(state = true) {
let currentVoiceEntries = osmd.cursor.iterator.currentVoiceEntries
if (typeof currentVoiceEntries != "undefined") {
for (i = 0; i < currentVoiceEntries.length; i++) {
let note = currentVoiceEntries[i].notes[0]
let gNote = opensheetmusicdisplay.GraphicalNote.FromNote(note, osmd.rules)
let el = gNote.getSVGGElement()
var noteColor;
if (state) {
noteColor = '#20a162'
} else {
noteColor = '#FF453A'
}
// 过滤掉吉他谱的数字部分
if (el != null) {
el.querySelectorAll('path').forEach(item => {
item.setAttribute('fill', noteColor);
})
}
}
}
}
这种其实也是根据svg
的id
实现变色,但是获取方式不同。根据当前cursor
来获取可以保证不会发生高亮错位等问题
这样就解决了音符变色的问题,那么清除着色的音符怎么办,重新渲染吗?其实不需要
function clearAllNoteColor() {
document.querySelectorAll('path').forEach(item => {
item.setAttribute('fill', '#000000');
})
}
需求二:高亮选中区域
方法一:移动光标根据音符位置进行绘制
function drawShadeByNote(jsonStr, totalSize) {
let rangeArray = jsonStr['ranges']
// 重置 Cursor
osmd.cursor.reset()
// 绘制高亮区间
if (rangeArray.length > 0) {
// 以下参数可根据实际显示情况自行调整
// 绘制点 y 坐标偏移量
let offsetY = 2
// 绘制点 x 左右偏移
let paddingX = 1
rangeArray.forEach(range => {
let start = range[0]
let end = range[1]
// 移动 cursor 到开始位置
for (var i = 1; i < start; i++) {
osmd.cursor.next()
}
let startNote = osmd.cursor.iterator.currentVoiceEntries[0].notes[0]
let gStartNote = opensheetmusicdisplay.GraphicalNote.FromNote(startNote, osmd.rules)
var startPosition = gStartNote.PositionAndShape.AbsolutePosition
var startPoint = new opensheetmusicdisplay.PointF2D(startPosition.x - paddingX, startPosition.y + offsetY)
// 保存上一个 position
var prePosition
// 移动 cursor 到结束位置
for (var i = start; i < end; i++) {
// 检查是否换行
let note = osmd.cursor.iterator.currentVoiceEntries[0].notes[0]
let gNote = opensheetmusicdisplay.GraphicalNote.FromNote(note, osmd.rules)
let position = gNote.PositionAndShape.AbsolutePosition
if (typeof prePosition != 'undefined' && prePosition.x > position.x) {
// 发生换行,先绘制这部分区间
let point = new opensheetmusicdisplay.PointF2D(prePosition.x + paddingX, startPosition.y + offsetY)
osmd.Drawer.DrawOverlayLine(startPoint, point, gStartNote.ParentMusicPage, '#e3b4b880', 15)
// 重新赋值 start 相关属性
startPosition = position
startPoint = new opensheetmusicdisplay.PointF2D(position.x - paddingX, position.y + offsetY)
console.log(prePosition, position, startPoint)
}
prePosition = position
osmd.cursor.next()
}
let endNote = osmd.cursor.iterator.currentVoiceEntries[0].notes[0]
let gEndNote = opensheetmusicdisplay.GraphicalNote.FromNote(endNote, osmd.rules)
let endPosition = gEndNote.PositionAndShape.AbsolutePosition
let endPoint = new opensheetmusicdisplay.PointF2D(endPosition.x + paddingX, startPosition.y + offsetY)
osmd.Drawer.DrawOverlayLine(startPoint, endPoint, gStartNote.ParentMusicPage, '#e3b4b880', 15)
})
}
// 隐藏 cursor
osmd.cursor.hide()
}
输入参数:drawShadeByNote({"ranges": [[1, 5], [15, 20]]}, 20)
效果如下:
可以看到虽然实现了效果,但是因为是根据当前音符来定位,这样难免会因为音符处在五线谱的不同位置而造成显示错位
缺点:绘制的遮罩不能完美对齐五线谱
方式二:移动 Cursor,根据 Cursor 位置进行绘制
function drawShadeByCursor(jsonStr, totalSize) {
let rangeArray = jsonStr['ranges']
// 重置 Cursor
osmd.cursor.reset()
// 高度偏移,可选
let offsetHeight = 10
// 绘制高亮区间
if (rangeArray.length > 0) {
let cursorWidth = document.querySelector('#cursorImg-0').width
// 高度需要实时获取,因为曲谱的不同小节音符位置不一样
// Cursor 的高度也会发生变化
// let cursorHeight = document.querySelector('#cursorImg-0').height
rangeArray.forEach(range => {
let start = range[0]
let end = range[1]
// 创建遮罩元素,使用光标的坐标来定位
let rangeMask = document.createElement('div')
rangeMask.id = "rangeMask"
rangeMask.style.backgroundColor = '#e3b4b880'
rangeMask.style.position = 'absolute'
// 移动 cursor 到开始位置
for (var i = 1; i < start; i++) {
osmd.cursor.next()
}
let startCursorLeft = document.querySelector('#cursorImg-0').style.left
let startCursorTop = document.querySelector('#cursorImg-0').style.top
// 设置遮罩位置
rangeMask.style.left = startCursorLeft
rangeMask.style.top = `${parseFloat(startCursorTop) - offsetHeight}px`
// 换行条件使用 cursor left 会出现奇怪的换行情况,暂时发现问题原因
// 所以换行条件使用 cursor top
// 保存上一个 cursor top
let preCursorTop
let perCursorLeft
// 移动 cursor 到结束位置
for (var i = start; i < end; i++) {
let cursorLeft = document.querySelector('#cursorImg-0').style.left
let cursorTop = document.querySelector('#cursorImg-0').style.top
if (typeof preCursorTop != 'undefined' && parseFloat(cursorTop) > parseFloat(preCursorTop)) {
// 发生换行
rangeMask.style.width = `${parseFloat(perCursorLeft) - parseFloat(startCursorLeft) + cursorWidth}px`
rangeMask.style.height = `${document.querySelector('#cursorImg-0').height + 2 * offsetHeight}px`
document.getElementById('osmdCanvasPage1').appendChild(rangeMask)
// 重新设置相关属性
startCursorLeft = cursorLeft
startCursorTop = cursorTop
rangeMask = document.createElement('div')
rangeMask.id = "rangeMask"
rangeMask.style.backgroundColor = '#e3b4b880'
rangeMask.style.position = 'absolute'
rangeMask.style.left = cursorLeft
rangeMask.style.top = `${parseFloat(startCursorTop) - offsetHeight}px`
}
preCursorTop = cursorTop
perCursorLeft = cursorLeft
osmd.cursor.next()
}
let endCursorLeft = document.querySelector('#cursorImg-0').style.left
rangeMask.style.width = `${parseFloat(endCursorLeft) - parseFloat(startCursorLeft) + cursorWidth}px`
rangeMask.style.height = `${document.querySelector('#cursorImg-0').height + 2 * offsetHeight}px`
document.getElementById('osmdCanvasPage1').appendChild(rangeMask)
})
}
// 隐藏 cursor
osmd.cursor.hide()
}
这种就能够完美匹配五线谱的大小
效果如下:
当然清除这些遮罩也是很简单的,因为我们之前设置了id
function clearAllMessyRange() {
osmd.cursor.show()
document.querySelectorAll('#rangeMask').forEach(e => { e.remove() })
}
结尾
基本我碰到的就这些问题,说实话这些研究起来还是比较费时间的,如果各位有什么更好的解决办法欢迎给我留言
PS:以上代码我是改了部分命名,可能会有某些参数名字对不上,如果有也可以在评论提出
后续可能还会有更新,到时候再开一篇