# 简谱渲染细节规范 > **文档状态:** ✅ 已完成 > **创建日期:** 2026-01-29 > **用途:** 定义简谱各元素的精确渲染规则 > **适用于:** 简谱渲染引擎 - 绘制模块 --- ## 目录 1. [视觉尺寸标准](#1-视觉尺寸标准) 2. [音符数字渲染](#2-音符数字渲染) 3. [增时线渲染规范](#3-增时线渲染规范) 4. [减时线渲染规范](#4-减时线渲染规范) 5. [高低音点规范](#5-高低音点规范) 6. [附点规范](#6-附点规范) 7. [升降号规范](#7-升降号规范) 8. [休止符规范](#8-休止符规范) 9. [小节线规范](#9-小节线规范) 10. [歌词渲染规范](#10-歌词渲染规范) 11. [布局规范](#11-布局规范) --- ## 1. 视觉尺寸标准 ### 1.1 默认配置常量 ```typescript const RENDER_CONSTANTS = { // ===== 间距配置 ===== /** 四分音符水平间距(像素) */ QUARTER_NOTE_SPACING: 50, /** 小节左右内边距(像素) */ MEASURE_PADDING: 20, /** 行间距(像素) */ SYSTEM_SPACING: 100, /** 声部间距(像素) */ VOICE_SPACING: 60, // ===== 字体配置 ===== /** 音符数字字体大小(像素) */ NOTE_FONT_SIZE: 24, /** 歌词字体大小(像素) */ LYRIC_FONT_SIZE: 14, /** 升降号字体大小(像素) */ ACCIDENTAL_FONT_SIZE: 12, /** 音符字体 */ NOTE_FONT: 'Arial, "Noto Sans SC", sans-serif', /** 歌词字体 */ LYRIC_FONT: '"Microsoft YaHei", "Noto Sans SC", sans-serif', // ===== 尺寸配置 ===== /** 高低音点半径(像素) */ OCTAVE_DOT_RADIUS: 2.5, /** 附点半径(像素) */ DURATION_DOT_RADIUS: 2, /** 减时线高度/粗细(像素) */ UNDERLINE_HEIGHT: 1.5, /** 减时线间距(像素) */ UNDERLINE_GAP: 3, /** 增时线高度/粗细(像素) */ EXTENSION_LINE_HEIGHT: 1.5, /** 小节线粗细(像素) */ BARLINE_WIDTH: 1, // ===== 颜色配置 ===== /** 音符颜色 */ NOTE_COLOR: '#000000', /** 线条颜色 */ LINE_COLOR: '#000000', /** 歌词颜色 */ LYRIC_COLOR: '#333333', }; ``` ### 1.2 尺寸比例关系 ``` 音符区域高度分配: ┌─────────────────────────────┐ │ 升降号区域 (12px) │ ← accidental ├─────────────────────────────┤ │ 高音点区域 (8px) │ ← octave dots (high) ├─────────────────────────────┤ │ │ │ 音符数字 (24px) │ ← note number │ │ ├─────────────────────────────┤ │ 低音点区域 (8px) │ ← octave dots (low) ├─────────────────────────────┤ │ 减时线区域 (12px) │ ← underlines ├─────────────────────────────┤ │ 歌词区域 (20px) │ ← lyrics └─────────────────────────────┘ 总高度约:84px(单声部无歌词约64px) ``` --- ## 2. 音符数字渲染 ### 2.1 基本渲染规则 ```typescript interface NoteRenderSpec { /** 数字内容:1-7 或 0(休止符) */ text: string; /** 字体大小 */ fontSize: number; /** 字体粗细 */ fontWeight: 'normal' | 'bold'; /** 水平对齐 */ textAnchor: 'middle'; /** 垂直对齐基线 */ dominantBaseline: 'central'; } ``` ### 2.2 SVG实现 ```typescript function drawNoteNumber( x: number, y: number, pitch: number, config: RenderConfig ): SVGTextElement { const text = document.createElementNS(SVG_NS, 'text'); text.setAttribute('x', String(x)); text.setAttribute('y', String(y)); text.setAttribute('font-size', String(config.noteFontSize)); text.setAttribute('font-family', config.noteFont); text.setAttribute('text-anchor', 'middle'); text.setAttribute('dominant-baseline', 'central'); text.setAttribute('fill', config.noteColor); text.setAttribute('class', 'vf-numbered-note-head'); text.textContent = String(pitch); return text; } ``` ### 2.3 音符宽度计算 ```typescript /** * 计算音符显示宽度 * 基于时值计算,保证固定时间比例 */ function calculateNoteWidth(realValue: number, quarterSpacing: number): number { return realValue * quarterSpacing; } // 示例: // 四分音符(1.0) → 50px // 八分音符(0.5) → 25px // 二分音符(2.0) → 100px // 全音符(4.0) → 200px ``` --- ## 3. 增时线渲染规范 ### 3.1 核心规则 ⭐⭐⭐ **增时线是简谱渲染的核心难点!** ``` 增时线规则: 1. 只有时值 >= 1.0(四分音符及以上)才可能有增时线 2. 数量 = Math.floor(realValue) - 1 3. 每条增时线代表1个四分音符的时值 4. 增时线应该按时值均匀分布在占用的空间中 ``` ### 3.2 数量计算 | 音符类型 | 时值(realValue) | 增时线数量 | 视觉表示 | |---------|----------------|-----------|---------| | 四分音符 | 1.0 | 0 | `5` | | 附点四分 | 1.5 | 0 | `5·` | | 二分音符 | 2.0 | 1 | `5 —` | | 附点二分 | 3.0 | 2 | `5· — —` | | 全音符 | 4.0 | 3 | `5 — — —` | ```typescript function calcExtensionLineCount(realValue: number): number { if (realValue < 1.0) return 0; return Math.floor(realValue) - 1; } ``` ### 3.3 位置计算 ⭐⭐⭐ **关键:增时线要按四分音符间距均匀分布!** ```typescript interface ExtensionLinePosition { /** 增时线起始X坐标 */ x: number; /** 增时线Y坐标(与音符数字基线平齐) */ y: number; /** 增时线长度 */ width: number; } /** * 计算增时线位置 * @param noteX 音符中心X坐标 * @param noteY 音符中心Y坐标 * @param lineIndex 增时线索引(0开始) * @param quarterSpacing 四分音符间距 */ function calcExtensionLinePosition( noteX: number, noteY: number, lineIndex: number, quarterSpacing: number ): ExtensionLinePosition { // 每条增时线占据1个四分音符的空间 // 第1条增时线在音符后的第1个四分音符位置 // 第2条增时线在音符后的第2个四分音符位置... const lineStartX = noteX + (lineIndex + 1) * quarterSpacing; const lineWidth = quarterSpacing * 0.7; // 占空间的70%,居中 const lineX = lineStartX - lineWidth / 2; return { x: lineX, y: noteY, width: lineWidth, }; } ``` ### 3.4 视觉示意图 ``` 二分音符(2拍): ┌─────────┬─────────┐ │ 5 │ — │ │ 第1拍 │ 第2拍 │ └─────────┴─────────┘ 音符 增时线 全音符(4拍): ┌─────────┬─────────┬─────────┬─────────┐ │ 5 │ — │ — │ — │ │ 第1拍 │ 第2拍 │ 第3拍 │ 第4拍 │ └─────────┴─────────┴─────────┴─────────┘ 音符 增时线1 增时线2 增时线3 ``` ### 3.5 增时线样式 ```typescript function drawExtensionLine( x: number, y: number, width: number, config: RenderConfig ): SVGRectElement { const line = document.createElementNS(SVG_NS, 'rect'); line.setAttribute('x', String(x)); line.setAttribute('y', String(y - config.extensionLineHeight / 2)); line.setAttribute('width', String(width)); line.setAttribute('height', String(config.extensionLineHeight)); line.setAttribute('fill', config.lineColor); line.setAttribute('class', 'vf-extension-line'); return line; } ``` ### 3.6 附点+增时线组合 ``` 附点二分音符(3拍): ┌───────────────┬─────────┬─────────┐ │ 5· │ — │ — │ │ 1.5拍 │ 第2拍 │ 第3拍 │ └───────────────┴─────────┴─────────┘ 附点四分音符占1.5拍,后面2条增时线各占1拍 计算方式: - realValue = 3.0 - 增时线数量 = Math.floor(3.0) - 1 = 2 - 第1条增时线位置:noteX + quarterSpacing * 1.5(从附点结束位置开始) - 第2条增时线位置:noteX + quarterSpacing * 2.5 ``` ### 3.7 多声部场景 ``` 多声部时,增时线要与其他声部的音符垂直对齐: 声部1: 5 — — — (全音符) 声部2: 1 2 3 4 (四个四分音符) ↑ ↑ ↑ ↑ X坐标必须对齐 规则:先进行多声部对齐计算,再绘制增时线 ``` --- ## 4. 减时线渲染规范 ### 4.1 数量计算 | 音符类型 | 时值(realValue) | 减时线数量 | 视觉表示 | |---------|----------------|-----------|---------| | 四分音符 | 1.0 | 0 | `5` | | 八分音符 | 0.5 | 1 | `5` + 1条线 | | 十六分音符 | 0.25 | 2 | `5` + 2条线 | | 三十二分音符 | 0.125 | 3 | `5` + 3条线 | ```typescript function calcUnderlineCount(realValue: number): number { if (realValue >= 1.0) return 0; return Math.round(Math.log2(1 / realValue)); } ``` ### 4.2 位置和间距 ```typescript interface UnderlineSpec { /** 减时线宽度 */ width: number; /** 减时线高度(粗细) */ height: number; /** 第一条线距离音符底部的距离 */ topOffset: number; /** 多条线之间的间距 */ gap: number; } const UNDERLINE_SPEC: UnderlineSpec = { width: 16, // 略宽于数字宽度 height: 1.5, // 线条粗细 topOffset: 4, // 距音符底部4px gap: 3, // 多条线间距3px }; ``` ### 4.3 视觉示意图 ``` 八分音符(1条减时线): 5 ─── 十六分音符(2条减时线): 5 ─── ─── 三十二分音符(3条减时线): 5 ─── ─── ─── ``` ### 4.4 SVG实现 ```typescript function drawUnderlines( noteX: number, noteBottomY: number, count: number, config: RenderConfig ): SVGGElement { const group = document.createElementNS(SVG_NS, 'g'); group.setAttribute('class', 'vf-underlines'); const width = UNDERLINE_SPEC.width; const startX = noteX - width / 2; for (let i = 0; i < count; i++) { const y = noteBottomY + UNDERLINE_SPEC.topOffset + i * (UNDERLINE_SPEC.height + UNDERLINE_SPEC.gap); const line = document.createElementNS(SVG_NS, 'rect'); line.setAttribute('x', String(startX)); line.setAttribute('y', String(y)); line.setAttribute('width', String(width)); line.setAttribute('height', String(UNDERLINE_SPEC.height)); line.setAttribute('fill', config.lineColor); line.setAttribute('class', 'vf-underline'); group.appendChild(line); } return group; } ``` ### 4.5 连续音符的减时线连接 ``` 连续的相同时值音符,减时线应该连接: 分离绘制: 连接绘制(推荐): 1 2 3 4 1 2 3 4 _ _ _ _ ───────────── 规则: - 同一拍内的连续短音符,减时线连接 - 跨拍的音符,减时线可以分开 - 当前实现先使用分离绘制,后续优化为连接绘制 ``` --- ## 5. 高低音点规范 ### 5.1 位置规则 ``` 高音点:在音符数字上方 低音点:在音符数字下方 高两个八度: ·· 5 高一个八度: · 5 中音: 5 低一个八度: 5 · 低两个八度: 5 ·· ``` ### 5.2 尺寸和间距 ```typescript interface OctaveDotSpec { /** 点的半径 */ radius: number; /** 第一个点距离数字的距离 */ offset: number; /** 多个点之间的间距 */ gap: number; } const OCTAVE_DOT_SPEC: OctaveDotSpec = { radius: 2.5, offset: 6, // 距离数字6px gap: 5, // 点间距5px }; ``` ### 5.3 SVG实现 ```typescript function drawOctaveDots( noteX: number, noteY: number, octaveOffset: number, config: RenderConfig ): SVGGElement { const group = document.createElementNS(SVG_NS, 'g'); group.setAttribute('class', 'vf-octave-dots'); const count = Math.abs(octaveOffset); const isHigh = octaveOffset > 0; // 高音点在上方,低音点在下方 const baseY = isHigh ? noteY - config.noteFontSize / 2 - OCTAVE_DOT_SPEC.offset : noteY + config.noteFontSize / 2 + OCTAVE_DOT_SPEC.offset; for (let i = 0; i < count; i++) { const dotY = isHigh ? baseY - i * OCTAVE_DOT_SPEC.gap : baseY + i * OCTAVE_DOT_SPEC.gap; const dot = document.createElementNS(SVG_NS, 'circle'); dot.setAttribute('cx', String(noteX)); dot.setAttribute('cy', String(dotY)); dot.setAttribute('r', String(OCTAVE_DOT_SPEC.radius)); dot.setAttribute('fill', config.noteColor); dot.setAttribute('class', isHigh ? 'vf-high-dot' : 'vf-low-dot'); group.appendChild(dot); } return group; } ``` --- ## 6. 附点规范 ### 6.1 位置规则 ``` 附点位置:在音符数字的右侧,垂直居中 单附点: 5· 双附点: 5·· 三附点: 5···(极少见) ``` ### 6.2 尺寸和间距 ```typescript interface DurationDotSpec { /** 点的半径 */ radius: number; /** 第一个点距离数字右边的距离 */ offset: number; /** 多个点之间的间距 */ gap: number; } const DURATION_DOT_SPEC: DurationDotSpec = { radius: 2, offset: 4, // 距离数字右边4px gap: 4, // 点间距4px }; ``` ### 6.3 SVG实现 ```typescript function drawDurationDots( noteX: number, noteY: number, dotCount: number, config: RenderConfig ): SVGGElement { const group = document.createElementNS(SVG_NS, 'g'); group.setAttribute('class', 'vf-duration-dots'); // 数字宽度约为字体大小的0.6 const digitWidth = config.noteFontSize * 0.6; const startX = noteX + digitWidth / 2 + DURATION_DOT_SPEC.offset; for (let i = 0; i < dotCount; i++) { const dotX = startX + i * (DURATION_DOT_SPEC.radius * 2 + DURATION_DOT_SPEC.gap); const dot = document.createElementNS(SVG_NS, 'circle'); dot.setAttribute('cx', String(dotX)); dot.setAttribute('cy', String(noteY)); dot.setAttribute('r', String(DURATION_DOT_SPEC.radius)); dot.setAttribute('fill', config.noteColor); dot.setAttribute('class', 'vf-duration-dot'); group.appendChild(dot); } return group; } ``` --- ## 7. 升降号规范 ### 7.1 位置规则 ``` 升降号位置:在音符数字的左上方 升号: # 5 降号: ♭ 5 还原号: ♮ 5 ``` ### 7.2 符号字符 | 类型 | Unicode | 显示 | |------|---------|------| | 升号 | `#` 或 `\u266F` | # 或 ♯ | | 降号 | `\u266D` | ♭ | | 还原号 | `\u266E` | ♮ | | 重升 | `\u00D7` 或 `##` | × 或 ## | | 重降 | `\u266D\u266D` | ♭♭ | ### 7.3 尺寸和间距 ```typescript interface AccidentalSpec { /** 字体大小(相对于音符字体) */ fontSizeRatio: number; /** 水平偏移(向左) */ offsetX: number; /** 垂直偏移(向上) */ offsetY: number; } const ACCIDENTAL_SPEC: AccidentalSpec = { fontSizeRatio: 0.5, // 音符字体的50% offsetX: 8, // 向左偏移8px offsetY: 8, // 向上偏移8px }; ``` ### 7.4 SVG实现 ```typescript const ACCIDENTAL_SYMBOLS: Record = { 'sharp': '#', 'flat': '♭', 'natural': '♮', 'double-sharp': '×', 'double-flat': '♭♭', }; function drawAccidental( noteX: number, noteY: number, accidental: string, config: RenderConfig ): SVGTextElement { const text = document.createElementNS(SVG_NS, 'text'); const fontSize = config.noteFontSize * ACCIDENTAL_SPEC.fontSizeRatio; const x = noteX - config.noteFontSize * 0.3 - ACCIDENTAL_SPEC.offsetX; const y = noteY - config.noteFontSize * 0.3 - ACCIDENTAL_SPEC.offsetY; text.setAttribute('x', String(x)); text.setAttribute('y', String(y)); text.setAttribute('font-size', String(fontSize)); text.setAttribute('font-family', config.noteFont); text.setAttribute('text-anchor', 'middle'); text.setAttribute('fill', config.noteColor); text.setAttribute('class', 'vf-accidental'); text.textContent = ACCIDENTAL_SYMBOLS[accidental] || accidental; return text; } ``` --- ## 8. 休止符规范 ### 8.1 渲染规则 ``` 休止符用数字"0"表示,位置与普通音符相同 四分休止符: 0 八分休止符: 0 ─ 二分休止符: 0 —(不画增时线,用空白表示) 全休止符: 0 — — —(不画增时线,用空白表示) ``` ### 8.2 特殊处理 ```typescript function renderRest( x: number, y: number, realValue: number, config: RenderConfig ): SVGGElement { const group = document.createElementNS(SVG_NS, 'g'); group.setAttribute('class', 'vf-rest'); // 绘制数字"0" const text = drawNoteNumber(x, y, 0, config); group.appendChild(text); // 休止符的减时线正常绘制 if (realValue < 1.0) { const underlineCount = calcUnderlineCount(realValue); const underlines = drawUnderlines(x, y + config.noteFontSize / 2, underlineCount, config); group.appendChild(underlines); } // 休止符不绘制增时线! // 长休止符通过占据空间来表示 return group; } ``` --- ## 9. 小节线规范 ### 9.1 类型 | 类型 | 样式 | 说明 | |------|------|------| | single | `│` | 普通单线 | | double | `║` | 双线(段落结束) | | repeat-start | `║:` | 反复开始 | | repeat-end | `:║` | 反复结束 | | final | `║█` | 终止线 | ### 9.2 尺寸 ```typescript interface BarlineSpec { /** 单线粗细 */ thinWidth: number; /** 粗线粗细 */ thickWidth: number; /** 双线间距 */ doubleGap: number; /** 反复点半径 */ repeatDotRadius: number; /** 反复点间距 */ repeatDotGap: number; } const BARLINE_SPEC: BarlineSpec = { thinWidth: 1, thickWidth: 3, doubleGap: 3, repeatDotRadius: 2, repeatDotGap: 8, }; ``` ### 9.3 SVG实现 ```typescript function drawBarline( x: number, topY: number, bottomY: number, type: string, config: RenderConfig ): SVGGElement { const group = document.createElementNS(SVG_NS, 'g'); group.setAttribute('class', `vf-barline vf-barline-${type}`); const height = bottomY - topY; switch (type) { case 'single': group.appendChild(createLine(x, topY, x, bottomY, BARLINE_SPEC.thinWidth)); break; case 'double': group.appendChild(createLine(x - BARLINE_SPEC.doubleGap, topY, x - BARLINE_SPEC.doubleGap, bottomY, BARLINE_SPEC.thinWidth)); group.appendChild(createLine(x, topY, x, bottomY, BARLINE_SPEC.thinWidth)); break; case 'final': group.appendChild(createLine(x - BARLINE_SPEC.doubleGap - BARLINE_SPEC.thickWidth, topY, x - BARLINE_SPEC.doubleGap - BARLINE_SPEC.thickWidth, bottomY, BARLINE_SPEC.thinWidth)); group.appendChild(createRect(x - BARLINE_SPEC.thickWidth, topY, BARLINE_SPEC.thickWidth, height)); break; // ... 其他类型 } return group; } ``` --- ## 10. 歌词渲染规范 ### 10.1 位置规则 ``` 歌词位置:在音符下方,与音符水平对齐 单行歌词: 5 3 2 1 小 星 星 亮 多遍歌词(垂直排列): 5 3 2 1 小 星 星 亮 ← 第1遍 一 闪 一 闪 ← 第2遍 ``` ### 10.2 尺寸和间距 ```typescript interface LyricSpec { /** 字体大小 */ fontSize: number; /** 距离音符底部的距离 */ topOffset: number; /** 多遍歌词行间距 */ lineHeight: number; } const LYRIC_SPEC: LyricSpec = { fontSize: 14, topOffset: 25, // 距离音符底部25px lineHeight: 18, // 行间距18px }; ``` ### 10.3 DOM结构要求 ```html 歌词文字 ``` ### 10.4 SVG实现 ```typescript function drawLyric( noteX: number, noteBottomY: number, text: string, lyricIndex: number, noteId: string, config: RenderConfig ): SVGTextElement { const textEl = document.createElementNS(SVG_NS, 'text'); const y = noteBottomY + LYRIC_SPEC.topOffset + (lyricIndex - 1) * LYRIC_SPEC.lineHeight; textEl.setAttribute('x', String(noteX)); textEl.setAttribute('y', String(y)); textEl.setAttribute('font-size', String(LYRIC_SPEC.fontSize)); textEl.setAttribute('font-family', config.lyricFont); textEl.setAttribute('text-anchor', 'middle'); textEl.setAttribute('fill', config.lyricColor); textEl.setAttribute('class', `vf-lyric lyric${noteId}`); textEl.setAttribute('lyricIndex', String(lyricIndex)); textEl.setAttribute('data-note-id', noteId); textEl.textContent = text; return textEl; } ``` --- ## 11. 布局规范 ### 11.1 小节宽度计算 ```typescript /** * 计算小节宽度(固定时间比例) * 公式:(拍数 / 单位拍 × 4) × 四分音符间距 + 左右padding */ function calculateMeasureWidth( beats: number, beatType: number, quarterSpacing: number, padding: number ): number { // 小节总时值(以四分音符为单位) const totalBeats = beats / (beatType / 4); // 内容宽度 const contentWidth = totalBeats * quarterSpacing; // 总宽度 return contentWidth + padding * 2; } // 示例: // 4/4拍: (4 / 1) × 50 + 20 × 2 = 240px // 3/4拍: (3 / 1) × 50 + 20 × 2 = 190px // 6/8拍: (6 / 2) × 50 + 20 × 2 = 190px ``` ### 11.2 音符X坐标计算 ```typescript /** * 计算音符X坐标(固定时间比例) * 公式:小节X + padding + 时间戳 × 四分音符间距 */ function calculateNoteX( measureX: number, timestamp: number, padding: number, quarterSpacing: number ): number { return measureX + padding + timestamp * quarterSpacing; } ``` ### 11.3 自动换行规则 ```typescript interface LineBreakSpec { /** 最大行宽 */ maxWidth: number; /** 最小小节数/行 */ minMeasuresPerLine: number; /** 最大小节数/行 */ maxMeasuresPerLine: number; } function calculateLineBreaks( measures: JianpuMeasure[], maxWidth: number ): number[][] { const lines: number[][] = []; let currentLine: number[] = []; let currentWidth = 0; measures.forEach((measure, index) => { if (currentWidth + measure.width > maxWidth && currentLine.length > 0) { lines.push(currentLine); currentLine = []; currentWidth = 0; } currentLine.push(index); currentWidth += measure.width; }); if (currentLine.length > 0) { lines.push(currentLine); } return lines; } ``` ### 11.4 多声部Y坐标计算 ```typescript /** * 计算声部Y坐标 */ function calculateVoiceY( baseY: number, voiceIndex: number, voiceCount: number, voiceSpacing: number ): number { // 多声部垂直居中分布 const totalHeight = (voiceCount - 1) * voiceSpacing; const startY = baseY - totalHeight / 2; return startY + voiceIndex * voiceSpacing; } ``` --- ## 验收标准检查 - [x] 所有渲染元素都有精确的位置和尺寸定义 - [x] 包含视觉示意图(ASCII图) - [x] 包含特殊情况的处理规则 - [x] 包含完整的SVG实现代码模板 - [x] 包含增时线的详细布局算法 --- **文档版本:** v1.0 **最后更新:** 2026-01-29 **维护者:** 简谱渲染引擎开发团队