Skip to content

fix: correct hsl saturation and hue#94

Merged
afc163 merged 3 commits into
ant-design:mainfrom
XGHeaven:fix/saturation-algorithm
Dec 19, 2025
Merged

fix: correct hsl saturation and hue#94
afc163 merged 3 commits into
ant-design:mainfrom
XGHeaven:fix/saturation-algorithm

Conversation

@XGHeaven
Copy link
Copy Markdown
Contributor

@XGHeaven XGHeaven commented Dec 12, 2025

close #93 and #90

顺手将这两个问题一起修复了,又调研了下,发现 hslhsv 对 s 的计算公式是不同的,所以这里将 getSaturation 拆分成了 getHSLSaturationgetHSVSaturation 匹配不同的计算公式。原先这个我就废弃掉了

Summary by CodeRabbit

  • 新功能

    • 新增分别获取 HSV 与 HSL 饱和度的公开接口,并保留兼容的旧接口封装(已标注弃用)。
  • 改进

    • 引入按通道饱和度缓存与惰性初始化,改进色相规范化与缓存传播,提升转换一致性与性能。
    • 复制/输入时保留目标格式的饱和度状态以提高转换精度。
  • 测试

    • 新增全面 HSL/HSV 测试,覆盖转换、归一化、边界与亮度/饱和度行为。

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 12, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

此次变更在 FastColor 中引入独立的 HSL/HSV 饱和度缓存与懒加载计算(_hsl_s / _hsv_sgetHSLSaturation() / getHSVSaturation()),修正 HSL 饱和度公式与色调归一化逻辑,并新增 HSL/HSV 行为相关测试覆盖。

Changes

文件组 / 文件路径 变更摘要
核心饱和度计算与状态管理
src/FastColor.ts
引入 _hsl_s_hsv_s 缓存及懒初始化;新增 getHSVSaturation()getHSLSaturation();保留兼容的弃用包装 getSaturation()(委托到 HSV);HSV 饱和度按 delta/max 计算并缓存,HSL 饱和度修正为 (delta/255) / (1 - Math.abs(2*l - 1)) 并缓存;fromHsl() / fromHsv() 将色调标准化到 [0,360) 并设置相应饱和度缓存;复制/克隆路径传播新缓存字段;toHsl/toHslString/toHsv 等转换改为使用对应饱和度 getter。
HSL 测试套件
tests/hsl.test.ts
新增全面 HSL 测试:hex⇄HSL 往返、setHue 行为、HSLA 字符串解析与标准化、0 与 360 色相等价、s=0 灰度行为、toHslString 往返、darken/lighten 边界与 alpha 传播等断言。
HSV 测试(归一化)
tests/hsv.test.ts
新增 HSV 输入色相归一化测试,验证超范围色相被环绕到 [0,360) 并产生预期十六进制输出。

Estimated code review effort

🎯 3 (中等) | ⏱️ ~20 分钟

需要重点检查的区域:

  • HSL 饱和度公式在 lightness 接近 0 或 1 时的数值稳定性与除零保护;
  • _hsl_s_hsv_s 的懒初始化、缓存失效与复制/克隆路径(from*、复制构造);
  • fromHsl() / fromHsv() 色调归一化(模运算)与 s<=0 的早期处理是否与期望一致;
  • 新增测试中的公差与期望值是否匹配实现(lightness/saturation 四舍五入或归一化差异)。

诗句

