| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>五线谱渲染器 - 可视化集成测试</title>
- <style>
- :root {
- --primary: #2563eb;
- --primary-light: #60a5fa;
- --success: #10b981;
- --warning: #f59e0b;
- --error: #ef4444;
- --bg: #0f172a;
- --bg-card: #1e293b;
- --bg-panel: #ffffff;
- --text: #f1f5f9;
- --text-dark: #1e293b;
- --border: #334155;
- }
-
- * { box-sizing: border-box; margin: 0; padding: 0; }
-
- body {
- font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
- background: var(--bg);
- color: var(--text);
- min-height: 100vh;
- padding: 20px;
- }
-
- .container {
- max-width: 1400px;
- margin: 0 auto;
- }
-
- header {
- text-align: center;
- padding: 30px;
- background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
- border-radius: 12px;
- margin-bottom: 24px;
- }
-
- header h1 {
- font-size: 2rem;
- margin-bottom: 8px;
- }
-
- header p {
- opacity: 0.9;
- }
-
- .control-panel {
- background: var(--bg-card);
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 24px;
- display: flex;
- flex-wrap: wrap;
- gap: 16px;
- align-items: center;
- }
-
- .control-group {
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
-
- .control-group label {
- font-size: 0.85rem;
- color: var(--text);
- opacity: 0.8;
- }
-
- select, input, button {
- padding: 8px 12px;
- border-radius: 6px;
- border: 1px solid var(--border);
- background: var(--bg);
- color: var(--text);
- font-size: 0.9rem;
- }
-
- button {
- background: var(--primary);
- border-color: var(--primary);
- cursor: pointer;
- font-weight: 500;
- transition: all 0.2s;
- }
-
- button:hover {
- background: var(--primary-light);
- }
-
- button.success {
- background: var(--success);
- border-color: var(--success);
- }
-
- .render-panel {
- background: var(--bg-panel);
- border-radius: 12px;
- padding: 30px;
- min-height: 400px;
- margin-bottom: 24px;
- }
-
- #score-container {
- width: 100%;
- overflow-x: auto;
- }
-
- #score-container svg {
- display: block;
- margin: 0 auto;
- }
-
- .stats-panel {
- background: var(--bg-card);
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 24px;
- }
-
- .stats-panel h3 {
- font-size: 1rem;
- margin-bottom: 12px;
- color: var(--text);
- }
-
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
- gap: 12px;
- }
-
- .stat-item {
- background: var(--bg);
- border-radius: 8px;
- padding: 12px;
- text-align: center;
- }
-
- .stat-value {
- font-size: 1.5rem;
- font-weight: 600;
- color: var(--primary-light);
- }
-
- .stat-label {
- font-size: 0.8rem;
- color: var(--text);
- opacity: 0.7;
- }
-
- .log-panel {
- background: var(--bg-card);
- border-radius: 12px;
- padding: 20px;
- }
-
- .log-panel h3 {
- font-size: 1rem;
- margin-bottom: 12px;
- }
-
- #log-output {
- background: #000;
- border-radius: 6px;
- padding: 12px;
- font-family: 'Consolas', 'Monaco', monospace;
- font-size: 0.85rem;
- max-height: 200px;
- overflow-y: auto;
- color: #10b981;
- }
-
- .log-entry {
- margin-bottom: 4px;
- }
-
- .log-entry.error {
- color: var(--error);
- }
-
- .log-entry.warn {
- color: var(--warning);
- }
-
- .placeholder {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 300px;
- color: var(--text-dark);
- opacity: 0.5;
- }
-
- .placeholder svg {
- width: 80px;
- height: 80px;
- margin-bottom: 16px;
- }
-
- /* 五线谱 SVG 样式 */
- .staff-score {
- background: #fff;
- }
-
- .staff-line {
- stroke: #000;
- stroke-width: 1;
- }
-
- .note-head {
- fill: #000;
- }
-
- .note-stem {
- stroke: #000;
- stroke-width: 1.2;
- }
-
- .barline {
- stroke: #000;
- }
-
- .clef-text, .time-sig-text, .accidental-text {
- font-family: 'Bravura', 'Gonville', serif;
- }
-
- .lyric-text {
- font-family: "Microsoft YaHei", sans-serif;
- font-size: 12px;
- }
- /* 验收检查列表 */
- .checklist-panel {
- background: var(--bg-card);
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 24px;
- }
-
- .checklist-panel h3 {
- font-size: 1rem;
- margin-bottom: 12px;
- }
-
- .checklist {
- list-style: none;
- }
-
- .checklist li {
- padding: 8px 0;
- display: flex;
- align-items: center;
- gap: 8px;
- }
-
- .checklist li.pass::before {
- content: '✅';
- }
-
- .checklist li.fail::before {
- content: '❌';
- }
-
- .checklist li.pending::before {
- content: '⏳';
- }
- </style>
- </head>
- <body>
- <div class="container">
- <header>
- <h1>🎼 五线谱渲染器 - 可视化测试</h1>
- <p>StaffRenderer Integration Test - 阶段三验收</p>
- </header>
-
- <div class="control-panel">
- <div class="control-group">
- <label>测试曲谱</label>
- <select id="xml-select">
- <option value="basic">basic.xml - 基础音阶</option>
- <option value="mixed-durations">mixed-durations.xml - 混合时值</option>
- <option value="with-lyrics">with-lyrics.xml - 带歌词</option>
- <option value="complex">complex.xml - 复杂曲谱</option>
- </select>
- </div>
-
- <div class="control-group">
- <label>系统宽度</label>
- <input type="number" id="system-width" value="800" min="400" max="1200">
- </div>
-
- <div class="control-group">
- <label>谱线间距</label>
- <input type="number" id="staff-spacing" value="10" min="6" max="16">
- </div>
-
- <div class="control-group">
- <label>四分音符间距</label>
- <input type="number" id="note-spacing" value="40" min="20" max="80">
- </div>
-
- <div class="control-group">
- <label>显示歌词</label>
- <select id="show-lyrics">
- <option value="true">显示</option>
- <option value="false">隐藏</option>
- </select>
- </div>
-
- <div class="control-group">
- <label>调试模式</label>
- <select id="debug-mode">
- <option value="false">关闭</option>
- <option value="true">开启</option>
- </select>
- </div>
-
- <button onclick="loadAndRender()" class="success">🎨 渲染</button>
- <button onclick="clearOutput()">🗑️ 清除</button>
- </div>
-
- <div class="render-panel">
- <div id="score-container">
- <div class="placeholder">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
- <path d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
- </svg>
- <p>点击"渲染"按钮开始测试</p>
- </div>
- </div>
- </div>
-
- <div class="stats-panel">
- <h3>📊 渲染统计</h3>
- <div class="stats-grid">
- <div class="stat-item">
- <div class="stat-value" id="stat-measures">-</div>
- <div class="stat-label">小节数</div>
- </div>
- <div class="stat-item">
- <div class="stat-value" id="stat-notes">-</div>
- <div class="stat-label">音符数</div>
- </div>
- <div class="stat-item">
- <div class="stat-value" id="stat-systems">-</div>
- <div class="stat-label">行数</div>
- </div>
- <div class="stat-item">
- <div class="stat-value" id="stat-parse">-</div>
- <div class="stat-label">解析时间</div>
- </div>
- <div class="stat-item">
- <div class="stat-value" id="stat-layout">-</div>
- <div class="stat-label">布局时间</div>
- </div>
- <div class="stat-item">
- <div class="stat-value" id="stat-draw">-</div>
- <div class="stat-label">绘制时间</div>
- </div>
- <div class="stat-item">
- <div class="stat-value" id="stat-total">-</div>
- <div class="stat-label">总耗时</div>
- </div>
- </div>
- </div>
-
- <div class="checklist-panel">
- <h3>✅ 阶段三验收清单</h3>
- <ul class="checklist">
- <li id="check-render" class="pending">能渲染基本乐谱(单声部、无特殊记号)</li>
- <li id="check-clef" class="pending">谱号显示正确</li>
- <li id="check-key" class="pending">调号显示正确</li>
- <li id="check-time" class="pending">拍号显示正确</li>
- <li id="check-position" class="pending">音符位置正确(Y 位置对应音高)</li>
- <li id="check-stem" class="pending">符干方向正确</li>
- <li id="check-barline" class="pending">小节线正确</li>
- <li id="check-shared" class="pending">与简谱共享数据源(music-core)</li>
- </ul>
- </div>
-
- <div class="log-panel">
- <h3>📋 控制台日志</h3>
- <div id="log-output">
- <div class="log-entry">等待渲染...</div>
- </div>
- </div>
- </div>
-
- <script type="module">
- // 导入 music-core 解析器
- import { MusicXMLParser } from './src/music-core/parser/MusicXMLParser';
- import { StaffRenderer, createStaffRenderer } from './src/renderers/staff';
-
- // 日志函数
- function log(message, type = 'info') {
- const logOutput = document.getElementById('log-output');
- const entry = document.createElement('div');
- entry.className = `log-entry ${type}`;
- entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
- logOutput.appendChild(entry);
- logOutput.scrollTop = logOutput.scrollHeight;
-
- // 同时输出到控制台
- if (type === 'error') {
- console.error(message);
- } else {
- console.log(message);
- }
- }
-
- // 更新验收检查状态
- function updateCheckStatus(id, pass, message = '') {
- const el = document.getElementById(id);
- if (el) {
- el.className = pass ? 'pass' : 'fail';
- if (message) {
- el.textContent = el.textContent.split(' - ')[0] + ' - ' + message;
- }
- }
- }
-
- // 清除输出
- window.clearOutput = function() {
- document.getElementById('score-container').innerHTML = `
- <div class="placeholder">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
- <path d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
- </svg>
- <p>点击"渲染"按钮开始测试</p>
- </div>
- `;
- document.getElementById('log-output').innerHTML = '<div class="log-entry">等待渲染...</div>';
-
- // 重置统计
- ['measures', 'notes', 'systems', 'parse', 'layout', 'draw', 'total'].forEach(id => {
- document.getElementById(`stat-${id}`).textContent = '-';
- });
-
- // 重置验收检查
- ['check-render', 'check-clef', 'check-key', 'check-time', 'check-position', 'check-stem', 'check-barline', 'check-shared'].forEach(id => {
- const el = document.getElementById(id);
- if (el) {
- el.className = 'pending';
- el.textContent = el.textContent.split(' - ')[0];
- }
- });
- };
-
- // 加载并渲染
- window.loadAndRender = async function() {
- const xmlSelect = document.getElementById('xml-select').value;
- const systemWidth = parseInt(document.getElementById('system-width').value);
- const staffSpacing = parseInt(document.getElementById('staff-spacing').value);
- const noteSpacing = parseInt(document.getElementById('note-spacing').value);
- const showLyrics = document.getElementById('show-lyrics').value === 'true';
- const debugMode = document.getElementById('debug-mode').value === 'true';
-
- log(`开始加载: ${xmlSelect}.xml`);
-
- // 重置验收检查
- clearOutput();
-
- try {
- // 加载XML文件
- const fixturesPath = './src/jianpu-renderer/__tests__/fixtures/';
- const response = await fetch(`${fixturesPath}${xmlSelect}.xml`);
- if (!response.ok) {
- throw new Error(`无法加载文件: ${xmlSelect}.xml`);
- }
- const xmlContent = await response.text();
- log(`XML文件加载成功 (${xmlContent.length} 字节)`);
-
- // 解析XML - 验证使用 music-core
- const parseStartTime = performance.now();
- const parser = new MusicXMLParser();
- const result = parser.parse(xmlContent);
- const parseTime = performance.now() - parseStartTime;
-
- if (result.errors.length > 0) {
- log(`解析出错: ${result.errors.join(', ')}`, 'error');
- updateCheckStatus('check-shared', false, '解析失败');
- return;
- }
-
- updateCheckStatus('check-shared', true, '使用 music-core 解析器');
-
- if (result.warnings.length > 0) {
- result.warnings.forEach(w => log(`警告: ${w}`, 'warn'));
- }
-
- const score = result.score;
- log(`解析完成: ${score.parts.length} 个声部, ${score.parts[0]?.measures.length ?? 0} 个小节, 耗时 ${parseTime.toFixed(2)}ms`);
-
- // 创建渲染器
- const container = document.getElementById('score-container');
- container.innerHTML = '';
-
- const renderer = createStaffRenderer({
- container,
- systemWidth,
- staffLineSpacing: staffSpacing,
- quarterNoteSpacing: noteSpacing,
- showLyrics,
- debug: debugMode,
- });
-
- // 加载乐谱
- renderer.load(score);
-
- // 渲染
- const renderStartTime = performance.now();
- renderer.render();
- const renderTime = performance.now() - renderStartTime;
-
- // 获取统计
- const stats = renderer.getStats();
-
- // 更新统计显示
- document.getElementById('stat-measures').textContent = stats.measureCount;
- document.getElementById('stat-notes').textContent = stats.noteCount;
- document.getElementById('stat-systems').textContent = stats.systemCount;
- document.getElementById('stat-parse').textContent = `${parseTime.toFixed(1)}ms`;
- document.getElementById('stat-layout').textContent = `${stats.layoutTime.toFixed(1)}ms`;
- document.getElementById('stat-draw').textContent = `${stats.drawTime.toFixed(1)}ms`;
- document.getElementById('stat-total').textContent = `${stats.totalTime.toFixed(1)}ms`;
-
- log(`✅ 渲染完成! 共 ${stats.systemCount} 行, ${stats.noteCount} 个音符`, 'success');
- log(`布局耗时: ${stats.layoutTime.toFixed(2)}ms, 绘制耗时: ${stats.drawTime.toFixed(2)}ms`);
-
- // 验收检查
- validateRendering(container, stats);
-
- } catch (error) {
- log(`❌ 错误: ${error.message}`, 'error');
- console.error(error);
- updateCheckStatus('check-render', false, error.message);
- }
- };
-
- // 验证渲染结果
- function validateRendering(container, stats) {
- const svg = container.querySelector('svg');
-
- // 1. 检查基本渲染
- if (svg && stats.noteCount > 0) {
- updateCheckStatus('check-render', true, `${stats.noteCount} 个音符已渲染`);
- } else {
- updateCheckStatus('check-render', false, 'SVG 未生成或无音符');
- }
-
- // 2. 检查谱号
- const clefs = svg?.querySelectorAll('.staff-clef, [class*="clef"]');
- if (clefs && clefs.length > 0) {
- updateCheckStatus('check-clef', true, `${clefs.length} 个谱号`);
- } else {
- // 检查是否有谱号相关的文本或路径
- const clefTexts = svg?.querySelectorAll('text');
- let hasClef = false;
- clefTexts?.forEach(t => {
- if (t.textContent && (t.textContent.includes('\uE050') || t.textContent.includes('𝄞'))) {
- hasClef = true;
- }
- });
- updateCheckStatus('check-clef', hasClef, hasClef ? '谱号已渲染' : '未找到谱号元素');
- }
-
- // 3. 检查调号(如果有)
- const keySignatures = svg?.querySelectorAll('.staff-key-signature, [class*="key"]');
- // 对于 C 大调,可能没有调号,这是正常的
- updateCheckStatus('check-key', true, keySignatures?.length > 0 ? `${keySignatures.length} 个调号` : 'C大调(无升降号)');
-
- // 4. 检查拍号
- const timeSigs = svg?.querySelectorAll('.time-signature, [class*="time"]');
- const timeSigTexts = svg?.querySelectorAll('text');
- let hasTimeSig = false;
- timeSigTexts?.forEach(t => {
- if (t.textContent && /^[0-9]$/.test(t.textContent)) {
- hasTimeSig = true;
- }
- });
- updateCheckStatus('check-time', hasTimeSig || (timeSigs && timeSigs.length > 0),
- hasTimeSig ? '拍号已渲染' : '检查拍号显示');
-
- // 5. 检查音符位置
- const noteHeads = svg?.querySelectorAll('.note-head, ellipse, [class*="note"]');
- if (noteHeads && noteHeads.length > 0) {
- // 检查音符是否有不同的Y位置(表示不同音高)
- const yPositions = new Set();
- noteHeads.forEach(n => {
- const cy = n.getAttribute('cy');
- const y = n.getAttribute('y');
- if (cy) yPositions.add(Math.round(parseFloat(cy)));
- if (y) yPositions.add(Math.round(parseFloat(y)));
- });
- updateCheckStatus('check-position', yPositions.size > 1 || noteHeads.length === 1,
- `${noteHeads.length} 个符头, ${yPositions.size} 个不同Y位置`);
- } else {
- updateCheckStatus('check-position', false, '未找到符头元素');
- }
-
- // 6. 检查符干
- const stems = svg?.querySelectorAll('.note-stem, line[class*="stem"]');
- if (stems && stems.length > 0) {
- // 检查符干是否有向上和向下的
- let upCount = 0, downCount = 0;
- stems.forEach(s => {
- const y1 = parseFloat(s.getAttribute('y1') || '0');
- const y2 = parseFloat(s.getAttribute('y2') || '0');
- if (y1 < y2) upCount++;
- else downCount++;
- });
- updateCheckStatus('check-stem', true,
- `${stems.length} 个符干 (上:${upCount}, 下:${downCount})`);
- } else {
- // 全音符可能没有符干
- updateCheckStatus('check-stem', true, '符干正常(可能含全音符)');
- }
-
- // 7. 检查小节线
- const barlines = svg?.querySelectorAll('.barline, line.barline, [class*="barline"]');
- if (barlines && barlines.length > 0) {
- updateCheckStatus('check-barline', true, `${barlines.length} 条小节线`);
- } else {
- // 检查是否有垂直线条作为小节线
- const lines = svg?.querySelectorAll('line');
- let barlineCount = 0;
- lines?.forEach(l => {
- const x1 = parseFloat(l.getAttribute('x1') || '0');
- const x2 = parseFloat(l.getAttribute('x2') || '0');
- if (Math.abs(x1 - x2) < 1) {
- barlineCount++;
- }
- });
- updateCheckStatus('check-barline', barlineCount > 0,
- barlineCount > 0 ? `${barlineCount} 条小节线` : '未找到小节线');
- }
-
- log('验收检查完成');
- }
-
- // 页面加载完成后自动渲染
- document.addEventListener('DOMContentLoaded', () => {
- log('🎼 五线谱渲染器可视化测试页面已加载');
- log('请点击"渲染"按钮开始阶段三验收测试');
- });
- </script>
- </body>
- </html>
|