Преглед изворни кода

频谱的可以在播放器设置类型和开关

onecold пре 6 месеци
родитељ
комит
c9fc556a85

+ 85 - 30
entry/src/main/ets/view/LocalMusic.ets

@@ -1,5 +1,5 @@
 import TitleBar from './TitleBar'
-import { curves, display, MenuModifier,PiPWindow, promptAction, router, SymbolGlyphModifier, window } from '@kit.ArkUI';
+import { curves, display, MenuModifier, PiPWindow, promptAction, router, SymbolGlyphModifier, window } from '@kit.ArkUI';
 import { VideoItem } from '../viewmodel/VideoItem';
 import {  LengthMetrics, SegmentButton,SegmentButtonOptions } from '@kit.ArkUI';
 import { getWebdavVideoItems, getWebdavCurrentPlayIndex } from '../pages/WebDavMainPage';
@@ -1206,6 +1206,8 @@ export struct LocalMusic {
     context.getApplicationContext().setColorMode(colorMode)
   }
   initSetting() {
+    this.isShowSpectrum  = PreferencesUtil.getBooleanSync('isShowSpectrum', false)
+    this.spectrumModeIndex = PreferencesUtil.getNumberSync('spectrumModeIndex', 0)
     this.enablePointLight = PreferencesUtil.getBooleanSync('enablePointLight', true)
     AppStorage.setOrCreate('EnablePointLight', this.enablePointLight);
     this.showSimple = PreferencesUtil.getBooleanSync('showSimple', true)
@@ -1829,10 +1831,7 @@ export struct LocalMusic {
     this.historyPath = this.rootPath + '/' + LocalMusic.STR_HISTORY_MUSIC
     Logger.info('onecold rootPath = ' + this.rootPath)
 
-    Logger.info('onecold lockPath = ' + this.lockPath)
-    if (!FileUtil.accessSync(this.lockPath)) {
-      FileUtil.mkdirSync(this.lockPath)
-    }
+
     if (!FileUtil.accessSync(this.favPath)) {
       FileUtil.mkdirSync(this.favPath)
     }
@@ -10771,6 +10770,9 @@ export struct LocalMusic {
 
         if (this.isPhoneLan()) {
           Column() {
+            if (this.isShowSpectrum) {
+              this.InlineSpectrumView()
+            }
             this.playProgressView()
           }
           .margin({ bottom: 32, right: this.currentLyricAlignMode == 0 ? 0 : 30 })
@@ -10807,6 +10809,9 @@ export struct LocalMusic {
   @Builder
   BottomControl() {
     Column() {
+      if (this.isShowSpectrum) {
+        this.InlineSpectrumView()
+      }
       this.TopPlayProgressView()
       //播放 暂停,下一首,上一首
       this.playCenterView()
@@ -11401,16 +11406,8 @@ export struct LocalMusic {
       })
 
 
-      if (this.isPhonePortrait()  ) {
-        Stack(){
-          if (true) {
-            this.InlineSpectrumView()
-          }
-          if (this.isShowSingleLineLyric) {
-            this.SingleLyricView()
-          }
-        }
-
+      if (this.isShowSingleLineLyric&&this.isPhonePortrait()) {
+        this.SingleLyricView()
       }
       if (this.isPhoneLan()) {
         Column() {
@@ -12820,14 +12817,25 @@ export struct LocalMusic {
                   .id('AVCastPicker')
 
               } else {
-                Image(more.image)
-                  .width(24)
-                  .height(24)
-                  .fillColor(Color.White)
-                  .margin({ left: 10, right: 10 })
-                  .onClick(() => {
-                    this.doMore(more.id)
-                  })
+                if(more.id >= 22){
+                  SymbolGlyph(more.image)
+                    .fontSize(24)
+                    .fontColor([Color.White])
+                    .alignSelf(ItemAlign.Center)
+                    .margin({ left: 10, right: 10 })
+                    .onClick(() => {
+                      this.doMore(more.id)
+                    })
+                }else{
+                  Image(more.image)
+                    .width(24)
+                    .height(24)
+                    .fillColor(Color.White)
+                    .margin({ left: 10, right: 10 })
+                    .onClick(() => {
+                      this.doMore(more.id)
+                    })
+                }
                 Text(more.id == 17 ? (this.isLandscape ? '竖屏模式' : '横屏模式') : more.title)
                   .fontSize(16)
 
@@ -12964,6 +12972,49 @@ export struct LocalMusic {
                   .width(48)
                   .height(24);
 
+                Toggle({ type: ToggleType.Switch, isOn: this.isShowSpectrum })
+                  .selectedColor(this.themeColor)
+                  .switchPointColor(Color.White)
+                  .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 })
+                  .margin({ right: 15 })
+                  .visibility(more.id == 25 ? Visibility.Visible : Visibility.None)
+                  .onChange((checked: boolean) => { // 选择开关状态变化时触发事件
+                    if (more.id == 25) {
+                      this.isShowSpectrum = checked;
+                      PreferencesUtil.put("isShowSpectrum", this.isShowSpectrum)
+                    }
+
+                  })
+                  .width(48)
+                  .height(24);
+
+                // 频谱特效类型选择器
+                Select([
+                  { value: '柱状' },
+                  { value: '波形' },
+                  { value: '圆形' },
+                  { value: '粒子' },
+                  { value: '脉冲' },
+                  { value: '镜像柱' },
+                  { value: '影子柱' },
+                  { value: '轨道线' },
+                  { value: '点阵' },
+                  { value: '星爆' }
+                ])
+                  .font({ size: 14, weight: FontWeight.Medium })
+                  .fontColor(Color.White)
+                  .margin({ left: 25 })
+                  .visibility(more.id === 26 ? Visibility.Visible : Visibility.None)
+                  .selected(this.spectrumModeIndex)
+                  .value(['柱状', '波形', '圆形', '粒子', '脉冲', '镜像柱', '影子柱', '轨道线', '点阵', '星爆'][this.spectrumModeIndex])
+                  .onSelect(async (index: number, _text?: string) => {
+                    this.spectrumModeIndex = index;
+                    PreferencesUtil.put('spectrumModeIndex', this.spectrumModeIndex);
+                    AppStorage.setOrCreate<number>('spectrumMode', this.spectrumModeIndex);
+                    const modeNames = ['柱状', '波形', '圆形', '粒子', '脉冲', '镜像柱', '影子柱', '轨道线', '点阵', '星爆'];
+                    Logger.info('heanup-LocalMusic', '频谱特效已切换为: ' + modeNames[index]);
+                  });
+
                 Toggle({ type: ToggleType.Switch, isOn: this.showSimple })
                   .selectedColor(this.themeColor)
                   .switchPointColor(Color.White)
@@ -13056,12 +13107,13 @@ export struct LocalMusic {
     .justifyContent(FlexAlign.Start)
   }
 
+
   private moreItems: MoreItem[] = [
     { id: 1, image: $r('app.media.speed'), title: '倍速' },
 
     { id: 2, image: $r('app.media.lyric'), title: '歌词' },
 
-    { id: 24, image: $r('app.media.equalizer'), title: '均衡器' },
+    { id: 24, image: $r('sys.symbol.slider_vertical_3'), title: '均衡器' },
 
     { id: 17, image: $r('app.media.full_4'), title: '横屏模式' },
 
@@ -13087,15 +13139,16 @@ export struct LocalMusic {
 
     { id: 9, image: $r('app.media.volume_white'), title: '调整音量' },
 
-
+    { id: 25, image: $r('sys.symbol.ranking'), title: '音乐频谱' },
+    { id: 26, image: $r('sys.symbol.eight_diagram'), title: '频谱类型' },
     // { id: 6, image: $r('app.media.memory'), title: '记忆播放' },
 
 
     { id: 13, image: $r('app.media.rectangle'), title: '封面方形' },
 
-    { id : 22, image: $r('app.media.skip'), title: '沉浸式' },
+    { id : 22, image: $r('sys.symbol.satellite_map'), title: '沉浸式' },
 
-    { id : 23, image: $r('app.media.more_do'), title: '点光源效果' },
+    { id : 23, image: $r('sys.symbol.lightbulb'), title: '点光源效果' },
 
     { id : 21, image: $r('app.media.kp_music'), title: '背景双边流光' },
 
@@ -13116,6 +13169,8 @@ export struct LocalMusic {
   @State isJumpSetting: boolean = false
   @State isShowEdit: boolean = false
   @State showPipLyric: boolean = false
+  @State isShowSpectrum: boolean = false//是否显示频谱
+  @State spectrumModeIndex: number = 0 //选中的频谱特效索引
 
   async setRingTone() {
     if (StrUtil.isEmpty(this.videoUrl) || StrUtil.isEmpty(this.name)) {
@@ -15302,9 +15357,9 @@ export struct LocalMusic {
       isVisible: this.currentSwiperIndex === 0,
       isPlaying: this.CONTROL_PlayStatus === PlayStatus.PLAY
     })
-      .width(this.isLandscape ? '82%' : '90%')
-      .height(this.isCoverOpacity() ? 120 : 150)
-      .margin({ top: 3, bottom: 12 })
+      .width( '84%')
+      .height(this.isCoverOpacity() ? 80 : 100)
+      .margin({bottom: 3 })
       .clip(true)
   }
 

+ 46 - 14
entry/src/main/ets/view/spectrum/BarSpectrumRenderer.ets

@@ -10,8 +10,31 @@ export class BarSpectrumRenderer implements SpectrumRenderer {
 
   render(ctx: CanvasRenderingContext2D, bands: number[], energy: number,
     width: number, height: number, brandColor: string, _options?: SpectrumRenderOptions): void {
-    const bandCount: number = bands.length
-    if (bandCount === 0) return
+    const originalBandCount: number = bands.length
+    if (originalBandCount === 0) return
+
+    // 根据宽度动态计算最佳柱子数量和尺寸
+    const minBarWidth: number = 3 // 最小柱子宽度
+    const maxBarWidth: number = 8 // 最大柱子宽度
+    const gap: number = 1.5 // 柱子之间的间隙
+
+    // 计算在这个宽度下能容纳多少个柱子
+    let optimalBarCount: number = Math.floor(width / (minBarWidth + gap))
+    optimalBarCount = Math.min(optimalBarCount, originalBandCount) // 不超过原始频段数
+    optimalBarCount = Math.max(optimalBarCount, 8) // 至少8个柱子
+
+    // 根据优化后的柱子数量计算实际柱子宽度
+    const actualBarWidth: number = Math.min(maxBarWidth, (width - gap * (optimalBarCount + 1)) / optimalBarCount)
+
+    // 重采样 bands 数据以匹配 optimalBarCount
+    const resampledBands: number[] = []
+    const step: number = originalBandCount / optimalBarCount
+    for (let i = 0; i < optimalBarCount; i++) {
+      const sourceIndex: number = Math.min(Math.floor(i * step), originalBandCount - 1)
+      resampledBands.push(bands[sourceIndex])
+    }
+
+    const bandCount: number = optimalBarCount
 
     // 初始化峰值数组
     if (this.peaks.length !== bandCount) {
@@ -26,10 +49,8 @@ export class BarSpectrumRenderer implements SpectrumRenderer {
     // 轻微拖影,接近参考图中的荧光残影(使用透明背景)
     ctx.clearRect(0, 0, width, height)
 
-    const gap: number = 2.2 // 恢复柱子之间的间隙
-    const barWidth: number = (width - gap * (bandCount + 1)) / bandCount
     const usableHeight: number = height * 0.80
-    const segmentHeight: number = Math.max(3, height * 0.015) // 增加单段高度
+    const segmentHeight: number = Math.max(2, height * 0.012) // 减小单段高度
     const segmentGap: number = 0 // 去掉间隙,让方格紧密相连
     const segmentCount: number = Math.max(10, Math.floor(usableHeight / (segmentHeight + segmentGap)))
     const palette = buildSpectrumPalette(brandColor)
@@ -37,10 +58,14 @@ export class BarSpectrumRenderer implements SpectrumRenderer {
 
     // 地平线已移除,保持透明背景
 
+    // 计算起始位置,使柱子居中
+    const totalContentWidth: number = bandCount * actualBarWidth + (bandCount - 1) * gap
+    const startX: number = (width - totalContentWidth) / 2
+
     for (let i = 0; i < bandCount; i++) {
-      const level: number = Math.max(0, Math.min(1, bands[i]))
+      const level: number = Math.max(0, Math.min(1, resampledBands[i]))
       const activeSegments: number = Math.floor(level * segmentCount)
-      const x: number = gap + i * (barWidth + gap)
+      const x: number = startX + i * (actualBarWidth + gap)
       const hue: number = (palette.primaryHue + (i / bandCount) * hueSpan) % 360
 
       for (let seg = 0; seg < segmentCount; seg++) {
@@ -57,11 +82,11 @@ export class BarSpectrumRenderer implements SpectrumRenderer {
         const alpha: number = 0.55 + level * 0.45
         ctx.globalAlpha = alpha
         ctx.fillStyle = hslToHex(hue, 0.86, lightness)
-        ctx.fillRect(x, segY, barWidth, segmentHeight)
+        ctx.fillRect(x, segY, actualBarWidth, segmentHeight)
       }
       ctx.globalAlpha = 1.0
 
-      // 峰值下落指示器(亮点
+      // 峰值下落指示器(使用与当前柱子相同的颜色
       if (level > this.peaks[i]) {
         this.peaks[i] = level
         this.peakVelocities[i] = 0
@@ -72,11 +97,18 @@ export class BarSpectrumRenderer implements SpectrumRenderer {
       }
 
       const peakSeg: number = Math.floor(this.peaks[i] * segmentCount)
-      const peakY: number = height - (peakSeg + 1) * (segmentHeight + segmentGap)
-      ctx.fillStyle = hslToHex(palette.accentHue, 0.95, 0.72)
-      ctx.globalAlpha = 0.95
-      ctx.fillRect(x, peakY, barWidth, segmentHeight + 1)
-      ctx.globalAlpha = 1.0
+      if (peakSeg >= 0) {
+        const peakY: number = height - (peakSeg + 1) * (segmentHeight + segmentGap)
+
+        // 使用当前柱子的颜色作为峰值指示器颜色
+        const currentPeakLightness: number = 0.34 + (this.peaks[i] || 0) * 0.34
+        const currentPeakColor: string = hslToHex(hue, 0.86, currentPeakLightness)
+
+        ctx.fillStyle = currentPeakColor
+        ctx.globalAlpha = 0.95
+        ctx.fillRect(x, peakY, actualBarWidth, segmentHeight + 1)
+        ctx.globalAlpha = 1.0
+      }
     }
   }
 

+ 1 - 4
entry/src/main/ets/view/spectrum/DotMatrixSpectrumRenderer.ets

@@ -29,7 +29,7 @@ export class DotMatrixSpectrumRenderer implements SpectrumRenderer {
         const lit: boolean = r >= rows - activeRows
         const x: number = c * cellW + cellW * 0.5
         const y: number = r * cellH + cellH * 0.5
-        const alpha: number = lit ? (0.30 + value * 0.65) : 0.08
+        const alpha: number = lit ? (0.30 + value * 0.65) : 0 // 未点亮的方格完全透明
         const lightness: number = lit ? (0.45 + 0.28 * (r / rows)) : 0.22
         ctx.fillStyle = rgbaFromHex(hslToHex(hue, 0.82, lightness), alpha)
         ctx.beginPath()
@@ -37,9 +37,6 @@ export class DotMatrixSpectrumRenderer implements SpectrumRenderer {
         ctx.fill()
       }
     }
-
-    ctx.fillStyle = rgbaFromHex(hslToHex(palette.accentHue, 0.88, 0.60), 0.10 + energy * 0.18)
-    ctx.fillRect(0, height * 0.48, width, 2)
   }
 
   reset(): void {

+ 173 - 0
entry/src/main/ets/view/spectrum/ShadowBarSpectrumRenderer.ets

@@ -0,0 +1,173 @@
+import { SpectrumRenderer, SpectrumRenderOptions, buildSpectrumPalette, hslToHex, rgbaFromHex } from './SpectrumRenderer'
+
+/**
+ * 影子柱状频谱渲染器
+ * 每个柱子都带有一个影子柱子,具有峰值下落指示器
+ */
+export class ShadowBarSpectrumRenderer implements SpectrumRenderer {
+  private peaks: number[] = []
+  private peakVelocities: number[] = []
+  private reflectionPeaks: number[] = []
+  private reflectionPeakVelocities: number[] = []
+
+  render(ctx: CanvasRenderingContext2D, bands: number[], energy: number,
+    width: number, height: number, brandColor: string, _options?: SpectrumRenderOptions): void {
+    const originalBandCount: number = bands.length
+    if (originalBandCount === 0) return
+
+    // 根据宽度动态计算最佳柱子数量和尺寸
+    const targetBarWidth: number = 2.5 // 目标柱子宽度
+    const gap: number = 1.5 // 柱子之间的间隙
+
+    // 计算在这个宽度下能容纳多少个柱子
+    let optimalBarCount: number = Math.floor(width / (targetBarWidth + gap))
+    optimalBarCount = Math.min(optimalBarCount, originalBandCount) // 不超过原始频段数
+    optimalBarCount = Math.max(optimalBarCount, 16) // 至少16个柱子,增加柱子数量
+
+    // 根据优化后的柱子数量计算实际柱子宽度
+    const actualBarWidth: number = (width - gap * (optimalBarCount + 1)) / optimalBarCount
+
+    // 重采样 bands 数据以匹配 optimalBarCount
+    const resampledBands: number[] = []
+    const step: number = originalBandCount / optimalBarCount
+    for (let i = 0; i < optimalBarCount; i++) {
+      const sourceIndex: number = Math.min(Math.floor(i * step), originalBandCount - 1)
+      resampledBands.push(bands[sourceIndex])
+    }
+
+    const bandCount: number = optimalBarCount
+
+    // 初始化峰值数组
+    if (this.peaks.length !== bandCount) {
+      this.peaks = new Array<number>(bandCount).fill(0)
+      this.peakVelocities = new Array<number>(bandCount).fill(0)
+      this.reflectionPeaks = new Array<number>(bandCount).fill(0)
+      this.reflectionPeakVelocities = new Array<number>(bandCount).fill(0)
+    }
+
+    // 清空画布
+    ctx.clearRect(0, 0, width, height)
+
+    const mainBarUsableHeight: number = height * 0.7 // 主柱子使用55%的高度
+    const segmentHeight: number = Math.max(2, height * 0.012)
+    const segmentGap: number = 0
+    const segmentCount: number = Math.max(10, Math.floor(mainBarUsableHeight / (segmentHeight + segmentGap)))
+    const palette = buildSpectrumPalette(brandColor)
+    const hueSpan: number = (palette.complementaryHue + 360 - palette.primaryHue) % 360
+
+    // 计算起始位置,使柱子居中
+    const totalContentWidth: number = bandCount * actualBarWidth + (bandCount - 1) * gap
+    const startX: number = (width - totalContentWidth) / 2
+
+    // 计算主柱子的底部Y坐标(留出空间给倒影)
+    const mainBarBottomY: number = height * 0.55
+
+    for (let i = 0; i < bandCount; i++) {
+      const level: number = Math.max(0, Math.min(1, resampledBands[i]))
+      const activeSegments: number = Math.floor(level * segmentCount)
+      const x: number = startX + i * (actualBarWidth + gap)
+      const hue: number = (palette.primaryHue + (i / bandCount) * hueSpan) % 360
+
+      // 绘制主柱子(分段式,从 mainBarBottomY 向上绘制)
+      for (let seg = 0; seg < segmentCount; seg++) {
+        const segY: number = mainBarBottomY - (seg + 1) * (segmentHeight + segmentGap)
+        const lit: boolean = seg < activeSegments
+
+        if (!lit) {
+          continue
+        }
+
+        const t: number = seg / segmentCount
+        const lightness: number = 0.34 + t * 0.34
+        ctx.globalAlpha = 0.7 // 主柱透明度固定0.7
+        ctx.fillStyle = hslToHex(hue, 0.86, lightness)
+        ctx.fillRect(x, segY, actualBarWidth, segmentHeight)
+      }
+      ctx.globalAlpha = 1.0
+
+      // 绘制倒影(在主柱子下方,透明度0.4,宽度与主柱子相同,高度为主柱子的50%)
+      const reflectionSegmentCount: number = Math.floor(activeSegments * 0.5)
+      const reflectionStartY: number = mainBarBottomY + 2 // 从主柱子底部下方2像素开始
+
+      for (let seg = 0; seg < activeSegments; seg++) {
+        // 倒影只绘制前 50% 的段
+        if (seg >= reflectionSegmentCount) {
+          break
+        }
+
+        // 倒影从主柱子底部继续向下延伸
+        const segY: number = reflectionStartY + seg * (segmentHeight + segmentGap)
+
+        // 倒影颜色与主柱完全对称
+        // 倒影第0段(最靠近主柱)对应主柱最底段,颜色最暗
+        // 倒影第 reflectionSegmentCount-1 段(最远)对应主柱较高段,颜色较亮
+        const mainBarCorrespondingSeg: number = activeSegments - 1 - seg
+        const t: number = Math.max(0, mainBarCorrespondingSeg / segmentCount)
+        const lightness: number = 0.34 + t * 0.34
+
+        // 倒影透明度固定0.4
+        ctx.globalAlpha = 0.4
+        ctx.fillStyle = hslToHex(hue, 0.86, lightness)
+        ctx.fillRect(x, segY, actualBarWidth, segmentHeight)
+      }
+      ctx.globalAlpha = 1.0
+
+      // 倒影峰值下落指示器
+      // 倒影的峰值应该映射到倒影段数,而不是主柱段数
+      if (level > this.reflectionPeaks[i]) {
+        this.reflectionPeaks[i] = level
+        this.reflectionPeakVelocities[i] = 0
+      } else {
+        this.reflectionPeakVelocities[i] += 0.0018
+        this.reflectionPeaks[i] -= this.reflectionPeakVelocities[i]
+        if (this.reflectionPeaks[i] < 0) this.reflectionPeaks[i] = 0
+      }
+
+      const reflectionPeakSeg: number = Math.floor(this.reflectionPeaks[i] * reflectionSegmentCount)
+      if (reflectionPeakSeg >= 0 && reflectionPeakSeg < reflectionSegmentCount) {
+        // 倒影峰值从顶部开始,向下递增位置(与主柱相反)
+        const reflectionPeakY: number = reflectionStartY + reflectionPeakSeg * (segmentHeight + segmentGap)
+
+        // 使用当前柱子的颜色作为倒影峰值指示器颜色
+        const reflectionPeakLightness: number = 0.34 + (this.reflectionPeaks[i] || 0) * 0.34
+        const reflectionPeakColor: string = hslToHex(hue, 0.86, reflectionPeakLightness)
+
+        ctx.fillStyle = reflectionPeakColor
+        ctx.globalAlpha = 0.4 // 倒影峰值指示器透明度与倒影一致
+        ctx.fillRect(x, reflectionPeakY, actualBarWidth, segmentHeight + 1)
+        ctx.globalAlpha = 1.0
+      }
+
+      // 主柱峰值下落指示器(使用与当前柱子相同的颜色)
+      if (level > this.peaks[i]) {
+        this.peaks[i] = level
+        this.peakVelocities[i] = 0
+      } else {
+        this.peakVelocities[i] += 0.0018
+        this.peaks[i] -= this.peakVelocities[i]
+        if (this.peaks[i] < 0) this.peaks[i] = 0
+      }
+
+      const peakSeg: number = Math.floor(this.peaks[i] * segmentCount)
+      if (peakSeg >= 0) {
+        const peakY: number = mainBarBottomY - (peakSeg + 1) * (segmentHeight + segmentGap)
+
+        // 使用当前柱子的颜色作为峰值指示器颜色
+        const currentPeakLightness: number = 0.34 + (this.peaks[i] || 0) * 0.34
+        const currentPeakColor: string = hslToHex(hue, 0.86, currentPeakLightness)
+
+        ctx.fillStyle = currentPeakColor
+        ctx.globalAlpha = 0.7 // 主柱峰值指示器透明度与主柱一致
+        ctx.fillRect(x, peakY, actualBarWidth, segmentHeight + 1)
+        ctx.globalAlpha = 1.0
+      }
+    }
+  }
+
+  reset(): void {
+    this.peaks = []
+    this.peakVelocities = []
+    this.reflectionPeaks = []
+    this.reflectionPeakVelocities = []
+  }
+}

+ 8 - 0
entry/src/main/ets/view/spectrum/SpectrumEffectRegistry.ets

@@ -5,6 +5,7 @@ import { CircleSpectrumRenderer } from './CircleSpectrumRenderer'
 import { ParticleSpectrumRenderer } from './ParticleSpectrumRenderer'
 import { RadialPulseSpectrumRenderer } from './RadialPulseSpectrumRenderer'
 import { MirrorBarSpectrumRenderer } from './MirrorBarSpectrumRenderer'
+import { ShadowBarSpectrumRenderer } from './ShadowBarSpectrumRenderer'
 import { OrbitLineSpectrumRenderer } from './OrbitLineSpectrumRenderer'
 import { DotMatrixSpectrumRenderer } from './DotMatrixSpectrumRenderer'
 import { StarburstSpectrumRenderer } from './StarburstSpectrumRenderer'
@@ -69,6 +70,13 @@ const EFFECTS: SpectrumEffectDefinition[] = [
     performance: { targetFps: 30, minFps: 20, maxBandCount: 30, complexityFloor: 0.68 },
     createRenderer: (): SpectrumRenderer => new MirrorBarSpectrumRenderer()
   },
+  {
+    effectId: 'shadow-bar',
+    displayName: '影子柱',
+    performanceLevel: 'medium',
+    performance: { targetFps: 30, minFps: 18, maxBandCount: 32, complexityFloor: 0.60 },
+    createRenderer: (): SpectrumRenderer => new ShadowBarSpectrumRenderer()
+  },
   {
     effectId: 'orbit-line',
     displayName: '轨道线',

+ 52 - 21
entry/src/main/ets/view/spectrum/SpectrumView.ets

@@ -14,7 +14,7 @@ import { normalizeHexColor } from './SpectrumRenderer'
 import Logger from '../../common/util/Logger'
 
 const TAG: string = 'heanup-SpectrumView'
-const BAND_COUNT: number = 32
+const BAND_COUNT: number = 48 // 增加频段数量,从32增加到48
 const PREFERENCE_SELECTED_EFFECT: string = 'selectedSpectrumEffectId'
 
 @Component
@@ -22,6 +22,7 @@ export struct SpectrumView {
   @Prop @Watch('onPlayingChange') isPlaying: boolean = false
   @StorageProp('themeColor') themeColor: string = '#FF0050'
   @Prop @Watch('onVisibilityChange') isVisible: boolean = false
+  @StorageProp('spectrumMode') @Watch('onSpectrumModeChange') spectrumMode: number = 0
 
   @State private selectedEffectId: string = getDefaultSpectrumEffectId()
   @State private canvasWidth: number = 0
@@ -47,6 +48,16 @@ export struct SpectrumView {
     this.updateRenderingState()
   }
 
+  onSpectrumModeChange(): void {
+    const effects = getSpectrumEffects();
+    if (this.spectrumMode >= 0 && this.spectrumMode < effects.length) {
+      const targetEffectId = effects[this.spectrumMode].effectId;
+      if (targetEffectId !== this.selectedEffectId) {
+        this.switchEffect(targetEffectId);
+      }
+    }
+  }
+
   aboutToAppear(): void {
     this.ensureRenderersReady()
     this.restoreSelectedEffect()
@@ -70,15 +81,32 @@ export struct SpectrumView {
   }
 
   private restoreSelectedEffect(): void {
+    // 首先尝试从 Preferences 读取保存的索引
+    const storedIndex: number = PreferencesUtil.getNumberSync('spectrumModeIndex', -1)
+    if (storedIndex >= 0 && storedIndex < this.effects.length) {
+      this.selectedEffectId = this.effects[storedIndex].effectId
+      Logger.info(TAG, 'Restored spectrum mode from preferences: ' + storedIndex + ' (' + this.selectedEffectId + ')')
+      return
+    }
+
+    // 其次尝试从 AppStorage 读取 spectrumMode
+    if (this.spectrumMode >= 0 && this.spectrumMode < this.effects.length) {
+      this.selectedEffectId = this.effects[this.spectrumMode].effectId
+      Logger.info(TAG, 'Restored spectrum mode from AppStorage: ' + this.spectrumMode + ' (' + this.selectedEffectId + ')')
+      return
+    }
+
+    // 再次尝试从 Preferences 读取 effectId
     const storedEffectId: string = PreferencesUtil.getStringSync(PREFERENCE_SELECTED_EFFECT, '')
     if (storedEffectId) {
       this.selectedEffectId = normalizeSpectrumEffectId(storedEffectId)
+      Logger.info(TAG, 'Restored spectrum effectId from preferences: ' + this.selectedEffectId)
       return
     }
 
-    const legacyMode: number | undefined = AppStorage.get<number>('spectrumMode')
-    this.selectedEffectId = fromLegacySpectrumMode(legacyMode)
-    PreferencesUtil.putSync(PREFERENCE_SELECTED_EFFECT, this.selectedEffectId)
+    // 使用默认值
+    this.selectedEffectId = getDefaultSpectrumEffectId()
+    Logger.info(TAG, 'Using default spectrum effect: ' + this.selectedEffectId)
   }
 
   private startRendering(): void {
@@ -221,7 +249,10 @@ export struct SpectrumView {
         break
       }
     }
+    // 同时保存到 AppStorage 和 Preferences
     AppStorage.setOrCreate<number>('spectrumMode', modeIndex)
+    PreferencesUtil.put('spectrumModeIndex', modeIndex)
+    Logger.info(TAG, 'Persisted spectrum mode index: ' + modeIndex)
   }
 
   private switchEffect(effectId: string): void {
@@ -278,23 +309,23 @@ export struct SpectrumView {
           this.canvasHeight = newVal.height as number
         })
 
-      Row() {
-        SymbolGlyph($r('sys.symbol.music'))
-          .fontSize(14)
-          .fontColor([$r('app.color.brand')])
-        Text(this.getCurrentEffectName())
-          .fontSize(12)
-          .fontColor($r('app.color.brand'))
-          .margin({ left: 4 })
-      }
-      .padding({ top: 5, bottom: 5, left: 10, right: 10 })
-      .borderRadius(16)
-      .backgroundColor(Color.Transparent)
-      .shadow({ radius: 6, color: 'rgba(0, 0, 0, 0.45)', offsetX: 0, offsetY: 0 })
-      .margin({ top: 8, right: 8 })
-      .onClick(() => {
-        this.nextEffect()
-      })
+      // Row() {
+      //   SymbolGlyph($r('sys.symbol.music'))
+      //     .fontSize(14)
+      //     .fontColor([$r('app.color.brand')])
+      //   Text(this.getCurrentEffectName())
+      //     .fontSize(12)
+      //     .fontColor($r('app.color.brand'))
+      //     .margin({ left: 4 })
+      // }
+      // .padding({ top: 5, bottom: 5, left: 10, right: 10 })
+      // .borderRadius(16)
+      // .backgroundColor(Color.Transparent)
+      // .shadow({ radius: 6, color: 'rgba(0, 0, 0, 0.45)', offsetX: 0, offsetY: 0 })
+      // .margin({ top: 8, right: 8 })
+      // .onClick(() => {
+      //   this.nextEffect()
+      // })
     }
     .width('100%')
     .height('100%')