🐰 我在调色盘上跳跃,追逐亮暗与饱和,
分家了 HSL 与 HSV 的小秘密,
色相回环归位,数值悄悄被记住,
公式修正后,颜色又唱起了歌,
小小缓存,让转换更温柔。 🎨✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR标题准确总结了核心变更:修复HSL饱和度计算算法并调整色调处理。
Linked Issues check ✅ Passed PR完整解决了#93的问题:用getHSLSaturation()实现正确的HSL饱和度公式(delta/255)/(1-|2*l-1|),用getHSVSaturation()处理HSV饱和度。
Out of Scope Changes check ✅ Passed 所有变更均在scope内:更新FastColor.ts的饱和度计算、添加新的getter方法、完善HSL/HSV相关测试,无无关改动。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @XGHeaven, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves the accuracy of HSL and HSV color model conversions by addressing discrepancies in saturation calculations. It introduces distinct methods for HSL and HSV saturation, deprecating a generic one, and updates all relevant conversion functions to use these new, precise calculations. Additionally, it includes minor fixes in the HSL parsing logic and a new, thorough test suite to ensure the correctness and robustness of these color transformations.

Highlights

  • Saturation Calculation Refinement: The getSaturation method has been deprecated and split into getHSLSaturation and getHSVSaturation to correctly differentiate between HSL and HSV saturation calculations.
  • HSL/HSV Conversion Updates: Methods like toHsl, toHslString, and toHsv have been updated to use the appropriate new saturation getters (getHSLSaturation or getHSVSaturation).
  • fromHsl Method Improvements: Minor corrections were made in the fromHsl method, including ensuring the hue value is correctly assigned after modulo operation and adding an early return for specific conditions.
  • New HSL Test Suite: A new test file (tests/hsl.test.ts) has been added, introducing a comprehensive set of tests for HSL color conversions, including hex-to-HSL, HSLA alpha, hue equivalence, grayscale, and darken/lighten functionality.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

您好,感谢您对 hsl 色彩模型的修正。将饱和度计算拆分为 getHSLSaturationgetHSVSaturation 是一个很好的改进,因为它明确了 HSL 和 HSV 两种模型下饱和度的不同计算方式。同时,新增的 hsl.test.ts 测试文件也大大提高了代码的健壮性。

在 review 过程中,我发现了一些可以改进的地方,主要有两点:

  1. 关键问题getHSLSaturationgetHSVSaturation 共用了同一个私有缓存属性 _s,这会导致在交替调用这两个方法时返回错误的饱和度值。我已经在相关代码行提出了具体的修改建议,主要是将缓存属性拆分开。
  2. fromHsl 方法在处理负数色相(hue)值时存在一个 bug,会导致颜色转换不正确。我也提出了相应的修复建议。

另外,还有一个小发现:darkenlighten 方法内部调用了 getSaturation(),根据您这次的修改,它现在会返回 HSV 的饱和度。但 darken/lighten 是 HSL 模型下的操作,它们后续创建新颜色时需要的是 HSL 饱和度。这可能会导致这两个方法产生非预期的颜色结果。由于这部分代码不在本次 diff 范围内,仅作为提醒供您参考。

总的来说,这是一个很棒的修复,解决了重要的问题。期待您后续的更新!

