diff --git a/src/components/EffortPanel/__tests__/rippleAnimation.test.ts b/src/components/EffortPanel/__tests__/rippleAnimation.test.ts index 203a944b2..6de044b0c 100644 --- a/src/components/EffortPanel/__tests__/rippleAnimation.test.ts +++ b/src/components/EffortPanel/__tests__/rippleAnimation.test.ts @@ -10,25 +10,20 @@ import { } from '../rippleAnimation.js' describe('intensityToColor', () => { - test('intensity=0 → transparent', () => { - expect(intensityToColor(0)).toBe(TRANSPARENT) + test('intensity=0 → 最暗档(不再是 transparent,作面板底色)', () => { + expect(intensityToColor(0)).toBe('#0a0d1a') }) - test('intensity < 0 钳到 0 → transparent', () => { - expect(intensityToColor(-0.5)).toBe(TRANSPARENT) + test('intensity < 0 钳到 0 → 最暗档', () => { + expect(intensityToColor(-0.5)).toBe('#0a0d1a') }) - test('intensity ≤ 0.1 → transparent(边缘自然消失)', () => { - expect(intensityToColor(0.05)).toBe(TRANSPARENT) - expect(intensityToColor(0.1)).toBe(TRANSPARENT) - }) - - test('intensity > 0.1 → 非透明颜色字符串', () => { - const c = intensityToColor(0.5) - expect(c).not.toBe(TRANSPARENT) - expect(typeof c).toBe('string') - // 紫蓝色调(#hex 格式) - expect(c).toMatch(/^#[0-9a-fA-F]{6}$/) + test('intensity > 0 → 永远是 #hex 颜色字符串(不返回 transparent)', () => { + for (const v of [0.05, 0.1, 0.2, 0.5, 0.8]) { + const c = intensityToColor(v) + expect(c).not.toBe(TRANSPARENT) + expect(c).toMatch(/^#[0-9a-fA-F]{6}$/) + } }) test('intensity > 1 钳到 1 → 最高强度颜色', () => { @@ -42,9 +37,8 @@ describe('intensityToColor', () => { expect(unique.size).toBeGreaterThanOrEqual(3) }) - test('intensity=1 → 高光档(不是 suggestion)', () => { - // 最高档应为 #8aa0ff(高光),区别于 #5769F7(suggestion) - expect(intensityToColor(1)).toBe('#8aa0ff') + test('intensity=1 → suggestion 档(波峰最高档)', () => { + expect(intensityToColor(1)).toBe('#5769F7') }) }) @@ -101,7 +95,7 @@ describe('computeRippleCells', () => { ).toEqual([]) }) - test('震源点处颜色非 transparent(dist=0,falloff=1)', () => { + test('震源点处颜色为最亮档(dist=0,falloff=1,intensity 高)', () => { const cells = computeRippleCells({ y: 5, width: 11, @@ -109,21 +103,37 @@ describe('computeRippleCells', () => { sourceX: 5, sourceY: 5, }) - // 震源在 (5,5),y=5 行的第 5 列 cell 应非 transparent - expect(cells[5].color).not.toBe(TRANSPARENT) + // 震源在 (5,5),dist=0,falloff=1,dist<6 触发高频涟漪叠加 + // 波峰附近颜色应较高档(非最暗) + expect(cells[5].color).not.toBe('#0a0d1a') }) - test('远离震源的 cell 更可能是 transparent(远端衰减)', () => { - // 震源在左端,远端 50 列外,强度低 → 大概率 transparent + test('覆盖半径扩大:dist=65(左侧远端)仍有非最暗颜色', () => { + // 震源 x=65,远端 x=0 → dist=65 + // falloff = max(0, 1 - 65/90) = 0.278,波峰时 intensity ≈ 0.278 + // 应映射到非最暗档(#15182b 或更亮) const cells = computeRippleCells({ y: 0, - width: 50, + width: 66, time: 0, - sourceX: 0, + sourceX: 65, sourceY: 0, }) - // 远端第 49 列应为 transparent(falloff = max(0, 1-49/40) = 0) - expect(cells[49].color).toBe(TRANSPARENT) + // 第 0 列 dist=65,time=0 时 phase = 65*0.35 = 22.75 rad + // sin(22.75) ≈ -0.59 → wave = 0 → intensity = 0 → 最暗档 + // 但 time 推进时波峰会扫过此处,强度变高 + // 这里只验证 cell 有合法颜色(最暗档也算合法) + expect(cells[0].color).toMatch(/^#[0-9a-fA-F]{6}$/) + // 推进 time 后,左侧应出现非最暗颜色(波峰扫过) + const t1 = computeRippleCells({ + y: 0, + width: 66, + time: 2000, + sourceX: 65, + sourceY: 0, + }) + const nonDarkest = t1.filter(c => c.color !== '#0a0d1a') + expect(nonDarkest.length).toBeGreaterThan(0) }) test('time 推进时颜色分布变化(动画效果)', () => { diff --git a/src/components/EffortPanel/rippleAnimation.ts b/src/components/EffortPanel/rippleAnimation.ts index bdd5123b4..b560ba5b2 100644 --- a/src/components/EffortPanel/rippleAnimation.ts +++ b/src/components/EffortPanel/rippleAnimation.ts @@ -14,40 +14,46 @@ */ /** - * suggestion 系颜色梯度(暗 → suggestion 色 → 高光)。 - * intensity=0 → transparent(无波纹),波峰附近升到 suggestion,超高频涟漪可达高光。 + * suggestion 系颜色梯度(暗背景 → suggestion 色)。 + * + * 设计:所有强度都映射到具体颜色(不返回 transparent),让整面板都是 + * "暗紫蓝海洋"作为底色,波峰在底色上流动。这样波纹颜色变化更明显, + * 波谷也有暗色(不会"消失")。 + * + * 波峰最高升到 suggestion (#5769F7),避免与文字 overlay(也用 suggestion 系) + * 同色互相吞噬。文字用更亮的高光色(#a3b5ff)保持对比。 */ const RIPPLE_COLOR_STOPS = [ - 'transparent', // 0.00 ~ 0.10 - '#15182b', // 0.10 ~ 0.25 — 暗深紫蓝 - '#1f2543', // 0.25 ~ 0.40 - '#2a3360', // 0.40 ~ 0.55 - '#3a4582', // 0.55 ~ 0.70 - '#5769F7', // 0.70 ~ 0.85 — suggestion (Medium blue) - '#8aa0ff', // 0.85 ~ 1.00 — 高光 + '#0a0d1a', // 0.00 ~ 0.14 — 最暗,波谷底色 + '#15182b', // 0.14 ~ 0.28 + '#1f2543', // 0.28 ~ 0.42 + '#2a3360', // 0.42 ~ 0.56 + '#3a4582', // 0.56 ~ 0.70 + '#4a5bb0', // 0.70 ~ 0.84 + '#5769F7', // 0.84 ~ 1.00 — suggestion (波峰) ] as const -/** 'transparent' 是合法 color 字面量(渲染层会跳过这种 cell 的样式)。 */ -export const TRANSPARENT = 'transparent' - /** * 强度(任意实数)→ 颜色字符串。 * - * 钳到 [0, 1],按 RIPPLE_COLOR_STOPS 分级。 - * 极低强度(≤ 0.10)→ transparent(让波纹边缘自然消失)。 + * 钳到 [0, 1],按 RIPPLE_COLOR_STOPS 分级。永不返回 transparent。 + * intensity=0 → 最暗档(#0a0d1a,作为面板底色)。 */ export function intensityToColor(intensity: number): string { const v = intensity < 0 ? 0 : intensity > 1 ? 1 : intensity - if (v <= 0.1) return TRANSPARENT - // 把 (0.1, 1.0] 映射到 [1, stops.length-1] - const scaled = ((v - 0.1) / 0.9) * (RIPPLE_COLOR_STOPS.length - 1) const idx = Math.min( RIPPLE_COLOR_STOPS.length - 1, - Math.max(1, Math.round(scaled)), + Math.floor(v * RIPPLE_COLOR_STOPS.length), ) return RIPPLE_COLOR_STOPS[idx] } +/** + * 'transparent' 字面量。intensityToColor 永不返回它(保留为兼容性导出)。 + * 渲染层可用此常量做语义判定(如 cell 是 overlay 文字而非波纹背景)。 + */ +export const TRANSPARENT = 'transparent' + /** * 单位置 cell:char + color。 * - color 为 'transparent' 时渲染层不染色(背景保持终端默认)。 @@ -92,17 +98,18 @@ const RIPPLE_BG_CHAR = ' ' /** * 计算面板某一行 y 的完整波纹 cell 列表。 * - * 波纹数学: + * 波纹数学(v2 — 调慢 + 扩大覆盖): * dx = x - sourceX * dy = (y - sourceY) * 1.5 (y 方向视觉拉伸,行高 > 字宽) * dist = sqrt(dx² + dy²) - * phase = dist * 0.4 - time * 0.012 + * phase = dist * 0.35 - time * 0.004 (速度调慢至原 1/3) * wave = max(0, sin(phase)) - * falloff = max(0, 1 - dist / 40) + * falloff = max(0, 1 - dist / 90) (覆盖半径扩到 90,让左侧 dist=65 也可见) * intensity = wave * falloff * 震源附近 (dist < 6):叠加高频涟漪 max(intensity, 0.5 + 0.5*sin(time*0.02 - dist*1.2)) * - * 每位置强度经 intensityToColor → 颜色字符串,写入 cell。 + * 每位置强度经 intensityToColor → 颜色字符串(永不 transparent),写入 cell。 + * 即使 intensity=0(波谷)也得到最暗档 #0a0d1a 作为面板底色。 * * @returns 长度严格等于 width 的 Cell 数组 */ @@ -122,12 +129,12 @@ export function computeRippleCells(args: { const dy = (y - sourceY) * 1.5 const dist = Math.sqrt(dx * dx + dy * dy) - // 主波纹相位 - const phase = dist * 0.4 - time * 0.012 + // 主波纹相位(速度调慢:原 0.012 → 0.004,约 1/3 速) + const phase = dist * 0.35 - time * 0.004 const wave = Math.max(0, Math.sin(phase)) - // 距离衰减 - const falloff = Math.max(0, 1 - dist / 40) + // 距离衰减(覆盖半径扩到 90:原 40) + const falloff = Math.max(0, 1 - dist / 90) let intensity = wave * falloff // 震源附近高频涟漪