import { defineComponent, onMounted, PropType, ref, Ref, toRefs, onUpdated, onUnmounted, watch } from 'vue' import { EngravingRules, IMusicScore, OpenSheetMusicDisplay, OSMDOptions } from '/osmd-extended/src' import { useEngravingRules, useOsmd, useOsmdLoader } from './uses' import SettingState from '/src/pages/detail/setting-state' import detailState from '/src/pages/detail/state' import event from '/src/components/music-score/event' import styles from '/src/components/music-score/index.module.less' import SectionWrap from '../pages/detail/section-box' import runtime, * as RuntimeUtils from '/src/pages/detail/runtime' import { Toast } from 'vant' import axios from 'umi-request' import { Cursor } from '../helpers/cursor' import { formatZoom } from '../helpers/utils' export type onRerenderType = (osmd?: OpenSheetMusicDisplay | null) => void export type SetRenderOptions = { score?: string } const rendered = ref(false) export default defineComponent({ name: 'music-score', props: { isSoundEffect: { type: Boolean, default: false, }, score: { type: String, default: '', }, showPartNames: { type: Boolean, default: false, }, opotions: { type: Object as PropType, default: () => {}, }, EngravingRules: { type: Object as PropType<{ [P in keyof EngravingRules]?: EngravingRules[P] | undefined }>, default: () => {}, }, showSection: { type: Boolean, default: true, }, }, emits: ['rerender', 'startRender', 'renderError', 'loaddingEnd'], setup(props, { emit, expose }) { // 获取svg文件 const getSvgXml = async (url: string) => { event.off('section-click', RuntimeUtils.noteClick) emit('startRender') const svg = await axios.get(url) if (container.value) { let svgDiv = document.createElement('div') svgDiv.innerHTML = svg container.value?.appendChild(svgDiv.firstElementChild!) //innerHTML = svg let svgEL = document.querySelector('#osmdSvgPage1') const svgContainer = document.getElementById('svgContainer') let width: any = svgEL?.getAttribute('width') let height: any = svgEL?.getAttribute('height') width = isNaN(Number(width)) ? 0 : Number(width) height = isNaN(Number(height)) ? 0 : Number(height) if (width && height) { let ratio = height / width let _w: any = svgContainer?.offsetWidth || document.body.clientWidth detailState.zoom = _w / width svgEL?.setAttribute('width', _w) svgEL?.setAttribute('height', _w * ratio + '') } } } const getNotePosition = (times: any[]) => { for (let i = 0; i < times.length; i++) { const item = times[i] if (item?.svgElelent?.bbox) { item.svgElelent.bbox.x *= detailState.zoom item.svgElelent.bbox.y *= detailState.zoom item.svgElelent.bbox.w *= detailState.zoom item.svgElelent.bbox.h *= detailState.zoom } if (item.noteElement?.sourceMeasure?.verticalMeasureList?.[0]?.boundingBox) { item.noteElement.sourceMeasure.verticalMeasureList[0].boundingBox.absolutePosition.x *= detailState.zoom item.noteElement.sourceMeasure.verticalMeasureList[0].boundingBox.absolutePosition.y *= detailState.zoom item.noteElement.sourceMeasure.verticalMeasureList[0].boundingBox.size.width *= detailState.zoom item.noteElement.sourceMeasure.verticalMeasureList[0].boundingBox.size.height *= detailState.zoom } if (item.noteElement?.sourceMeasure?.verticalMeasureList?.[0]?.stave) { item.noteElement.sourceMeasure.verticalMeasureList[0].stave.x *= detailState.zoom item.noteElement.sourceMeasure.verticalMeasureList[0].stave.y *= detailState.zoom item.noteElement.sourceMeasure.verticalMeasureList[0].stave.width *= detailState.zoom item.noteElement.sourceMeasure.verticalMeasureList[0].stave.height *= detailState.zoom item.noteElement.sourceMeasure.verticalMeasureList[0].stave.start_x *= detailState.zoom item.noteElement.sourceMeasure.verticalMeasureList[0].stave.end_x *= detailState.zoom } if (item?.cursorBox?.move) { console.log(detailState.zoom) // console.log("🚀 ~ item.cursorBox.x", item.cursorBox.x * detailState.zoom, item?.svgElelent?.bbox?.x) // item.cursorBox.x = item?.svgElelent?.bbox?.x ? formatZoom(item.svgElelent.bbox.x) : item.cursorBox.x * detailState.zoom item.cursorBox.x *= detailState.zoom item.cursorBox.y = item.cursorBox.y * detailState.zoom + 10 item.cursorBox.w *= detailState.zoom item.cursorBox.h *= detailState.zoom } } return times } // 获取json数据 const getMusicJson = async (url: string) => { const res = await axios.get(url) SettingState.sett.scoreSize = res?.osmd?.scoreSize || 'middle' if (res && Array.isArray(res.times)) { detailState.times = getNotePosition(res.times) // res.times if (detailState.times?.[0]?.cursorBox?.move) { // 初始化cursor res.osmd.cursor = new Cursor({ ...detailState.times?.[0]?.cursorBox }) // console.log("🚀 ~ res.osmd.cursor", res.osmd.cursor) container.value?.appendChild(res.osmd.cursor.img) } detailState.renderType = 'cache' emit('rerender', res.osmd) runtime.isFirstPlay = false event.on('section-click', RuntimeUtils.noteClick) } } const { EngravingRules, opotions, showPartNames, score } = toRefs(props) const container: Ref = ref() let osmd: Ref = ref() const setOsdm = () => { event.off('section-click', RuntimeUtils.noteClick) emit('startRender') osmd.value = useOsmd( container, { ...opotions.value, drawPartNames: showPartNames.value }, EngravingRules.value ).value if (osmd.value) { useEngravingRules(osmd.value, EngravingRules.value) runtime.isFirstPlay = false event.on('section-click', RuntimeUtils.noteClick) } } // 直接读取渲染数据 const productJsonAndSvg = async (renderData: any) => { await getSvgXml(renderData.svg) getMusicJson(renderData.json) } onMounted(async () => { if (rendered.value) return // console.log( SettingState.sett.keySignature, detailState.activeDetail) if ( (SettingState.sett.type === 'staff' && !detailState?.activeDetail?.musicSvg) || // 没有五线谱缓存数据 (SettingState.sett.type === 'jianpu' && SettingState.sett.keySignature && !detailState?.activeDetail?.musicFirstSvg) || // 没有固定调缓存数据 (SettingState.sett.type === 'jianpu' && !SettingState.sett.keySignature && !detailState?.activeDetail?.musicJianSvg) // 没有首调缓存数据 ) { setOsdm() return } let renderData: any = null try { if (SettingState.sett.type === 'staff' && detailState.activeDetail?.musicSvg) { renderData = JSON.parse(detailState.activeDetail.musicSvg) } else { if (SettingState.sett.keySignature && detailState.activeDetail?.musicFirstSvg) { //固定调 renderData = JSON.parse(detailState.activeDetail.musicFirstSvg) } else { // 首调 if (detailState.activeDetail?.musicJianSvg) { renderData = JSON.parse(detailState.activeDetail.musicJianSvg) } } } } catch (error) { console.error(error) detailState.renderType = 'native' } if (renderData && !props.isSoundEffect) { productJsonAndSvg(renderData) } else { setOsdm() } }) onUnmounted(() => { event.off('section-click', RuntimeUtils.noteClick) }) watch( [score, osmd], async () => { if (osmd && osmd.value && score.value) { try { emit('startRender') await useOsmdLoader(osmd.value, score.value) emit('rerender', osmd.value) event.emit('loaded') resetFormate() } catch (error) { console.error(error) emit('renderError') } } }, { immediate: true } ) /** * 重新渲染曲谱方法 * @param {SetRenderOptions} options */ const setRender = async ({ score: s }: SetRenderOptions = {}) => { const renderScore = s || score.value // console.log(renderScore) // Toast('加载中,请稍后...') setTimeout(async () => { if (osmd && osmd.value && renderScore) { await osmd.value.clear() setOsdm() try { detailState.section = [] detailState.sectionStatus = false await useOsmdLoader(osmd.value, renderScore) emit('rerender', osmd.value) event.emit('loaded') resetFormate() } catch (error) { console.error(error) emit('renderError') } } }, 100) } const reRender = async () => { // if (detailState.renderType == 'cache'){ // detailState.times = getNotePosition(detailState.times) // } await osmd.value?.render() } const resetFormate = () => { const stafflines: SVGAElement[] = Array.from((container.value as HTMLElement).querySelectorAll('.staffline')) const baseStep = 4 // 两个元素相间,的间距 for (let i = 0, len = stafflines.length; i < len; i++) { const staffline = stafflines[i] const stafflineBox = staffline.getBBox() const stafflineCenter = stafflineBox.y + stafflineBox.height / 2 const vfmeasures: SVGAElement[] = Array.from(staffline.querySelectorAll('.vf-measure')) const vfcurve: SVGAElement[] = Array.from(staffline.querySelectorAll('.vf-curve')) const vfvoices: SVGAElement[] = Array.from(staffline.querySelectorAll('.vf-measure > .vf-voices')) const vfbeams: SVGAElement[] = Array.from(staffline.querySelectorAll('.vf-measure > .vf-beams')) const vfties: SVGAElement[] = Array.from(staffline.querySelectorAll('.vf-ties')) const vflines: SVGAElement[] = Array.from(staffline.querySelectorAll('.vf-line')) const texts: SVGAElement[] = Array.from(staffline.querySelectorAll('.vf-measure > .vf-stave text')) const rects: SVGAElement[] = Array.from(staffline.querySelectorAll('.vf-measure > .vf-stave rect[fill=none]')) const staveSection: SVGAElement[] = Array.from(staffline.querySelectorAll('.vf-measure .vf-staveSection')) // 反复标记 和 小节碰撞 const repetWord = ['To Coda', 'D.S. al Coda', 'Coda'] texts .filter((n) => repetWord.includes(n.textContent || '')) .forEach((t) => { vfbeams.forEach((curve) => { const result = collisionDetection(t, curve) const prePath: SVGAElement = t?.previousSibling as unknown as SVGAElement if (result.isCollision) { const shift_y = Number(t.getAttribute('y')) - (result.b1 - result.t2) - baseStep + '' t.setAttribute('y', shift_y) if ( prePath && prePath.getAttribute('stroke-width') === '0.3' && prePath.getAttribute('stroke') === 'none' && (prePath.getAttribute('d')?.length || 0) > 3000 ) { prePath.style.transform = `translateY(${-(result.b1 - result.t2 + baseStep)}px)` } } }) vfvoices.forEach((curve) => { const result = collisionDetection(t, curve) const prePath: SVGAElement = t?.previousSibling as unknown as SVGAElement if (result.isCollision) { const shift_y = Number(t.getAttribute('y')) - (result.b1 - result.t2) - baseStep + '' t.setAttribute('y', shift_y) if ( prePath && prePath.getAttribute('stroke-width') === '0.3' && prePath.getAttribute('stroke') === 'none' && (prePath.getAttribute('d')?.length || 0) > 3000 ) { prePath.style.transform = `translateY(${-(result.b1 - result.t2 + baseStep)}px)` } } }) }) // 文字方框和飞线碰撞 // console.log(staveSection, 'staveSection out') staveSection.forEach((t) => { let shift_y = 0 ;[...vfcurve, ...vfties, ...vfvoices].forEach((curve) => { const result = collisionDetection(t, curve) // console.log(result, curve) if (result.isCollision) { shift_y = Math.min(shift_y, result.t2 - result.b1 - baseStep) } }) t.style.transform = `translateY(${shift_y}px)` }) // 文字和小节碰撞 let vftexts = Array.from(staffline.querySelectorAll('.vf-text > text')).filter( (n: any) => n.getBBox().y < stafflineCenter ) for (let i = 0; i < vftexts.length; i++) { const _text = vftexts[i] for (let j = 0; j < vftexts.length; j++) { if (_text.parentNode === vftexts[j].parentNode) continue const result = collisionDetection(_text as SVGAElement, vftexts[j] as SVGAElement) if (result.isCollision) { if (_text.textContent === vftexts[j].textContent) { vftexts[j].parentNode?.removeChild(vftexts[j]) continue } } } } vftexts = Array.from(staffline.querySelectorAll('.vf-text > text')).filter( (n: any) => n.getBBox().y < stafflineCenter ) let maxY = 0 let _vftexts: SVGAElement[] = [] vftexts.forEach((vftext: any) => { const textBox = vftext.getBBox() if (textBox.y < stafflineCenter) { maxY = Math.max(maxY, textBox.y + textBox.height) _vftexts.push(vftext as SVGAElement) } }) if (maxY !== 0 && _vftexts.length > 1) { _vftexts.forEach((vftext) => { vftext.setAttribute('y', maxY + '') }) } vftexts.forEach((vftext) => { ;[...vfcurve, ...vfmeasures, ...vflines].forEach((vfmeasure) => { let result = collisionDetection(vftext as SVGAElement, vfmeasure) if (result.isCollision && result.b1 < result.b2 && result.t1 < result.b2 - (result.b2 - result.t2) / 2) { const shift_y = Number(vftext.getAttribute('y')) - (result.b1 - result.t2) - baseStep + '' vftext.setAttribute('y', shift_y) } }) }) vftexts.forEach((vftext) => { vftexts.forEach((text) => { if ( vftext.parentNode !== text.parentNode && !['marcato', 'legato'].includes(vftext.textContent as string) ) { if (['marcato', 'legato'].includes(text.textContent as string)) { const result = collisionDetection(vftext as SVGAElement, text as SVGAElement, 30, 30) if (result.isCollision) { const textBBox = (vftext as SVGAElement).getBBox() text.setAttribute('x', textBBox.x + textBBox.width + 5 + '') text.setAttribute('y', textBBox.y + textBBox.height - 5 + '') } } else { const result = collisionDetection(vftext as SVGAElement, text as SVGAElement) if (result.isCollision) { const _y = Number(vftext.getAttribute('y')) const shift_y = result.b2 - result.t2 < 24 ? 24 : result.b2 - result.t2 text.setAttribute('y', _y - shift_y - 0.5 + '') } } } }) }) const vftextBottom = Array.from(staffline.querySelectorAll('.vf-text > text')).filter( (n: any) => n.getBBox().y > stafflineCenter ) const vflineBottom = Array.from(staffline.querySelectorAll('.vf-line')).filter( (n: any) => n.getBBox().y > stafflineCenter ) // 去重 for (let i = 0; i < vftextBottom.length; i++) { const _text = vftextBottom[i] for (let j = 0; j < vftextBottom.length; j++) { if (_text.parentNode === vftextBottom[j].parentNode) continue const result = collisionDetection(_text as SVGAElement, vftextBottom[j] as SVGAElement) if (result.isCollision) { if (_text.textContent === vftextBottom[j].textContent) { vftextBottom[j].parentNode?.removeChild(vftextBottom[j]) continue } } } } // 1,2线谱底部文字重叠问题 vftextBottom.forEach((vftext) => { ;[...vfmeasures].forEach((n) => { let result = collisionDetection(vftext as SVGAElement, n) if (result.isCollision) { vftext.setAttribute('y', result.b2 + Math.abs(result.t1 - Number(vftext.getAttribute('y'))) + '') } }) }) // 如果渐弱渐强有平行的文字 vflineBottom.forEach((line) => { const texts: any[] = [] if (line.nextElementSibling?.classList.contains('vf-line')) { vftextBottom.forEach((text) => { let result = collisionDetection(line as SVGAElement, text as SVGAElement, 20, 20) if (result.isCollision) { texts.push({ text: text as SVGAElement, result, }) } }) } if (texts.length === 1) { const result = texts[0].result const text = texts[0].text if (result.x2 + result.w2 < result.x1) { // 左 if (Math.abs(result.y2 - result.y1) > 10) { text.setAttribute('y', result.y1 + result.h2 / 2 + '') } } else if (result.x2 > result.x1 + result.w1) { // 右 if (Math.abs(result.y2 - result.y1) > 10) { text.setAttribute('y', result.y1 + result.h2 / 2 + '') } } else { if (Math.abs(result.x2 - result.x1) < Math.abs(result.x2 + result.w2 - result.x1 - result.w1)) { // console.log(text, '有交集', '靠左') text.setAttribute('x', result.x1 - result.w2 - 5 + '') if (Math.abs(result.y2 - result.y1) > 10) { text.setAttribute('y', result.y1 + result.h2 / 2 + '') } } else { // console.log(text, '有交集', '靠右') text.setAttribute('x', result.x1 + result.w1 + 5 + '') if (Math.abs(result.y2 - result.y1) > 10) { text.setAttribute('y', result.y1 + result.h2 / 2 + '') } } } } else if (texts.length === 2) { const result1 = texts[0].result const text1 = texts[0].text const result2 = texts[1].result const text2 = texts[1].text text1.setAttribute('x', result1.x1 - result1.w2 - 5 + '') if (Math.abs(result1.y2 - result1.y1) > 10) { text1.setAttribute('y', result1.y1 + result1.h2 / 2 + '') } text2.setAttribute('x', result2.x1 + result2.w1 + 5 + '') if (Math.abs(result2.y2 - result2.y1) > 10) { text2.setAttribute('y', result2.y1 + result2.h2 / 2 + '') } } else if (texts.length === 3) { // console.log(texts) } }) vftextBottom.forEach((vftext) => { vftextBottom.forEach((text) => { if ( vftext.parentNode !== text.parentNode && !['marcato', 'legato', 'cresc.', 'Cantabile'].includes(vftext.textContent as string) ) { if (['marcato', 'legato', 'cresc.', 'Cantabile'].includes(text.textContent as string)) { const result = collisionDetection(vftext as SVGAElement, text as SVGAElement, 30, 30) if (result.isCollision) { const textBBox = (vftext as SVGAElement).getBBox() text.setAttribute('x', textBBox.x + textBBox.width + 5 + '') text.setAttribute('y', textBBox.y + textBBox.height - 5 + '') } } else { const result = collisionDetection(vftext as SVGAElement, text as SVGAElement) if (result.isCollision) { text.setAttribute('y', result.y1 + result.h1 + result.h2 + '') } } } }) }) } setTimeout(() => resetGlobalText()) } // 技巧文本 const resetGlobalText = () => { if (!container.value) return const svg = container.value.querySelector('svg') if (!svg) return const svgBBox = svg.getBBox() let vfstavetempo: SVGAElement[] = Array.from(container.value.querySelectorAll('.vf-stavetempo')).reduce( (eles: SVGAElement[], value: any) => { if (eles.find((n) => n.outerHTML === value.outerHTML)) value?.parentNode?.removeChild(value) else eles.push(value) return eles }, [] ) const staffline: SVGAElement[] = Array.from(container.value.querySelectorAll('.staffline')) const vfmeasures: SVGAElement[] = Array.from(container.value.querySelectorAll('.staffline > .vf-measure')) const vftexts: SVGAElement[] = Array.from(container.value.querySelectorAll('.staffline > .vf-text')) const vfcurves: SVGAElement[] = Array.from(container.value.querySelectorAll('.staffline > .vf-curve')) vfstavetempo.forEach((child: SVGAElement) => { let _y = 0 ;[...vfmeasures, ...vftexts, ...vfcurves].forEach((ele) => { const result = collisionDetection(child as SVGAElement, ele) if (result.isCollision && (result.b1 < result.b2 || result.r1 > result.l2 || result.l1 < result.r2)) { _y = Math.min(_y, result.t2 - result.b1) } }) if (_y !== 0) { child.style.transform = `translateY(${_y}px)` } const childBBox = child.getBBox() const rightY = (childBBox.x + childBBox.width) * 0.7 - Number(svg.getAttribute('width')) if (rightY > 0) { ;[...staffline, ...vfstavetempo].forEach((tempo) => { if (child != tempo) { const result = collisionDetection(child as SVGAElement, tempo, Math.abs(rightY), Math.abs(_y)) if (result.isCollision) { _y = result.t2 - result.b1 } } }) child.style.transform = `translate(-${rightY / 0.7}px,${_y}px)` } }) if (svgBBox.y < 0) { svg.setAttribute('height', Number(svg.getAttribute('height')) - svgBBox.y + 10 + '') } } // 碰撞检测 const collisionDetection = (a: SVGAElement, b: SVGAElement, distance: number = 0, distance_y: number = 0) => { const abbox = a.getBBox() const bbbox = b.getBBox() let t1 = abbox.y - distance_y let l1 = abbox.x - distance let r1 = abbox.x + abbox.width + distance let b1 = abbox.y + abbox.height + distance_y let t2 = bbbox.y let l2 = bbbox.x let r2 = bbbox.x + bbbox.width let b2 = bbbox.y + bbbox.height if (b1 < t2 || l1 > r2 || t1 > b2 || r1 < l2) { // 表示没碰上 return { isCollision: false, t1, l1, r1, b1, t2, l2, r2, b2, x1: abbox.x, y1: abbox.y, x2: bbbox.x, y2: bbbox.y, h1: abbox.height, h2: bbbox.height, w1: abbox.width, w2: bbbox.width, } } else { return { isCollision: true, t1, l1, r1, b1, t2, l2, r2, b2, x1: abbox.x, y1: abbox.y, x2: bbbox.x, y2: bbbox.y, h1: abbox.height, h2: bbbox.height, w1: abbox.width, w2: bbbox.width, } } } expose({ setRender, reRender, }) return () => { return (
{/* 小节覆盖层 */} {props.showSection && ( )}
) } }, })