Comment thread src/FastColor.ts
Comment on lines +249 to +260
getHSLSaturation(): number {
if (typeof this._s === 'undefined') {
const delta = this.getMax() - this.getMin();
if (delta === 0) {
this._s = 0;
} else {
const l = this.getLightness();
this._s = (delta/255) / (1 - Math.abs(2 * l - 1));
}
}
return this._s;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

getHSLSaturationgetHSVSaturation 方法都使用 this._s 来缓存饱和度值。但 HSL 和 HSV 的饱和度计算方式不同,共用一个缓存属性会导致计算错误。
例如,当一个颜色先调用 getHSVSaturation()this._s 会被设置为 HSV 的饱和度。之后再调用 getHSLSaturation(),它会直接返回缓存的 this._s 值,也就是错误的 HSV 饱和度值。

建议为 HSL 和 HSV 的饱和度使用不同的私有缓存属性,例如 _hsl_s_hsv_s
这需要修改以下几个地方:

  1. 在类属性定义中,将 _s 拆分为 _hsl_s_hsv_s
  2. getHSLSaturation 方法使用 _hsl_s 进行缓存。
  3. getHSVSaturation 方法使用 _hsv_s 进行缓存。
  4. fromHsl 方法设置 _hsl_s
  5. fromHsv 方法设置 _hsv_s
  6. constructor 中克隆 FastColor 实例时,需要同时复制 _hsl_s_hsv_s

Comment thread src/FastColor.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/FastColor.ts (2)

237-247: 关键问题:HSV 和 HSL 饱和度共享缓存变量。

getHSVSaturation()getHSLSaturation() 都使用相同的 _s 缓存变量(第 238 行和第 250 行),这会导致严重的缓存冲突。如果先调用 getHSLSaturation() 再调用 getHSVSaturation()(或反之),第二次调用将返回错误的缓存值而不是重新计算。

示例问题场景:

const color = new FastColor('#3d5eff');
const hslS = color.getHSLSaturation(); // 计算并缓存 HSL 饱和度到 _s
const hsvS = color.getHSVSaturation(); // 错误地返回 HSL 饱和度,因为 _s 已定义

建议为 HSV 和 HSL 饱和度使用独立的缓存变量:

   // HSV privates
   private _h?: number;
-  private _s?: number;
+  private _hsv_s?: number;
+  private _hsl_s?: number;
   private _l?: number;
   private _v?: number;

然后更新 getHSVSaturation()getHSLSaturation() 使用各自的缓存变量。


290-308: 关键问题:darken 和 lighten 方法使用了错误的饱和度类型。

darken()lighten() 方法在第 292 行和第 302 行调用 getSaturation()(返回 HSV 饱和度),然后在第 297 行和第 307 行使用 { h, s, l, a } 创建新颜色。构造函数会将此对象识别为 HSL 格式(第 156 行的 matchFormat('hsl') 检查),但 s 值实际上是 HSV 饱和度,这会导致颜色转换错误。

应用此差异以使用正确的 HSL 饱和度:

   darken(amount = 10) {
     const h = this.getHue();
-    const s = this.getSaturation();
+    const s = this.getHSLSaturation();
     let l = this.getLightness() - amount / 100;
     if (l < 0) {
       l = 0;
     }
     return this._c({ h, s, l, a: this.a });
   }
 
   lighten(amount = 10) {
     const h = this.getHue();
-    const s = this.getSaturation();
+    const s = this.getHSLSaturation();
     let l = this.getLightness() + amount / 100;
     if (l > 1) {
       l = 1;
     }
     return this._c({ h, s, l, a: this.a });
   }
♻️ Duplicate comments (2)
src/FastColor.ts (2)

249-260: HSL 饱和度公式正确,但存在缓存冲突。

HSL 饱和度的计算公式 (delta/255) / (1 - Math.abs(2 * l - 1)) 正确实现了 issue #93 中描述的修复。然而,与 getHSVSaturation() 共享 _s 缓存变量会导致缓存冲突(已在前面的评论中标记)。


504-508: 相关:饱和度缓存冲突也影响 fromHsl 和 fromHsv。

第 506 行缓存 HSL 饱和度到 _s,而 fromHsv()(第 554 行)缓存 HSV 饱和度到同一个 _s 变量。这加剧了之前标记的缓存冲突问题。

示例问题:

const color = new FastColor({ h: 230, s: 1, l: 0.5 }); // HSL - _s 缓存 HSL 饱和度
const hsvS = color.getHSVSaturation(); // 错误地返回 HSL 饱和度

这与 getHSVSaturation()getHSLSaturation() 中已标记的缓存冲突是同一个问题。解决方案是为 HSV 和 HSL 饱和度使用独立的缓存变量。

🧹 Nitpick comments (1)
tests/hsl.test.ts (1)

39-45: 简化冗余的断言。

第 41 行将 base.getLightness() 与相同颜色的新实例进行比较,这个断言是冗余的。可以直接移除此行,因为第 44 行已经验证了 setHue 保持亮度不变的核心行为。

应用此差异来简化测试:

 it('setHue should not change lightness', () => {
   const base = new FastColor('#1677ff');
-  expect(base.getLightness()).toBeCloseTo(new FastColor('#1677ff').getLightness(), 4);
 
   const turn = base.setHue(233);
   expect(turn.getLightness()).toBeCloseTo(base.getLightness(), 4);
 });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a21583 and 84ebaa9.

📒 Files selected for processing (2)
  • src/FastColor.ts (7 hunks)
  • tests/hsl.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/hsl.test.ts (1)
src/FastColor.ts (1)
  • FastColor (61-636)
🔇 Additional comments (11)
tests/hsl.test.ts (6)

26-37: 测试逻辑正确。

hex 到 HSL 的转换测试和往返测试覆盖全面,正确使用了 toBeCloseTo 处理浮点数比较。


47-57: LGTM!

HSLA 字符串解析和透明度处理的测试覆盖良好,正确验证了字符串标准化和透明度提取。


59-63: LGTM!

色相 0 和 360 等价性测试很好地验证了色相环绕行为,这是 HSL 色彩模型的重要边界情况。


65-69: LGTM!

饱和度为 0 时的灰度测试正确验证了当饱和度为零时色相被忽略的行为。


71-89: LGTM!

往返字符串测试和 darken/lighten 测试覆盖全面,正确验证了亮度调整和边界裁剪行为([0, 1] 范围)。


22-22: 移除重复的测试夹具。

行 20 和行 22 包含相同的测试用例 { hex: '#3d5dff', hsl: { h: 230, s: 1, l: 0.6196 } }。请删除其中一个重复项。

Likely an incorrect or invalid review comment.

src/FastColor.ts (5)

230-235: 向后兼容的弃用处理正确。

getSaturation() 正确地委托给 getHSVSaturation() 以保持向后兼容性,并通过 JSDoc 注释提示开发者使用更明确的方法。


404-411: LGTM!

toHsl() 正确使用 getHSLSaturation() 返回 HSL 色彩空间的饱和度值,符合修复目标。


414-422: LGTM!

toHslString() 正确使用 getHSLSaturation() 生成 HSL/HSLA 字符串,确保输出的饱和度百分比与 HSL 色彩模型一致。


425-432: LGTM!

toHsv() 正确使用 getHSVSaturation() 返回 HSV 色彩空间的饱和度值,使色彩空间的区分更加明确。


504-516: LGTM!

fromHsl() 的改进很好:

  • 第 505 行:使用 h = h % 360 标准化色相值,同时缓存到 _h,这是一个优雅的优化。
  • 第 515 行:当饱和度 ≤ 0 时提前返回,避免后续不必要的计算,提高了灰度颜色的处理效率。

@afc163
Copy link
Copy Markdown
Member

afc163 commented Dec 15, 2025

  1. 看下 gemini 的评论
  2. ci 挂了

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/FastColor.ts (1)

292-310: 严重:darken 和 lighten 使用了错误的饱和度类型!

darken()lighten() 方法调用 getSaturation()(返回 HSV 饱和度),但随后用 { h, s, l, a } 构造颜色对象,该对象会被路由到 fromHsl,后者期望接收 HSL 饱和度。这导致饱和度类型不匹配,产生错误的颜色计算结果。

虽然这是旧代码中已存在的问题(旧 getSaturation 也使用 HSV 公式),但在修正饱和度计算的 PR 中应一并修复。

🔎 应用此 diff 修复饱和度类型错误:
 darken(amount = 10) {
   const h = this.getHue();
-  const s = this.getSaturation();
+  const s = this.getHSLSaturation();
   let l = this.getLightness() - amount / 100;
   if (l < 0) {
     l = 0;
   }
   return this._c({ h, s, l, a: this.a });
 }

 lighten(amount = 10) {
   const h = this.getHue();
-  const s = this.getSaturation();
+  const s = this.getHSLSaturation();
   let l = this.getLightness() + amount / 100;
   if (l > 1) {
     l = 1;
   }
   return this._c({ h, s, l, a: this.a });
 }
🧹 Nitpick comments (1)
src/FastColor.ts (1)

170-184: 建议:考虑改进缓存失效机制

当前实现中,setR/setG/setB 方法通过 _sc 克隆对象并修改 RGB 值,但不会清空 HSL/HSV 相关的缓存字段(_h_hsl_s_hsv_s_l_v)。这可能导致 RGB 改变后,HSL/HSV 属性返回过期的缓存值。

例如:

const color = new FastColor({ r: 255, g: 0, b: 0 }); // 红色
const hue = color.getHue(); // 缓存色相值
const newColor = color.setR(0); // RGB 变为 (0,0,0) - 黑色
const newHue = newColor.getHue(); // 仍返回红色的色相!

这是一个已存在的架构问题,不是本次 PR 引入的。建议在后续优化中考虑在 RGB 变更时清空相关缓存,或使用更健壮的缓存失效策略。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 63c07f9 and 00f5a3e.

📒 Files selected for processing (2)
  • src/FastColor.ts (9 hunks)
  • tests/hsl.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/hsl.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/FastColor.ts (1)
src/types.ts (3)
  • OptionalA (22-22)
  • HSL (8-13)
  • HSV (15-20)
🔇 Additional comments (8)
src/FastColor.ts (8)

89-90: 完美解决了缓存共享问题!

_s 拆分为 _hsl_s_hsv_s 正确地为 HSL 和 HSV 饱和度提供了独立缓存,避免了之前 Gemini 评审中指出的缓存冲突问题。


147-148: LGTM!

克隆逻辑正确地复制了 _hsl_s_hsv_s 两个独立的饱和度缓存字段。


232-237: 良好的向后兼容策略!

通过 @deprecated 注解并委托给 getHSVSaturation(),既保持了向后兼容性(旧实现使用的是 HSV 公式),又引导开发者使用更明确的新方法。


239-249: HSV 饱和度计算公式正确!

使用标准 HSV 饱和度公式 S = delta / max,并正确处理了 delta === 0 的边界情况。缓存逻辑符合懒加载模式。


251-262: HSL 饱和度公式修正正确!

实现了 Issue #93 中提出的正确 HSL 饱和度公式:(delta/255) / (1 - Math.abs(2 * l - 1))。边界情况(delta === 0)得到妥善处理,避免了除零错误。


406-434: 转换方法正确使用了对应的饱和度计算!

  • toHsl()toHslString() 使用 getHSLSaturation()
  • toHsv() 使用 getHSVSaturation()

确保了输出格式与所用饱和度公式的一致性。


506-519: 色相归一化问题已修复!

Line 507 使用 ((h % 360) + 360) % 360 正确地将色相值归一化到 [0, 360) 区间,修复了之前 Gemini 评审中指出的负数色相导致计算错误的问题(例如 h = -60 现在正确映射为 300)。同时正确设置了 _hsl_s 缓存。


555-560: HSV 转换同样修复了色相归一化!

fromHsl 一致,使用相同的色相归一化公式,并正确设置了 _hsv_s 缓存。

@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 19, 2025

Codecov Report

❌ Patch coverage is 96.29630% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.75%. Comparing base (7a21583) to head (00f5a3e).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/FastColor.ts 96.29% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main      #94       +/-   ##
===========================================
+ Coverage   79.43%   93.75%   +14.31%     
===========================================
  Files           1        1               
  Lines         321      336       +15     
  Branches       95       93        -2     
===========================================
+ Hits          255      315       +60     
+ Misses         65       21       -44     
+ Partials        1        0        -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@afc163 afc163 merged commit 37aae7f into ant-design:main Dec 19, 2025
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S 饱和度的计算算法不正确

2 participants