jianpu-test.html 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>简谱渲染引擎 - 在线测试</title>
  7. <style>
  8. :root {
  9. --primary: #4f46e5;
  10. --primary-light: #818cf8;
  11. --success: #10b981;
  12. --warning: #f59e0b;
  13. --error: #ef4444;
  14. --bg: #f8fafc;
  15. --bg-card: #ffffff;
  16. --text: #1e293b;
  17. --border: #e2e8f0;
  18. }
  19. * { box-sizing: border-box; margin: 0; padding: 0; }
  20. body {
  21. font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
  22. background: var(--bg);
  23. color: var(--text);
  24. min-height: 100vh;
  25. padding: 20px;
  26. }
  27. .container {
  28. max-width: 1400px;
  29. margin: 0 auto;
  30. }
  31. header {
  32. text-align: center;
  33. padding: 24px;
  34. background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
  35. border-radius: 12px;
  36. margin-bottom: 20px;
  37. color: white;
  38. }
  39. header h1 {
  40. font-size: 1.8rem;
  41. margin-bottom: 8px;
  42. }
  43. .control-panel {
  44. background: var(--bg-card);
  45. border-radius: 12px;
  46. padding: 20px;
  47. margin-bottom: 20px;
  48. box-shadow: 0 2px 4px rgba(0,0,0,0.05);
  49. border: 1px solid var(--border);
  50. }
  51. .control-panel h3 {
  52. margin-bottom: 16px;
  53. font-size: 1rem;
  54. color: var(--primary);
  55. }
  56. .control-group {
  57. display: flex;
  58. flex-wrap: wrap;
  59. gap: 16px;
  60. align-items: flex-end;
  61. }
  62. .control-item {
  63. display: flex;
  64. flex-direction: column;
  65. gap: 4px;
  66. }
  67. .control-item label {
  68. font-size: 0.85rem;
  69. color: #64748b;
  70. }
  71. input, select, button {
  72. padding: 10px 14px;
  73. border-radius: 8px;
  74. border: 1px solid var(--border);
  75. background: white;
  76. color: var(--text);
  77. font-size: 0.9rem;
  78. }
  79. input:focus, select:focus {
  80. outline: none;
  81. border-color: var(--primary);
  82. box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
  83. }
  84. button {
  85. background: var(--primary);
  86. color: white;
  87. border: none;
  88. cursor: pointer;
  89. font-weight: 500;
  90. transition: all 0.2s;
  91. }
  92. button:hover {
  93. background: var(--primary-light);
  94. transform: translateY(-1px);
  95. }
  96. button:active {
  97. transform: translateY(0);
  98. }
  99. button.success {
  100. background: var(--success);
  101. }
  102. button.warning {
  103. background: var(--warning);
  104. }
  105. #xml-url {
  106. flex: 1;
  107. min-width: 300px;
  108. }
  109. .render-panel {
  110. background: var(--bg-card);
  111. border-radius: 12px;
  112. padding: 30px;
  113. min-height: 400px;
  114. margin-bottom: 20px;
  115. box-shadow: 0 2px 4px rgba(0,0,0,0.05);
  116. border: 1px solid var(--border);
  117. }
  118. #score-container {
  119. width: 100%;
  120. overflow-x: auto;
  121. min-height: 300px;
  122. }
  123. #score-container svg {
  124. display: block;
  125. margin: 0 auto;
  126. }
  127. .stats-panel {
  128. background: var(--bg-card);
  129. border-radius: 12px;
  130. padding: 20px;
  131. margin-bottom: 20px;
  132. box-shadow: 0 2px 4px rgba(0,0,0,0.05);
  133. border: 1px solid var(--border);
  134. }
  135. .stats-panel h3 {
  136. font-size: 1rem;
  137. margin-bottom: 12px;
  138. color: var(--primary);
  139. }
  140. .stats-grid {
  141. display: grid;
  142. grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  143. gap: 12px;
  144. }
  145. .stat-item {
  146. background: var(--bg);
  147. border-radius: 8px;
  148. padding: 12px;
  149. text-align: center;
  150. }
  151. .stat-value {
  152. font-size: 1.3rem;
  153. font-weight: 600;
  154. color: var(--primary);
  155. }
  156. .stat-label {
  157. font-size: 0.75rem;
  158. color: #64748b;
  159. margin-top: 4px;
  160. }
  161. .log-panel {
  162. background: var(--bg-card);
  163. border-radius: 12px;
  164. padding: 20px;
  165. box-shadow: 0 2px 4px rgba(0,0,0,0.05);
  166. border: 1px solid var(--border);
  167. }
  168. .log-panel h3 {
  169. font-size: 1rem;
  170. margin-bottom: 12px;
  171. color: var(--primary);
  172. }
  173. #log-output {
  174. background: #0f172a;
  175. border-radius: 8px;
  176. padding: 16px;
  177. font-family: 'Consolas', 'Monaco', monospace;
  178. font-size: 0.85rem;
  179. max-height: 200px;
  180. overflow-y: auto;
  181. color: #10b981;
  182. }
  183. .log-entry {
  184. margin-bottom: 4px;
  185. padding: 2px 0;
  186. }
  187. .log-entry.error { color: var(--error); }
  188. .log-entry.warn { color: var(--warning); }
  189. .log-entry.success { color: var(--success); }
  190. .log-entry.info { color: #60a5fa; }
  191. .placeholder {
  192. display: flex;
  193. flex-direction: column;
  194. align-items: center;
  195. justify-content: center;
  196. height: 300px;
  197. color: #94a3b8;
  198. }
  199. .placeholder svg {
  200. width: 60px;
  201. height: 60px;
  202. margin-bottom: 16px;
  203. }
  204. .loading {
  205. display: inline-block;
  206. width: 20px;
  207. height: 20px;
  208. border: 2px solid #f3f3f3;
  209. border-top: 2px solid var(--primary);
  210. border-radius: 50%;
  211. animation: spin 1s linear infinite;
  212. margin-right: 8px;
  213. }
  214. @keyframes spin {
  215. 0% { transform: rotate(0deg); }
  216. 100% { transform: rotate(360deg); }
  217. }
  218. .compare-panel {
  219. display: grid;
  220. grid-template-columns: 1fr 1fr;
  221. gap: 20px;
  222. }
  223. .compare-panel .panel {
  224. background: var(--bg-card);
  225. border-radius: 12px;
  226. padding: 20px;
  227. box-shadow: 0 2px 4px rgba(0,0,0,0.05);
  228. border: 1px solid var(--border);
  229. }
  230. .compare-panel .panel h4 {
  231. margin-bottom: 12px;
  232. color: var(--primary);
  233. }
  234. /* 全屏样式 */
  235. .render-panel {
  236. position: relative;
  237. }
  238. .render-panel.fullscreen {
  239. position: fixed;
  240. top: 0;
  241. left: 0;
  242. right: 0;
  243. bottom: 0;
  244. z-index: 9999;
  245. margin: 0;
  246. border-radius: 0;
  247. padding: 20px;
  248. display: flex;
  249. flex-direction: column;
  250. }
  251. .render-panel.fullscreen #score-container {
  252. flex: 1;
  253. min-height: auto;
  254. height: 100%;
  255. }
  256. .fullscreen-btn {
  257. position: absolute;
  258. top: 10px;
  259. right: 10px;
  260. z-index: 10;
  261. padding: 8px 16px !important;
  262. font-size: 0.85rem !important;
  263. display: flex;
  264. align-items: center;
  265. gap: 6px;
  266. }
  267. .fullscreen-btn svg {
  268. width: 16px;
  269. height: 16px;
  270. }
  271. .render-panel.fullscreen .fullscreen-btn {
  272. position: fixed;
  273. top: 20px;
  274. right: 20px;
  275. }
  276. /* 全屏时的缩放控制 */
  277. .zoom-controls {
  278. position: absolute;
  279. top: 10px;
  280. left: 10px;
  281. z-index: 10;
  282. display: flex;
  283. gap: 8px;
  284. align-items: center;
  285. background: rgba(255,255,255,0.95);
  286. padding: 6px 12px;
  287. border-radius: 8px;
  288. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  289. }
  290. .render-panel.fullscreen .zoom-controls {
  291. position: fixed;
  292. top: 20px;
  293. left: 20px;
  294. }
  295. .zoom-controls button {
  296. padding: 6px 12px !important;
  297. font-size: 0.8rem !important;
  298. min-width: 36px;
  299. }
  300. .zoom-controls span {
  301. font-size: 0.85rem;
  302. color: var(--text);
  303. min-width: 50px;
  304. text-align: center;
  305. }
  306. </style>
  307. </head>
  308. <body>
  309. <div class="container">
  310. <header>
  311. <h1>🎵 简谱渲染引擎测试</h1>
  312. <p>JianpuRenderer - 自主开发的简谱渲染引擎</p>
  313. </header>
  314. <!-- 控制面板 -->
  315. <div class="control-panel">
  316. <h3>📦 MusicXML 数据源</h3>
  317. <div class="control-group">
  318. <div class="control-item" style="flex: 1;">
  319. <label>MusicXML URL(可从接口获取 xmlFileUrl)</label>
  320. <input type="text" id="xml-url" placeholder="请输入MusicXML文件的URL地址...">
  321. </div>
  322. <button onclick="fetchFromCurrentPage()" class="warning">📋 从当前曲谱获取URL</button>
  323. <button onclick="loadAndRender()" class="success">🎨 渲染简谱</button>
  324. </div>
  325. </div>
  326. <div class="control-panel">
  327. <h3>⚙️ 渲染配置</h3>
  328. <div class="control-group">
  329. <div class="control-item">
  330. <label>四分音符间距</label>
  331. <input type="number" id="spacing" value="50" min="30" max="100">
  332. </div>
  333. <div class="control-item">
  334. <label>字体大小</label>
  335. <input type="number" id="font-size" value="20" min="14" max="32">
  336. </div>
  337. <div class="control-item">
  338. <label>行宽度</label>
  339. <input type="number" id="system-width" value="1200" min="400" max="1600">
  340. </div>
  341. <div class="control-item">
  342. <label>显示歌词</label>
  343. <select id="show-lyrics">
  344. <option value="true">显示</option>
  345. <option value="false">隐藏</option>
  346. </select>
  347. </div>
  348. <div class="control-item">
  349. <label>调试模式</label>
  350. <select id="debug-mode">
  351. <option value="false">关闭</option>
  352. <option value="true">开启</option>
  353. </select>
  354. </div>
  355. <button onclick="clearOutput()">🗑️ 清除</button>
  356. </div>
  357. </div>
  358. <!-- 渲染面板 -->
  359. <div class="render-panel" id="render-panel">
  360. <!-- 缩放控制 -->
  361. <div class="zoom-controls">
  362. <button onclick="zoomOut()">➖</button>
  363. <span id="zoom-level">100%</span>
  364. <button onclick="zoomIn()">➕</button>
  365. <button onclick="zoomReset()">重置</button>
  366. </div>
  367. <!-- 全屏按钮 -->
  368. <button class="fullscreen-btn" onclick="toggleFullscreen()">
  369. <svg id="fullscreen-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  370. <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
  371. </svg>
  372. <span id="fullscreen-text">全屏</span>
  373. </button>
  374. <div id="score-container">
  375. <div class="placeholder">
  376. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
  377. <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"/>
  378. </svg>
  379. <p>请输入MusicXML URL,然后点击"渲染简谱"</p>
  380. <p style="font-size: 0.8rem; margin-top: 8px; color: #94a3b8;">
  381. 或者从当前打开的曲谱页面点击"从当前曲谱获取URL"自动填充
  382. </p>
  383. </div>
  384. </div>
  385. </div>
  386. <!-- 统计面板 -->
  387. <div class="stats-panel">
  388. <h3>📊 渲染统计</h3>
  389. <div class="stats-grid">
  390. <div class="stat-item">
  391. <div class="stat-value" id="stat-measures">-</div>
  392. <div class="stat-label">小节数</div>
  393. </div>
  394. <div class="stat-item">
  395. <div class="stat-value" id="stat-notes">-</div>
  396. <div class="stat-label">音符数</div>
  397. </div>
  398. <div class="stat-item">
  399. <div class="stat-value" id="stat-systems">-</div>
  400. <div class="stat-label">行数</div>
  401. </div>
  402. <div class="stat-item">
  403. <div class="stat-value" id="stat-parse">-</div>
  404. <div class="stat-label">解析时间</div>
  405. </div>
  406. <div class="stat-item">
  407. <div class="stat-value" id="stat-layout">-</div>
  408. <div class="stat-label">布局时间</div>
  409. </div>
  410. <div class="stat-item">
  411. <div class="stat-value" id="stat-draw">-</div>
  412. <div class="stat-label">绘制时间</div>
  413. </div>
  414. <div class="stat-item">
  415. <div class="stat-value" id="stat-total">-</div>
  416. <div class="stat-label">总耗时</div>
  417. </div>
  418. </div>
  419. </div>
  420. <!-- 日志面板 -->
  421. <div class="log-panel">
  422. <h3>📋 控制台日志</h3>
  423. <div id="log-output">
  424. <div class="log-entry info">等待加载MusicXML...</div>
  425. </div>
  426. </div>
  427. </div>
  428. <script type="module">
  429. import { JianpuRenderer } from '/src/jianpu-renderer/JianpuRenderer.ts';
  430. import { OSMDCompatibilityAdapter } from '/src/jianpu-renderer/adapters/OSMDCompatibilityAdapter.ts';
  431. // ========== 全屏和缩放功能 ==========
  432. let currentZoom = 100;
  433. let isFullscreen = false;
  434. // 全屏切换
  435. window.toggleFullscreen = function() {
  436. const panel = document.getElementById('render-panel');
  437. const icon = document.getElementById('fullscreen-icon');
  438. const text = document.getElementById('fullscreen-text');
  439. isFullscreen = !isFullscreen;
  440. if (isFullscreen) {
  441. panel.classList.add('fullscreen');
  442. text.textContent = '退出';
  443. icon.innerHTML = '<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>';
  444. document.body.style.overflow = 'hidden';
  445. } else {
  446. panel.classList.remove('fullscreen');
  447. text.textContent = '全屏';
  448. icon.innerHTML = '<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>';
  449. document.body.style.overflow = '';
  450. }
  451. };
  452. // ESC键退出全屏
  453. document.addEventListener('keydown', (e) => {
  454. if (e.key === 'Escape' && isFullscreen) {
  455. window.toggleFullscreen();
  456. }
  457. });
  458. // 缩放功能
  459. function updateZoom() {
  460. const container = document.getElementById('score-container');
  461. const svg = container.querySelector('svg');
  462. if (svg) {
  463. svg.style.transform = `scale(${currentZoom / 100})`;
  464. svg.style.transformOrigin = 'top center';
  465. }
  466. document.getElementById('zoom-level').textContent = `${currentZoom}%`;
  467. }
  468. window.zoomIn = function() {
  469. currentZoom = Math.min(300, currentZoom + 20);
  470. updateZoom();
  471. };
  472. window.zoomOut = function() {
  473. currentZoom = Math.max(20, currentZoom - 20);
  474. updateZoom();
  475. };
  476. window.zoomReset = function() {
  477. currentZoom = 100;
  478. updateZoom();
  479. };
  480. // ========== 日志和工具函数 ==========
  481. // 日志函数
  482. function log(message, type = 'info') {
  483. const logOutput = document.getElementById('log-output');
  484. const entry = document.createElement('div');
  485. entry.className = `log-entry ${type}`;
  486. entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
  487. logOutput.appendChild(entry);
  488. logOutput.scrollTop = logOutput.scrollHeight;
  489. console.log(`[${type}]`, message);
  490. }
  491. // 清除输出
  492. window.clearOutput = function() {
  493. document.getElementById('score-container').innerHTML = `
  494. <div class="placeholder">
  495. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
  496. <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"/>
  497. </svg>
  498. <p>请输入MusicXML URL,然后点击"渲染简谱"</p>
  499. </div>
  500. `;
  501. document.getElementById('log-output').innerHTML = '<div class="log-entry info">等待加载MusicXML...</div>';
  502. ['measures', 'notes', 'systems', 'parse', 'layout', 'draw', 'total'].forEach(id => {
  503. document.getElementById(`stat-${id}`).textContent = '-';
  504. });
  505. };
  506. // 从当前页面获取XML URL
  507. window.fetchFromCurrentPage = function() {
  508. // 尝试从 window.state 获取 xmlUrl
  509. if (window.opener && window.opener.state && window.opener.state.xmlUrl) {
  510. document.getElementById('xml-url').value = window.opener.state.xmlUrl;
  511. log('从父窗口获取到xmlUrl', 'success');
  512. return;
  513. }
  514. // 提示用户
  515. const example = 'https://oss.dayaedu.com/daya/music/1706164502047.xml';
  516. document.getElementById('xml-url').value = example;
  517. log('已填入示例URL,请替换为实际的xmlFileUrl', 'warn');
  518. };
  519. // 加载并渲染
  520. window.loadAndRender = async function() {
  521. const xmlUrl = document.getElementById('xml-url').value.trim();
  522. if (!xmlUrl) {
  523. log('请输入MusicXML URL', 'error');
  524. return;
  525. }
  526. const spacing = parseInt(document.getElementById('spacing').value);
  527. const fontSize = parseInt(document.getElementById('font-size').value);
  528. const systemWidth = parseInt(document.getElementById('system-width').value);
  529. const showLyrics = document.getElementById('show-lyrics').value === 'true';
  530. const debug = document.getElementById('debug-mode').value === 'true';
  531. log(`开始加载: ${xmlUrl}`);
  532. // 显示加载状态
  533. document.getElementById('score-container').innerHTML = `
  534. <div class="placeholder">
  535. <div class="loading"></div>
  536. <p>正在加载MusicXML...</p>
  537. </div>
  538. `;
  539. try {
  540. // 加载XML
  541. const response = await fetch(xmlUrl);
  542. if (!response.ok) {
  543. throw new Error(`无法加载文件: ${response.status} ${response.statusText}`);
  544. }
  545. const xmlContent = await response.text();
  546. log(`XML加载成功 (${(xmlContent.length / 1024).toFixed(1)} KB)`, 'success');
  547. // 清空容器
  548. const container = document.getElementById('score-container');
  549. container.innerHTML = '';
  550. // 创建JianpuRenderer
  551. const startTime = performance.now();
  552. const renderer = new JianpuRenderer(container, {
  553. quarterNoteSpacing: spacing,
  554. noteFontSize: fontSize,
  555. systemWidth: systemWidth,
  556. drawLyrics: showLyrics,
  557. debug: debug,
  558. });
  559. // 由于JianpuRenderer.load()需要OSMD对象,这里我们需要先创建一个简单的解析适配
  560. // 直接使用MusicXML字符串
  561. log('开始解析MusicXML...');
  562. // 注意:当前JianpuRenderer.load()需要OSMD对象或者类似结构
  563. // 这里我们创建一个简单的OSMD模拟对象用于测试
  564. const osmdObject = await createOSMDLikeObject(xmlContent);
  565. const parseStartTime = performance.now();
  566. await renderer.load(osmdObject);
  567. const parseTime = performance.now() - parseStartTime;
  568. log(`解析完成,耗时 ${parseTime.toFixed(2)}ms`, 'success');
  569. // 渲染
  570. const renderStartTime = performance.now();
  571. renderer.render();
  572. const renderTime = performance.now() - renderStartTime;
  573. log(`渲染完成,耗时 ${renderTime.toFixed(2)}ms`, 'success');
  574. // 获取统计信息
  575. const stats = renderer.getStats();
  576. const totalTime = performance.now() - startTime;
  577. // 更新统计
  578. document.getElementById('stat-measures').textContent = stats.measureCount;
  579. document.getElementById('stat-notes').textContent = stats.noteCount;
  580. document.getElementById('stat-systems').textContent = stats.systemCount;
  581. document.getElementById('stat-parse').textContent = `${stats.parseTime.toFixed(1)}ms`;
  582. document.getElementById('stat-layout').textContent = `${stats.layoutTime.toFixed(1)}ms`;
  583. document.getElementById('stat-draw').textContent = `${stats.drawTime.toFixed(1)}ms`;
  584. document.getElementById('stat-total').textContent = `${totalTime.toFixed(1)}ms`;
  585. log(`✅ 渲染完成! ${stats.measureCount}小节, ${stats.noteCount}音符, ${stats.systemCount}行`, 'success');
  586. // 生成times数组测试兼容性
  587. const adapter = new OSMDCompatibilityAdapter(renderer, debug);
  588. const times = adapter.generateTimesArray();
  589. log(`兼容层: 生成了 ${times.length} 个times项`, 'info');
  590. // 将renderer和times暴露到全局用于调试
  591. window.jianpuRenderer = renderer;
  592. window.jianpuTimes = times;
  593. log('💡 调试: window.jianpuRenderer / window.jianpuTimes 可用', 'info');
  594. // 重置缩放
  595. currentZoom = 100;
  596. document.getElementById('zoom-level').textContent = '100%';
  597. } catch (error) {
  598. log(`❌ 错误: ${error.message}`, 'error');
  599. console.error(error);
  600. document.getElementById('score-container').innerHTML = `
  601. <div class="placeholder" style="color: #ef4444;">
  602. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  603. <circle cx="12" cy="12" r="10"/>
  604. <line x1="15" y1="9" x2="9" y2="15"/>
  605. <line x1="9" y1="9" x2="15" y2="15"/>
  606. </svg>
  607. <p>渲染失败: ${error.message}</p>
  608. </div>
  609. `;
  610. }
  611. };
  612. /**
  613. * 创建类似OSMD的对象结构
  614. * JianpuRenderer需要这种格式的输入
  615. */
  616. async function createOSMDLikeObject(xmlContent) {
  617. // 导入解析器
  618. const { DivisionsHandler } = await import('/src/jianpu-renderer/core/parser/DivisionsHandler.ts');
  619. const parser = new DOMParser();
  620. const doc = parser.parseFromString(xmlContent, 'text/xml');
  621. const parseError = doc.querySelector('parsererror');
  622. if (parseError) {
  623. throw new Error(`XML解析错误: ${parseError.textContent}`);
  624. }
  625. const divisionsHandler = new DivisionsHandler();
  626. const title = doc.querySelector('work-title')?.textContent ?? 'Untitled';
  627. const measureElements = doc.querySelectorAll('measure');
  628. const sourceMeasures = [];
  629. const notes = [];
  630. let currentDivisions = 256;
  631. let currentTempo = 120;
  632. let currentTimeSignature = { numerator: 4, denominator: 4 };
  633. let currentKeySignature = { keyTypeOriginal: 0, Mode: 0 };
  634. measureElements.forEach((measureEl, measureIndex) => {
  635. const attributes = measureEl.querySelector('attributes');
  636. if (attributes) {
  637. const divisions = attributes.querySelector('divisions');
  638. if (divisions) {
  639. currentDivisions = parseInt(divisions.textContent ?? '256');
  640. divisionsHandler.setDivisions(currentDivisions);
  641. }
  642. const time = attributes.querySelector('time');
  643. if (time) {
  644. currentTimeSignature = {
  645. numerator: parseInt(time.querySelector('beats')?.textContent ?? '4'),
  646. denominator: parseInt(time.querySelector('beat-type')?.textContent ?? '4'),
  647. };
  648. }
  649. const key = attributes.querySelector('key');
  650. if (key) {
  651. const fifths = parseInt(key.querySelector('fifths')?.textContent ?? '0');
  652. currentKeySignature = { keyTypeOriginal: fifths, Mode: 0 };
  653. }
  654. }
  655. const sound = measureEl.querySelector('sound[tempo]');
  656. if (sound) {
  657. currentTempo = parseInt(sound.getAttribute('tempo') ?? '120');
  658. }
  659. const sourceMeasure = {
  660. MeasureNumberXML: measureIndex + 1,
  661. measureListIndex: measureIndex,
  662. tempoInBPM: currentTempo,
  663. ActiveTimeSignature: currentTimeSignature,
  664. ActiveKeySignature: currentKeySignature,
  665. Duration: { RealValue: currentTimeSignature.numerator / currentTimeSignature.denominator },
  666. lastRepetitionInstructions: [],
  667. verticalMeasureList: [],
  668. };
  669. sourceMeasures.push(sourceMeasure);
  670. const noteElements = measureEl.querySelectorAll('note');
  671. let timestamp = 0;
  672. noteElements.forEach((noteEl) => {
  673. const note = parseNote(noteEl, measureIndex, sourceMeasure, timestamp, divisionsHandler);
  674. if (note) {
  675. notes.push({ note, measureIndex, timestamp });
  676. if (!noteEl.querySelector('chord')) {
  677. timestamp += note.length.realValue;
  678. }
  679. }
  680. });
  681. });
  682. // 构建cursor迭代器
  683. let noteIndex = 0;
  684. const iterator = {
  685. EndReached: notes.length === 0,
  686. currentVoiceEntries: notes.length > 0 ? [{
  687. Notes: [notes[0]?.note],
  688. notes: [notes[0]?.note],
  689. ParentVoice: { VoiceId: 0 },
  690. }] : [],
  691. CurrentVoiceEntries: [],
  692. currentMeasureIndex: 0,
  693. currentTimeStamp: { RealValue: 0, realValue: 0 },
  694. moveToNextVisibleVoiceEntry: () => {
  695. noteIndex++;
  696. if (noteIndex >= notes.length) {
  697. iterator.EndReached = true;
  698. } else {
  699. const { note, measureIndex, timestamp } = notes[noteIndex];
  700. iterator.currentVoiceEntries = [{
  701. Notes: [note],
  702. notes: [note],
  703. ParentVoice: { VoiceId: 0 },
  704. }];
  705. iterator.CurrentVoiceEntries = iterator.currentVoiceEntries;
  706. iterator.currentMeasureIndex = measureIndex;
  707. iterator.currentTimeStamp = { RealValue: timestamp, realValue: timestamp };
  708. }
  709. },
  710. };
  711. iterator.CurrentVoiceEntries = iterator.currentVoiceEntries;
  712. return {
  713. Sheet: {
  714. Title: { text: title },
  715. SourceMeasures: sourceMeasures,
  716. userStartTempoInBPM: currentTempo,
  717. },
  718. GraphicSheet: {
  719. MeasureList: sourceMeasures.map(m => [{ parentSourceMeasure: m }]),
  720. },
  721. cursor: {
  722. Iterator: iterator,
  723. reset: () => {
  724. noteIndex = 0;
  725. iterator.EndReached = notes.length === 0;
  726. if (notes.length > 0) {
  727. const { note, measureIndex, timestamp } = notes[0];
  728. iterator.currentVoiceEntries = [{
  729. Notes: [note],
  730. notes: [note],
  731. ParentVoice: { VoiceId: 0 },
  732. }];
  733. iterator.CurrentVoiceEntries = iterator.currentVoiceEntries;
  734. iterator.currentMeasureIndex = measureIndex;
  735. iterator.currentTimeStamp = { RealValue: timestamp, realValue: timestamp };
  736. }
  737. },
  738. next: () => iterator.moveToNextVisibleVoiceEntry(),
  739. },
  740. };
  741. }
  742. function parseNote(noteEl, measureIndex, sourceMeasure, timestamp, divisionsHandler) {
  743. const isRest = noteEl.querySelector('rest') !== null;
  744. const durationEl = noteEl.querySelector('duration');
  745. const duration = parseInt(durationEl?.textContent ?? '256');
  746. const realValue = divisionsHandler.toRealValue(duration);
  747. let pitch = null;
  748. let halfTone = 60;
  749. if (!isRest) {
  750. const pitchEl = noteEl.querySelector('pitch');
  751. if (pitchEl) {
  752. const step = pitchEl.querySelector('step')?.textContent ?? 'C';
  753. const octave = parseInt(pitchEl.querySelector('octave')?.textContent ?? '4');
  754. const alter = parseInt(pitchEl.querySelector('alter')?.textContent ?? '0');
  755. pitch = { step, octave, alter };
  756. const stepToSemitone = { 'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11 };
  757. halfTone = (octave + 1) * 12 + (stepToSemitone[step] ?? 0) + alter;
  758. }
  759. }
  760. const dots = noteEl.querySelectorAll('dot').length;
  761. const typeEl = noteEl.querySelector('type');
  762. const noteType = typeEl?.textContent ?? 'quarter';
  763. const lyrics = [];
  764. const lyricElements = noteEl.querySelectorAll('lyric');
  765. lyricElements.forEach((lyricEl) => {
  766. const number = parseInt(lyricEl.getAttribute('number') ?? '1');
  767. const text = lyricEl.querySelector('text')?.textContent ?? '';
  768. lyrics.push({ number, text });
  769. });
  770. return {
  771. pitch,
  772. halfTone,
  773. length: { realValue, RealValue: realValue },
  774. isRestFlag: isRest,
  775. IsRest: isRest,
  776. IsGraceNote: noteEl.querySelector('grace') !== null,
  777. IsChordNote: noteEl.querySelector('chord') !== null,
  778. dots,
  779. DotsXml: dots,
  780. noteTypeXml: noteType,
  781. sourceMeasure,
  782. duration,
  783. lyrics,
  784. };
  785. }
  786. // 初始化
  787. log('简谱渲染引擎测试页面已加载', 'success');
  788. log('请输入MusicXML URL并点击"渲染简谱"', 'info');
  789. </script>
  790. </body>
  791. </html>