diff --git a/ShengShengBuXi.ConsoleApp/Models/PhoneBoothConfig.cs b/ShengShengBuXi.ConsoleApp/Models/PhoneBoothConfig.cs
index 31b03b9..e64c191 100644
--- a/ShengShengBuXi.ConsoleApp/Models/PhoneBoothConfig.cs
+++ b/ShengShengBuXi.ConsoleApp/Models/PhoneBoothConfig.cs
@@ -126,6 +126,31 @@ public class AudioFilesConfig
///
public int MinKeyTonePlayTimeMs { get; set; } = 200;
+ ///
+ /// 按键音音量倍数 (正常音量为1.0)
+ ///
+ public float KeyToneVolume { get; set; } = 1.0f;
+
+ ///
+ /// 待机嘟声音量倍数 (正常音量为1.0)
+ ///
+ public float WaitingToneVolume { get; set; } = 1.0f;
+
+ ///
+ /// 拨出嘟声音量倍数 (正常音量为1.0)
+ ///
+ public float DialToneVolume { get; set; } = 0.9f;
+
+ ///
+ /// 提示留言音量倍数 (正常音量为1.0)
+ ///
+ public float PromptAfterBeepVolume { get; set; } = 1.0f;
+
+ ///
+ /// 风铃提示音文件名
+ ///
+ public string WindChimeFile { get; set; } = "风铃.wav";
+
///
/// 获取完整的音频文件路径
///
@@ -234,6 +259,16 @@ public class RecordingConfig
/// 背景音乐音量 (0.0-1.0)
///
public float BackgroundMusicVolume { get; set; } = 0.1f;
+
+ ///
+ /// 无声音播放风铃提示的时间(秒)
+ ///
+ public float WindChimePromptSeconds { get; set; } = 7.5f;
+
+ ///
+ /// 风铃声淡出的时间(毫秒)
+ ///
+ public int WindChimeFadeOutMs { get; set; } = 2000;
}
///
diff --git a/ShengShengBuXi.ConsoleApp/README.md b/ShengShengBuXi.ConsoleApp/README.md
index 1400643..ca0713b 100644
--- a/ShengShengBuXi.ConsoleApp/README.md
+++ b/ShengShengBuXi.ConsoleApp/README.md
@@ -25,9 +25,15 @@
"WaitingToneFile": "等待嘟音.wav", // 等待嘟音文件
"WaitForPickupFile": "等待接电话.mp3", // 等待接电话音频文件
"PhonePickupFile": "电话接起.mp3", // 电话接起音频文件
- "PromptUserRecordFile": "提示用户录音.mp3", // 提示用户录音音频文件
+ "PromptUserRecordFile": "提示用户录音.mp3", // 提示用户录音音频文件
"BeepPromptFile": "滴提示音.wav", // 滴提示音文件
- "DigitToneFileTemplate": "{0}.mp3" // 数字按键音频文件模板
+ "DigitToneFileTemplate": "{0}.mp3", // 数字按键音频文件模板
+ "MinKeyTonePlayTimeMs": 200, // 按键音的最小播放时间(毫秒)
+ "KeyToneVolume": 1.0, // 按键音量倍数 (0.0-1.0)
+ "WaitingToneVolume": 1.0, // 待机嘟声音量倍数 (0.0-1.0)
+ "DialToneVolume": 0.9, // 拨出嘟声音量倍数 (0.0-1.0)
+ "PromptAfterBeepVolume": 1.0, // 提示留言音量倍数 (0.0-1.0)
+ "WindChimeFile": "风铃.wav" // 风铃提示音文件名
},
"Dial": {
"MinDigitsToDialOut": 8, // 拨号所需的最小位数
@@ -39,15 +45,22 @@
"RecordingDeviceNumber": 0, // 录音设备编号
"SampleRate": 16000, // 录音采样率
"Channels": 1, // 录音通道数(1=单声道,2=立体声)
- "BufferMilliseconds": 100, // 录音缓冲区大小(毫秒)
- "SilenceThreshold": 0.02, // 静音检测阈值
+ "BufferMilliseconds": 50, // 录音缓冲区大小(毫秒)
+ "SilenceThreshold": 0.05, // 静音检测阈值
"SilenceTimeoutSeconds": 30, // 无声音自动挂断的时间(秒)
- "AllowUserHangup": true // 是否允许用户手动挂断(按回车键)
+ "AllowUserHangup": true, // 是否允许用户手动挂断(按回车键)
+ "UploadRecordingToServer": false, // 是否上传录音文件到服务器
+ "EnableBackgroundMusic": true, // 是否在录音时播放背景音乐
+ "BackgroundMusicFile": "bj.mp3", // 录音背景音乐文件名
+ "BackgroundMusicVolume": 0.1, // 背景音乐音量 (0.0-1.0)
+ "WindChimePromptSeconds": 7.5, // 无声音播放风铃提示的时间(秒)
+ "WindChimeFadeOutMs": 3000 // 风铃声淡出的时间(毫秒)
},
"CallFlow": {
"WaitForPickupMinSeconds": 3, // 等待接电话音频的最小持续时间(秒)
"WaitForPickupMaxSeconds": 6, // 等待接电话音频的最大持续时间(秒)
- "PlayPickupProbability": 0.15 // 播放电话接起音频的概率(0-1之间的小数)
+ "PlayPickupProbability": 0.15, // 播放电话接起音频的概率(0-1之间的小数)
+ "ResetSystemAfterHangup": false // 用户挂断后是否重置系统,false表示直接退出程序
}
}
```
diff --git a/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs b/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs
index 0158810..0607505 100644
--- a/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs
+++ b/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs
@@ -70,6 +70,14 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
private CancellationTokenSource? _backgroundMusicCts = null;
private volatile bool _isBackgroundMusicPlaying = false;
+ // 风铃提示音相关
+ private WaveOutEvent? _windChimeDevice = null;
+ private AudioFileReader? _windChimeReader = null;
+ private CancellationTokenSource? _windChimeCts = null;
+ private volatile bool _isWindChimePlaying = false;
+ private Timer? _windChimeTimer = null;
+ private DateTime _lastSpeechTime = DateTime.MinValue;
+
[StructLayout(LayoutKind.Sequential)]
private struct KBDLLHOOKSTRUCT
{
@@ -282,13 +290,12 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
_waitingToneDevice = new WaveOutEvent();
_waitingToneReader = new AudioFileReader(_config.AudioFiles.GetFullPath(_config.AudioFiles.WaitingToneFile));
_waitingToneDevice.Init(_waitingToneReader);
-
- Console.WriteLine("音频设备初始化成功");
+ // 设置待机嘟声音量,确保在0.0-1.0范围内
+ _waitingToneDevice.Volume = Math.Clamp(_config.AudioFiles.WaitingToneVolume > 0 ? _config.AudioFiles.WaitingToneVolume : 1.0f, 0.0f, 1.0f);
}
catch (Exception ex)
{
Console.WriteLine($"初始化音频设备失败: {ex.Message}");
- throw;
}
}
@@ -334,6 +341,28 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
_isBackgroundMusicPlaying = false;
+ // 释放风铃提示音设备
+ _windChimeCts?.Cancel();
+ _windChimeCts?.Dispose();
+ _windChimeCts = null;
+
+ if (_windChimeDevice != null)
+ {
+ _windChimeDevice.Stop();
+ _windChimeDevice.Dispose();
+ _windChimeDevice = null;
+ }
+
+ if (_windChimeReader != null)
+ {
+ _windChimeReader.Dispose();
+ _windChimeReader = null;
+ }
+
+ _isWindChimePlaying = false;
+ _windChimeTimer?.Dispose();
+ _windChimeTimer = null;
+
foreach (var (device, reader) in _keyToneDevices.Values)
{
device.Stop();
@@ -552,14 +581,18 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
// 启动静音检测计时器
_lastSoundTime = DateTime.Now;
+ _lastSpeechTime = DateTime.Now; // 初始化最后说话时间
_silenceTimer = new Timer(CheckSilence, recordingCompletionSource, 1000, 1000);
+
+ // 启动风铃提示计时器
+ _windChimeTimer = new Timer(CheckWindChimePrompt, null, 1000, 1000);
Console.WriteLine("播放滴提示音...");
try
{
await PlayAudioAndWait(
_config.AudioFiles.GetFullPath(_config.AudioFiles.BeepPromptFile),
- null, false, recordingCts.Token, 0.1f);
+ null, false, recordingCts.Token);
}
catch (OperationCanceledException)
{
@@ -838,6 +871,14 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
_silenceTimer?.Change(Timeout.Infinite, Timeout.Infinite);
_silenceTimer?.Dispose();
_silenceTimer = null;
+
+ // 停止风铃提示音计时器
+ _windChimeTimer?.Change(Timeout.Infinite, Timeout.Infinite);
+ _windChimeTimer?.Dispose();
+ _windChimeTimer = null;
+
+ // 停止风铃提示音
+ _ = StopWindChime(false);
// 停止录音
if (_waveIn != null)
@@ -909,6 +950,19 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
_lasHasoudDateTime = DateTime.Now;
_isSpeaking = false;
}
+
+ // 如果监测到说话,更新最后说话时间
+ if (isSpeaking)
+ {
+ _lastSpeechTime = DateTime.Now;
+
+ // 如果风铃声正在播放,平滑停止它
+ if (_isWindChimePlaying)
+ {
+ _ = StopWindChime(true); // 平滑淡出
+ }
+ }
+
// 如果最大音量超过阈值,则认为有声音
return isSpeaking;
}
@@ -925,6 +979,9 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
// 确保背景音乐被停止
StopBackgroundMusic();
+
+ // 确保风铃提示音被停止
+ _ = StopWindChime(false);
// 清除录音状态
_isRecording = false;
@@ -1238,6 +1295,8 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
var device = new WaveOutEvent();
var reader = new AudioFileReader(_config.AudioFiles.GetDigitToneFilePath(digit));
device.Init(reader);
+ // 设置按键音量,确保在0.0-1.0范围内
+ device.Volume = Math.Clamp(_config.AudioFiles.KeyToneVolume > 0 ? _config.AudioFiles.KeyToneVolume : 1.0f, 0.0f, 1.0f);
// 记录开始播放时间
var startTime = DateTime.Now;
@@ -1245,6 +1304,7 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
if (_keyToneDevices.TryAdd(digit, (device, reader)))
{
+
device.Play();
// 确保至少播放最小时长
@@ -1301,17 +1361,44 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
{
device = new WaveOutEvent();
reader = new AudioFileReader(audioPath);
- //device.Volume
- if (volume != null)
+
+ // 如果没有指定音量,根据音频文件类型设置音量
+ if (volume == null)
{
- device.Volume = volume.Value;
+ // 根据文件名判断音频类型并设置对应的音量
+ if (audioPath.Contains(_config.AudioFiles.WaitForPickupFile))
+ {
+ // 拨出嘟声音量
+ device.Volume = Math.Clamp(_config.AudioFiles.DialToneVolume > 0 ? _config.AudioFiles.DialToneVolume : 1.0f, 0.0f, 1.0f);
+ }
+ else if (audioPath.Contains(_config.AudioFiles.PromptUserRecordFile))
+ {
+ // 提示用户录音的音量
+ device.Volume = Math.Clamp(_config.AudioFiles.PromptAfterBeepVolume > 0 ? _config.AudioFiles.PromptAfterBeepVolume : 1.0f, 0.0f, 1.0f);
+ }
+ else if (audioPath.Contains(_config.AudioFiles.BeepPromptFile))
+ {
+ // 滴提示音的音量
+ device.Volume = Math.Clamp(_config.AudioFiles.PromptAfterBeepVolume > 0 ? _config.AudioFiles.PromptAfterBeepVolume : 1.0f, 0.0f, 1.0f);
+ }
+ else if (audioPath.Contains(_config.AudioFiles.WaitingToneFile))
+ {
+ // 待机嘟声音量 (虽然这个可能不会通过这个方法播放)
+ device.Volume = Math.Clamp(_config.AudioFiles.WaitingToneVolume > 0 ? _config.AudioFiles.WaitingToneVolume : 1.0f, 0.0f, 1.0f);
+ }
+ else
+ {
+ // 默认音量为1.0
+ device.Volume = 1.0f;
+ }
}
+ else
+ {
+ device.Volume = Math.Clamp(volume.Value, 0.0f, 1.0f);
+ }
+
device.Init(reader);
- //device.Volume
- if (volume != null)
- {
- device.Volume = volume.Value;
- }
+
// 如果需要循环播放,创建一个循环播放的任务
if (loop && maxDuration.HasValue)
{
@@ -1518,4 +1605,182 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
Console.WriteLine($"停止背景音乐失败: {ex.Message}");
}
}
+
+ ///
+ /// 检查是否需要播放风铃提示音
+ ///
+ private void CheckWindChimePrompt(object? state)
+ {
+ try
+ {
+ // 如果用户已挂断或者录音已停止,不执行操作
+ if (_isHangUpKeyPressed || !_isRecording)
+ {
+ return;
+ }
+
+ // 检查是否超过配置的无声音时间但还未达到挂断时间
+ float windChimePromptSeconds = _config.Recording.WindChimePromptSeconds;
+ int silenceTimeoutSeconds = _config.Recording.SilenceTimeoutSeconds;
+ double secondsSinceLastSpeech = (DateTime.Now - _lastSpeechTime).TotalSeconds;
+
+ // 如果超过风铃提示时间但未达到挂断时间,并且风铃没在播放,开始播放风铃
+ if (secondsSinceLastSpeech >= windChimePromptSeconds &&
+ secondsSinceLastSpeech < silenceTimeoutSeconds &&
+ !_isWindChimePlaying)
+ {
+ Console.WriteLine($"检测到{windChimePromptSeconds}秒无声音,播放风铃提示音...");
+ _ = PlayWindChime();
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"检查风铃提示音错误: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 播放风铃提示音
+ ///
+ private async Task PlayWindChime()
+ {
+ try
+ {
+ // 如果已经在播放,先停止
+ if (_isWindChimePlaying)
+ {
+ await StopWindChime(true); // 平滑淡出
+ }
+
+ // 防止重复启动
+ if (_isWindChimePlaying)
+ {
+ return;
+ }
+
+ _windChimeCts?.Cancel();
+ _windChimeCts?.Dispose();
+ _windChimeCts = CancellationTokenSource.CreateLinkedTokenSource(_programCts.Token);
+ var token = _windChimeCts.Token;
+
+ string windChimeFilePath = _config.AudioFiles.GetFullPath(_config.AudioFiles.WindChimeFile);
+ if (!File.Exists(windChimeFilePath))
+ {
+ Console.WriteLine($"风铃提示音文件不存在: {windChimeFilePath}");
+ return;
+ }
+
+ // 初始化播放设备
+ _windChimeDevice = new WaveOutEvent();
+ _windChimeReader = new AudioFileReader(windChimeFilePath);
+ _windChimeDevice.Init(_windChimeReader);
+ _windChimeDevice.Volume = Math.Clamp(1.0f, 0.0f, 1.0f); // 初始音量设为最大
+
+ _isWindChimePlaying = true;
+ Console.WriteLine("开始播放风铃提示音");
+
+ // 开始播放
+ _windChimeDevice.Play();
+
+ // 单独开启一个任务检测是否有声音,如有则淡出停止
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ while (_isWindChimePlaying && !token.IsCancellationRequested)
+ {
+ // 如果检测到有声音,淡出停止
+ if ((DateTime.Now - _lastSpeechTime).TotalSeconds < 1.0)
+ {
+ Console.WriteLine("检测到用户说话,风铃提示音淡出...");
+ await StopWindChime(true); // 平滑淡出
+ break;
+ }
+
+ // 如果到达音频文件末尾,循环播放
+ if (_windChimeReader != null && _windChimeReader.Position >= _windChimeReader.Length)
+ {
+ _windChimeReader.Position = 0;
+ }
+
+ await Task.Delay(100, token);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // 正常取消
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"风铃提示音监控错误: {ex.Message}");
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"播放风铃提示音错误: {ex.Message}");
+ await StopWindChime(false); // 出错时直接停止,不淡出
+ }
+ }
+
+ ///
+ /// 停止风铃提示音
+ ///
+ /// 是否平滑淡出
+ private async Task StopWindChime(bool fadeOut)
+ {
+ try
+ {
+ if (!_isWindChimePlaying)
+ {
+ return; // 如果没有播放,不需要停止
+ }
+
+ // 如果需要淡出
+ if (fadeOut && _windChimeDevice != null)
+ {
+ int fadeOutMs = _config.Recording.WindChimeFadeOutMs;
+ float startVolume = _windChimeDevice.Volume;
+ int steps = 20; // 淡出的步骤数
+ int stepDelay = fadeOutMs / steps;
+
+ // 逐步降低音量实现淡出效果
+ for (int i = 0; i < steps; i++)
+ {
+ if (_windChimeDevice == null || !_isWindChimePlaying)
+ {
+ break; // 如果设备已释放或播放已停止,直接退出
+ }
+
+ float ratio = 1.0f - ((float)i / steps);
+ _windChimeDevice.Volume = startVolume * ratio;
+ await Task.Delay(stepDelay);
+ }
+ }
+
+ // 正式停止
+ _windChimeCts?.Cancel();
+
+ if (_windChimeDevice != null)
+ {
+ _windChimeDevice.Stop();
+ _windChimeDevice.Dispose();
+ _windChimeDevice = null;
+ }
+
+ if (_windChimeReader != null)
+ {
+ _windChimeReader.Dispose();
+ _windChimeReader = null;
+ }
+
+ _isWindChimePlaying = false;
+ Console.WriteLine("风铃提示音已停止");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"停止风铃提示音错误: {ex.Message}");
+ _isWindChimePlaying = false;
+ }
+ }
}
\ No newline at end of file
diff --git a/ShengShengBuXi.ConsoleApp/appsettings.json b/ShengShengBuXi.ConsoleApp/appsettings.json
index f1dd3dd..7700478 100644
--- a/ShengShengBuXi.ConsoleApp/appsettings.json
+++ b/ShengShengBuXi.ConsoleApp/appsettings.json
@@ -1,5 +1,5 @@
{
- "SignalRHubUrl": "http://localhost:81/audiohub",
+ "SignalRHubUrl": "http://115.159.44.16/audiohub",
"ConfigBackupPath": "config.json",
"AutoConnectToServer": true,
"AllowOfflineStart": false
diff --git a/ShengShengBuXi.ConsoleApp/config.json b/ShengShengBuXi.ConsoleApp/config.json
index ca0f75c..6f9867b 100644
--- a/ShengShengBuXi.ConsoleApp/config.json
+++ b/ShengShengBuXi.ConsoleApp/config.json
@@ -7,7 +7,12 @@
"PromptUserRecordFile": "\u63D0\u793A\u7528\u6237\u5F55\u97F3.mp3",
"BeepPromptFile": "\u6EF4\u63D0\u793A\u97F3.wav",
"DigitToneFileTemplate": "{0}.mp3",
- "MinKeyTonePlayTimeMs": 200
+ "MinKeyTonePlayTimeMs": 200,
+ "KeyToneVolume": 1.0,
+ "WaitingToneVolume": 1.0,
+ "DialToneVolume": 0.9,
+ "PromptAfterBeepVolume": 1.0,
+ "WindChimeFile": "\u98CE\u94C3.wav"
},
"Dial": {
"MinDigitsToDialOut": 8,
@@ -20,13 +25,15 @@
"SampleRate": 16000,
"Channels": 1,
"BufferMilliseconds": 50,
- "SilenceThreshold": 0.15,
- "SilenceTimeoutSeconds": 30,
+ "SilenceThreshold": 0.1,
+ "SilenceTimeoutSeconds": 15,
"AllowUserHangup": true,
"UploadRecordingToServer": false,
"EnableBackgroundMusic": true,
"BackgroundMusicFile": "bj.mp3",
- "BackgroundMusicVolume": 0.1
+ "BackgroundMusicVolume": 0.3,
+ "WindChimePromptSeconds": 7.5,
+ "WindChimeFadeOutMs": 3000
},
"CallFlow": {
"WaitForPickupMinSeconds": 3,
diff --git a/ShengShengBuXi/Pages/Index.cshtml b/ShengShengBuXi/Pages/Index.cshtml
index 638ebae..e58f9aa 100644
--- a/ShengShengBuXi/Pages/Index.cshtml
+++ b/ShengShengBuXi/Pages/Index.cshtml
@@ -137,10 +137,13 @@
},
// 右侧容器配置
rightContainer: {
- fontSize: '40px', // 右侧文字大小
- fontWeight: '700', // 右侧文字粗细
- fontStyle: 'italic', // 右侧文字样式
- typewriterSpeed: 2000 // 右侧文字打字机速度(毫秒),增加到2000毫秒
+ fontSize: '40px', // 右侧文字大小
+ fontWeight: '700', // 右侧文字粗细
+ fontStyle: 'italic', // 右侧文字样式
+ typewriterSpeed: 2000, // 右侧文字打字机速度(毫秒),增加到2000毫秒
+ fadeChars: 4, // 渐变字符数量,值越大渐变效果越长
+ fadeStepTime: 500, // 字符透明度过渡时间(毫秒)
+ fadeDelayFactor: 0.25 // 每个字符的透明度递减因子(0-1)
},
// 水波纹效果配置
waterEffect: {
@@ -199,34 +202,92 @@
if (isAnimating) return;
isAnimating = true;
- var i = 0;
- $(selector).html('').css('opacity', 1);
+ // 获取渐变相关配置
+ const fadeChars = CONFIG.rightContainer.fadeChars || 4; // 渐变字符数量
+ const fadeStepTime = CONFIG.rightContainer.fadeStepTime || 500; // 过渡时间
+ const fadeDelayFactor = CONFIG.rightContainer.fadeDelayFactor || 0.25; // 透明度递减因子
- function typeWriter() {
- if (i < text.length) {
- // 创建一个新的 span 元素包含当前字符
- const charSpan = $('').text(text.charAt(i));
- charSpan.css('opacity', 0); // 初始透明度为0
- $(selector).append(charSpan); // 添加到容器
-
- // 对这个字符应用淡入效果
- charSpan.animate({opacity: 1}, speed * 0.8, function() {
- // 淡入完成后继续下一个字符
- i++;
- setTimeout(typeWriter, speed * 0.2);
- });
- } else {
- isAnimating = false;
- if (callback) callback();
-
- // 打字效果完成后,等待随机4-7秒,然后请求下一条文本
- const delay = Math.floor(Math.random() * 3000) + 4000; // 4000-7000毫秒
- console.log(`文本显示完成,将在${delay/1000}秒后请求下一条文本`);
-
- setTimeout(requestNextText, delay);
- }
+ // 清空容器并设置初始可见度
+ $(selector).html('').css('opacity', 1);
+
+ // 创建所有字符元素,但初始透明度为0
+ for (let i = 0; i < text.length; i++) {
+ const charSpan = $('').text(text.charAt(i));
+ charSpan.css({
+ 'opacity': 0,
+ 'display': 'inline-block', // 确保每个字符是独立的块
+ 'transition': `opacity ${fadeStepTime}ms ease-in-out` // 平滑的透明度过渡效果
+ });
+ $(selector).append(charSpan);
}
- typeWriter();
+
+ // 获取所有字符span元素
+ const chars = $(selector).find('span');
+ const totalChars = chars.length;
+
+ // 设置初始不透明度,形成从左到右的渐变效果
+ // 左侧字符较深,右侧字符较浅
+ const setInitialOpacity = () => {
+ for (let i = 0; i < totalChars; i++) {
+ // 计算不透明度:第一个字符不透明度最高,往后逐渐降低
+ let position = i / totalChars; // 字符在文本中的相对位置 (0-1)
+ let opacity = Math.max(0, 0.8 - (position * fadeChars * 0.2));
+
+ // 超出可见范围的字符完全透明
+ if (i >= fadeChars) {
+ opacity = 0;
+ }
+
+ // 设置不透明度
+ $(chars[i]).css('opacity', opacity);
+ }
+ };
+
+ // 执行渐变动画
+ const animateText = () => {
+ let progress = 0;
+
+ const step = () => {
+ if (progress >= 1) {
+ // 完成动画
+ chars.css('opacity', 1);
+ isAnimating = false;
+ if (callback) callback();
+
+ // 等待随机4-7秒后请求下一条文本
+ const delay = Math.floor(Math.random() * 3000) + 4000; // 4000-7000毫秒
+ console.log(`文本显示完成,将在${delay/1000}秒后请求下一条文本`);
+ setTimeout(requestNextText, delay);
+ return;
+ }
+
+ // 更新每个字符的透明度
+ for (let i = 0; i < totalChars; i++) {
+ let targetOpacity = 1; // 目标不透明度(完全显示)
+ let currentPosition = i / totalChars; // 字符的相对位置
+
+ // 计算当前动画进度下该位置的字符应有的透明度
+ // 小于当前进度的字符应该更不透明
+ let opacity = targetOpacity * Math.max(0, 1 - Math.max(0, (currentPosition - progress) * 3));
+
+ // 设置不透明度
+ $(chars[i]).css('opacity', opacity);
+ }
+
+ // 增加进度
+ progress += 0.02;
+
+ // 继续下一步动画
+ setTimeout(step, speed / 50);
+ };
+
+ // 开始动画
+ step();
+ };
+
+ // 设置初始不透明度并开始动画
+ setInitialOpacity();
+ setTimeout(animateText, 50);
}
/**
@@ -422,6 +483,17 @@
CONFIG.rightContainer.fontStyle = newConfig.rightContainer.fontStyle || CONFIG.rightContainer.fontStyle;
CONFIG.rightContainer.typewriterSpeed = newConfig.rightContainer.typewriterSpeed || CONFIG.rightContainer.typewriterSpeed;
+ // 更新渐显效果相关的配置
+ if (newConfig.rightContainer.fadeChars !== undefined) {
+ CONFIG.rightContainer.fadeChars = newConfig.rightContainer.fadeChars;
+ }
+ if (newConfig.rightContainer.fadeStepTime !== undefined) {
+ CONFIG.rightContainer.fadeStepTime = newConfig.rightContainer.fadeStepTime;
+ }
+ if (newConfig.rightContainer.fadeDelayFactor !== undefined) {
+ CONFIG.rightContainer.fadeDelayFactor = newConfig.rightContainer.fadeDelayFactor;
+ }
+
// 应用到CSS
document.documentElement.style.setProperty('--right-font-size', CONFIG.rightContainer.fontSize);
document.documentElement.style.setProperty('--right-font-weight', CONFIG.rightContainer.fontWeight);
diff --git a/ShengShengBuXi/config/display.json b/ShengShengBuXi/config/display.json
index f89ff55..4c90e81 100644
--- a/ShengShengBuXi/config/display.json
+++ b/ShengShengBuXi/config/display.json
@@ -8,7 +8,10 @@
"fontSize": "40px",
"fontWeight": "bolder",
"fontStyle": "italic",
- "typewriterSpeed": 500
+ "typewriterSpeed": 6000,
+ "fadeChars": 1,
+ "fadeStepTime": 1000,
+ "fadeDelayFactor": 0.20
},
"waterEffect": {
"enabled": true,