SpectrumRenderer.ets 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. /**
  2. * 频谱渲染器接口
  3. * 所有频谱特效均需实现此接口
  4. */
  5. export interface SpectrumRenderer {
  6. /**
  7. * 渲染一帧频谱动画
  8. * @param ctx Canvas 2D 渲染上下文
  9. * @param bands 频段幅值数组 (0.0 ~ 1.0)
  10. * @param energy 总体能量值 (0.0 ~ 1.0)
  11. * @param width Canvas 宽度
  12. * @param height Canvas 高度
  13. * @param brandColor 主题品牌色 (hex string, e.g. '#FF0050')
  14. */
  15. render(ctx: CanvasRenderingContext2D, bands: number[], energy: number,
  16. width: number, height: number, brandColor: string, options?: SpectrumRenderOptions): void
  17. /**
  18. * 重置渲染器状态
  19. */
  20. reset(): void
  21. }
  22. export interface SpectrumRenderOptions {
  23. complexityScale: number
  24. }
  25. export interface SpectrumPalette {
  26. primaryHue: number
  27. warmHue: number
  28. coolHue: number
  29. complementaryHue: number
  30. accentHue: number
  31. }
  32. /**
  33. * 规范化 hex 颜色字符串,异常时回退到默认值
  34. */
  35. export function normalizeHexColor(color: string, fallback: string = '#FF0050'): string {
  36. if (!color || color.length < 7 || color[0] !== '#') {
  37. return fallback
  38. }
  39. const hexBody: string = color.substring(1, 7)
  40. const valid: boolean = /^[0-9a-fA-F]{6}$/.test(hexBody)
  41. return valid ? '#' + hexBody : fallback
  42. }
  43. /**
  44. * 基于品牌色构建频谱调色板(HSL 色相偏移)
  45. */
  46. export function buildSpectrumPalette(brandColor: string): SpectrumPalette {
  47. const baseColor: string = normalizeHexColor(brandColor)
  48. const baseHue: number = hexToHue(baseColor)
  49. return {
  50. primaryHue: baseHue,
  51. warmHue: (baseHue - 28 + 360) % 360,
  52. coolHue: (baseHue + 28) % 360,
  53. complementaryHue: (baseHue + 180) % 360,
  54. accentHue: (baseHue + 55) % 360
  55. }
  56. }
  57. /**
  58. * 生成 rgba 字符串,便于 Canvas 透明度控制
  59. */
  60. export function rgbaFromHex(hex: string, alpha: number): string {
  61. const safeHex: string = normalizeHexColor(hex)
  62. const safeAlpha: number = Math.max(0, Math.min(1, alpha))
  63. const r: number = parseInt(safeHex.substring(1, 3), 16)
  64. const g: number = parseInt(safeHex.substring(3, 5), 16)
  65. const b: number = parseInt(safeHex.substring(5, 7), 16)
  66. return `rgba(${r}, ${g}, ${b}, ${safeAlpha})`
  67. }
  68. /**
  69. * HSL 转 RGB hex 字符串
  70. */
  71. export function hslToHex(h: number, s: number, l: number): string {
  72. const hNorm: number = ((h % 360) + 360) % 360
  73. const sNorm: number = Math.max(0, Math.min(1, s))
  74. const lNorm: number = Math.max(0, Math.min(1, l))
  75. const c: number = (1 - Math.abs(2 * lNorm - 1)) * sNorm
  76. const x: number = c * (1 - Math.abs((hNorm / 60) % 2 - 1))
  77. const m: number = lNorm - c / 2
  78. let r: number = 0
  79. let g: number = 0
  80. let b: number = 0
  81. if (hNorm < 60) { r = c; g = x; b = 0 }
  82. else if (hNorm < 120) { r = x; g = c; b = 0 }
  83. else if (hNorm < 180) { r = 0; g = c; b = x }
  84. else if (hNorm < 240) { r = 0; g = x; b = c }
  85. else if (hNorm < 300) { r = x; g = 0; b = c }
  86. else { r = c; g = 0; b = x }
  87. const ri: number = Math.round((r + m) * 255)
  88. const gi: number = Math.round((g + m) * 255)
  89. const bi: number = Math.round((b + m) * 255)
  90. const toHex = (v: number): string => {
  91. const hex: string = v.toString(16)
  92. return hex.length < 2 ? '0' + hex : hex
  93. }
  94. return '#' + toHex(ri) + toHex(gi) + toHex(bi)
  95. }
  96. /**
  97. * 从 hex 字符串解析 HSL 色相
  98. */
  99. export function hexToHue(hex: string): number {
  100. let r: number = 0
  101. let g: number = 0
  102. let b: number = 0
  103. if (hex.length >= 7) {
  104. r = parseInt(hex.substring(1, 3), 16) / 255
  105. g = parseInt(hex.substring(3, 5), 16) / 255
  106. b = parseInt(hex.substring(5, 7), 16) / 255
  107. }
  108. const max: number = Math.max(r, g, b)
  109. const min: number = Math.min(r, g, b)
  110. const d: number = max - min
  111. let h: number = 0
  112. if (d !== 0) {
  113. if (max === r) h = ((g - b) / d) % 6
  114. else if (max === g) h = (b - r) / d + 2
  115. else h = (r - g) / d + 4
  116. h *= 60
  117. if (h < 0) h += 360
  118. }
  119. return h
  120. }