From 71db19a4bec611229529f64dda86deec6aeee7ad Mon Sep 17 00:00:00 2001 From: zpc Date: Sat, 29 Mar 2025 01:05:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/PhoneBoothConfig.cs | 35 +++ ShengShengBuXi.ConsoleApp/README.md | 25 +- .../Services/PhoneBoothService.cs | 289 +++++++++++++++++- ShengShengBuXi.ConsoleApp/appsettings.json | 2 +- ShengShengBuXi.ConsoleApp/config.json | 15 +- ShengShengBuXi/Pages/Index.cshtml | 132 ++++++-- ShengShengBuXi/config/display.json | 5 +- 7 files changed, 449 insertions(+), 54 deletions(-) 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,