[Open Sheet Music Display] 一:音符着色及区域高亮

介绍

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++
}

这种通过获得当前节点的音符对应的svgid,以实现直接修改音符颜色,这种方式不需要重新渲染。但是有个限制条件:就是只适用于单行曲谱

缺点:只能用于单行曲谱

方式三:根据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++
    }
}

这个方法弥补了之前只能作用于当行曲谱的缺陷,考虑到上下曲谱可能会个数不一致,所以使用最大长度作为遍历条件。确保能全部遍历到,但是测试了一些曲谱还是发现了问题:


图1.png

可以看第二小节的第二个节点出现问题了,本来应该要等下行的到第五个上面才应该变色的,但是实际代码获取的列表并不会这么理想,获取到的只是向下都是个数为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);
                })
            }
        }
    }
}

这种其实也是根据svgid实现变色,但是获取方式不同。根据当前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)

效果如下:


图2.png

可以看到虽然实现了效果,但是因为是根据当前音符来定位,这样难免会因为音符处在五线谱的不同位置而造成显示错位

缺点:绘制的遮罩不能完美对齐五线谱

方式二:移动 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()
}

这种就能够完美匹配五线谱的大小

效果如下:


图3.png

当然清除这些遮罩也是很简单的,因为我们之前设置了id

function clearAllMessyRange() {
    osmd.cursor.show()
    document.querySelectorAll('#rangeMask').forEach(e => { e.remove() })
}

结尾

基本我碰到的就这些问题,说实话这些研究起来还是比较费时间的,如果各位有什么更好的解决办法欢迎给我留言

PS:以上代码我是改了部分命名,可能会有某些参数名字对不上,如果有也可以在评论提出

后续可能还会有更新,到时候再开一篇

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容