|
|
@@ -0,0 +1,819 @@
|
|
|
+<!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: #6366f1;
|
|
|
+ --primary-light: #818cf8;
|
|
|
+ --success: #10b981;
|
|
|
+ --warning: #f59e0b;
|
|
|
+ --error: #ef4444;
|
|
|
+ --bg: #0f172a;
|
|
|
+ --bg-card: #1e293b;
|
|
|
+ --bg-hover: #334155;
|
|
|
+ --text: #f1f5f9;
|
|
|
+ --text-muted: #94a3b8;
|
|
|
+ --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);
|
|
|
+ line-height: 1.6;
|
|
|
+ min-height: 100vh;
|
|
|
+ }
|
|
|
+
|
|
|
+ .container {
|
|
|
+ max-width: 1400px;
|
|
|
+ margin: 0 auto;
|
|
|
+ padding: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 头部 */
|
|
|
+ header {
|
|
|
+ text-align: center;
|
|
|
+ padding: 40px 20px;
|
|
|
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%);
|
|
|
+ border-radius: 16px;
|
|
|
+ margin-bottom: 32px;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ header::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
|
|
+ opacity: 0.3;
|
|
|
+ }
|
|
|
+
|
|
|
+ header h1 {
|
|
|
+ font-size: 2.5rem;
|
|
|
+ font-weight: 700;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ header p {
|
|
|
+ font-size: 1.1rem;
|
|
|
+ opacity: 0.9;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .version-badge {
|
|
|
+ display: inline-block;
|
|
|
+ background: rgba(255,255,255,0.2);
|
|
|
+ padding: 6px 16px;
|
|
|
+ border-radius: 20px;
|
|
|
+ font-size: 0.9rem;
|
|
|
+ margin-top: 16px;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 进度概览 */
|
|
|
+ .progress-overview {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 32px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .progress-card {
|
|
|
+ background: var(--bg-card);
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 20px;
|
|
|
+ text-align: center;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ transition: transform 0.2s, box-shadow 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .progress-card:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
|
|
+ }
|
|
|
+
|
|
|
+ .progress-card .icon {
|
|
|
+ font-size: 2rem;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .progress-card .value {
|
|
|
+ font-size: 2rem;
|
|
|
+ font-weight: 700;
|
|
|
+ color: var(--primary-light);
|
|
|
+ }
|
|
|
+
|
|
|
+ .progress-card .label {
|
|
|
+ font-size: 0.85rem;
|
|
|
+ color: var(--text-muted);
|
|
|
+ margin-top: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .progress-card.success .value { color: var(--success); }
|
|
|
+ .progress-card.warning .value { color: var(--warning); }
|
|
|
+
|
|
|
+ /* 阶段进度 */
|
|
|
+ .stages {
|
|
|
+ background: var(--bg-card);
|
|
|
+ border-radius: 16px;
|
|
|
+ padding: 24px;
|
|
|
+ margin-bottom: 32px;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ }
|
|
|
+
|
|
|
+ .stages h2 {
|
|
|
+ font-size: 1.3rem;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 16px;
|
|
|
+ background: var(--bg);
|
|
|
+ border-radius: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage-status {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 1rem;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage-status.done { background: var(--success); }
|
|
|
+ .stage-status.active { background: var(--primary); animation: pulse 2s infinite; }
|
|
|
+ .stage-status.pending { background: var(--border); color: var(--text-muted); }
|
|
|
+
|
|
|
+ @keyframes pulse {
|
|
|
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); }
|
|
|
+ 50% { box-shadow: 0 0 0 8px rgba(99, 102, 241, 0); }
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage-info {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage-info h3 {
|
|
|
+ font-size: 1rem;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage-info p {
|
|
|
+ font-size: 0.85rem;
|
|
|
+ color: var(--text-muted);
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage-progress {
|
|
|
+ width: 120px;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage-progress .bar {
|
|
|
+ height: 6px;
|
|
|
+ background: var(--border);
|
|
|
+ border-radius: 3px;
|
|
|
+ overflow: hidden;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage-progress .bar-fill {
|
|
|
+ height: 100%;
|
|
|
+ background: linear-gradient(90deg, var(--primary), var(--success));
|
|
|
+ transition: width 0.5s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage-progress .text {
|
|
|
+ font-size: 0.8rem;
|
|
|
+ color: var(--text-muted);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 演示区域 */
|
|
|
+ .demo-section {
|
|
|
+ background: var(--bg-card);
|
|
|
+ border-radius: 16px;
|
|
|
+ padding: 24px;
|
|
|
+ margin-bottom: 32px;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ }
|
|
|
+
|
|
|
+ .demo-section h2 {
|
|
|
+ font-size: 1.3rem;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .demo-tabs {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .demo-tab {
|
|
|
+ padding: 10px 20px;
|
|
|
+ background: var(--bg);
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 0.9rem;
|
|
|
+ color: var(--text-muted);
|
|
|
+ transition: all 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .demo-tab:hover {
|
|
|
+ background: var(--bg-hover);
|
|
|
+ color: var(--text);
|
|
|
+ }
|
|
|
+
|
|
|
+ .demo-tab.active {
|
|
|
+ background: var(--primary);
|
|
|
+ border-color: var(--primary);
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .demo-content {
|
|
|
+ background: var(--bg);
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 24px;
|
|
|
+ min-height: 300px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* SVG演示 */
|
|
|
+ .svg-demo {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 24px;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .note-demo {
|
|
|
+ background: white;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 20px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .note-demo svg {
|
|
|
+ display: block;
|
|
|
+ margin: 0 auto 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .note-demo .label {
|
|
|
+ font-size: 0.8rem;
|
|
|
+ color: #64748b;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 测试统计 */
|
|
|
+ .test-stats {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
+ gap: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .test-stat {
|
|
|
+ background: var(--bg);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 16px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .test-stat .icon {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 8px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 1.2rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .test-stat .icon.success { background: rgba(16, 185, 129, 0.2); }
|
|
|
+ .test-stat .icon.info { background: rgba(99, 102, 241, 0.2); }
|
|
|
+
|
|
|
+ .test-stat .info h4 {
|
|
|
+ font-size: 1.2rem;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .test-stat .info p {
|
|
|
+ font-size: 0.8rem;
|
|
|
+ color: var(--text-muted);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 模块列表 */
|
|
|
+ .modules-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
|
+ gap: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .module-card {
|
|
|
+ background: var(--bg);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 16px;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ }
|
|
|
+
|
|
|
+ .module-card h4 {
|
|
|
+ font-size: 0.95rem;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .module-card .status-badge {
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 0.7rem;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .status-badge.done { background: rgba(16, 185, 129, 0.2); color: var(--success); }
|
|
|
+ .status-badge.wip { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
|
|
|
+ .status-badge.pending { background: rgba(148, 163, 184, 0.2); color: var(--text-muted); }
|
|
|
+
|
|
|
+ .module-card p {
|
|
|
+ font-size: 0.8rem;
|
|
|
+ color: var(--text-muted);
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .module-card .tests {
|
|
|
+ font-size: 0.75rem;
|
|
|
+ color: var(--success);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 底部 */
|
|
|
+ footer {
|
|
|
+ text-align: center;
|
|
|
+ padding: 24px;
|
|
|
+ color: var(--text-muted);
|
|
|
+ font-size: 0.85rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ footer a {
|
|
|
+ color: var(--primary-light);
|
|
|
+ text-decoration: none;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="container">
|
|
|
+ <header>
|
|
|
+ <h1>🎵 简谱渲染引擎</h1>
|
|
|
+ <p>JianpuRenderer - 高性能简谱渲染解决方案</p>
|
|
|
+ <div class="version-badge">开发版本 v0.3.0 | 阶段3进行中</div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <!-- 进度概览 -->
|
|
|
+ <div class="progress-overview">
|
|
|
+ <div class="progress-card success">
|
|
|
+ <div class="icon">✅</div>
|
|
|
+ <div class="value">193</div>
|
|
|
+ <div class="label">测试用例通过</div>
|
|
|
+ </div>
|
|
|
+ <div class="progress-card">
|
|
|
+ <div class="icon">📦</div>
|
|
|
+ <div class="value">12</div>
|
|
|
+ <div class="label">已实现模块</div>
|
|
|
+ </div>
|
|
|
+ <div class="progress-card">
|
|
|
+ <div class="icon">📄</div>
|
|
|
+ <div class="value">~3500</div>
|
|
|
+ <div class="label">代码行数</div>
|
|
|
+ </div>
|
|
|
+ <div class="progress-card warning">
|
|
|
+ <div class="icon">🎯</div>
|
|
|
+ <div class="value">45%</div>
|
|
|
+ <div class="label">总体进度</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 阶段进度 -->
|
|
|
+ <div class="stages">
|
|
|
+ <h2>📊 开发阶段进度</h2>
|
|
|
+ <div class="stage-list">
|
|
|
+ <div class="stage-item">
|
|
|
+ <div class="stage-status done">✓</div>
|
|
|
+ <div class="stage-info">
|
|
|
+ <h3>阶段0:准备工作</h3>
|
|
|
+ <p>项目结构、数据模型、测试环境</p>
|
|
|
+ </div>
|
|
|
+ <div class="stage-progress">
|
|
|
+ <div class="bar"><div class="bar-fill" style="width: 100%"></div></div>
|
|
|
+ <div class="text">100% 完成</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="stage-item">
|
|
|
+ <div class="stage-status done">✓</div>
|
|
|
+ <div class="stage-info">
|
|
|
+ <h3>阶段0.5:规范文档编写</h3>
|
|
|
+ <p>MusicXML映射、渲染规范、VexFlow兼容</p>
|
|
|
+ </div>
|
|
|
+ <div class="stage-progress">
|
|
|
+ <div class="bar"><div class="bar-fill" style="width: 100%"></div></div>
|
|
|
+ <div class="text">100% 完成</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="stage-item">
|
|
|
+ <div class="stage-status done">✓</div>
|
|
|
+ <div class="stage-info">
|
|
|
+ <h3>阶段1:核心解析器</h3>
|
|
|
+ <p>Divisions处理、OSMD数据解析、时间计算</p>
|
|
|
+ </div>
|
|
|
+ <div class="stage-progress">
|
|
|
+ <div class="bar"><div class="bar-fill" style="width: 100%"></div></div>
|
|
|
+ <div class="text">100% 完成</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="stage-item">
|
|
|
+ <div class="stage-status done">✓</div>
|
|
|
+ <div class="stage-info">
|
|
|
+ <h3>阶段2:布局引擎</h3>
|
|
|
+ <p>小节布局、多声部对齐、行布局、Y坐标计算</p>
|
|
|
+ </div>
|
|
|
+ <div class="stage-progress">
|
|
|
+ <div class="bar"><div class="bar-fill" style="width: 100%"></div></div>
|
|
|
+ <div class="text">100% 完成</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="stage-item">
|
|
|
+ <div class="stage-status active">▶</div>
|
|
|
+ <div class="stage-info">
|
|
|
+ <h3>阶段3:绘制引擎</h3>
|
|
|
+ <p>音符绘制、线条绘制、歌词绘制、修饰符绘制</p>
|
|
|
+ </div>
|
|
|
+ <div class="stage-progress">
|
|
|
+ <div class="bar"><div class="bar-fill" style="width: 25%"></div></div>
|
|
|
+ <div class="text">25% 进行中</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="stage-item">
|
|
|
+ <div class="stage-status pending">○</div>
|
|
|
+ <div class="stage-info">
|
|
|
+ <h3>阶段4:兼容层实现</h3>
|
|
|
+ <p>OSMD兼容适配器、业务集成</p>
|
|
|
+ </div>
|
|
|
+ <div class="stage-progress">
|
|
|
+ <div class="bar"><div class="bar-fill" style="width: 0%"></div></div>
|
|
|
+ <div class="text">待开始</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="stage-item">
|
|
|
+ <div class="stage-status pending">○</div>
|
|
|
+ <div class="stage-info">
|
|
|
+ <h3>阶段5:测试与优化</h3>
|
|
|
+ <p>完整性测试、兼容性测试、性能优化</p>
|
|
|
+ </div>
|
|
|
+ <div class="stage-progress">
|
|
|
+ <div class="bar"><div class="bar-fill" style="width: 0%"></div></div>
|
|
|
+ <div class="text">待开始</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 演示区域 -->
|
|
|
+ <div class="demo-section">
|
|
|
+ <h2>🎨 渲染演示</h2>
|
|
|
+ <div class="demo-tabs">
|
|
|
+ <button class="demo-tab active" onclick="showDemo('notes')">音符绘制</button>
|
|
|
+ <button class="demo-tab" onclick="showDemo('octaves')">高低音点</button>
|
|
|
+ <button class="demo-tab" onclick="showDemo('accidentals')">升降号</button>
|
|
|
+ <button class="demo-tab" onclick="showDemo('durations')">附点音符</button>
|
|
|
+ <button class="demo-tab" onclick="showDemo('complex')">复杂音符</button>
|
|
|
+ </div>
|
|
|
+ <div class="demo-content" id="demo-content">
|
|
|
+ <!-- 动态内容 -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 测试统计 -->
|
|
|
+ <div class="demo-section">
|
|
|
+ <h2>🧪 测试覆盖</h2>
|
|
|
+ <div class="test-stats">
|
|
|
+ <div class="test-stat">
|
|
|
+ <div class="icon success">✓</div>
|
|
|
+ <div class="info">
|
|
|
+ <h4>39 通过</h4>
|
|
|
+ <p>DivisionsHandler.test.ts</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="test-stat">
|
|
|
+ <div class="icon success">✓</div>
|
|
|
+ <div class="info">
|
|
|
+ <h4>30 通过</h4>
|
|
|
+ <p>OSMDDataParser.test.ts</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="test-stat">
|
|
|
+ <div class="icon success">✓</div>
|
|
|
+ <div class="info">
|
|
|
+ <h4>23 通过</h4>
|
|
|
+ <p>TimeCalculator.test.ts</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="test-stat">
|
|
|
+ <div class="icon success">✓</div>
|
|
|
+ <div class="info">
|
|
|
+ <h4>31 通过</h4>
|
|
|
+ <p>MeasureLayoutEngine.test.ts</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="test-stat">
|
|
|
+ <div class="icon success">✓</div>
|
|
|
+ <div class="info">
|
|
|
+ <h4>31 通过</h4>
|
|
|
+ <p>MultiVoiceAligner.test.ts</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="test-stat">
|
|
|
+ <div class="icon success">✓</div>
|
|
|
+ <div class="info">
|
|
|
+ <h4>40 通过</h4>
|
|
|
+ <p>SystemLayoutEngine.test.ts</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="test-stat">
|
|
|
+ <div class="icon success">✓</div>
|
|
|
+ <div class="info">
|
|
|
+ <h4>35 通过</h4>
|
|
|
+ <p>NotePositionCalculator.test.ts</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="test-stat">
|
|
|
+ <div class="icon success">✓</div>
|
|
|
+ <div class="info">
|
|
|
+ <h4>49 通过</h4>
|
|
|
+ <p>NoteDrawer.test.ts</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 模块列表 -->
|
|
|
+ <div class="demo-section">
|
|
|
+ <h2>📦 已实现模块</h2>
|
|
|
+ <div class="modules-grid">
|
|
|
+ <div class="module-card">
|
|
|
+ <h4>DivisionsHandler <span class="status-badge done">完成</span></h4>
|
|
|
+ <p>MusicXML divisions处理,时值转换</p>
|
|
|
+ <div class="tests">✓ 39个测试通过</div>
|
|
|
+ </div>
|
|
|
+ <div class="module-card">
|
|
|
+ <h4>OSMDDataParser <span class="status-badge done">完成</span></h4>
|
|
|
+ <p>OSMD数据解析,音符提取</p>
|
|
|
+ <div class="tests">✓ 30个测试通过</div>
|
|
|
+ </div>
|
|
|
+ <div class="module-card">
|
|
|
+ <h4>TimeCalculator <span class="status-badge done">完成</span></h4>
|
|
|
+ <p>时间计算,BPM处理,弱起检测</p>
|
|
|
+ <div class="tests">✓ 23个测试通过</div>
|
|
|
+ </div>
|
|
|
+ <div class="module-card">
|
|
|
+ <h4>MeasureLayoutEngine <span class="status-badge done">完成</span></h4>
|
|
|
+ <p>小节布局,固定时间比例算法</p>
|
|
|
+ <div class="tests">✓ 31个测试通过</div>
|
|
|
+ </div>
|
|
|
+ <div class="module-card">
|
|
|
+ <h4>MultiVoiceAligner <span class="status-badge done">完成</span></h4>
|
|
|
+ <p>多声部对齐,时间戳统一</p>
|
|
|
+ <div class="tests">✓ 31个测试通过</div>
|
|
|
+ </div>
|
|
|
+ <div class="module-card">
|
|
|
+ <h4>SystemLayoutEngine <span class="status-badge done">完成</span></h4>
|
|
|
+ <p>行布局,自动换行,Y坐标</p>
|
|
|
+ <div class="tests">✓ 40个测试通过</div>
|
|
|
+ </div>
|
|
|
+ <div class="module-card">
|
|
|
+ <h4>NotePositionCalculator <span class="status-badge done">完成</span></h4>
|
|
|
+ <p>音符Y坐标,声部间距</p>
|
|
|
+ <div class="tests">✓ 35个测试通过</div>
|
|
|
+ </div>
|
|
|
+ <div class="module-card">
|
|
|
+ <h4>NoteDrawer <span class="status-badge done">完成</span></h4>
|
|
|
+ <p>音符SVG绘制,高低音点,附点,升降号</p>
|
|
|
+ <div class="tests">✓ 49个测试通过</div>
|
|
|
+ </div>
|
|
|
+ <div class="module-card">
|
|
|
+ <h4>LineDrawer <span class="status-badge wip">开发中</span></h4>
|
|
|
+ <p>增时线、减时线、小节线绘制</p>
|
|
|
+ <div class="tests">待实现</div>
|
|
|
+ </div>
|
|
|
+ <div class="module-card">
|
|
|
+ <h4>LyricDrawer <span class="status-badge pending">待开始</span></h4>
|
|
|
+ <p>歌词绘制,多遍歌词</p>
|
|
|
+ <div class="tests">待实现</div>
|
|
|
+ </div>
|
|
|
+ <div class="module-card">
|
|
|
+ <h4>ModifierDrawer <span class="status-badge pending">待开始</span></h4>
|
|
|
+ <p>装饰音、连音符绘制</p>
|
|
|
+ <div class="tests">待实现</div>
|
|
|
+ </div>
|
|
|
+ <div class="module-card">
|
|
|
+ <h4>OSMDCompatibilityAdapter <span class="status-badge pending">待开始</span></h4>
|
|
|
+ <p>OSMD兼容层,state.times生成</p>
|
|
|
+ <div class="tests">待实现</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <footer>
|
|
|
+ <p>简谱渲染引擎 | 开发中 | <a href="compare.html">对比测试</a> | <a href="collect-baseline.html">基准收集</a></p>
|
|
|
+ </footer>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ // SVG命名空间
|
|
|
+ const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
|
+
|
|
|
+ // 创建SVG容器
|
|
|
+ function createSVG(width, height) {
|
|
|
+ const svg = document.createElementNS(SVG_NS, 'svg');
|
|
|
+ svg.setAttribute('width', width);
|
|
|
+ svg.setAttribute('height', height);
|
|
|
+ svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
|
+ return svg;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制音符
|
|
|
+ function drawNote(x, y, pitch, options = {}) {
|
|
|
+ const { octave = 0, dots = 0, accidental = null, fontSize = 24 } = options;
|
|
|
+
|
|
|
+ const g = document.createElementNS(SVG_NS, 'g');
|
|
|
+ g.setAttribute('transform', `translate(${x}, ${y})`);
|
|
|
+
|
|
|
+ // 升降号
|
|
|
+ if (accidental) {
|
|
|
+ const accText = document.createElementNS(SVG_NS, 'text');
|
|
|
+ accText.setAttribute('x', '-12');
|
|
|
+ accText.setAttribute('y', '-8');
|
|
|
+ accText.setAttribute('font-size', '12');
|
|
|
+ accText.setAttribute('fill', '#333');
|
|
|
+ accText.setAttribute('text-anchor', 'end');
|
|
|
+ accText.textContent = accidental === 'sharp' ? '#' : accidental === 'flat' ? '♭' : '♮';
|
|
|
+ g.appendChild(accText);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 高音点
|
|
|
+ if (octave > 0) {
|
|
|
+ for (let i = 0; i < octave; i++) {
|
|
|
+ const dot = document.createElementNS(SVG_NS, 'circle');
|
|
|
+ dot.setAttribute('cx', '0');
|
|
|
+ dot.setAttribute('cy', String(-fontSize/2 - 6 - i * 5));
|
|
|
+ dot.setAttribute('r', '2.5');
|
|
|
+ dot.setAttribute('fill', '#333');
|
|
|
+ g.appendChild(dot);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 音符数字
|
|
|
+ const text = document.createElementNS(SVG_NS, 'text');
|
|
|
+ text.setAttribute('x', '0');
|
|
|
+ text.setAttribute('y', '0');
|
|
|
+ text.setAttribute('font-size', String(fontSize));
|
|
|
+ text.setAttribute('font-family', 'Arial, sans-serif');
|
|
|
+ text.setAttribute('fill', '#333');
|
|
|
+ text.setAttribute('text-anchor', 'middle');
|
|
|
+ text.setAttribute('dominant-baseline', 'central');
|
|
|
+ text.textContent = String(pitch);
|
|
|
+ g.appendChild(text);
|
|
|
+
|
|
|
+ // 低音点
|
|
|
+ if (octave < 0) {
|
|
|
+ for (let i = 0; i < Math.abs(octave); i++) {
|
|
|
+ const dot = document.createElementNS(SVG_NS, 'circle');
|
|
|
+ dot.setAttribute('cx', '0');
|
|
|
+ dot.setAttribute('cy', String(fontSize/2 + 6 + i * 5));
|
|
|
+ dot.setAttribute('r', '2.5');
|
|
|
+ dot.setAttribute('fill', '#333');
|
|
|
+ g.appendChild(dot);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 附点
|
|
|
+ if (dots > 0) {
|
|
|
+ for (let i = 0; i < dots; i++) {
|
|
|
+ const dot = document.createElementNS(SVG_NS, 'circle');
|
|
|
+ dot.setAttribute('cx', String(fontSize * 0.4 + 4 + i * 6));
|
|
|
+ dot.setAttribute('cy', '0');
|
|
|
+ dot.setAttribute('r', '2');
|
|
|
+ dot.setAttribute('fill', '#333');
|
|
|
+ g.appendChild(dot);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return g;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示演示
|
|
|
+ function showDemo(type) {
|
|
|
+ // 更新标签状态
|
|
|
+ document.querySelectorAll('.demo-tab').forEach(tab => tab.classList.remove('active'));
|
|
|
+ event.target.classList.add('active');
|
|
|
+
|
|
|
+ const container = document.getElementById('demo-content');
|
|
|
+ container.innerHTML = '';
|
|
|
+
|
|
|
+ const demos = {
|
|
|
+ notes: [
|
|
|
+ { pitch: 1, label: 'Do (1)' },
|
|
|
+ { pitch: 2, label: 'Re (2)' },
|
|
|
+ { pitch: 3, label: 'Mi (3)' },
|
|
|
+ { pitch: 4, label: 'Fa (4)' },
|
|
|
+ { pitch: 5, label: 'Sol (5)' },
|
|
|
+ { pitch: 6, label: 'La (6)' },
|
|
|
+ { pitch: 7, label: 'Si (7)' },
|
|
|
+ { pitch: 0, label: '休止符 (0)' },
|
|
|
+ ],
|
|
|
+ octaves: [
|
|
|
+ { pitch: 5, octave: 2, label: '高两个八度' },
|
|
|
+ { pitch: 5, octave: 1, label: '高一个八度' },
|
|
|
+ { pitch: 5, octave: 0, label: '中音' },
|
|
|
+ { pitch: 5, octave: -1, label: '低一个八度' },
|
|
|
+ { pitch: 5, octave: -2, label: '低两个八度' },
|
|
|
+ ],
|
|
|
+ accidentals: [
|
|
|
+ { pitch: 4, accidental: 'sharp', label: '升号 #4' },
|
|
|
+ { pitch: 7, accidental: 'flat', label: '降号 ♭7' },
|
|
|
+ { pitch: 3, accidental: 'natural', label: '还原号 ♮3' },
|
|
|
+ ],
|
|
|
+ durations: [
|
|
|
+ { pitch: 5, dots: 0, label: '四分音符' },
|
|
|
+ { pitch: 5, dots: 1, label: '附点四分' },
|
|
|
+ { pitch: 5, dots: 2, label: '双附点四分' },
|
|
|
+ ],
|
|
|
+ complex: [
|
|
|
+ { pitch: 5, octave: 1, dots: 1, accidental: 'sharp', label: '升5·(高)' },
|
|
|
+ { pitch: 3, octave: -1, dots: 2, accidental: 'flat', label: '降3··(低)' },
|
|
|
+ { pitch: 1, octave: 2, dots: 1, label: '高高Do·' },
|
|
|
+ { pitch: 7, octave: -2, accidental: 'sharp', label: '升Si(低低)' },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+
|
|
|
+ const items = demos[type] || demos.notes;
|
|
|
+
|
|
|
+ const wrapper = document.createElement('div');
|
|
|
+ wrapper.className = 'svg-demo';
|
|
|
+
|
|
|
+ items.forEach(item => {
|
|
|
+ const noteDemo = document.createElement('div');
|
|
|
+ noteDemo.className = 'note-demo';
|
|
|
+
|
|
|
+ const svg = createSVG(80, 80);
|
|
|
+ const note = drawNote(40, 40, item.pitch, {
|
|
|
+ octave: item.octave || 0,
|
|
|
+ dots: item.dots || 0,
|
|
|
+ accidental: item.accidental || null,
|
|
|
+ });
|
|
|
+ svg.appendChild(note);
|
|
|
+
|
|
|
+ const label = document.createElement('div');
|
|
|
+ label.className = 'label';
|
|
|
+ label.textContent = item.label;
|
|
|
+
|
|
|
+ noteDemo.appendChild(svg);
|
|
|
+ noteDemo.appendChild(label);
|
|
|
+ wrapper.appendChild(noteDemo);
|
|
|
+ });
|
|
|
+
|
|
|
+ container.appendChild(wrapper);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化
|
|
|
+ document.addEventListener('DOMContentLoaded', () => {
|
|
|
+ showDemo('notes');
|
|
|
+ });
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|