交互式二胡弹奏网页
头部(head):引入Tailwind CSS、Font Awesome,配置Tailwind自定义主题(颜色、字体、动画),定义工具类(@layer utilities)与自定义样式(滚动条、鼠标、模态框等);- 全局变量:存储音频上下文(audioContext)、合成器(erhuSynth)、演奏状态(isPlaying、currentNote)、参数配置(volume、bowPressur
该文档用于详细介绍“交互式二胡弹奏 | 白色极简版”网页的功能、结构、技术实现及使用方法,帮助开发者理解代码逻辑与用户快速上手操作。
一、核心概述
- 功能定位:一款基于HTML5、Web Audio API和Canvas的网页端虚拟乐器应用,通过可视化交互模拟二胡演奏,支持自定义参数调节、乐谱播放、录音回放等功能。
- 设计风格:白色极简风,以黑白两色为主色调,搭配简洁线条装饰,界面层次清晰,交互反馈直观。
- 适配场景:支持PC端与移动端,兼容现代主流浏览器(Chrome、Firefox等),无需安装额外插件即可运行。
二、页面结构
网页分为3个核心屏幕(区域)+ 2个模态框 + 页脚,采用“滚动差分效果”实现屏幕间平滑过渡。
模块 核心内容 功能作用
首屏(screen1) 标题“交互式二胡弹奏”、功能简介、二胡图片、操作按钮 应用入口,引导用户“开始演奏”或“了解更多”
演奏区域(screen2) 左侧虚拟二胡(Canvas绘制)、右侧控制面板 核心交互区,实现二胡弹奏、参数调节
帮助与设置(screen3) 使用帮助(操作指南、演奏技巧)、应用设置(视觉效果、音频质量等) 辅助功能区,提供操作说明与个性化配置
录音回放模态框 播放/暂停、进度条、保存/删除按钮 展示录音内容,支持回放、保存或删除录音
乐谱显示模态框 乐谱标题、音符网格(含音名、八度、拍数) 展示预设乐谱,同步高亮当前演奏音符
页脚 版权信息、技术栈说明、社交图标 版权声明与外部链接入口
三、核心功能与交互
1. 基础演奏功能
- 琴弓拖拽:鼠标/手指点击Canvas中的琴弓并拖动,琴弓移动速度决定音量大小,纵向位置决定音高(上方为高音,下方为低音)。
- 按弦定位:点击Canvas中琴弦(中间两条竖线),生成“手指指示器”,模拟二胡按弦动作,改变当前音高。
- 键盘控制:开启“键盘控制”后,通过A、W、S、E等按键(对应C4、C#4、D4等音符)快速演奏,释放按键停止发声。
2. 参数调节功能
右侧控制面板支持3类核心参数调节,实时作用于演奏效果:
- 音量控制:
- 主音量:调节整体输出音量(0-100);
- 弓压:模拟二胡弓毛与琴弦的压力,数值越高音色越浑厚;
- 音色:通过低通滤波器调节音色明暗(数值越高,高频保留越多,音色越亮)。
- 乐谱选择:提供4首预设乐谱(小星星、茉莉花、梁祝、赛马),点击后弹出乐谱模态框并自动播放。
- 演奏技巧:4种二胡常用技巧,点击按钮切换,按钮高亮表示当前激活状态:
- 颤音(tremolo):快速交替按弦,产生颤动音效;
- 滑音(glissando):音高渐变,模拟手指在琴弦上滑动;
- 顿弓(staccato):快速加重弓压后衰减,产生短促重音;
- 拨弦(pizzicato):模拟手指直接拨弦,音效短促清脆。
3. 录音与回放
- 录音:点击演奏区右上角“录音按钮”(圆形图标),按钮变为“停止”图标,开始记录演奏的音符(音高、时间、时长);再次点击停止录音,自动弹出“录音回放模态框”。
- 回放:在录音模态框中点击“播放”按钮,同步还原录制的演奏效果,进度条实时更新播放位置。
- 保存/删除:支持保存录音(当前为提示性功能)或删除录音(清空已录制的音符数据)。
4. 个性化设置
在“帮助与设置”屏可配置3项核心参数,点击“保存设置”生效:
- 视觉效果:开启/关闭琴弦振动、音量波纹等动态效果;
- 音频质量:高/中/低三档,影响泛音数量与滤波器参数(高音质泛音更丰富);
- 键盘控制:开启/关闭键盘按键演奏功能。
四、技术实现
1. 核心技术栈
- 前端框架/工具:HTML5(语义化标签)、Tailwind CSS(样式开发)、Font Awesome(图标);
- 交互与音频:JavaScript(逻辑控制)、Web Audio API(合成二胡音色)、Canvas(绘制虚拟二胡与动态效果);
- 响应式适配:Tailwind CSS的flex/grid布局、viewport元标签,实现PC/移动端适配。
2. 关键技术逻辑
- 二胡音色合成(Web Audio API):
1. 主振荡器:使用“锯齿波(sawtooth)”作为基础音色,模拟二胡的明亮质感;
2. 泛音叠加:生成2-5倍频的泛音振荡器,增强音色层次感;
3. 滤波器:通过低通滤波器(lowpass)调节音色,“音色滑块”控制滤波器频率(1000-5000Hz);
4. 音量包络:根据演奏技巧(如顿弓、拨弦)调整ADSR包络,模拟真实演奏的音量变化。
- Canvas动态绘制:
1. 静态元素:二胡琴筒、琴杆、琴轴、琴弦等基础结构,初始化时绘制;
2. 动态元素:琴弓(跟随鼠标/手指移动)、手指指示器(按弦时显示)、琴弦振动(演奏时生成正弦波动效果)、音量波纹(根据音量大小生成同心圆)。
- 滚动差分效果:监听window.scroll事件,根据滚动距离计算“进度值”,通过transform: translateY()控制各屏幕的位置,实现“前覆盖后”“后覆盖前”的平滑过渡。
五、使用说明
1. 基础操作步骤
1. 打开网页,进入首屏后点击“开始演奏”,滚动至“演奏区域”;
2. 鼠标点击Canvas中的琴弓并拖动,体验基础演奏;
3. 点击琴弦调整按弦位置,改变音高;
4. 在右侧控制面板调节“音量”“音色”,或切换“演奏技巧”;
5. 选择预设乐谱(如“小星星”),观看乐谱模态框并聆听自动演奏;
6. 点击“录音按钮”录制个人演奏,录制完成后在模态框中回放。
2. 常见问题
- 无声音输出:检查浏览器是否支持Web Audio API(建议使用Chrome),或是否开启“静音模式”;
- 移动端交互异常:确保触摸操作时点击琴弓/琴弦区域,避免点击边缘空白处;
- 录音无内容:需在“开始演奏”后点击录音按钮,确保录制过程中有琴弓拖拽操作。
六、代码结构说明
1. HTML部分
- 头部(head):引入Tailwind CSS、Font Awesome,配置Tailwind自定义主题(颜色、字体、动画),定义工具类(@layer utilities)与自定义样式(滚动条、鼠标、模态框等);
- 主体(body):按“屏幕-模态框-页脚”顺序编写DOM结构,为交互元素添加唯一ID(如erhuCanvas、recordBtn),便于JS获取。
2. JavaScript部分
- 全局变量:存储音频上下文(audioContext)、合成器(erhuSynth)、演奏状态(isPlaying、currentNote)、参数配置(volume、bowPressure)等核心数据;
- 初始化函数(init):页面加载后执行,完成Canvas尺寸设置、音频上下文初始化、事件监听绑定、自定义鼠标初始化;
- 核心逻辑函数:
- 音频相关:createErhuSynth(创建合成器)、erhuSynth.play/stop(播放/停止音符);
- 交互相关:handleMouseDown/Move/Up(鼠标事件)、handleTouchStart/Move/End(触摸事件);
- 绘制相关:drawErhu(绘制二胡)、animate(动画循环,调用requestAnimationFrame);
- 功能相关:toggleRecording(切换录音)、playScore(播放乐谱)、saveSettings(保存设置)。
七、扩展建议
1. 新增乐谱:在 scores 对象中添加新乐谱数组(含note(音高)、duration(时长)),即可在“乐谱选择”中显示;
2. 自定义音色:扩展“音色滑块”功能,增加“高通滤波器”“失真效果”等选项,丰富音色种类;
3. 分享功能:对接第三方分享接口,支持将录音或演奏视频分享至社交平台;
4. 多乐器切换:基于现有架构,扩展小提琴、古筝等其他乐器的音色合成逻辑。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>交互式二胡弹奏 | 白色极简版</title>
<!-- Tailwind CSS v3 -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- 统一的 Tailwind 配置 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#000000',
secondary: '#666666',
accent: '#333333',
light: '#ffffff',
dark: '#111111'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
animation: {
'vibrate': 'vibrate 0.3s ease-in-out infinite',
},
keyframes: {
vibrate: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-1px)' },
}
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.text-shadow {
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
}
.line-element {
@apply h-px bg-black my-4 transition-all duration-300;
}
.hover-scale {
@apply transition-transform duration-300 hover:scale-105;
}
.diagonal-line {
@apply absolute h-24 w-px bg-black origin-bottom-left transform rotate-45;
}
.horizontal-line {
@apply absolute left-0 w-full h-px bg-black opacity-70;
}
.corner-line {
@apply absolute;
}
.corner-line::before {
content: '';
@apply absolute w-16 h-px bg-black;
}
.corner-line::after {
content: '';
@apply absolute h-16 w-px bg-black;
}
}
</style>
<style>
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #cccccc;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #999999;
}
/* 自定义鼠标样式 */
#custom-cursor {
position: fixed;
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid #000;
pointer-events: none;
mix-blend-mode: difference;
z-index: 50;
transform: translate(-50%, -50%);
opacity: 0;
transition: opacity 0.3s ease;
}
/* 滚动差分效果 */
.screen {
position: relative;
min-height: 100vh;
transition: transform 0.5s ease-out;
}
/* 线条装饰 */
.decorative-line {
position: absolute;
background-color: #000;
transition: all 0.3s ease;
}
/* 按钮悬停效果 */
.minimal-btn {
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.minimal-btn::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 1px;
background-color: #000;
transition: width 0.3s ease;
}
.minimal-btn:hover::after {
width: 100%;
}
/* 滑块样式 */
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 1px;
background: #000;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #000;
cursor: pointer;
transition: all 0.2s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
/* 模态框动画 */
.modal {
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.modal.active {
opacity: 1;
visibility: visible;
}
.modal-content {
transform: translateY(20px);
transition: transform 0.3s ease;
}
.modal.active .modal-content {
transform: translateY(0);
}
</style>
</head>
<body class="bg-white min-h-screen font-sans text-black overflow-x-hidden">
<!-- 自定义鼠标 -->
<div id="custom-cursor" class="fixed w-6 h-6 rounded-full border border-black pointer-events-none mix-blend-difference z-50 transform -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity duration-300"></div>
<!-- 线条装饰 -->
<div class="decorative-line w-1/3 h-px top-20 left-0"></div>
<div class="decorative-line w-px h-1/3 top-0 left-20"></div>
<div class="decorative-line w-1/3 h-px bottom-20 right-0"></div>
<div class="decorative-line w-px h-1/3 bottom-0 right-20"></div>
<!-- 主容器 -->
<div class="relative min-h-screen overflow-hidden">
<!-- 首屏 -->
<section id="screen1" class="screen relative min-h-screen flex items-center justify-center z-50">
<div class="container mx-auto px-4 py-12">
<div class="flex flex-col md:flex-row items-center justify-between">
<div class="w-full md:w-1/2 mb-8 md:mb-0">
<div class="relative">
<h1 class="text-5xl font-light mb-6 relative inline-block">
<span class="relative z-10">交互式二胡弹奏</span>
<div class="absolute bottom-2 left-0 w-full h-1 bg-black"></div>
</h1>
<p class="text-lg text-gray-600 mb-8 max-w-md">
通过现代网页技术,体验中国传统乐器二胡的魅力。简单直观的交互方式,让任何人都能轻松演奏。
</p>
<div class="flex space-x-4">
<a href="#screen2" class="minimal-btn px-6 py-2 inline-block">
开始演奏
<i class="fa fa-arrow-right ml-2"></i>
</a>
<a href="#help" class="minimal-btn px-6 py-2 inline-block">
了解更多
</a>
</div>
<!-- 装饰线条 -->
<div class="absolute -right-10 top-10 w-20 h-px bg-black"></div>
<div class="absolute -right-10 top-10 w-px h-20 bg-black"></div>
</div>
</div>
<div class="w-full md:w-1/2 flex justify-center">
<div class="relative w-full max-w-md">
<img src="https://p11-doubao-search-sign.byteimg.com/tos-cn-i-be4g95zd3a/871555826284101651~tplv-be4g95zd3a-image.jpeg?rk3s=542c0f93&x-expires=1776927777&x-signature=0NERVdr0sTEl54o1Z90Dxxltr18%3D"
alt="二胡"
class="w-full h-auto object-contain hover-scale">
<!-- 装饰线条 -->
<div class="absolute -left-4 top-1/4 w-px h-32 bg-black"></div>
<div class="absolute -right-4 bottom-1/4 w-px h-32 bg-black"></div>
</div>
</div>
</div>
</div>
</section>
<!-- 第二屏:演奏区域 -->
<section id="screen2" class="screen relative min-h-screen bg-white z-40 flex items-center">
<div class="container mx-auto px-4 py-12">
<div class="mb-8">
<h2 class="text-3xl font-light mb-2">演奏区域</h2>
<div class="w-24 h-px bg-black"></div>
</div>
<div class="flex flex-col lg:flex-row gap-8">
<!-- 左侧:虚拟二胡区域 -->
<div class="lg:w-2/3 bg-white border border-gray-200 p-4 relative overflow-hidden">
<div class="absolute top-4 right-4 flex space-x-2">
<button id="recordBtn" class="w-10 h-10 rounded-full bg-black text-white flex items-center justify-center hover:bg-gray-800 transition-all">
<i class="fa fa-circle"></i>
</button>
<button id="playBtn" class="w-10 h-10 rounded-full bg-black text-white flex items-center justify-center hover:bg-gray-800 transition-all">
<i class="fa fa-play"></i>
</button>
</div>
<div class="relative">
<canvas id="erhuCanvas" class="w-full border border-gray-200 bg-white" height="500"></canvas>
<!-- 按弦位置指示器 -->
<div id="fingerIndicator" class="absolute hidden w-6 h-6 bg-black rounded-full transform -translate-x-1/2 -translate-y-1/2 z-10">
<div class="absolute -top-1 -left-1 w-8 h-8 border-2 border-gray-300 rounded-full animate-ping"></div>
</div>
<!-- 音量指示器 -->
<div class="absolute bottom-4 left-4 bg-white border border-gray-200 p-2">
<div class="flex items-center">
<i class="fa fa-volume-up mr-2"></i>
<div id="volumeBar" class="w-24 h-1 bg-gray-200">
<div id="volumeLevel" class="h-full bg-black w-0 transition-all"></div>
</div>
</div>
</div>
</div>
<!-- 当前音符显示 -->
<div class="mt-4 text-center">
<div id="currentNoteDisplay" class="text-4xl font-light text-black">C4</div>
<div id="noteInfo" class="text-gray-600">点击琴弦并拖动琴弓演奏</div>
</div>
</div>
<!-- 右侧:控制面板 -->
<div class="lg:w-1/3 flex flex-col gap-6">
<!-- 音量控制 -->
<div class="bg-white border border-gray-200 p-6">
<h3 class="text-xl font-light mb-4 flex items-center">
<i class="fa fa-volume-up text-black mr-2"></i> 音量控制
</h3>
<div class="flex flex-col space-y-4">
<div>
<label for="volumeSlider" class="block text-sm text-gray-600 mb-1">主音量</label>
<input type="range" id="volumeSlider" min="0" max="100" value="80" class="w-full h-1 bg-black rounded-lg appearance-none cursor-pointer">
</div>
<div>
<label for="bowPressureSlider" class="block text-sm text-gray-600 mb-1">弓压</label>
<input type="range" id="bowPressureSlider" min="0" max="100" value="50" class="w-full h-1 bg-black rounded-lg appearance-none cursor-pointer">
</div>
<div>
<label for="toneSlider" class="block text-sm text-gray-600 mb-1">音色</label>
<input type="range" id="toneSlider" min="0" max="100" value="70" class="w-full h-1 bg-black rounded-lg appearance-none cursor-pointer">
</div>
</div>
</div>
<!-- 乐谱选择 -->
<div class="bg-white border border-gray-200 p-6">
<h3 class="text-xl font-light mb-4 flex items-center">
<i class="fa fa-music text-black mr-2"></i> 乐谱选择
</h3>
<div class="space-y-2">
<button class="score-btn w-full text-left px-4 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex justify-between items-center" data-score="小星星">
<span>小星星</span>
<i class="fa fa-play-circle text-black"></i>
</button>
<button class="score-btn w-full text-left px-4 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex justify-between items-center" data-score="茉莉花">
<span>茉莉花</span>
<i class="fa fa-play-circle text-black"></i>
</button>
<button class="score-btn w-full text-left px-4 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex justify-between items-center" data-score="梁祝">
<span>梁祝</span>
<i class="fa fa-play-circle text-black"></i>
</button>
<button class="score-btn w-full text-left px-4 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex justify-between items-center" data-score="赛马">
<span>赛马</span>
<i class="fa fa-play-circle text-black"></i>
</button>
</div>
</div>
<!-- 演奏技巧 -->
<div class="bg-white border border-gray-200 p-6">
<h3 class="text-xl font-light mb-4 flex items-center">
<i class="fa fa-sliders text-black mr-2"></i> 演奏技巧
</h3>
<div class="grid grid-cols-2 gap-2">
<button id="tremoloBtn" class="px-3 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex items-center justify-center">
<i class="fa fa-refresh mr-1"></i> 颤音
</button>
<button id="glissandoBtn" class="px-3 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex items-center justify-center">
<i class="fa fa-arrows-h mr-1"></i> 滑音
</button>
<button id="staccatoBtn" class="px-3 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex items-center justify-center">
<i class="fa fa-stop mr-1"></i> 顿弓
</button>
<button id="pizzicatoBtn" class="px-3 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex items-center justify-center">
<i class="fa fa-hand-paper-o mr-1"></i> 拨弦
</button>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 第三屏:帮助与设置 -->
<section id="screen3" class="screen relative min-h-screen bg-white z-30 flex items-center">
<div class="container mx-auto px-4 py-12">
<div class="mb-8">
<h2 class="text-3xl font-light mb-2">帮助与设置</h2>
<div class="w-24 h-px bg-black"></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- 帮助部分 -->
<div id="help" class="bg-white border border-gray-200 p-6">
<h3 class="text-xl font-light mb-4">使用帮助</h3>
<div class="space-y-4">
<div>
<h4 class="text-lg font-medium mb-2">基本操作</h4>
<ul class="list-disc pl-5 space-y-1 text-gray-600">
<li>点击并拖动琴弓来演奏二胡</li>
<li>点击琴弦上的不同位置来改变音高</li>
<li>使用右侧控制面板调整音量、音色等参数</li>
</ul>
</div>
<div>
<h4 class="text-lg font-medium mb-2">演奏技巧</h4>
<ul class="list-disc pl-5 space-y-1 text-gray-600">
<li><strong>颤音</strong>:快速交替按弦产生颤动效果</li>
<li><strong>滑音</strong>:手指在琴弦上滑动,音高逐渐变化</li>
<li><strong>顿弓</strong>:突然加重弓压并迅速减轻,产生重音效果</li>
<li><strong>拨弦</strong>:用手指直接拨动琴弦,产生短促的音</li>
</ul>
</div>
</div>
</div>
<!-- 设置部分 -->
<div class="bg-white border border-gray-200 p-6">
<h3 class="text-xl font-light mb-4">应用设置</h3>
<div class="space-y-4">
<div>
<label for="visualEffectToggle" class="flex items-center justify-between cursor-pointer">
<span class="text-gray-700">视觉效果</span>
<div class="relative">
<input type="checkbox" id="visualEffectToggle" class="sr-only" checked>
<div class="block bg-gray-200 w-14 h-8 rounded-full"></div>
<div class="dot absolute left-1 top-1 bg-black w-6 h-6 rounded-full transition-transform translate-x-6"></div>
</div>
</label>
</div>
<div>
<label for="audioQualitySelect" class="block text-sm text-gray-600 mb-1">音频质量</label>
<select id="audioQualitySelect" class="w-full px-3 py-2 border border-gray-200 bg-white">
<option value="high">高</option>
<option value="medium" selected>中</option>
<option value="low">低</option>
</select>
</div>
<div>
<label for="keyboardControlToggle" class="flex items-center justify-between cursor-pointer">
<span class="text-gray-700">键盘控制</span>
<div class="relative">
<input type="checkbox" id="keyboardControlToggle" class="sr-only" checked>
<div class="block bg-gray-200 w-14 h-8 rounded-full"></div>
<div class="dot absolute left-1 top-1 bg-black w-6 h-6 rounded-full transition-transform translate-x-6"></div>
</div>
</label>
</div>
<div class="pt-4">
<button id="saveSettingsBtn" class="w-full px-4 py-2 bg-black text-white hover:bg-gray-800 transition-all">
保存设置
</button>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<!-- 录音回放模态框 -->
<div id="recordingModal" class="modal fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="modal-content bg-white p-6 max-w-md w-full">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-light">录音回放</h2>
<button id="closeRecordingBtn" class="text-gray-500 hover:text-black">
<i class="fa fa-times text-xl"></i>
</button>
</div>
<div class="space-y-4">
<div class="flex justify-center">
<button id="playRecordingBtn" class="w-16 h-16 rounded-full bg-black text-white flex items-center justify-center hover:bg-gray-800 transition-all">
<i class="fa fa-play text-2xl"></i>
</button>
</div>
<div>
<label for="recordingProgress" class="block text-sm text-gray-600 mb-1">进度</label>
<input type="range" id="recordingProgress" min="0" max="100" value="0" class="w-full h-1 bg-black rounded-lg appearance-none cursor-pointer">
</div>
<div class="flex justify-between text-sm text-gray-500">
<span id="recordingCurrentTime">00:00</span>
<span id="recordingTotalTime">00:00</span>
</div>
<div class="flex space-x-2 pt-4">
<button id="saveRecordingBtn" class="flex-1 px-4 py-2 bg-black text-white hover:bg-gray-800 transition-all">
<i class="fa fa-save mr-1"></i> 保存
</button>
<button id="deleteRecordingBtn" class="flex-1 px-4 py-2 border border-gray-200 hover:bg-gray-50 transition-all">
<i class="fa fa-trash mr-1"></i> 删除
</button>
</div>
</div>
</div>
</div>
<!-- 乐谱显示模态框 -->
<div id="scoreModal" class="modal fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="modal-content bg-white p-6 max-w-4xl w-full max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h2 id="scoreTitle" class="text-2xl font-light">乐谱</h2>
<button id="closeScoreBtn" class="text-gray-500 hover:text-black">
<i class="fa fa-times text-xl"></i>
</button>
</div>
<div id="scoreContent" class="space-y-4">
<!-- 乐谱内容将通过JavaScript动态插入 -->
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="bg-white border-t border-gray-200 py-8">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="mb-4 md:mb-0">
<p class="text-gray-600">© 2025 交互式二胡弹奏 | 基于HTML5 Canvas和Web Audio API</p>
</div>
<div class="flex space-x-4">
<a href="#" class="text-gray-600 hover:text-black transition-colors">
<i class="fa fa-github text-xl"></i>
</a>
<a href="#" class="text-gray-600 hover:text-black transition-colors">
<i class="fa fa-twitter text-xl"></i>
</a>
<a href="#" class="text-gray-600 hover:text-black transition-colors">
<i class="fa fa-weixin text-xl"></i>
</a>
</div>
</div>
</div>
</footer>
<script>
// 全局变量
const canvas = document.getElementById('erhuCanvas');
const ctx = canvas.getContext('2d');
let audioContext;
let erhuSynth;
let isDragging = false;
let bowPosition = { x: 350, y: 300 };
let lastBowPosition = null;
let bowSpeed = 0;
let isPlaying = false;
let currentNote = 60; // C4
let fingerPosition = null;
let volume = 0.8;
let bowPressure = 0.5;
let tone = 0.7;
let currentTechnique = 'normal';
let isRecording = false;
let recordedNotes = [];
let recordingStartTime = 0;
let isPlayback = false;
let playbackIndex = 0;
let playbackInterval = null;
let visualEffectsEnabled = true;
let keyboardControlEnabled = true;
let audioQuality = 'medium';
let currentScore = null;
let scorePlaybackInterval = null;
// 音符名称映射
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
// 预设乐谱
const scores = {
'小星星': [
{ note: 60, duration: 0.5 }, // C4
{ note: 60, duration: 0.5 }, // C4
{ note: 67, duration: 0.5 }, // G4
{ note: 67, duration: 0.5 }, // G4
{ note: 69, duration: 0.5 }, // A4
{ note: 69, duration: 0.5 }, // A4
{ note: 67, duration: 1 }, // G4
{ note: 65, duration: 0.5 }, // F4
{ note: 65, duration: 0.5 }, // F4
{ note: 64, duration: 0.5 }, // E4
{ note: 64, duration: 0.5 }, // E4
{ note: 62, duration: 0.5 }, // D4
{ note: 62, duration: 0.5 }, // D4
{ note: 60, duration: 1 }, // C4
],
'茉莉花': [
{ note: 65, duration: 0.5 }, // F4
{ note: 69, duration: 0.5 }, // A4
{ note: 72, duration: 1 }, // C5
{ note: 69, duration: 0.5 }, // A4
{ note: 72, duration: 1.5 }, // C5
{ note: 71, duration: 0.5 }, // B4
{ note: 69, duration: 1 }, // A4
{ note: 65, duration: 0.5 }, // F4
{ note: 67, duration: 0.5 }, // G4
{ note: 69, duration: 1.5 }, // A4
{ note: 67, duration: 0.5 }, // G4
{ note: 65, duration: 1 }, // F4
{ note: 64, duration: 0.5 }, // E4
{ note: 65, duration: 2 }, // F4
],
'梁祝': [
{ note: 65, duration: 1 }, // F4
{ note: 62, duration: 0.5 }, // D4
{ note: 64, duration: 0.5 }, // E4
{ note: 65, duration: 1 }, // F4
{ note: 69, duration: 0.5 }, // A4
{ note: 67, duration: 0.5 }, // G4
{ note: 65, duration: 1 }, // F4
{ note: 64, duration: 0.5 }, // E4
{ note: 62, duration: 0.5 }, // D4
{ note: 64, duration: 2 }, // E4
{ note: 62, duration: 1 }, // D4
{ note: 60, duration: 1 }, // C4
{ note: 62, duration: 2 }, // D4
],
'赛马': [
{ note: 62, duration: 0.25 }, // D4
{ note: 64, duration: 0.25 }, // E4
{ note: 65, duration: 0.25 }, // F4
{ note: 67, duration: 0.25 }, // G4
{ note: 69, duration: 0.25 }, // A4
{ note: 71, duration: 0.25 }, // B4
{ note: 72, duration: 0.25 }, // C5
{ note: 71, duration: 0.25 }, // B4
{ note: 69, duration: 0.25 }, // A4
{ note: 67, duration: 0.25 }, // G4
{ note: 65, duration: 0.25 }, // F4
{ note: 64, duration: 0.25 }, // E4
{ note: 62, duration: 0.25 }, // D4
{ note: 60, duration: 0.25 }, // C4
]
};
// 初始化函数
function init() {
// 设置Canvas尺寸
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// 初始化音频上下文
try {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioContext();
createErhuSynth();
} catch (e) {
console.error('Web Audio API is not supported in this browser', e);
alert('您的浏览器不支持Web Audio API,请使用最新版Chrome或Firefox浏览器');
}
// 初始化事件监听
initEventListeners();
// 初始化自定义鼠标
initCustomCursor();
// 初始化滚动差分效果
initScrollEffects();
// 开始动画循环
animate();
}
// 调整Canvas尺寸
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = 500;
// 重新绘制二胡
drawErhu();
}
// 创建二胡合成器
function createErhuSynth() {
// 主振荡器
const oscillator = audioContext.createOscillator();
oscillator.type = 'sawtooth';
// 共鸣滤波器
const filter = audioContext.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 3000;
// 音量控制
const gainNode = audioContext.createGain();
gainNode.gain.value = 0;
// 连接节点
oscillator.connect(filter);
filter.connect(gainNode);
gainNode.connect(audioContext.destination);
// 创建泛音
const harmonics = [];
for (let i = 2; i <= 5; i++) {
const harmonicOsc = audioContext.createOscillator();
const harmonicGain = audioContext.createGain();
harmonicOsc.type = 'sawtooth';
harmonicGain.gain.value = 0.1 / i;
harmonicOsc.connect(harmonicGain);
harmonicGain.connect(gainNode);
harmonics.push({
oscillator: harmonicOsc,
gain: harmonicGain
});
}
// 保存合成器对象
erhuSynth = {
oscillator,
filter,
gainNode,
harmonics,
playing: false,
play(note, duration = 0.1, velocity = 1) {
if (!this.playing) {
this.playing = true;
// 计算频率
const frequency = 440 * Math.pow(2, (note - 69) / 12);
// 设置主振荡器
this.oscillator.frequency.value = frequency;
// 设置泛音
this.harmonics.forEach((harmonic, index) => {
harmonic.oscillator.frequency.value = frequency * (index + 2);
harmonic.oscillator.start();
});
// 开始主振荡器
this.oscillator.start();
// 更新显示
updateNoteDisplay(note);
}
// 调整音量
const now = audioContext.currentTime;
const currentVolume = volume * velocity * bowPressure;
// 根据当前技巧调整ADSR包络
if (currentTechnique === 'staccato') {
// 顿弓:快速攻击,快速衰减
this.gainNode.gain.cancelScheduledValues(now);
this.gainNode.gain.setValueAtTime(0, now);
this.gainNode.gain.linearRampToValueAtTime(currentVolume, now + 0.01);
this.gainNode.gain.linearRampToValueAtTime(0, now + duration);
} else if (currentTechnique === 'pizzicato') {
// 拨弦:快速攻击,快速衰减
this.gainNode.gain.cancelScheduledValues(now);
this.gainNode.gain.setValueAtTime(0, now);
this.gainNode.gain.linearRampToValueAtTime(currentVolume * 0.8, now + 0.005);
this.gainNode.gain.linearRampToValueAtTime(0, now + 0.3);
} else if (currentTechnique === 'tremolo') {
// 颤音:快速波动
this.gainNode.gain.cancelScheduledValues(now);
this.gainNode.gain.setValueAtTime(0, now);
this.gainNode.gain.linearRampToValueAtTime(currentVolume, now + 0.02);
// 创建颤音效果
for (let i = 0; i < duration * 20; i++) {
const time = now + 0.02 + i * 0.05;
const value = currentVolume * (0.7 + Math.sin(i * Math.PI * 4) * 0.3);
this.gainNode.gain.setValueAtTime(value, time);
}
this.gainNode.gain.linearRampToValueAtTime(0, now + duration);
} else {
// 普通演奏
this.gainNode.gain.cancelScheduledValues(now);
this.gainNode.gain.setValueAtTime(0, now);
this.gainNode.gain.linearRampToValueAtTime(currentVolume, now + 0.05);
this.gainNode.gain.linearRampToValueAtTime(currentVolume * 0.8, now + 0.1);
this.gainNode.gain.linearRampToValueAtTime(currentVolume * 0.8, now + duration - 0.1);
this.gainNode.gain.linearRampToValueAtTime(0, now + duration);
}
// 更新音量指示器
updateVolumeIndicator(currentVolume);
// 如果正在录音,记录音符
if (isRecording) {
recordedNotes.push({
note,
time: Date.now() - recordingStartTime,
duration
});
}
},
stop() {
if (this.playing) {
const now = audioContext.currentTime;
// 停止所有振荡器
this.oscillator.stop(now + 0.1);
this.harmonics.forEach(harmonic => {
harmonic.oscillator.stop(now + 0.1);
});
// 重置状态
this.playing = false;
// 重新创建振荡器(因为一旦停止就不能再次启动)
setTimeout(() => {
this.oscillator = audioContext.createOscillator();
this.oscillator.type = 'sawtooth';
this.oscillator.connect(this.filter);
this.harmonics.forEach((harmonic, index) => {
harmonic.oscillator = audioContext.createOscillator();
harmonic.oscillator.type = 'sawtooth';
harmonic.oscillator.connect(harmonic.gain);
});
}, 100);
}
}
};
}
// 初始化事件监听
function initEventListeners() {
// 琴弓拖拽交互
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('mouseleave', handleMouseUp);
// 触摸设备支持
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
canvas.addEventListener('touchend', handleTouchEnd);
canvas.addEventListener('touchcancel', handleTouchEnd);
// 控制面板滑块
document.getElementById('volumeSlider').addEventListener('input', handleVolumeChange);
document.getElementById('bowPressureSlider').addEventListener('input', handleBowPressureChange);
document.getElementById('toneSlider').addEventListener('input', handleToneChange);
// 演奏技巧按钮
document.getElementById('tremoloBtn').addEventListener('click', () => setCurrentTechnique('tremolo'));
document.getElementById('glissandoBtn').addEventListener('click', () => setCurrentTechnique('glissando'));
document.getElementById('staccatoBtn').addEventListener('click', () => setCurrentTechnique('staccato'));
document.getElementById('pizzicatoBtn').addEventListener('click', () => setCurrentTechnique('pizzicato'));
// 录音按钮
document.getElementById('recordBtn').addEventListener('click', toggleRecording);
// 播放按钮
document.getElementById('playBtn').addEventListener('click', togglePlayback);
// 乐谱按钮
document.querySelectorAll('.score-btn').forEach(btn => {
btn.addEventListener('click', () => {
const scoreName = btn.getAttribute('data-score');
playScore(scoreName);
});
});
// 设置按钮
document.getElementById('saveSettingsBtn').addEventListener('click', saveSettings);
// 视觉效果开关
document.getElementById('visualEffectToggle').addEventListener('change', toggleVisualEffect);
// 键盘控制开关
document.getElementById('keyboardControlToggle').addEventListener('change', toggleKeyboardControl);
// 音频质量选择
document.getElementById('audioQualitySelect').addEventListener('change', changeAudioQuality);
// 录音回放按钮
document.getElementById('playRecordingBtn').addEventListener('click', toggleRecordingPlayback);
document.getElementById('saveRecordingBtn').addEventListener('click', saveRecording);
document.getElementById('deleteRecordingBtn').addEventListener('click', deleteRecording);
document.getElementById('closeRecordingBtn').addEventListener('click', () => {
document.getElementById('recordingModal').classList.remove('active');
});
// 乐谱关闭按钮
document.getElementById('closeScoreBtn').addEventListener('click', () => {
document.getElementById('scoreModal').classList.remove('active');
stopScorePlayback();
});
// 键盘控制
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);
// 模态框点击外部关闭
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('active');
}
});
});
// 平滑滚动
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
window.scrollTo({
top: targetElement.offsetTop,
behavior: 'smooth'
});
}
});
});
}
// 初始化自定义鼠标
function initCustomCursor() {
const cursor = document.getElementById('custom-cursor');
let mouseX = 0;
let mouseY = 0;
let cursorX = 0;
let cursorY = 0;
// 初始显示光标
document.addEventListener('mousemove', (e) => {
cursor.style.opacity = '1';
mouseX = e.clientX;
mouseY = e.clientY;
});
// 平滑跟随效果
function animateCursor() {
const speed = 0.2; // 跟随速度
cursorX += (mouseX - cursorX) * speed;
cursorY += (mouseY - cursorY) * speed;
cursor.style.transform = `translate(${cursorX}px, ${cursorY}px)`;
requestAnimationFrame(animateCursor);
}
animateCursor();
// 悬停效果
const hoverElements = document.querySelectorAll('button, a, input, select');
hoverElements.forEach(element => {
element.addEventListener('mouseenter', () => {
cursor.classList.add('scale-150');
});
element.addEventListener('mouseleave', () => {
cursor.classList.remove('scale-150');
});
});
}
// 初始化滚动差分效果
function initScrollEffects() {
const screens = document.querySelectorAll('.screen');
window.addEventListener('scroll', () => {
const scrollY = window.scrollY;
const windowHeight = window.innerHeight;
// 前覆盖后模式 (第一屏和第二屏之间)
if (scrollY < windowHeight) {
// 确定第二屏的显示程度 (0到1之间)
const progress = Math.min(1, scrollY / windowHeight);
// 第二屏从下方出现
const translateY = -windowHeight * (1 - progress);
screens[1].style.transform = `translateY(${translateY}px)`;
}
// 后覆盖前模式 (第二屏和第三屏之间)
if (scrollY >= windowHeight && scrollY < windowHeight * 2) {
// 计算第三屏的过渡进度
const progress = Math.min(1, (scrollY - windowHeight) / windowHeight);
// 第二屏向上移动
const translateY = windowHeight * 0.3 * progress;
screens[1].style.transform = `translateY(${translateY}px)`;
// 第三屏从下方出现
const translateY2 = -windowHeight * (1 - progress);
screens[2].style.transform = `translateY(${translateY2}px)`;
}
// 更新装饰线条位置
updateDecorativeLines(scrollY, windowHeight);
});
}
// 更新装饰线条位置
function updateDecorativeLines(scrollY, windowHeight) {
const lines = document.querySelectorAll('.decorative-line');
const progress = scrollY / (document.body.offsetHeight - windowHeight);
lines.forEach((line, index) => {
if (index === 0) { // 顶部水平线
line.style.width = `${33 + progress * 34}%`;
} else if (index === 1) { // 左侧垂直线
line.style.height = `${33 + progress * 34}%`;
} else if (index === 2) { // 底部水平线
line.style.width = `${33 + progress * 34}%`;
} else if (index === 3) { // 右侧垂直线
line.style.height = `${33 + progress * 34}%`;
}
});
}
// 鼠标按下事件处理
function handleMouseDown(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 检查是否点击琴弓 - 扩大点击区域
if (Math.abs(x - bowPosition.x) < 100 && Math.abs(y - bowPosition.y) < 30) {
isDragging = true;
lastBowPosition = { x, y };
}
// 检查是否点击琴弦 - 扩大点击区域
if (x >= 340 && x <= 370) {
setFingerPosition(y);
}
}
// 鼠标移动事件处理
function handleMouseMove(e) {
if (!isDragging) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 更新琴弓位置
bowPosition = { x, y };
// 计算弓速
if (lastBowPosition) {
const dx = x - lastBowPosition.x;
const dy = y - lastBowPosition.y;
bowSpeed = Math.sqrt(dx * dx + dy * dy);
}
lastBowPosition = { x, y };
// 播放声音 - 只要拖动就持续发声
const note = getNoteFromPosition(y);
currentNote = note;
if (erhuSynth) {
if (!erhuSynth.playing) {
erhuSynth.play(note, 1.0, Math.max(0.1, bowSpeed / 10));
} else {
// 如果已经在播放,更新音符和音量
const frequency = 440 * Math.pow(2, (note - 69) / 12);
erhuSynth.oscillator.frequency.value = frequency;
// 更新泛音
erhuSynth.harmonics.forEach((harmonic, index) => {
harmonic.oscillator.frequency.value = frequency * (index + 2);
});
// 更新音量
const now = audioContext.currentTime;
const velocity = Math.max(0.1, bowSpeed / 10);
const currentVolume = volume * velocity * bowPressure;
erhuSynth.gainNode.gain.cancelScheduledValues(now);
erhuSynth.gainNode.gain.setValueAtTime(currentVolume, now);
// 更新显示
updateNoteDisplay(note);
updateVolumeIndicator(currentVolume);
}
}
}
// 鼠标释放事件处理
function handleMouseUp() {
isDragging = false;
lastBowPosition = null;
bowSpeed = 0;
if (erhuSynth && erhuSynth.playing) {
erhuSynth.stop();
}
}
// 触摸开始事件处理
function handleTouchStart(e) {
e.preventDefault();
if (e.touches.length === 1) {
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousedown', {
clientX: touch.clientX,
clientY: touch.clientY
});
handleMouseDown(mouseEvent);
}
}
// 触摸移动事件处理
function handleTouchMove(e) {
e.preventDefault();
if (e.touches.length === 1) {
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousemove', {
clientX: touch.clientX,
clientY: touch.clientY
});
handleMouseMove(mouseEvent);
}
}
// 触摸结束事件处理
function handleTouchEnd(e) {
e.preventDefault();
handleMouseUp();
}
// 音量变化事件处理
function handleVolumeChange(e) {
volume = parseInt(e.target.value) / 100;
}
// 弓压变化事件处理
function handleBowPressureChange(e) {
bowPressure = parseInt(e.target.value) / 100;
}
// 音色变化事件处理
function handleToneChange(e) {
tone = parseInt(e.target.value) / 100;
// 调整滤波器频率
if (erhuSynth) {
const frequency = 1000 + tone * 4000;
erhuSynth.filter.frequency.value = frequency;
}
}
// 设置当前演奏技巧
function setCurrentTechnique(technique) {
currentTechnique = technique;
// 更新按钮样式
document.querySelectorAll('#tremoloBtn, #glissandoBtn, #staccatoBtn, #pizzicatoBtn').forEach(btn => {
btn.classList.remove('bg-black', 'text-white');
btn.classList.add('border', 'border-gray-200');
});
document.getElementById(`${technique}Btn`).classList.remove('border', 'border-gray-200');
document.getElementById(`${technique}Btn`).classList.add('bg-black', 'text-white');
}
// 切换录音状态
function toggleRecording() {
const recordBtn = document.getElementById('recordBtn');
if (!isRecording) {
// 开始录音
isRecording = true;
recordedNotes = [];
recordingStartTime = Date.now();
recordBtn.innerHTML = '<i class="fa fa-stop"></i>';
} else {
// 停止录音
isRecording = false;
recordBtn.innerHTML = '<i class="fa fa-circle"></i>';
// 显示录音回放模态框
if (recordedNotes.length > 0) {
document.getElementById('recordingModal').classList.add('active');
document.getElementById('recordingTotalTime').textContent = formatTime(recordedNotes[recordedNotes.length - 1].time);
}
}
}
// 切换回放状态
function togglePlayback() {
const playBtn = document.getElementById('playBtn');
if (!isPlayback) {
// 开始回放
if (recordedNotes.length === 0) return;
isPlayback = true;
playbackIndex = 0;
playBtn.innerHTML = '<i class="fa fa-pause"></i>';
// 开始回放
playNextRecordedNote();
} else {
// 停止回放
isPlayback = false;
playBtn.innerHTML = '<i class="fa fa-play"></i>';
// 停止当前播放的音符
if (erhuSynth && erhuSynth.playing) {
erhuSynth.stop();
}
// 清除定时器
if (playbackInterval) {
clearTimeout(playbackInterval);
playbackInterval = null;
}
}
}
// 播放下一个录制的音符
function playNextRecordedNote() {
if (!isPlayback || playbackIndex >= recordedNotes.length) {
// 回放结束
isPlayback = false;
document.getElementById('playBtn').innerHTML = '<i class="fa fa-play"></i>';
return;
}
const noteData = recordedNotes[playbackIndex];
// 更新进度条
const progress = (noteData.time / recordedNotes[recordedNotes.length - 1].time) * 100;
document.getElementById('recordingProgress').value = progress;
document.getElementById('recordingCurrentTime').textContent = formatTime(noteData.time);
// 播放音符
if (erhuSynth) {
erhuSynth.play(noteData.note, noteData.duration);
// 设置手指位置
setFingerPosition(getPositionFromNote(noteData.note));
}
// 准备播放下一个音符
playbackIndex++;
// 计算下一个音符的延迟
let delay = noteData.duration * 500; // 默认延迟
if (playbackIndex < recordedNotes.length) {
const nextNoteTime = recordedNotes[playbackIndex].time;
delay = nextNoteTime - noteData.time;
}
// 设置定时器
playbackInterval = setTimeout(playNextRecordedNote, delay);
}
// 播放乐谱
function playScore(scoreName) {
if (!scores[scoreName]) return;
// 停止当前的乐谱播放
stopScorePlayback();
// 设置当前乐谱
currentScore = scores[scoreName];
// 显示乐谱模态框
document.getElementById('scoreTitle').textContent = scoreName;
document.getElementById('scoreContent').innerHTML = generateScoreHTML(currentScore);
document.getElementById('scoreModal').classList.add('active');
// 开始播放
let index = 0;
function playNextNote() {
if (index >= currentScore.length) {
// 播放结束
return;
}
const noteData = currentScore[index];
// 高亮当前音符
highlightCurrentNote(index);
// 播放音符
if (erhuSynth) {
erhuSynth.play(noteData.note, noteData.duration);
// 设置手指位置
setFingerPosition(getPositionFromNote(noteData.note));
}
// 准备播放下一个音符
index++;
// 设置定时器
scorePlaybackInterval = setTimeout(playNextNote, noteData.duration * 500);
}
// 开始播放
playNextNote();
}
// 停止乐谱播放
function stopScorePlayback() {
if (scorePlaybackInterval) {
clearTimeout(scorePlaybackInterval);
scorePlaybackInterval = null;
}
// 停止当前播放的音符
if (erhuSynth && erhuSynth.playing) {
erhuSynth.stop();
}
// 隐藏手指位置
hideFingerPosition();
}
// 生成乐谱HTML
function generateScoreHTML(score) {
let html = '<div class="grid grid-cols-4 gap-2">';
score.forEach((noteData, index) => {
const noteName = getNoteName(noteData.note);
const octave = Math.floor(noteData.note / 12) - 1;
html += `
<div class="score-note p-2 border border-gray-200 text-center transition-all" data-index="${index}">
<div class="text-xl font-light">${noteName}</div>
<div class="text-sm text-gray-500">${octave}</div>
<div class="text-xs mt-1">${noteData.duration}拍</div>
</div>
`;
});
html += '</div>';
return html;
}
// 高亮当前音符
function highlightCurrentNote(index) {
document.querySelectorAll('.score-note').forEach((el, i) => {
if (i === index) {
el.classList.add('bg-black', 'text-white');
} else {
el.classList.remove('bg-black', 'text-white');
}
});
}
// 切换录音回放
function toggleRecordingPlayback() {
const playRecordingBtn = document.getElementById('playRecordingBtn');
if (!isPlayback) {
// 开始回放
if (recordedNotes.length === 0) return;
isPlayback = true;
playbackIndex = 0;
playRecordingBtn.innerHTML = '<i class="fa fa-pause text-2xl"></i>';
// 开始回放
playNextRecordedNote();
} else {
// 停止回放
isPlayback = false;
playRecordingBtn.innerHTML = '<i class="fa fa-play text-2xl"></i>';
// 停止当前播放的音符
if (erhuSynth && erhuSynth.playing) {
erhuSynth.stop();
}
// 清除定时器
if (playbackInterval) {
clearTimeout(playbackInterval);
playbackInterval = null;
}
}
}
// 保存录音
function saveRecording() {
alert('录音已保存!');
document.getElementById('recordingModal').classList.remove('active');
}
// 删除录音
function deleteRecording() {
recordedNotes = [];
document.getElementById('recordingModal').classList.remove('active');
}
// 保存设置
function saveSettings() {
// 保存设置
visualEffectsEnabled = document.getElementById('visualEffectToggle').checked;
keyboardControlEnabled = document.getElementById('keyboardControlToggle').checked;
audioQuality = document.getElementById('audioQualitySelect').value;
// 显示保存成功提示
alert('设置已保存!');
}
// 切换视觉效果
function toggleVisualEffect(e) {
visualEffectsEnabled = e.target.checked;
// 更新开关样式
const dot = e.target.nextElementSibling.nextElementSibling;
if (visualEffectsEnabled) {
dot.classList.remove('translate-x-0');
dot.classList.add('translate-x-6');
} else {
dot.classList.remove('translate-x-6');
dot.classList.add('translate-x-0');
}
}
// 切换键盘控制
function toggleKeyboardControl(e) {
keyboardControlEnabled = e.target.checked;
// 更新开关样式
const dot = e.target.nextElementSibling.nextElementSibling;
if (keyboardControlEnabled) {
dot.classList.remove('translate-x-0');
dot.classList.add('translate-x-6');
} else {
dot.classList.remove('translate-x-6');
dot.classList.add('translate-x-0');
}
}
// 改变音频质量
function changeAudioQuality(e) {
audioQuality = e.target.value;
// 根据音频质量调整合成器参数
if (erhuSynth) {
if (audioQuality === 'high') {
// 高音质:增加泛音数量
erhuSynth.filter.Q.value = 1.5;
} else if (audioQuality === 'medium') {
// 中等音质:默认设置
erhuSynth.filter.Q.value = 1.0;
} else {
// 低音质:减少泛音数量
erhuSynth.filter.Q.value = 0.5;
}
}
}
// 键盘按下事件处理
function handleKeyDown(e) {
if (!keyboardControlEnabled) return;
// 映射键盘按键到音符
const keyToNote = {
'a': 60, // C4
'w': 61, // C#4
's': 62, // D4
'e': 63, // D#4
'd': 64, // E4
'f': 65, // F4
't': 66, // F#4
'g': 67, // G4
'y': 68, // G#4
'h': 69, // A4
'u': 70, // A#4
'j': 71, // B4
'k': 72, // C5
'o': 73, // C#5
'l': 74, // D5
'p': 75, // D#5
';': 76 // E5
};
if (keyToNote[e.key]) {
currentNote = keyToNote[e.key];
// 播放音符
if (erhuSynth && !erhuSynth.playing) {
erhuSynth.play(currentNote, 0.5, 1);
}
// 设置手指位置
setFingerPosition(getPositionFromNote(currentNote));
}
}
// 键盘释放事件处理
function handleKeyUp(e) {
if (!keyboardControlEnabled) return;
// 停止当前播放的音符
if (erhuSynth && erhuSynth.playing) {
erhuSynth.stop();
}
// 隐藏手指位置
hideFingerPosition();
}
// 根据位置计算音符
function getNoteFromPosition(y) {
// 将y坐标映射到音符
// 假设琴杆长度为300像素,对应2个八度
const minNote = 60; // C4
const maxNote = 84; // C6
const noteRange = maxNote - minNote;
// 反转y轴,因为上方是高音
const normalizedY = 1 - (y - 100) / 300;
const note = Math.round(minNote + normalizedY * noteRange);
return note;
}
// 根据音符计算位置
function getPositionFromNote(note) {
// 将音符映射到y坐标
const minNote = 60; // C4
const maxNote = 84; // C6
const noteRange = maxNote - minNote;
const normalizedNote = (note - minNote) / noteRange;
const y = 100 + (1 - normalizedNote) * 300;
return y;
}
// 获取音符名称
function getNoteName(note) {
const octave = Math.floor(note / 12) - 1;
const noteIndex = note % 12;
return noteNames[noteIndex] + octave;
}
// 设置手指位置
function setFingerPosition(y) {
fingerPosition = y;
// 显示手指指示器
const fingerIndicator = document.getElementById('fingerIndicator');
const stringX = canvas.width / 2;
fingerIndicator.style.left = `${stringX + 5}px`;
fingerIndicator.style.top = `${y}px`;
fingerIndicator.classList.remove('hidden');
}
// 隐藏手指位置
function hideFingerPosition() {
fingerPosition = null;
// 隐藏手指指示器
const fingerIndicator = document.getElementById('fingerIndicator');
fingerIndicator.classList.add('hidden');
}
// 更新音符显示
function updateNoteDisplay(note) {
const noteName = getNoteName(note);
document.getElementById('currentNoteDisplay').textContent = noteName;
document.getElementById('noteInfo').textContent = `频率: ${Math.round(440 * Math.pow(2, (note - 69) / 12))} Hz`;
}
// 更新音量指示器
function updateVolumeIndicator(volume) {
const volumeLevel = document.getElementById('volumeLevel');
const percentage = Math.round(volume * 100);
volumeLevel.style.width = `${percentage}%`;
}
// 格式化时间
function formatTime(timeMs) {
const seconds = Math.floor(timeMs / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}
// 绘制二胡
function drawErhu() {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 计算琴弦位置(居中)
const stringX = canvas.width / 2;
// 绘制琴筒
ctx.fillStyle = '#000000';
ctx.beginPath();
ctx.rect(stringX - 50, 400, 100, 80);
ctx.fill();
// 绘制琴筒纹理
ctx.fillStyle = '#333333';
ctx.beginPath();
ctx.arc(stringX, 440, 30, 0, Math.PI * 2);
ctx.fill();
// 绘制琴杆
ctx.fillStyle = '#000000';
ctx.beginPath();
ctx.rect(stringX - 10, 100, 20, 300);
ctx.fill();
// 绘制琴头
ctx.fillStyle = '#000000';
ctx.beginPath();
ctx.moveTo(stringX - 20, 100);
ctx.lineTo(stringX + 20, 100);
ctx.lineTo(stringX + 15, 80);
ctx.lineTo(stringX - 15, 80);
ctx.closePath();
ctx.fill();
// 绘制琴弦
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1;
// 第一根弦
ctx.beginPath();
ctx.moveTo(stringX - 5, 80);
ctx.lineTo(stringX - 5, 400);
ctx.stroke();
// 第二根弦
ctx.beginPath();
ctx.moveTo(stringX + 5, 80);
ctx.lineTo(stringX + 5, 400);
ctx.stroke();
// 绘制琴轴
ctx.fillStyle = '#333333';
ctx.beginPath();
ctx.arc(stringX - 15, 120, 8, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(stringX + 15, 120, 8, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(stringX - 15, 160, 8, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(stringX + 15, 160, 8, 0, Math.PI * 2);
ctx.fill();
// 绘制千斤
ctx.fillStyle = '#333333';
ctx.beginPath();
ctx.rect(stringX - 15, 200, 30, 3);
ctx.fill();
// 绘制琴码
ctx.fillStyle = '#333333';
ctx.beginPath();
ctx.rect(stringX - 8, 380, 16, 8);
ctx.fill();
// 绘制琴弓
if (bowPosition) {
const bowX = bowPosition.x;
const bowY = bowPosition.y;
// 绘制琴弓杆
ctx.strokeStyle = '#000000';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(bowX - 100, bowY);
ctx.lineTo(bowX + 100, bowY);
ctx.stroke();
// 绘制马尾 - 连接到琴弦
ctx.strokeStyle = '#666666';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(bowX - 50, bowY);
ctx.lineTo(stringX - 5, bowY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(stringX + 5, bowY);
ctx.lineTo(bowX + 50, bowY);
ctx.stroke();
// 如果正在演奏,添加振动效果
if (erhuSynth && erhuSynth.playing && visualEffectsEnabled) {
// 绘制琴弦振动效果
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1;
// 第一根弦
ctx.beginPath();
ctx.moveTo(stringX - 5, 80);
for (let y = 80; y <= 400; y += 10) {
const vibration = Math.sin(y / 10 + Date.now() / 100) * 2;
ctx.lineTo(stringX - 5 + vibration, y);
}
ctx.lineTo(stringX - 5, 400);
ctx.stroke();
// 第二根弦
ctx.beginPath();
ctx.moveTo(stringX + 5, 80);
for (let y = 80; y <= 400; y += 10) {
const vibration = Math.sin(y / 10 + Date.now() / 100 + Math.PI) * 2;
ctx.lineTo(stringX + 5 + vibration, y);
}
ctx.lineTo(stringX + 5, 400);
ctx.stroke();
// 添加音量波纹效果
const volume = erhuSynth.gainNode.gain.value;
if (volume > 0.1) {
const rippleRadius = volume * 50;
const rippleOpacity = 1 - volume;
ctx.strokeStyle = `rgba(0, 0, 0, ${rippleOpacity})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(stringX, bowY, rippleRadius, 0, Math.PI * 2);
ctx.stroke();
ctx.strokeStyle = `rgba(0, 0, 0, ${rippleOpacity * 0.7})`;
ctx.beginPath();
ctx.arc(stringX, bowY, rippleRadius * 1.5, 0, Math.PI * 2);
ctx.stroke();
}
}
}
// 如果有手指位置,绘制手指
if (fingerPosition !== null && visualEffectsEnabled) {
// 绘制手指
ctx.fillStyle = '#000000';
ctx.beginPath();
ctx.arc(stringX, fingerPosition, 10, 0, Math.PI * 2);
ctx.fill();
// 绘制按弦效果
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(stringX, fingerPosition, 15, 0, Math.PI * 2);
ctx.stroke();
}
}
// 动画循环
function animate() {
// 绘制二胡
drawErhu();
// 继续动画循环
requestAnimationFrame(animate);
}
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
更多推荐


所有评论(0)