调色性能优化 + 新增色域方块拾色 + 取值胶囊高对比配色 + 数据点标记修复
1. 修复自定义颜色滑动卡顿(VERY_BIG_DATA 全信号可见时 36ms/步 → 约 12ms/步)
- 根因:场景缓存的内容指纹把逐信号的颜色/不透明度/线宽/模式都算了进去,
改一次颜色就整幅失效,于是所有网格的波形都要重新光栅化。
- 新增"不含外观样式"的几何指纹与样式快照:只有在网格结构、视图几何、信号集合与
文字都不变、仅外观样式变化时,走"局部重画"快路径 —— 只重画颜色变了的那个网格,
再把结果写回场景缓存(下一帧回到纯贴图)。
- 补丁矩形向内对齐到整数像素,避免把属于相邻网格的一行擦掉而留下 1px 接缝;
底色填充按补丁矩形本身进行(大面积不透明填充会走 GDI 快路径,不受 GDI+ 裁剪约束)。
- 该优化对调色板点击、线宽/不透明度滑块、绘制模式切换同样生效。
2. 自定义颜色新增"方块色域图"拾色
- 新增饱和度-明度方块(横轴饱和度、纵轴明度)与纵向色相条,可直接在色域图上取色。
- 与面板内其它控件完全联动:RGB 滑块、十六进制输入、预览色块同源(RGB 为唯一数据源),
并记忆色相,消色差时仍可用色相条换回颜色。
- 色域方块按当前色相预渲染缓存,色相不变时不重复绘制;色域内用十字光标、色相条用纵向光标。
3. 取值胶囊的对比度描边与文本色改为对信号配色线性响应
- 原实现只有一个亮度阈值,文字/描边只有黑、白两套固定取值。现按底色亮度选定亮/暗一侧,
再与底色的互补色按固定比例线性混合(互补色亮度恰为 1−底色亮度),
因此文字与描边的亮度随信号配色线性变化,且任意底色下都保持高对比。
4. 修复光标数据点越界三角的判定与绘制
- 是否改用三角,以"数据点本身是否超出绘制边界"为准,不再因为数值标注被贴边而改变形状。
- 三角方向修正为尖端朝外(此前上越界画下三角、左越界画右三角,方向完全相反),
内三角改按质心收缩,描边不再是歪的;上下越界时三角与显示的数值纵向居中对齐。
This commit is contained in:
1 parent
d3c83237f1
commit
40b12d6e8b
7 files changed
+873
-120
No files matched your search
@@ -348,6 +348,10 @@ namespace jdi
|
|||||||
ReleaseBackBuffer();
|
ReleaseBackBuffer();
|
||||||
m_fonts.Destroy();
|
m_fonts.Destroy();
|
||||||
|
|
||||||
|
// 拾色器缓存了 GDI+ 位图(色域方块):必须在 GdiplusShutdown 之前释放,
|
||||||
|
// 否则位图析构会发生在 GDI+ 关闭之后,直接访问违例。
|
||||||
|
m_colorPicker.Close();
|
||||||
|
|
||||||
if (m_gdiplusReady)
|
if (m_gdiplusReady)
|
||||||
{
|
{
|
||||||
gd::GdiplusShutdown(m_gdiplusToken);
|
gd::GdiplusShutdown(m_gdiplusToken);
|
||||||
|
|||||||
@@ -835,12 +835,13 @@ namespace jdi
|
|||||||
bool IsOnCursorSpanLabel(int x, int y) const;
|
bool IsOnCursorSpanLabel(int x, int y) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 画文字时给底色胶囊加一层与文字反色的细描边,提升与背景/曲线的对比度。
|
* @brief 画文字时给底色胶囊加一层细描边,提升与背景/曲线的对比度。
|
||||||
* @param [IN] rect 胶囊矩形。
|
* @param [IN] rect 胶囊矩形。
|
||||||
* @param [IN] radius 圆角半径。
|
* @param [IN] radius 圆角半径。
|
||||||
* @param [IN] textDark 文字是否用深色。
|
* @param [IN] outlineColor 描边色(由 `LabelInkFor` 与文字色一并算出的反相色)。
|
||||||
*/
|
*/
|
||||||
void DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius, bool textDark);
|
void DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius,
|
||||||
|
std::uint32_t outlineColor);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 精确检测鼠标位置是否命中某条可见信号曲线。
|
* @brief 精确检测鼠标位置是否命中某条可见信号曲线。
|
||||||
@@ -1404,9 +1405,65 @@ namespace jdi
|
|||||||
* 视图几何以外的任何内容变化(改颜色/线宽/透明度/样式、重命名、增删信号)
|
* 视图几何以外的任何内容变化(改颜色/线宽/透明度/样式、重命名、增删信号)
|
||||||
* 都会改变该指纹,从而让静态场景缓存自动失效,无需在各处手工调用失效接口。
|
* 都会改变该指纹,从而让静态场景缓存自动失效,无需在各处手工调用失效接口。
|
||||||
*
|
*
|
||||||
|
* @param [IN] includeStyles 是否把逐信号外观样式(颜色/不透明度/线宽/模式)计入指纹。
|
||||||
* @return 指纹值。
|
* @return 指纹值。
|
||||||
*/
|
*/
|
||||||
std::uint64_t SceneContentHash() const;
|
std::uint64_t SceneContentHash(bool includeStyles = true) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 场景内容中**不含逐信号外观样式**的指纹。
|
||||||
|
*
|
||||||
|
* 只包含网格结构、视图几何、网格幅值、信号集合与可见性、图例文案、选中项等
|
||||||
|
* "变化后必须整幅重建"的内容;逐信号的 `color / opacity / lineWidth / mode`
|
||||||
|
* 一律不参与。
|
||||||
|
*
|
||||||
|
* 用途:外观样式变化时(典型场景是拖动自定义颜色的滑块)波形几何完全不变,
|
||||||
|
* 只要该指纹一致,就可以只重画"颜色变了的那个网格"再写回缓存,
|
||||||
|
* 而不必整幅重绘全部网格(VERY_BIG_DATA 实测整幅 30ms+、单网格约 4ms)。
|
||||||
|
*
|
||||||
|
* @return 指纹值。
|
||||||
|
*/
|
||||||
|
std::uint64_t SceneGeometryHash() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 静态场景缓存中"网格内信号外观样式"的快照条目。
|
||||||
|
*/
|
||||||
|
struct SceneStyleEntry
|
||||||
|
{
|
||||||
|
std::uint32_t color = 0; /**< 信号色(未叠加不透明度)。 */
|
||||||
|
float opacity = 1.0f; /**< 不透明度。 */
|
||||||
|
float lineWidth = 1.0f; /**< 线宽。 */
|
||||||
|
int mode = 0; /**< 绘制模式。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 采集当前各网格内可见信号的外观样式快照。
|
||||||
|
* @param [OUT] out 采集结果(按网格分组)。
|
||||||
|
*/
|
||||||
|
void CollectSceneStyles(std::vector<std::vector<SceneStyleEntry>>& out) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断当前相对缓存是否"只有外观样式发生了变化"。
|
||||||
|
*
|
||||||
|
* 成立条件:`SceneGeometryHash()` 与缓存一致(网格结构、视图几何、信号集合
|
||||||
|
* 与文字都没变),且至少有一个网格内信号的外观样式不同。
|
||||||
|
*
|
||||||
|
* @param [OUT] lanes 外观发生变化的网格下标(升序)。
|
||||||
|
* @return 可以走"局部重画"快路径返回 true。
|
||||||
|
*/
|
||||||
|
bool FindStyleOnlyLanes(std::vector<int>& lanes) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 只重画指定网格并把结果写回静态场景缓存。
|
||||||
|
*
|
||||||
|
* 必须在 `BlitSceneCache()` 之后调用:贴图已经完成,这里把受影响网格的
|
||||||
|
* "底色 + 网格线 + 波形 + 标尺 + 图例"重画到画布上,再整块拷回缓存位图,
|
||||||
|
* 使缓存内容与当前样式一致(下一帧即可重新走"完全命中"快路径)。
|
||||||
|
*
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
* @param [IN] lanes 需要重画的网格下标。
|
||||||
|
*/
|
||||||
|
void RepaintSceneCachePatch(gd::Graphics& g, const std::vector<int>& lanes);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 当前应当绘入静态场景缓存的信号光晕(仅拖拽中的信号)。
|
* @brief 当前应当绘入静态场景缓存的信号光晕(仅拖拽中的信号)。
|
||||||
@@ -1485,6 +1542,10 @@ namespace jdi
|
|||||||
int m_sceneY = 0; /**< 缓存对应的客户区纵坐标(绘图区上边界)。 */
|
int m_sceneY = 0; /**< 缓存对应的客户区纵坐标(绘图区上边界)。 */
|
||||||
bool m_sceneValid = false; /**< 缓存内容是否有效。 */
|
bool m_sceneValid = false; /**< 缓存内容是否有效。 */
|
||||||
SceneCacheKey m_sceneKey; /**< 缓存对应的场景参数。 */
|
SceneCacheKey m_sceneKey; /**< 缓存对应的场景参数。 */
|
||||||
|
/** 缓存捕获时"不含逐信号外观样式"的内容指纹(用于识别仅样式变化)。 */
|
||||||
|
std::uint64_t m_sceneGeometryHash = 0;
|
||||||
|
/** 缓存捕获时各网格内信号的外观样式快照(按网格分组,与 m_sceneStyles 同序)。 */
|
||||||
|
std::vector<std::vector<SceneStyleEntry>> m_sceneStyles;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 查询坐标命中的图例条目。
|
* @brief 查询坐标命中的图例条目。
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ namespace jdi
|
|||||||
const size_t kMinPolylinePoints = 512; /**< 折线最少顶点数(窗口很窄时仍保留细节)。 */
|
const size_t kMinPolylinePoints = 512; /**< 折线最少顶点数(窗口很窄时仍保留细节)。 */
|
||||||
const float kEnvelopeMinPixelSpan = 0.5f; /**< 包络纵向跨度小于该像素值时合并为单个顶点。 */
|
const float kEnvelopeMinPixelSpan = 0.5f; /**< 包络纵向跨度小于该像素值时合并为单个顶点。 */
|
||||||
const float kCursorHandleReserve = 9.0f; /**< 光标顶部三角把手占用的高度(数据点贴边时需避让)。 */
|
const float kCursorHandleReserve = 9.0f; /**< 光标顶部三角把手占用的高度(数据点贴边时需避让)。 */
|
||||||
|
const float kMarkerEdgeInset = 7.0f; /**< 数据点越界贴边时距网格可见边界的间距。 */
|
||||||
|
const float kMarkerInsideMargin = 2.0f; /**< 数据点在视界内时允许贴到边界的最小余量。 */
|
||||||
const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */
|
const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */
|
||||||
const size_t kPolylineCacheLimit = 64; /**< 折线顶点缓存条目上限。 */
|
const size_t kPolylineCacheLimit = 64; /**< 折线顶点缓存条目上限。 */
|
||||||
const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */
|
const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */
|
||||||
@@ -407,19 +409,6 @@ namespace jdi
|
|||||||
return (argb & 0x00FFFFFFu) | (a << 24);
|
return (argb & 0x00FFFFFFu) | (a << 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 判断颜色是否偏暗(用于在信号色底板上选择可读的文字颜色)。
|
|
||||||
* @param [IN] color 0xAARRGGBB 颜色值。
|
|
||||||
* @return 相对亮度低于阈值返回 true。
|
|
||||||
*/
|
|
||||||
bool IsDarkColor(std::uint32_t color)
|
|
||||||
{
|
|
||||||
const double r = static_cast<double>((color >> 16) & 0xFFu);
|
|
||||||
const double g = static_cast<double>((color >> 8) & 0xFFu);
|
|
||||||
const double b = static_cast<double>(color & 0xFFu);
|
|
||||||
return (0.299 * r + 0.587 * g + 0.114 * b) < 140.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 将数值限制到合法区间。
|
* @brief 将数值限制到合法区间。
|
||||||
* @param [IN] value 输入值。
|
* @param [IN] value 输入值。
|
||||||
@@ -4234,12 +4223,9 @@ namespace jdi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 静态场景复用(不再局限于拖拽):只要视图几何、信号集合与显示样式都没变,
|
// 贴图之后、静态场景之外的全部内容(随鼠标变化或位于最上层)。
|
||||||
// "绘图区底色 + 全部网格 + 时间轴"就逐像素相同,直接贴图即可。
|
// 三个分支(完全命中 / 局部重画 / 整幅重绘)共用同一段,保证观感完全一致。
|
||||||
// 大文件下整幅重绘约 80ms,贴图约 1ms —— 鼠标悬停、拖拽选区这些
|
const auto paintOverlays = [this, &g]()
|
||||||
// "只改一小块叠加层"的交互因此不再重绘波形。
|
|
||||||
// 随鼠标变化的内容(悬停高亮、读数框、选区、光标、拖拽虚影、输入框)一律画在贴图之后。
|
|
||||||
if (SceneCacheMatches() && BlitSceneCache(g))
|
|
||||||
{
|
{
|
||||||
PaintHoverHighlight(g);
|
PaintHoverHighlight(g);
|
||||||
PaintLegendHighlight(g);
|
PaintLegendHighlight(g);
|
||||||
@@ -4248,6 +4234,7 @@ namespace jdi
|
|||||||
PaintEdgeCursor(g, m_lanes);
|
PaintEdgeCursor(g, m_lanes);
|
||||||
PaintSignalInteraction(g);
|
PaintSignalInteraction(g);
|
||||||
PaintHint(g);
|
PaintHint(g);
|
||||||
|
// 网格 Y 轴上下限的输入框绘制在最上层。
|
||||||
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
|
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
|
||||||
m_editTarget == PlotEditTarget::LANE_Y_MIN)
|
m_editTarget == PlotEditTarget::LANE_Y_MIN)
|
||||||
{
|
{
|
||||||
@@ -4258,9 +4245,32 @@ namespace jdi
|
|||||||
// 绘图区外框(比网格内部分界线更重,形成明确整体边界)。
|
// 绘图区外框(比网格内部分界线更重,形成明确整体边界)。
|
||||||
gd::Pen borderPen(ToGdiColor(m_theme->axisLine), 1.6f);
|
gd::Pen borderPen(ToGdiColor(m_theme->axisLine), 1.6f);
|
||||||
g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height);
|
g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 静态场景复用(不再局限于拖拽):只要视图几何、信号集合与显示样式都没变,
|
||||||
|
// "绘图区底色 + 全部网格 + 时间轴"就逐像素相同,直接贴图即可。
|
||||||
|
// 大文件下整幅重绘约 80ms,贴图约 1ms —— 鼠标悬停、拖拽选区这些
|
||||||
|
// "只改一小块叠加层"的交互因此不再重绘波形。
|
||||||
|
// 随鼠标变化的内容(悬停高亮、读数框、选区、光标、拖拽虚影、输入框)一律画在贴图之后。
|
||||||
|
if (SceneCacheMatches() && BlitSceneCache(g))
|
||||||
|
{
|
||||||
|
paintOverlays();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只有"外观样式"变化(典型场景:拖动自定义颜色滑块实时调色):网格结构、视图几何、
|
||||||
|
// 波形采样都没变,只是颜色/不透明度/线宽/模式不同 —— 此时只重画受影响的网格,
|
||||||
|
// 再写回缓存。(VERY_BIG_DATA 实测:整幅重绘 30ms+,单网格重画约 4ms。)
|
||||||
|
{
|
||||||
|
std::vector<int> patchLanes;
|
||||||
|
if (FindStyleOnlyLanes(patchLanes) && BlitSceneCache(g))
|
||||||
|
{
|
||||||
|
RepaintSceneCachePatch(g, patchLanes);
|
||||||
|
paintOverlays();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const double viewMin = Tab().ViewMin();
|
const double viewMin = Tab().ViewMin();
|
||||||
const double viewMax = Tab().ViewMax();
|
const double viewMax = Tab().ViewMax();
|
||||||
const double span = viewMax - viewMin;
|
const double span = viewMax - viewMin;
|
||||||
@@ -4288,25 +4298,7 @@ namespace jdi
|
|||||||
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
|
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
|
||||||
CaptureSceneCache(g);
|
CaptureSceneCache(g);
|
||||||
|
|
||||||
PaintHoverHighlight(g);
|
paintOverlays();
|
||||||
PaintLegendHighlight(g);
|
|
||||||
PaintRubberBand(g);
|
|
||||||
PaintCursor(g, m_lanes);
|
|
||||||
PaintEdgeCursor(g, m_lanes);
|
|
||||||
PaintSignalInteraction(g);
|
|
||||||
PaintHint(g);
|
|
||||||
|
|
||||||
// 网格 Y 轴上下限的输入框绘制在最上层。
|
|
||||||
if (m_editTarget == PlotEditTarget::LANE_Y_MAX || m_editTarget == PlotEditTarget::LANE_Y_MIN)
|
|
||||||
{
|
|
||||||
PaintEditField(g);
|
|
||||||
}
|
|
||||||
|
|
||||||
PaintScrollBar(g);
|
|
||||||
|
|
||||||
// 绘图区外框(比网格内部分界线更重,形成明确整体边界)。
|
|
||||||
gd::Pen borderPen(ToGdiColor(m_theme->axisLine), 1.6f);
|
|
||||||
g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlotView::PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved,
|
void PlotView::PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved,
|
||||||
@@ -4479,11 +4471,13 @@ namespace jdi
|
|||||||
return SignalRef();
|
return SignalRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uint64_t PlotView::SceneContentHash() const
|
std::uint64_t PlotView::SceneContentHash(bool includeStyles) const
|
||||||
{
|
{
|
||||||
// FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
|
// FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
|
||||||
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
|
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
|
||||||
// 不需要在各处手工调用 InvalidateSceneCache(容易漏)。
|
// 不需要在各处手工调用 InvalidateSceneCache(容易漏)。
|
||||||
|
// `includeStyles == false` 时跳过逐信号的外观样式(颜色/不透明度/线宽/模式),
|
||||||
|
// 得到的就是几何指纹:它一致说明“只有外观样式变了”。
|
||||||
std::uint64_t hash = 1469598103934665603ull;
|
std::uint64_t hash = 1469598103934665603ull;
|
||||||
const auto mix = [&hash](std::uint64_t value)
|
const auto mix = [&hash](std::uint64_t value)
|
||||||
{
|
{
|
||||||
@@ -4547,10 +4541,16 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// 以下四项是“只影响外观”的样式:仅在 includeStyles 时计入指纹。
|
||||||
|
// (不参与时得到的就是 SceneGeometryHash:它一致说明只有外观变了,
|
||||||
|
// 于是可以只重画颜色变了的网格,而不必整幅重绘。)
|
||||||
|
if (includeStyles)
|
||||||
|
{
|
||||||
mix(static_cast<std::uint64_t>(resolved.style->color));
|
mix(static_cast<std::uint64_t>(resolved.style->color));
|
||||||
mixFloat(resolved.style->opacity);
|
mixFloat(resolved.style->opacity);
|
||||||
mixFloat(resolved.style->lineWidth);
|
mixFloat(resolved.style->lineWidth);
|
||||||
mix(static_cast<std::uint64_t>(resolved.style->mode));
|
mix(static_cast<std::uint64_t>(resolved.style->mode));
|
||||||
|
}
|
||||||
// 图例文案参与绘制,重命名必须让缓存失效。
|
// 图例文案参与绘制,重命名必须让缓存失效。
|
||||||
mixText(resolved.data->Title());
|
mixText(resolved.data->Title());
|
||||||
if (resolved.file != nullptr)
|
if (resolved.file != nullptr)
|
||||||
@@ -4562,6 +4562,190 @@ namespace jdi
|
|||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::uint64_t PlotView::SceneGeometryHash() const
|
||||||
|
{
|
||||||
|
return SceneContentHash(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotView::CollectSceneStyles(std::vector<std::vector<SceneStyleEntry>>& out) const
|
||||||
|
{
|
||||||
|
out.clear();
|
||||||
|
out.resize(m_lanes.size());
|
||||||
|
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
|
||||||
|
{
|
||||||
|
const std::vector<SignalRef>& signals = Tab().Lanes()[laneIndex].signals;
|
||||||
|
std::vector<SceneStyleEntry>& lane = out[laneIndex];
|
||||||
|
lane.reserve(signals.size());
|
||||||
|
for (const SignalRef& ref : signals)
|
||||||
|
{
|
||||||
|
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||||
|
SceneStyleEntry entry;
|
||||||
|
if (resolved.style != nullptr)
|
||||||
|
{
|
||||||
|
entry.color = resolved.style->color;
|
||||||
|
entry.opacity = resolved.style->opacity;
|
||||||
|
entry.lineWidth = resolved.style->lineWidth;
|
||||||
|
entry.mode = static_cast<int>(resolved.style->mode);
|
||||||
|
}
|
||||||
|
lane.push_back(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlotView::FindStyleOnlyLanes(std::vector<int>& lanes) const
|
||||||
|
{
|
||||||
|
lanes.clear();
|
||||||
|
if (!m_sceneValid || m_sceneGeometryHash != SceneGeometryHash())
|
||||||
|
{
|
||||||
|
// 网格结构 / 视图几何 / 信号集合 / 图例文案有变化:必须整幅重绘。
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::vector<SceneStyleEntry>> current;
|
||||||
|
CollectSceneStyles(current);
|
||||||
|
if (current.size() != m_sceneStyles.size())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t laneIndex = 0; laneIndex < current.size(); ++laneIndex)
|
||||||
|
{
|
||||||
|
const std::vector<SceneStyleEntry>& now = current[laneIndex];
|
||||||
|
const std::vector<SceneStyleEntry>& before = m_sceneStyles[laneIndex];
|
||||||
|
if (now.size() != before.size())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool differs = false;
|
||||||
|
for (std::size_t index = 0; index < now.size() && !differs; ++index)
|
||||||
|
{
|
||||||
|
differs = (now[index].color != before[index].color) ||
|
||||||
|
(now[index].opacity != before[index].opacity) ||
|
||||||
|
(now[index].lineWidth != before[index].lineWidth) ||
|
||||||
|
(now[index].mode != before[index].mode);
|
||||||
|
}
|
||||||
|
if (differs)
|
||||||
|
{
|
||||||
|
lanes.push_back(static_cast<int>(laneIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !lanes.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotView::RepaintSceneCachePatch(gd::Graphics& g, const std::vector<int>& lanes)
|
||||||
|
{
|
||||||
|
if (lanes.empty() || m_sceneDc == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重画范围 = 受影响网格与绘图区的交集(并且不超出缓存区域)。
|
||||||
|
float top = 1.0e9f;
|
||||||
|
float bottom = -1.0e9f;
|
||||||
|
for (const int laneIndex : lanes)
|
||||||
|
{
|
||||||
|
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const gd::RectF& rect = m_lanes[static_cast<std::size_t>(laneIndex)].rect;
|
||||||
|
top = std::min(top, rect.Y);
|
||||||
|
bottom = std::max(bottom, rect.Y + rect.Height);
|
||||||
|
}
|
||||||
|
if (bottom <= top)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
top = std::max(top, m_plotRect.Y);
|
||||||
|
bottom = std::min(bottom, m_plotRect.Y + m_plotRect.Height);
|
||||||
|
if (bottom <= top)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠️ 补丁矩形必须**向内**对齐到整数像素(ceil 上边界、floor 下边界):
|
||||||
|
// 网格矩形是浮点数,而 GDI `FillRect`/`BitBlt` 会按 floor/ceil 向外取整,
|
||||||
|
// "向外"会多擦掉/多写回一整行,那一行属于相邻网格(例如它的分界线),
|
||||||
|
// 于是补丁边界上会留下一像素接缝。向内对齐后,边界外的行一律由缓存提供
|
||||||
|
// (缓存内容与整幅重绘逐像素一致,已由对照帧验证)。
|
||||||
|
const int x0 = std::max(static_cast<int>(std::ceil(m_area.X)), static_cast<int>(std::ceil(m_area.X)));
|
||||||
|
const int y0 = std::max(static_cast<int>(std::ceil(top)), static_cast<int>(std::ceil(m_area.Y)));
|
||||||
|
const int x1 = std::min(static_cast<int>(std::floor(m_area.X + m_area.Width)),
|
||||||
|
static_cast<int>(std::floor(m_area.X + m_area.Width)));
|
||||||
|
const int y1 = std::min(static_cast<int>(std::floor(bottom)),
|
||||||
|
static_cast<int>(std::floor(m_area.Y + m_area.Height)));
|
||||||
|
if (x1 <= x0 || y1 <= y0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gd::RectF patch(static_cast<float>(x0), static_cast<float>(y0),
|
||||||
|
static_cast<float>(x1 - x0), static_cast<float>(y1 - y0));
|
||||||
|
|
||||||
|
// 补丁区域必须被**完整**重画:凡是与它相交的网格都要重画一遍
|
||||||
|
// (补丁已向内对齐,正常只会命中受影响的那几个网格)。
|
||||||
|
std::vector<int> repaint;
|
||||||
|
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
|
||||||
|
{
|
||||||
|
const gd::RectF& rect = m_lanes[laneIndex].rect;
|
||||||
|
if (rect.Y < static_cast<float>(y1) && rect.Y + rect.Height > static_cast<float>(y0))
|
||||||
|
{
|
||||||
|
repaint.push_back(static_cast<int>(laneIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (repaint.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 与整幅重绘保持完全相同的绘制顺序:底色 → 逐网格内容(PaintLane 会与当前裁剪区求交)。
|
||||||
|
{
|
||||||
|
const int saveState = g.Save();
|
||||||
|
g.SetClip(patch, gd::CombineModeIntersect);
|
||||||
|
// ⚠️ 底色填充必须**按补丁矩形本身**填充,不能用 m_area / m_plotRect:
|
||||||
|
// `UiFillRect` 对大面积不透明矩形会走 GDI `FillRect` 快路径,而 GDI 绘制
|
||||||
|
// **不受 GDI+ 裁剪区约束**,会把已经贴好的缓存内容整片覆盖掉。
|
||||||
|
UiFillRect(g, patch, m_theme->plotBg);
|
||||||
|
const float cardTop = std::max(patch.Y, m_plotRect.Y);
|
||||||
|
const float cardBottom = std::min(patch.Y + patch.Height, m_plotRect.Y + m_plotRect.Height);
|
||||||
|
if (cardBottom > cardTop)
|
||||||
|
{
|
||||||
|
UiFillRect(g, gd::RectF(patch.X, cardTop, patch.Width, cardBottom - cardTop),
|
||||||
|
m_theme->cardBg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const double viewMin = Tab().ViewMin();
|
||||||
|
const double viewMax = Tab().ViewMax();
|
||||||
|
const double span = viewMax - viewMin;
|
||||||
|
const double timeStep = NiceStep((span > 0.0) ? span : 1.0, kTimeTickTarget);
|
||||||
|
std::vector<double> timeTicks;
|
||||||
|
BuildTicks(viewMin, viewMax, timeStep, timeTicks);
|
||||||
|
|
||||||
|
for (const int laneIndex : repaint)
|
||||||
|
{
|
||||||
|
if (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()))
|
||||||
|
{
|
||||||
|
PaintLane(g, laneIndex, m_lanes[static_cast<std::size_t>(laneIndex)], timeTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.Restore(saveState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 把重画结果写回缓存位图,使缓存与当前样式一致(下一帧即回到"完全命中"快路径)。
|
||||||
|
{
|
||||||
|
HDC target = g.GetHDC();
|
||||||
|
if (target != nullptr)
|
||||||
|
{
|
||||||
|
BitBlt(m_sceneDc, x0 - m_sceneX, y0 - m_sceneY, x1 - x0, y1 - y0, target, x0, y0, SRCCOPY);
|
||||||
|
g.ReleaseHDC(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_sceneKey = MakeSceneCacheKey();
|
||||||
|
m_sceneGeometryHash = SceneGeometryHash();
|
||||||
|
CollectSceneStyles(m_sceneStyles);
|
||||||
|
}
|
||||||
|
|
||||||
bool PlotView::EnsureSceneCache(int width, int height)
|
bool PlotView::EnsureSceneCache(int width, int height)
|
||||||
{
|
{
|
||||||
if (width <= 0 || height <= 0)
|
if (width <= 0 || height <= 0)
|
||||||
@@ -4638,6 +4822,8 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_sceneKey = MakeSceneCacheKey();
|
m_sceneKey = MakeSceneCacheKey();
|
||||||
|
m_sceneGeometryHash = SceneGeometryHash();
|
||||||
|
CollectSceneStyles(m_sceneStyles);
|
||||||
m_sceneValid = true;
|
m_sceneValid = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4663,6 +4849,8 @@ namespace jdi
|
|||||||
void PlotView::InvalidateSceneCache()
|
void PlotView::InvalidateSceneCache()
|
||||||
{
|
{
|
||||||
m_sceneValid = false;
|
m_sceneValid = false;
|
||||||
|
m_sceneStyles.clear();
|
||||||
|
m_sceneGeometryHash = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlotView::ReleaseSceneCache()
|
void PlotView::ReleaseSceneCache()
|
||||||
@@ -4793,7 +4981,11 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int saveState = g.Save();
|
const int saveState = g.Save();
|
||||||
g.SetClip(gd::RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop));
|
// 与当前裁剪区求交(而不是替换):局部重画(RepaintSceneCachePatch)时
|
||||||
|
// 外层已经按“需要重画的网格范围”设过裁剪区,这里必须保留,否则会重画到
|
||||||
|
// 其他网格上。整幅重绘时外层没有裁剪区,等同于原来的行为。
|
||||||
|
g.SetClip(gd::RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop),
|
||||||
|
gd::CombineModeIntersect);
|
||||||
|
|
||||||
|
|
||||||
// 幅值网格线(先画线,标尺稍后作为覆盖层绘制,避免被波形遮挡)。
|
// 幅值网格线(先画线,标尺稍后作为覆盖层绘制,避免被波形遮挡)。
|
||||||
@@ -5383,12 +5575,12 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void PlotView::DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius, bool textDark)
|
void PlotView::DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius,
|
||||||
|
std::uint32_t outlineColor)
|
||||||
{
|
{
|
||||||
// 用与文字同色的半透明细描边:深色文字 → 浅色描边,浅色文字 → 深色描边。
|
// 描边色由 `UiContrastInk` 与文字色一起算出(恒与文字色反相、且随信号配色线性变化)。
|
||||||
// 这样胶囊压在同类色背景或信号曲线上时仍有清晰的边界。
|
// 这样胶囊压在同类色背景或信号曲线上时仍有清晰的边界。
|
||||||
const std::uint32_t outline = textDark ? 0x99FDF6E3u : 0x99073642u;
|
UiDrawRoundRect(g, rect, radius, outlineColor, 1.0f);
|
||||||
UiDrawRoundRect(g, rect, radius, outline, 1.0f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gd::RectF PlotView::CursorSpanRect(bool& insideAxis) const
|
gd::RectF PlotView::CursorSpanRect(bool& insideAxis) const
|
||||||
@@ -5549,16 +5741,18 @@ namespace jdi
|
|||||||
const float rawY = ValueToY(value, lane);
|
const float rawY = ValueToY(value, lane);
|
||||||
// 上下越界的数据点不再被丢弃:与左右越界一致,贴边绘制指示方向的三角。
|
// 上下越界的数据点不再被丢弃:与左右越界一致,贴边绘制指示方向的三角。
|
||||||
// 上边界额外预留光标顶部把手的高度,避免数据点三角压住光标把手。
|
// 上边界额外预留光标顶部把手的高度,避免数据点三角压住光标把手。
|
||||||
const DataMarker marker =
|
// 非 const:上下越界时下面还要把方向三角与取值标注纵向居中对齐。
|
||||||
|
DataMarker marker =
|
||||||
MakeDataMarker(rawX, rawY, pinX, clipTop, clipBottom, kCursorHandleReserve * m_scale);
|
MakeDataMarker(rawX, rawY, pinX, clipTop, clipBottom, kCursorHandleReserve * m_scale);
|
||||||
|
|
||||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||||
|
|
||||||
// 数据点:视界内为圆点,任一方向越界时改为贴边三角。
|
// 数据点:视界内为圆点,任一方向越界时改为贴边三角。
|
||||||
|
// **先算好取值标注、再画数据点**:上下越界时的方向三角需要与标注
|
||||||
|
// 纵向居中对齐,而标注的位置要经过避让处理才能最终确定。
|
||||||
const float radius = 3.4f * m_scale;
|
const float radius = 3.4f * m_scale;
|
||||||
PaintDataMarker(g, marker, radius, signalColor, m_theme->cardBg);
|
|
||||||
|
|
||||||
// 取值标注:底色用信号色、文字用反色,并加一层细描边提升对比度。
|
// 取值标注:底色用信号色,文字/描边用随信号配色线性相关的高对比度配色。
|
||||||
std::wstring text = FormatValue(value);
|
std::wstring text = FormatValue(value);
|
||||||
if (showDelta)
|
if (showDelta)
|
||||||
{
|
{
|
||||||
@@ -5593,11 +5787,18 @@ namespace jdi
|
|||||||
// 依次向下(再向上)避让已占用的标注,保证不会互相覆盖。
|
// 依次向下(再向上)避让已占用的标注,保证不会互相覆盖。
|
||||||
PlaceCursorLabel(labelRect, labelTop, textHeight, clipTop, clipBottom);
|
PlaceCursorLabel(labelRect, labelTop, textHeight, clipTop, clipBottom);
|
||||||
|
|
||||||
|
// 上下越界的方向三角与最终落位的取值标注纵向居中对齐
|
||||||
|
// (用户明确要求“超过上下视界时三角需要与显示的数值居中对齐”)。
|
||||||
|
if (marker.offUp || marker.offDown)
|
||||||
|
{
|
||||||
|
marker.y = labelRect.Y + labelRect.Height * 0.5f;
|
||||||
|
}
|
||||||
|
PaintDataMarker(g, marker, radius, signalColor, m_theme->cardBg);
|
||||||
|
|
||||||
|
const UiInk ink = UiContrastInk(signalColor);
|
||||||
UiFillRoundRect(g, labelRect, 3.0f * m_scale, WithAlpha(signalColor, 0xF0));
|
UiFillRoundRect(g, labelRect, 3.0f * m_scale, WithAlpha(signalColor, 0xF0));
|
||||||
const bool dark = IsDarkColor(signalColor);
|
DrawLabelOutline(g, labelRect, 3.0f * m_scale, ink.outline);
|
||||||
DrawLabelOutline(g, labelRect, 3.0f * m_scale, dark);
|
UiText(g, text, m_fonts->monoSmall, labelRect, ink.text, TextAlign::CENTER);
|
||||||
UiText(g, text, m_fonts->monoSmall, labelRect,
|
|
||||||
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
|
|
||||||
|
|
||||||
// 「边沿计数(中心轴)」/「均值滤波」开启时,纵向光标还要额外标注**派生曲线**
|
// 「边沿计数(中心轴)」/「均值滤波」开启时,纵向光标还要额外标注**派生曲线**
|
||||||
// 在该时刻的取值(它不是信号,因此单独标注,不并入上面的取值文案)。
|
// 在该时刻的取值(它不是信号,因此单独标注,不并入上面的取值文案)。
|
||||||
@@ -5617,12 +5818,11 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const float rawCenterY = ValueToY(derivedValue, lane);
|
const float rawCenterY = ValueToY(derivedValue, lane);
|
||||||
const DataMarker centerMarker = MakeDataMarker(
|
DataMarker centerMarker = MakeDataMarker(
|
||||||
rawX, rawCenterY, pinX, clipTop, clipBottom, kCursorHandleReserve * m_scale);
|
rawX, rawCenterY, pinX, clipTop, clipBottom, kCursorHandleReserve * m_scale);
|
||||||
// 派生数据点的配色与原始信号一致(便于一一对应);
|
// 派生数据点的配色与原始信号一致(便于一一对应);
|
||||||
// 但**始终按线段模式**绘制(不跟随原信号的散点模式)。
|
// 但**始终按线段模式**绘制(不跟随原信号的散点模式)。
|
||||||
const std::uint32_t centerColor = StyleColor(*resolved.style);
|
const std::uint32_t centerColor = StyleColor(*resolved.style);
|
||||||
PaintDataMarker(g, centerMarker, radius, centerColor, m_theme->cardBg);
|
|
||||||
|
|
||||||
// 取值文本:数值在前,尾缀「(中心轴)/(均值滤波)」;双光标的右侧光标
|
// 取值文本:数值在前,尾缀「(中心轴)/(均值滤波)」;双光标的右侧光标
|
||||||
// 还要附上增量,形如「(+0.0008 中心轴)」(参考常规信号的增量显示)。
|
// 还要附上增量,形如「(+0.0008 中心轴)」(参考常规信号的增量显示)。
|
||||||
@@ -5665,12 +5865,18 @@ namespace jdi
|
|||||||
std::min(centerTop, clipBottom - textHeight - 1.0f * m_scale));
|
std::min(centerTop, clipBottom - textHeight - 1.0f * m_scale));
|
||||||
gd::RectF centerRect(centerLeft, centerTop, centerTextWidth, textHeight);
|
gd::RectF centerRect(centerLeft, centerTop, centerTextWidth, textHeight);
|
||||||
PlaceCursorLabel(centerRect, centerTop, textHeight, clipTop, clipBottom);
|
PlaceCursorLabel(centerRect, centerTop, textHeight, clipTop, clipBottom);
|
||||||
|
// 与原始信号同一规则:上下越界的方向三角与取值标注纵向居中对齐。
|
||||||
|
if (centerMarker.offUp || centerMarker.offDown)
|
||||||
|
{
|
||||||
|
centerMarker.y = centerRect.Y + centerRect.Height * 0.5f;
|
||||||
|
}
|
||||||
|
PaintDataMarker(g, centerMarker, radius, centerColor, m_theme->cardBg);
|
||||||
// 颜色样式与原始信号一致(同底色/同描边/同文字配色)。
|
// 颜色样式与原始信号一致(同底色/同描边/同文字配色)。
|
||||||
const bool centerDark = IsDarkColor(centerColor);
|
const UiInk centerInk = UiContrastInk(centerColor);
|
||||||
UiFillRoundRect(g, centerRect, 3.0f * m_scale, WithAlpha(centerColor, 0xE0));
|
UiFillRoundRect(g, centerRect, 3.0f * m_scale, WithAlpha(centerColor, 0xE0));
|
||||||
DrawLabelOutline(g, centerRect, 3.0f * m_scale, centerDark);
|
DrawLabelOutline(g, centerRect, 3.0f * m_scale, centerInk.outline);
|
||||||
UiText(g, centerText, m_fonts->monoSmall, centerRect,
|
UiText(g, centerText, m_fonts->monoSmall, centerRect, centerInk.text,
|
||||||
centerDark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
|
TextAlign::CENTER);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (m_tab->Lanes()[laneIndex].edgeCenter)
|
if (m_tab->Lanes()[laneIndex].edgeCenter)
|
||||||
@@ -5705,11 +5911,10 @@ namespace jdi
|
|||||||
const std::uint32_t color = CursorColor();
|
const std::uint32_t color = CursorColor();
|
||||||
UiFillRoundRect(g, labelRect, 3.0f * m_scale,
|
UiFillRoundRect(g, labelRect, 3.0f * m_scale,
|
||||||
highlighted ? color : WithAlpha(color, 0xE0));
|
highlighted ? color : WithAlpha(color, 0xE0));
|
||||||
const bool dark = IsDarkColor(color);
|
const UiInk ink = UiContrastInk(color);
|
||||||
DrawLabelOutline(g, labelRect, 3.0f * m_scale, dark);
|
DrawLabelOutline(g, labelRect, 3.0f * m_scale, ink.outline);
|
||||||
UiText(g, FormatCursorTime(which == 2 ? Tab().CursorTimeAlt() : Tab().CursorTime()),
|
UiText(g, FormatCursorTime(which == 2 ? Tab().CursorTimeAlt() : Tab().CursorTime()),
|
||||||
m_fonts->monoSmall, labelRect,
|
m_fonts->monoSmall, labelRect, ink.text, TextAlign::CENTER);
|
||||||
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 双光标差值:显示在两条时间读数中间(时间轴上方;过近时落到主时间轴上)。
|
// 双光标差值:显示在两条时间读数中间(时间轴上方;过近时落到主时间轴上)。
|
||||||
@@ -5724,10 +5929,10 @@ namespace jdi
|
|||||||
const std::uint32_t color = CursorColor();
|
const std::uint32_t color = CursorColor();
|
||||||
UiFillRoundRect(g, spanRect, 3.0f * m_scale,
|
UiFillRoundRect(g, spanRect, 3.0f * m_scale,
|
||||||
m_spanHover ? color : WithAlpha(color, 0xE0));
|
m_spanHover ? color : WithAlpha(color, 0xE0));
|
||||||
const bool dark = IsDarkColor(color);
|
const UiInk ink = UiContrastInk(color);
|
||||||
DrawLabelOutline(g, spanRect, 3.0f * m_scale, dark);
|
DrawLabelOutline(g, spanRect, 3.0f * m_scale, ink.outline);
|
||||||
UiText(g, FormatSpanText(CursorSpan()), m_fonts->monoSmall, spanRect,
|
UiText(g, FormatSpanText(CursorSpan()), m_fonts->monoSmall, spanRect, ink.text,
|
||||||
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
|
TextAlign::CENTER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -5868,15 +6073,33 @@ namespace jdi
|
|||||||
float clipBottom, float topReserve) const
|
float clipBottom, float topReserve) const
|
||||||
{
|
{
|
||||||
DataMarker marker;
|
DataMarker marker;
|
||||||
// 上下越界时"钉"在网格可见边缘(留出三角形的高度,保证整体可见);
|
// 形状**只**由“数据点本身是否越出绘制边界”决定:
|
||||||
// 另外预留 topReserve(时间光标的顶部三角把手),避免数据点三角压住把手。
|
// 旧实现拿“夹取之后”的位置反推越界方向,于是贴近边界、其实仍在视界内的数据点
|
||||||
const float margin = 7.0f * m_scale;
|
// 也会被误画成三角(用户报告的“数值显示超出边界就改变数据点形状”)。
|
||||||
|
const float left = m_plotRect.X;
|
||||||
|
const float right = m_plotRect.X + m_plotRect.Width;
|
||||||
|
marker.offLeft = (rawX < left);
|
||||||
|
marker.offRight = (rawX > right);
|
||||||
|
marker.offUp = (rawY < clipTop);
|
||||||
|
marker.offDown = (rawY > clipBottom);
|
||||||
|
|
||||||
marker.x = pinX;
|
marker.x = pinX;
|
||||||
marker.y = std::max(clipTop + margin + topReserve, std::min(rawY, clipBottom - margin));
|
if (marker.offUp)
|
||||||
marker.offLeft = (rawX < marker.x - 0.5f);
|
{
|
||||||
marker.offRight = (rawX > marker.x + 0.5f);
|
// 上越界:为方向三角与顶部把手预留高度,避免压住把手。
|
||||||
marker.offUp = (rawY < marker.y - 0.5f);
|
marker.y = clipTop + kMarkerEdgeInset * m_scale + topReserve;
|
||||||
marker.offDown = (rawY > marker.y + 0.5f);
|
}
|
||||||
|
else if (marker.offDown)
|
||||||
|
{
|
||||||
|
marker.y = clipBottom - kMarkerEdgeInset * m_scale;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 视界内:保留数据点的真实纵坐标(越界判定已在上面完成,这里夹取只为了让
|
||||||
|
// 圆点整体可见,不会影响形状)。
|
||||||
|
marker.y = std::max(clipTop + kMarkerInsideMargin * m_scale,
|
||||||
|
std::min(rawY, clipBottom - kMarkerInsideMargin * m_scale));
|
||||||
|
}
|
||||||
return marker;
|
return marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5885,44 +6108,56 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
gd::SolidBrush ring(ToGdiColor(ringColor));
|
gd::SolidBrush ring(ToGdiColor(ringColor));
|
||||||
gd::SolidBrush fill(ToGdiColor(fillColor));
|
gd::SolidBrush fill(ToGdiColor(fillColor));
|
||||||
const float arm = 5.0f * m_scale;
|
const float arm = 5.0f * m_scale; /**< 三角“半宽/半高”(垂直于朝向的半边长)。 */
|
||||||
const float inset = 1.4f * m_scale;
|
const float depth = arm * 1.4f; /**< 三角沿朝向的深度(与光标把手同比例)。 */
|
||||||
|
|
||||||
// 左右越界:三角底边在外侧、尖端朝向网格内部。
|
// 越界三角:**尖端朝外**(指向数据点所在的方向,即视界之外),底边在网格内侧;
|
||||||
|
// 三角外接矩形在横/纵两个方向都与锚点**居中**(上下越界时因此与取值标注纵向居中)。
|
||||||
|
// 旧实现方向是反的(上越界画下三角、左越界画右三角),而且内三角按“平移”而非
|
||||||
|
// “向质心收缩”构造,填充色会从三角尖端戳出来。
|
||||||
|
const auto paintTriangle = [&](const gd::PointF* outer)
|
||||||
|
{
|
||||||
|
g.FillPolygon(&ring, outer, 3);
|
||||||
|
const float centerX = (outer[0].X + outer[1].X + outer[2].X) / 3.0f;
|
||||||
|
const float centerY = (outer[0].Y + outer[1].Y + outer[2].Y) / 3.0f;
|
||||||
|
// 按质心等比收缩得到内三角:几何上恒在外三角内部,描边宽度也就均匀了。
|
||||||
|
const float shrink = 0.58f;
|
||||||
|
gd::PointF inner[3];
|
||||||
|
for (int index = 0; index < 3; ++index)
|
||||||
|
{
|
||||||
|
inner[index] = gd::PointF(centerX + (outer[index].X - centerX) * shrink,
|
||||||
|
centerY + (outer[index].Y - centerY) * shrink);
|
||||||
|
}
|
||||||
|
g.FillPolygon(&fill, inner, 3);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 左右越界:三角指向左/右(视界之外)。
|
||||||
if (marker.offLeft || marker.offRight)
|
if (marker.offLeft || marker.offRight)
|
||||||
{
|
{
|
||||||
const bool offLeft = marker.offLeft;
|
const bool offLeft = marker.offLeft;
|
||||||
const float baseX = offLeft ? (marker.x - arm) : (marker.x + arm);
|
const float tipX = offLeft ? (marker.x - depth) : (marker.x + depth);
|
||||||
const float inward = offLeft ? inset : -inset;
|
const float baseX = marker.x;
|
||||||
gd::PointF outer[3];
|
gd::PointF outer[3];
|
||||||
outer[0] = gd::PointF(baseX, marker.y - arm);
|
outer[0] = gd::PointF(baseX, marker.y - arm);
|
||||||
outer[1] = gd::PointF(baseX, marker.y + arm);
|
outer[1] = gd::PointF(baseX, marker.y + arm);
|
||||||
outer[2] = gd::PointF(marker.x, marker.y);
|
outer[2] = gd::PointF(tipX, marker.y);
|
||||||
g.FillPolygon(&ring, outer, 3);
|
paintTriangle(outer);
|
||||||
gd::PointF inner[3];
|
|
||||||
inner[0] = gd::PointF(baseX + inward, marker.y - arm * 0.72f);
|
|
||||||
inner[1] = gd::PointF(baseX + inward, marker.y + arm * 0.72f);
|
|
||||||
inner[2] = gd::PointF(marker.x + inward, marker.y);
|
|
||||||
g.FillPolygon(&fill, inner, 3);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上下越界:三角底边在上/下方、尖端朝向网格内部(与左右越界同一套观感)。
|
// 上下越界:三角指向上/下(视界之外),外接矩形以 marker.y 为纵向中心
|
||||||
|
// → 与取值标注的纵向中心对齐。
|
||||||
if (marker.offUp || marker.offDown)
|
if (marker.offUp || marker.offDown)
|
||||||
{
|
{
|
||||||
const bool offUp = marker.offUp;
|
const bool offUp = marker.offUp;
|
||||||
const float baseY = offUp ? (marker.y - arm) : (marker.y + arm);
|
const float half = depth * 0.5f;
|
||||||
const float inward = offUp ? inset : -inset;
|
const float tipY = offUp ? (marker.y - half) : (marker.y + half);
|
||||||
|
const float baseY = offUp ? (marker.y + half) : (marker.y - half);
|
||||||
gd::PointF outer[3];
|
gd::PointF outer[3];
|
||||||
outer[0] = gd::PointF(marker.x - arm, baseY);
|
outer[0] = gd::PointF(marker.x - arm, baseY);
|
||||||
outer[1] = gd::PointF(marker.x + arm, baseY);
|
outer[1] = gd::PointF(marker.x + arm, baseY);
|
||||||
outer[2] = gd::PointF(marker.x, marker.y);
|
outer[2] = gd::PointF(marker.x, tipY);
|
||||||
g.FillPolygon(&ring, outer, 3);
|
paintTriangle(outer);
|
||||||
gd::PointF inner[3];
|
|
||||||
inner[0] = gd::PointF(marker.x - arm * 0.72f, baseY + inward);
|
|
||||||
inner[1] = gd::PointF(marker.x + arm * 0.72f, baseY + inward);
|
|
||||||
inner[2] = gd::PointF(marker.x, marker.y + inward);
|
|
||||||
g.FillPolygon(&fill, inner, 3);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6115,7 +6350,7 @@ namespace jdi
|
|||||||
std::vector<Lane>& tabLanes = m_tab->Lanes();
|
std::vector<Lane>& tabLanes = m_tab->Lanes();
|
||||||
const std::size_t count = std::min(lanes.size(), tabLanes.size());
|
const std::size_t count = std::min(lanes.size(), tabLanes.size());
|
||||||
const std::uint32_t color = CursorColor();
|
const std::uint32_t color = CursorColor();
|
||||||
const bool dark = IsDarkColor(color);
|
const UiInk ink = UiContrastInk(color);
|
||||||
|
|
||||||
// ---- 横向光标(线与两端把手 + 左侧幅值读数)----
|
// ---- 横向光标(线与两端把手 + 左侧幅值读数)----
|
||||||
for (std::size_t index = 0; index < count; ++index)
|
for (std::size_t index = 0; index < count; ++index)
|
||||||
@@ -6172,9 +6407,8 @@ namespace jdi
|
|||||||
clipBottom - textHeight - 1.0f * m_scale));
|
clipBottom - textHeight - 1.0f * m_scale));
|
||||||
const gd::RectF labelRect(labelLeft, labelTop, textWidth, textHeight);
|
const gd::RectF labelRect(labelLeft, labelTop, textWidth, textHeight);
|
||||||
UiFillRoundRect(g, labelRect, 3.0f * m_scale, WithAlpha(color, 0xE0));
|
UiFillRoundRect(g, labelRect, 3.0f * m_scale, WithAlpha(color, 0xE0));
|
||||||
DrawLabelOutline(g, labelRect, 3.0f * m_scale, dark);
|
DrawLabelOutline(g, labelRect, 3.0f * m_scale, ink.outline);
|
||||||
UiText(g, text, m_fonts->monoSmall, labelRect, dark ? 0xFFFDF6E3u : 0xFF073642u,
|
UiText(g, text, m_fonts->monoSmall, labelRect, ink.text, TextAlign::CENTER);
|
||||||
TextAlign::CENTER);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 统计浮动面板(最后绘制,覆盖在波形之上)----
|
// ---- 统计浮动面板(最后绘制,覆盖在波形之上)----
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "PaintUtil.hpp"
|
#include "PaintUtil.hpp"
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
@@ -180,6 +181,17 @@ namespace jdi
|
|||||||
HEX = 4 /**< 十六进制色号。 */
|
HEX = 4 /**< 十六进制色号。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 面板内正在拖拽的控件。
|
||||||
|
*/
|
||||||
|
enum class DragTarget : int
|
||||||
|
{
|
||||||
|
NONE = 0, /**< 未拖拽。 */
|
||||||
|
SLIDER = 1, /**< RGB 分量滑块(具体下标见 `m_draggingSlider`)。 */
|
||||||
|
GAMUT = 2, /**< 色域方块(饱和度-明度平面)。 */
|
||||||
|
HUE = 3 /**< 色相条。 */
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 依据锚点矩形与客户区计算面板矩形与内部控件矩形。
|
* @brief 依据锚点矩形与客户区计算面板矩形与内部控件矩形。
|
||||||
* @param [IN] bounds 宿主客户区矩形。
|
* @param [IN] bounds 宿主客户区矩形。
|
||||||
@@ -222,6 +234,41 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void ApplySlider(int index, float x);
|
void ApplySlider(int index, float x);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在色域方块内按下/拖拽:按位置取出饱和度与明度。
|
||||||
|
* @param [IN] x 客户区横坐标。
|
||||||
|
* @param [IN] y 客户区纵坐标。
|
||||||
|
*/
|
||||||
|
void ApplyGamut(float x, float y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在色相条内按下/拖拽:按纵向位置取出色相。
|
||||||
|
* @param [IN] y 客户区纵坐标。
|
||||||
|
*/
|
||||||
|
void ApplyHue(float y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 当前有效色相(0-1)。灰度/黑色时 HSV 的色相无定义,沿用上一次的有效值。
|
||||||
|
* @return 色相。
|
||||||
|
*/
|
||||||
|
double CurrentHue() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief RGB 分量变化后刷新记忆色相(仅在饱和度足够大时更新)。
|
||||||
|
*/
|
||||||
|
void RefreshHueFromChannels();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 销毁色域方块缓存位图(尺寸或色相变化时重建)。
|
||||||
|
*/
|
||||||
|
void InvalidateGamutCache();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 确保色域方块缓存与当前色相/缩放匹配(必要时重建)。
|
||||||
|
* @return 可用返回 true。
|
||||||
|
*/
|
||||||
|
bool EnsureGamutCache();
|
||||||
|
|
||||||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||||||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||||
HWND m_owner = nullptr; /**< 剪贴板所有者窗口句柄。 */
|
HWND m_owner = nullptr; /**< 剪贴板所有者窗口句柄。 */
|
||||||
@@ -231,7 +278,9 @@ namespace jdi
|
|||||||
bool m_changed = false; /**< 取值是否发生变化。 */
|
bool m_changed = false; /**< 取值是否发生变化。 */
|
||||||
int m_draggingSlider = -1; /**< 正在拖拽的滑块下标,-1 表示未拖拽。 */
|
int m_draggingSlider = -1; /**< 正在拖拽的滑块下标,-1 表示未拖拽。 */
|
||||||
int m_hoverSlider = -1; /**< 当前悬停的滑块下标。 */
|
int m_hoverSlider = -1; /**< 当前悬停的滑块下标。 */
|
||||||
std::array<double, 3> m_channels = {0.0, 0.0, 0.0}; /**< RGB 分量(0-1)。 */
|
DragTarget m_drag = DragTarget::NONE; /**< 正在拖拽的控件。 */
|
||||||
|
std::array<double, 3> m_channels = {0.0, 0.0, 0.0}; /**< RGB 分量(0-1,唯一数据源)。 */
|
||||||
|
double m_hue = 0.0; /**< 记忆色相(0-1);灰度/黑色时保留上次取值。 */
|
||||||
|
|
||||||
gd::RectF m_rect; /**< 面板整体矩形。 */
|
gd::RectF m_rect; /**< 面板整体矩形。 */
|
||||||
gd::RectF m_titleRect; /**< 标题区域。 */
|
gd::RectF m_titleRect; /**< 标题区域。 */
|
||||||
@@ -239,6 +288,13 @@ namespace jdi
|
|||||||
std::array<gd::RectF, 3> m_values; /**< 数值文本矩形。 */
|
std::array<gd::RectF, 3> m_values; /**< 数值文本矩形。 */
|
||||||
gd::RectF m_previewRect; /**< 预览色块矩形。 */
|
gd::RectF m_previewRect; /**< 预览色块矩形。 */
|
||||||
gd::RectF m_hexRect; /**< 十六进制文本矩形。 */
|
gd::RectF m_hexRect; /**< 十六进制文本矩形。 */
|
||||||
|
gd::RectF m_gamutRect; /**< 色域方块矩形(横轴 S,纵轴 V)。 */
|
||||||
|
gd::RectF m_hueRect; /**< 色相条矩形(纵轴色相)。 */
|
||||||
|
|
||||||
|
/** 色域方块缓存位图:按当前色相预渲染,避免每帧重画数十条渐变。 */
|
||||||
|
std::unique_ptr<gd::Bitmap> m_gamutCache;
|
||||||
|
double m_gamutCacheHue = -1.0; /**< 缓存对应的色相。 */
|
||||||
|
int m_gamutCacheSize = 0; /**< 缓存位图边长(像素)。 */
|
||||||
|
|
||||||
EditField m_editField = EditField::NONE; /**< 当前处于输入状态的字段。 */
|
EditField m_editField = EditField::NONE; /**< 当前处于输入状态的字段。 */
|
||||||
TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */
|
TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */
|
||||||
|
|||||||
@@ -293,6 +293,40 @@ namespace jdi
|
|||||||
void UiDrawEditFieldContent(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font,
|
void UiDrawEditFieldContent(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font,
|
||||||
float scale);
|
float scale);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 取值胶囊底色上的高对比度文字色与描边色。
|
||||||
|
*/
|
||||||
|
struct UiInk
|
||||||
|
{
|
||||||
|
std::uint32_t text = 0xFF073642u; /**< 文字色(不透明)。 */
|
||||||
|
std::uint32_t outline = 0x99FDF6E3u; /**< 描边色(半透明,恒与文字色反相)。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 颜色的相对亮度(0-1,逐通道线性加权)。
|
||||||
|
* @param [IN] color 0xAARRGGBB 颜色值。
|
||||||
|
* @return 相对亮度。
|
||||||
|
*/
|
||||||
|
double UiColorLuminance(std::uint32_t color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 依据胶囊底色求高对比度的文字色与描边色(对底色配色作线性响应)。
|
||||||
|
*
|
||||||
|
* 求值分两步:
|
||||||
|
* 1. **亮度**:按"取最大对比"的一侧选定亮文字或暗文字。这是唯一的阈值判断,但它
|
||||||
|
* 本身就是"任意底色下都保持高对比"的必要条件 —— 任何连续的亮度取值都会在中性灰
|
||||||
|
* 附近同时远离两侧极值,对比度必然塌陷。
|
||||||
|
* 2. **配色**:把该侧的极值色与**底色的互补色**按固定比例线性混合。互补色的相对亮度
|
||||||
|
* 恰为 `1 - 底色亮度`,因此文字/描边的亮度是底色亮度的**线性**函数,色相也随信号
|
||||||
|
* 配色连续变化(不再是固定的黑/白两色)。
|
||||||
|
*
|
||||||
|
* 描边色取相反的一侧,保证"描边—文字—底色"三段式对比。
|
||||||
|
*
|
||||||
|
* @param [IN] background 胶囊底色(0xAARRGGBB)。
|
||||||
|
* @return 文字色与描边色。
|
||||||
|
*/
|
||||||
|
UiInk UiContrastInk(std::uint32_t background);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绘制一个带边框的复选框。
|
* @brief 绘制一个带边框的复选框。
|
||||||
* @param [IN,OUT] g 绘图上下文。
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ namespace jdi
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
const float kPanelWidth = 214.0f; /**< 面板宽度(逻辑像素)。 */
|
const float kPanelWidth = 324.0f; /**< 面板宽度(逻辑像素)。 */
|
||||||
const float kPadH = 12.0f; /**< 面板左右内边距。 */
|
const float kPadH = 12.0f; /**< 面板左右内边距。 */
|
||||||
const float kPadV = 10.0f; /**< 面板上下内边距。 */
|
const float kPadV = 10.0f; /**< 面板上下内边距。 */
|
||||||
const float kTitleHeight = 20.0f; /**< 标题行高度。 */
|
const float kTitleHeight = 20.0f; /**< 标题行高度。 */
|
||||||
const float kTitleGap = 8.0f; /**< 标题与滑块区间距。 */
|
const float kTitleGap = 8.0f; /**< 标题与内容区间距。 */
|
||||||
const float kRowHeight = 26.0f; /**< 单个滑块行高度。 */
|
const float kRowHeight = 26.0f; /**< 单个滑块行高度。 */
|
||||||
const float kRowGap = 2.0f; /**< 滑块行间距。 */
|
const float kRowGap = 2.0f; /**< 滑块行间距。 */
|
||||||
const float kPreviewGap = 10.0f; /**< 滑块区与预览区间距。 */
|
const float kPreviewGap = 10.0f; /**< 滑块区与预览区间距。 */
|
||||||
@@ -25,6 +25,11 @@ namespace jdi
|
|||||||
const float kValueWidth = 40.0f; /**< 数值文本宽度。 */
|
const float kValueWidth = 40.0f; /**< 数值文本宽度。 */
|
||||||
const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */
|
const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */
|
||||||
const float kCorner = 6.0f; /**< 面板圆角半径。 */
|
const float kCorner = 6.0f; /**< 面板圆角半径。 */
|
||||||
|
const float kGamutSize = 112.0f; /**< 色域方块边长。 */
|
||||||
|
const float kHueWidth = 14.0f; /**< 色相条宽度。 */
|
||||||
|
const float kGamutGap = 10.0f; /**< 色域方块与色相条间距。 */
|
||||||
|
const float kColumnGap = 12.0f; /**< 色相条与滑块列间距。 */
|
||||||
|
const float kGamutBorder = 1.0f; /**< 色域方块/色相条描边宽度。 */
|
||||||
|
|
||||||
/** @brief 通道显示名称。 */
|
/** @brief 通道显示名称。 */
|
||||||
const wchar_t* kChannelNames[3] = {L"R", L"G", L"B"};
|
const wchar_t* kChannelNames[3] = {L"R", L"G", L"B"};
|
||||||
@@ -32,6 +37,116 @@ namespace jdi
|
|||||||
/** @brief 通道滑块填充色。 */
|
/** @brief 通道滑块填充色。 */
|
||||||
const std::uint32_t kChannelColors[3] = {0xFFDC322Fu, 0xFF859900u, 0xFF268BD2u};
|
const std::uint32_t kChannelColors[3] = {0xFFDC322Fu, 0xFF859900u, 0xFF268BD2u};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 把数值限制到 0-1。
|
||||||
|
* @param [IN] value 输入值。
|
||||||
|
* @return 限制后的值。
|
||||||
|
*/
|
||||||
|
double Clamp01(double value)
|
||||||
|
{
|
||||||
|
if (value < 0.0)
|
||||||
|
{
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
if (value > 1.0)
|
||||||
|
{
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief HSV 颜色(色相/饱和度/明度,均为 0-1)。
|
||||||
|
*/
|
||||||
|
struct Hsv
|
||||||
|
{
|
||||||
|
double h = 0.0; /**< 色相。 */
|
||||||
|
double s = 0.0; /**< 饱和度。 */
|
||||||
|
double v = 0.0; /**< 明度。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief RGB(0-1 分量)转 HSV。
|
||||||
|
* @param [IN] rgb RGB 分量。
|
||||||
|
* @return HSV 值(灰色时色相为 0)。
|
||||||
|
*/
|
||||||
|
Hsv ToHsv(const std::array<double, 3>& rgb)
|
||||||
|
{
|
||||||
|
const double maximum = std::max(rgb[0], std::max(rgb[1], rgb[2]));
|
||||||
|
const double minimum = std::min(rgb[0], std::min(rgb[1], rgb[2]));
|
||||||
|
const double delta = maximum - minimum;
|
||||||
|
|
||||||
|
Hsv hsv;
|
||||||
|
hsv.v = maximum;
|
||||||
|
hsv.s = (maximum > 1.0e-9) ? (delta / maximum) : 0.0;
|
||||||
|
if (delta <= 1.0e-9)
|
||||||
|
{
|
||||||
|
hsv.h = 0.0;
|
||||||
|
return hsv;
|
||||||
|
}
|
||||||
|
|
||||||
|
double hue = 0.0;
|
||||||
|
if (maximum == rgb[0])
|
||||||
|
{
|
||||||
|
hue = (rgb[1] - rgb[2]) / delta;
|
||||||
|
}
|
||||||
|
else if (maximum == rgb[1])
|
||||||
|
{
|
||||||
|
hue = 2.0 + (rgb[2] - rgb[0]) / delta;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hue = 4.0 + (rgb[0] - rgb[1]) / delta;
|
||||||
|
}
|
||||||
|
hue /= 6.0;
|
||||||
|
if (hue < 0.0)
|
||||||
|
{
|
||||||
|
hue += 1.0;
|
||||||
|
}
|
||||||
|
hsv.h = hue;
|
||||||
|
return hsv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief HSV 转 RGB(0-1 分量)。
|
||||||
|
* @param [IN] h 色相(0-1,自动取模)。
|
||||||
|
* @param [IN] s 饱和度(0-1)。
|
||||||
|
* @param [IN] v 明度(0-1)。
|
||||||
|
* @return RGB 分量。
|
||||||
|
*/
|
||||||
|
std::array<double, 3> FromHsv(double h, double s, double v)
|
||||||
|
{
|
||||||
|
h = h - std::floor(h);
|
||||||
|
s = Clamp01(s);
|
||||||
|
v = Clamp01(v);
|
||||||
|
if (s <= 1.0e-9)
|
||||||
|
{
|
||||||
|
return {v, v, v};
|
||||||
|
}
|
||||||
|
|
||||||
|
const double sector = h * 6.0;
|
||||||
|
const int index = static_cast<int>(std::floor(sector)) % 6;
|
||||||
|
const double fraction = sector - std::floor(sector);
|
||||||
|
const double p = v * (1.0 - s);
|
||||||
|
const double q = v * (1.0 - s * fraction);
|
||||||
|
const double t = v * (1.0 - s * (1.0 - fraction));
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
return {v, t, p};
|
||||||
|
case 1:
|
||||||
|
return {q, v, p};
|
||||||
|
case 2:
|
||||||
|
return {p, v, t};
|
||||||
|
case 3:
|
||||||
|
return {p, q, v};
|
||||||
|
case 4:
|
||||||
|
return {t, p, v};
|
||||||
|
default:
|
||||||
|
return {v, p, q};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绘制水平滑块(与样式区共用同一实现,保证行为一致)。
|
* @brief 绘制水平滑块(与样式区共用同一实现,保证行为一致)。
|
||||||
* @param [IN,OUT] g 绘图上下文。
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
@@ -102,9 +217,11 @@ namespace jdi
|
|||||||
void ColorPickerPopup::BuildLayout(const gd::RectF& bounds, const gd::RectF& anchor)
|
void ColorPickerPopup::BuildLayout(const gd::RectF& bounds, const gd::RectF& anchor)
|
||||||
{
|
{
|
||||||
const float width = kPanelWidth * m_scale;
|
const float width = kPanelWidth * m_scale;
|
||||||
const float height = (kPadV * 2.0f + kTitleHeight + kTitleGap + kRowHeight * 3.0f + kRowGap * 2.0f +
|
// 高度:标题 + 内容区。内容区里"色域方块"是最高的控件,
|
||||||
kPreviewGap + kPreviewHeight) *
|
// 预览行/色号行落在它与滑块列的下方,仍在其纵向范围内,不再额外叠加高度。
|
||||||
m_scale;
|
const float contentHeight = std::max(kGamutSize, kRowHeight * 3.0f + kRowGap * 2.0f +
|
||||||
|
kPreviewGap + kPreviewHeight);
|
||||||
|
const float height = (kPadV * 2.0f + kTitleHeight + kTitleGap + contentHeight) * m_scale;
|
||||||
|
|
||||||
const float gap = 4.0f * m_scale;
|
const float gap = 4.0f * m_scale;
|
||||||
|
|
||||||
@@ -129,14 +246,26 @@ namespace jdi
|
|||||||
m_titleRect = gd::RectF(left + kPadH * m_scale, top + kPadV * m_scale,
|
m_titleRect = gd::RectF(left + kPadH * m_scale, top + kPadV * m_scale,
|
||||||
width - kPadH * 2.0f * m_scale, kTitleHeight * m_scale);
|
width - kPadH * 2.0f * m_scale, kTitleHeight * m_scale);
|
||||||
|
|
||||||
const float trackLeft = left + (kPadH + kLabelWidth + kLabelGap) * m_scale;
|
const float contentTop = m_titleRect.Y + m_titleRect.Height + kTitleGap * m_scale;
|
||||||
|
|
||||||
|
// 左侧:色域方块(横轴饱和度、纵轴明度)+ 色相条。
|
||||||
|
m_gamutRect = gd::RectF(left + kPadH * m_scale, contentTop, kGamutSize * m_scale,
|
||||||
|
kGamutSize * m_scale);
|
||||||
|
m_hueRect = gd::RectF(m_gamutRect.X + m_gamutRect.Width + kGamutGap * m_scale, contentTop,
|
||||||
|
kHueWidth * m_scale, kGamutSize * m_scale);
|
||||||
|
|
||||||
|
// 右侧:R/G/B 滑块 + 数值,紧随色相条之后。
|
||||||
|
const float columnLeft = m_hueRect.X + m_hueRect.Width + kColumnGap * m_scale;
|
||||||
|
const float columnWidth =
|
||||||
|
std::max(80.0f * m_scale, m_rect.X + m_rect.Width - kPadH * m_scale - columnLeft);
|
||||||
|
const float trackLeft = columnLeft + (kLabelWidth + kLabelGap) * m_scale;
|
||||||
const float trackWidth =
|
const float trackWidth =
|
||||||
std::max(40.0f, width - (trackLeft - left) - (kPadH + kValueWidth) * m_scale);
|
std::max(24.0f, columnWidth - (kLabelWidth + kLabelGap + kValueWidth) * m_scale);
|
||||||
const float firstTop = m_titleRect.Y + m_titleRect.Height + kTitleGap * m_scale;
|
|
||||||
|
|
||||||
for (int index = 0; index < 3; ++index)
|
for (int index = 0; index < 3; ++index)
|
||||||
{
|
{
|
||||||
const float rowTop = firstTop + static_cast<float>(index) * (kRowHeight + kRowGap) * m_scale;
|
const float rowTop =
|
||||||
|
contentTop + static_cast<float>(index) * (kRowHeight + kRowGap) * m_scale;
|
||||||
m_tracks[static_cast<std::size_t>(index)] =
|
m_tracks[static_cast<std::size_t>(index)] =
|
||||||
gd::RectF(trackLeft, rowTop + (kRowHeight * m_scale - kTrackHeight * m_scale) * 0.5f,
|
gd::RectF(trackLeft, rowTop + (kRowHeight * m_scale - kTrackHeight * m_scale) * 0.5f,
|
||||||
trackWidth, kTrackHeight * m_scale);
|
trackWidth, kTrackHeight * m_scale);
|
||||||
@@ -145,10 +274,11 @@ namespace jdi
|
|||||||
kValueWidth * m_scale - 6.0f * m_scale, kRowHeight * m_scale);
|
kValueWidth * m_scale - 6.0f * m_scale, kRowHeight * m_scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
const float previewTop = firstTop + 3.0f * (kRowHeight + kRowGap) * m_scale + kPreviewGap * m_scale;
|
const float previewTop =
|
||||||
|
contentTop + (3.0f * (kRowHeight + kRowGap) + kPreviewGap) * m_scale;
|
||||||
m_previewRect = gd::RectF(trackLeft, previewTop + 2.0f * m_scale, 52.0f * m_scale, 20.0f * m_scale);
|
m_previewRect = gd::RectF(trackLeft, previewTop + 2.0f * m_scale, 52.0f * m_scale, 20.0f * m_scale);
|
||||||
m_hexRect = gd::RectF(m_previewRect.X + m_previewRect.Width + 8.0f * m_scale, previewTop + 2.0f * m_scale,
|
m_hexRect = gd::RectF(m_previewRect.X + m_previewRect.Width + 8.0f * m_scale,
|
||||||
96.0f * m_scale, 20.0f * m_scale);
|
previewTop + 2.0f * m_scale, 96.0f * m_scale, 20.0f * m_scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ColorPickerPopup::Open(const gd::RectF& bounds, const gd::RectF& anchor, std::uint32_t color)
|
void ColorPickerPopup::Open(const gd::RectF& bounds, const gd::RectF& anchor, std::uint32_t color)
|
||||||
@@ -161,6 +291,10 @@ namespace jdi
|
|||||||
m_changed = false;
|
m_changed = false;
|
||||||
m_draggingSlider = -1;
|
m_draggingSlider = -1;
|
||||||
m_hoverSlider = -1;
|
m_hoverSlider = -1;
|
||||||
|
m_drag = DragTarget::NONE;
|
||||||
|
// 记住初始色相:之后即使把饱和度拖到 0(灰色)色相也不会丢失。
|
||||||
|
m_hue = ToHsv(m_channels).h;
|
||||||
|
InvalidateGamutCache();
|
||||||
|
|
||||||
BuildLayout(bounds, anchor);
|
BuildLayout(bounds, anchor);
|
||||||
}
|
}
|
||||||
@@ -175,6 +309,118 @@ namespace jdi
|
|||||||
m_open = false;
|
m_open = false;
|
||||||
m_draggingSlider = -1;
|
m_draggingSlider = -1;
|
||||||
m_hoverSlider = -1;
|
m_hoverSlider = -1;
|
||||||
|
m_drag = DragTarget::NONE;
|
||||||
|
// 释放色域方块缓存(它是 GDI+ 对象,必须在 GdiplusShutdown 之前销毁)。
|
||||||
|
// 下次打开时按需重建,代价只有一张小位图的填充。
|
||||||
|
m_gamutCache.reset();
|
||||||
|
m_gamutCacheSize = 0;
|
||||||
|
m_gamutCacheHue = -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ColorPickerPopup::CurrentHue() const
|
||||||
|
{
|
||||||
|
const Hsv hsv = ToHsv(m_channels);
|
||||||
|
// 灰度/黑色时 HSV 的色相没有定义:沿用记忆色相,避免拖动 RGB 滑块时色相跳变、
|
||||||
|
// 也保证"饱和度归零后仍能靠色相条换回颜色"。
|
||||||
|
return (hsv.s > 1.0e-4) ? hsv.h : m_hue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorPickerPopup::RefreshHueFromChannels()
|
||||||
|
{
|
||||||
|
const Hsv hsv = ToHsv(m_channels);
|
||||||
|
if (hsv.s > 1.0e-4)
|
||||||
|
{
|
||||||
|
m_hue = hsv.h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorPickerPopup::InvalidateGamutCache()
|
||||||
|
{
|
||||||
|
m_gamutCacheHue = -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ColorPickerPopup::EnsureGamutCache()
|
||||||
|
{
|
||||||
|
const int size = std::max(8, static_cast<int>(std::lround(kGamutSize * m_scale)));
|
||||||
|
if (m_gamutCache == nullptr || m_gamutCacheSize != size)
|
||||||
|
{
|
||||||
|
m_gamutCache.reset(new gd::Bitmap(size, size, PixelFormat32bppRGB));
|
||||||
|
m_gamutCacheSize = size;
|
||||||
|
m_gamutCacheHue = -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double hue = CurrentHue();
|
||||||
|
if (std::fabs(m_gamutCacheHue - hue) < 1.0e-4)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
gd::BitmapData data = {};
|
||||||
|
const gd::Rect lockRect(0, 0, size, size);
|
||||||
|
if (m_gamutCache->LockBits(&lockRect, gd::ImageLockModeWrite, PixelFormat32bppRGB, &data) != gd::Ok)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
unsigned char* base = static_cast<unsigned char*>(data.Scan0);
|
||||||
|
for (int y = 0; y < size; ++y)
|
||||||
|
{
|
||||||
|
// 纵轴明度(上 1 → 下 0),横轴饱和度(左 0 → 右 1)——与常见取色器一致。
|
||||||
|
const double value = 1.0 - (static_cast<double>(y) + 0.5) / static_cast<double>(size);
|
||||||
|
unsigned char* row = (data.Stride >= 0)
|
||||||
|
? (base + static_cast<std::ptrdiff_t>(y) * data.Stride)
|
||||||
|
: (base + static_cast<std::ptrdiff_t>(size - 1 - y) * (-data.Stride));
|
||||||
|
for (int x = 0; x < size; ++x)
|
||||||
|
{
|
||||||
|
const double saturation =
|
||||||
|
(static_cast<double>(x) + 0.5) / static_cast<double>(size);
|
||||||
|
const std::array<double, 3> rgb = FromHsv(hue, saturation, value);
|
||||||
|
row[x * 4 + 0] = static_cast<unsigned char>(ToByte(rgb[2])); // B
|
||||||
|
row[x * 4 + 1] = static_cast<unsigned char>(ToByte(rgb[1])); // G
|
||||||
|
row[x * 4 + 2] = static_cast<unsigned char>(ToByte(rgb[0])); // R
|
||||||
|
row[x * 4 + 3] = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_gamutCache->UnlockBits(&data);
|
||||||
|
m_gamutCacheHue = hue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorPickerPopup::ApplyGamut(float x, float y)
|
||||||
|
{
|
||||||
|
if (m_gamutRect.Width <= 0.0f || m_gamutRect.Height <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const double saturation = Clamp01((x - m_gamutRect.X) / m_gamutRect.Width);
|
||||||
|
const double value = 1.0 - Clamp01((y - m_gamutRect.Y) / m_gamutRect.Height);
|
||||||
|
const std::array<double, 3> rgb = FromHsv(CurrentHue(), saturation, value);
|
||||||
|
if (std::fabs(rgb[0] - m_channels[0]) < 1.0e-6 && std::fabs(rgb[1] - m_channels[1]) < 1.0e-6 &&
|
||||||
|
std::fabs(rgb[2] - m_channels[2]) < 1.0e-6)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_channels = rgb;
|
||||||
|
// 色相不变(明度归零时 FromHsv 会给出黑色,但记忆色相仍要保留)。
|
||||||
|
m_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorPickerPopup::ApplyHue(float y)
|
||||||
|
{
|
||||||
|
if (m_hueRect.Height <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const double hue = Clamp01((y - m_hueRect.Y) / m_hueRect.Height);
|
||||||
|
const Hsv hsv = ToHsv(m_channels);
|
||||||
|
m_hue = hue;
|
||||||
|
const std::array<double, 3> rgb = FromHsv(m_hue, hsv.s, hsv.v);
|
||||||
|
if (std::fabs(rgb[0] - m_channels[0]) < 1.0e-6 && std::fabs(rgb[1] - m_channels[1]) < 1.0e-6 &&
|
||||||
|
std::fabs(rgb[2] - m_channels[2]) < 1.0e-6)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_channels = rgb;
|
||||||
|
m_changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
gd::RectF ColorPickerPopup::EditFieldRect() const
|
gd::RectF ColorPickerPopup::EditFieldRect() const
|
||||||
@@ -292,6 +538,7 @@ namespace jdi
|
|||||||
m_channels[0] = static_cast<double>((value >> 16) & 0xFFu) / 255.0;
|
m_channels[0] = static_cast<double>((value >> 16) & 0xFFu) / 255.0;
|
||||||
m_channels[1] = static_cast<double>((value >> 8) & 0xFFu) / 255.0;
|
m_channels[1] = static_cast<double>((value >> 8) & 0xFFu) / 255.0;
|
||||||
m_channels[2] = static_cast<double>(value & 0xFFu) / 255.0;
|
m_channels[2] = static_cast<double>(value & 0xFFu) / 255.0;
|
||||||
|
RefreshHueFromChannels();
|
||||||
m_changed = true;
|
m_changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,6 +570,7 @@ namespace jdi
|
|||||||
if (index >= 0 && index < 3)
|
if (index >= 0 && index < 3)
|
||||||
{
|
{
|
||||||
m_channels[static_cast<std::size_t>(index)] = value / 255.0;
|
m_channels[static_cast<std::size_t>(index)] = value / 255.0;
|
||||||
|
RefreshHueFromChannels();
|
||||||
m_changed = true;
|
m_changed = true;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -417,8 +665,18 @@ namespace jdi
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拖拽过程中始终保持横向调整光标,避免指针移出轨道后光标跳变。
|
// 拖拽过程中始终保持对应光标,避免指针移出控件后光标跳变。
|
||||||
if (m_draggingSlider >= 0 || SliderAtPoint(x, y) >= 0)
|
if (m_drag == DragTarget::GAMUT || UiPointInRect(m_gamutRect, static_cast<float>(x),
|
||||||
|
static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
return LoadCursorW(nullptr, IDC_CROSS);
|
||||||
|
}
|
||||||
|
if (m_drag == DragTarget::HUE ||
|
||||||
|
UiPointInRect(m_hueRect, static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
return LoadCursorW(nullptr, IDC_SIZENS);
|
||||||
|
}
|
||||||
|
if (m_drag == DragTarget::SLIDER || m_draggingSlider >= 0 || SliderAtPoint(x, y) >= 0)
|
||||||
{
|
{
|
||||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
return LoadCursorW(nullptr, IDC_SIZEWE);
|
||||||
}
|
}
|
||||||
@@ -461,6 +719,7 @@ namespace jdi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_channels[slot] = ratio;
|
m_channels[slot] = ratio;
|
||||||
|
RefreshHueFromChannels();
|
||||||
m_changed = true;
|
m_changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,10 +730,20 @@ namespace jdi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_draggingSlider >= 0)
|
switch (m_drag)
|
||||||
{
|
{
|
||||||
|
case DragTarget::GAMUT:
|
||||||
|
ApplyGamut(static_cast<float>(x), static_cast<float>(y));
|
||||||
|
return true;
|
||||||
|
case DragTarget::HUE:
|
||||||
|
ApplyHue(static_cast<float>(y));
|
||||||
|
return true;
|
||||||
|
case DragTarget::SLIDER:
|
||||||
ApplySlider(m_draggingSlider, static_cast<float>(x));
|
ApplySlider(m_draggingSlider, static_cast<float>(x));
|
||||||
return true;
|
return true;
|
||||||
|
case DragTarget::NONE:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int hover = SliderAtPoint(x, y);
|
const int hover = SliderAtPoint(x, y);
|
||||||
@@ -523,9 +792,24 @@ namespace jdi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 色域方块与色相条:按下即取值,之后拖动持续更新。
|
||||||
|
if (UiPointInRect(m_gamutRect, static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
m_drag = DragTarget::GAMUT;
|
||||||
|
ApplyGamut(static_cast<float>(x), static_cast<float>(y));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (UiPointInRect(m_hueRect, static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
m_drag = DragTarget::HUE;
|
||||||
|
ApplyHue(static_cast<float>(y));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
m_draggingSlider = SliderAtPoint(x, y);
|
m_draggingSlider = SliderAtPoint(x, y);
|
||||||
if (m_draggingSlider >= 0)
|
if (m_draggingSlider >= 0)
|
||||||
{
|
{
|
||||||
|
m_drag = DragTarget::SLIDER;
|
||||||
ApplySlider(m_draggingSlider, static_cast<float>(x));
|
ApplySlider(m_draggingSlider, static_cast<float>(x));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -534,11 +818,12 @@ namespace jdi
|
|||||||
|
|
||||||
bool ColorPickerPopup::OnLButtonUp()
|
bool ColorPickerPopup::OnLButtonUp()
|
||||||
{
|
{
|
||||||
if (!m_open || m_draggingSlider < 0)
|
if (!m_open || (m_draggingSlider < 0 && m_drag == DragTarget::NONE))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
m_draggingSlider = -1;
|
m_draggingSlider = -1;
|
||||||
|
m_drag = DragTarget::NONE;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,6 +902,54 @@ namespace jdi
|
|||||||
|
|
||||||
UiText(g, L"自定义颜色", m_fonts->uiSmallBold, m_titleRect, m_theme->textMuted, TextAlign::LEFT);
|
UiText(g, L"自定义颜色", m_fonts->uiSmallBold, m_titleRect, m_theme->textMuted, TextAlign::LEFT);
|
||||||
|
|
||||||
|
// ---- 色域方块(横轴饱和度、纵轴明度)----
|
||||||
|
// 方块按当前色相预渲染到缓存位图里,避免每帧重画上百条渐变;
|
||||||
|
// 色相变化时才重建一次。
|
||||||
|
if (EnsureGamutCache())
|
||||||
|
{
|
||||||
|
g.DrawImage(m_gamutCache.get(), m_gamutRect);
|
||||||
|
}
|
||||||
|
UiDrawRoundRect(g, m_gamutRect, 2.0f * m_scale, m_theme->borderStrong, kGamutBorder);
|
||||||
|
|
||||||
|
// ---- 色相条(纵向 0-1,用 6 段线性渐变近似色相环)----
|
||||||
|
{
|
||||||
|
const Hsv hsv = ToHsv(m_channels);
|
||||||
|
const double saturation = (hsv.s > 1.0e-4) ? hsv.s : 1.0;
|
||||||
|
const double value = (hsv.v > 1.0e-4) ? hsv.v : 1.0;
|
||||||
|
const int bands = std::max(12, static_cast<int>(std::lround(m_hueRect.Height)));
|
||||||
|
for (int index = 0; index < bands; ++index)
|
||||||
|
{
|
||||||
|
const double hue = (static_cast<double>(index) + 0.5) / static_cast<double>(bands);
|
||||||
|
const std::array<double, 3> rgb = FromHsv(hue, 1.0, 1.0);
|
||||||
|
const std::uint32_t band = 0xFF000000u | (ToByte(rgb[0]) << 16) | (ToByte(rgb[1]) << 8) |
|
||||||
|
ToByte(rgb[2]);
|
||||||
|
const float top = m_hueRect.Y + m_hueRect.Height * index / static_cast<float>(bands);
|
||||||
|
const float bottom =
|
||||||
|
m_hueRect.Y + m_hueRect.Height * (index + 1) / static_cast<float>(bands);
|
||||||
|
UiFillRect(g, gd::RectF(m_hueRect.X, top, m_hueRect.Width, bottom - top), band);
|
||||||
|
}
|
||||||
|
UiDrawRoundRect(g, m_hueRect, 2.0f * m_scale, m_theme->borderStrong, kGamutBorder);
|
||||||
|
|
||||||
|
// 控件指示器:色域方块上标出 (S, V),色相条上标出当前色相。
|
||||||
|
// 两者都由 m_channels 反推,因此与 R/G/B 滑块、十六进制输入天然联动。
|
||||||
|
const float markerX = m_gamutRect.X + static_cast<float>(saturation) * m_gamutRect.Width;
|
||||||
|
const float markerY = m_gamutRect.Y + static_cast<float>(1.0 - value) * m_gamutRect.Height;
|
||||||
|
gd::Pen outerPen(ToGdiColor(0xFF1B1B1Bu), 2.0f * m_scale);
|
||||||
|
gd::Pen innerPen(ToGdiColor(0xFFFFFFFFu), 1.4f * m_scale);
|
||||||
|
const float outerRadius = 5.0f * m_scale;
|
||||||
|
g.DrawEllipse(&outerPen, markerX - outerRadius, markerY - outerRadius, outerRadius * 2.0f,
|
||||||
|
outerRadius * 2.0f);
|
||||||
|
const float innerRadius = 3.4f * m_scale;
|
||||||
|
g.DrawEllipse(&innerPen, markerX - innerRadius, markerY - innerRadius, innerRadius * 2.0f,
|
||||||
|
innerRadius * 2.0f);
|
||||||
|
|
||||||
|
const float hueMarkerY = m_hueRect.Y + static_cast<float>(CurrentHue()) * m_hueRect.Height;
|
||||||
|
UiFillRect(g, gd::RectF(m_hueRect.X - 2.0f * m_scale, hueMarkerY - 1.5f * m_scale,
|
||||||
|
m_hueRect.Width + 4.0f * m_scale, 3.0f * m_scale),
|
||||||
|
0xFF1B1B1Bu);
|
||||||
|
UiDrawRoundRect(g, m_hueRect, 2.0f * m_scale, m_theme->borderStrong, kGamutBorder);
|
||||||
|
}
|
||||||
|
|
||||||
for (int index = 0; index < 3; ++index)
|
for (int index = 0; index < 3; ++index)
|
||||||
{
|
{
|
||||||
const std::size_t slot = static_cast<std::size_t>(index);
|
const std::size_t slot = static_cast<std::size_t>(index);
|
||||||
|
|||||||
@@ -372,7 +372,38 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
return ratio;
|
return ratio;
|
||||||
}
|
}
|
||||||
|
double UiColorLuminance(std::uint32_t color)
|
||||||
|
{
|
||||||
|
const double r = static_cast<double>((color >> 16) & 0xFFu);
|
||||||
|
const double g = static_cast<double>((color >> 8) & 0xFFu);
|
||||||
|
const double b = static_cast<double>(color & 0xFFu);
|
||||||
|
return (0.299 * r + 0.587 * g + 0.114 * b) / 255.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
UiInk UiContrastInk(std::uint32_t background)
|
||||||
|
{
|
||||||
|
// 极值文字/描边色(Solarized base3 / base02 / base03),亮度差很大,
|
||||||
|
// 因此任意底色下文字与底色之间都有充足对比。
|
||||||
|
const std::uint32_t kInkLight = 0xFFFDF6E3u;
|
||||||
|
const std::uint32_t kInkDark = 0xFF073642u;
|
||||||
|
const std::uint32_t kOutlineLight = 0xFFFDF6E3u;
|
||||||
|
const std::uint32_t kOutlineDark = 0xFF002B36u;
|
||||||
|
/** 向底色互补色混入的比例:线性响应信号配色,同时把对比度损失限制在 15% 以内。 */
|
||||||
|
const double kTint = 0.15;
|
||||||
|
|
||||||
|
const double luminance = UiColorLuminance(background);
|
||||||
|
// 底色越亮 → 文字越暗(取最大对比的一侧)。
|
||||||
|
const bool darkText = (luminance >= 0.5);
|
||||||
|
// 底色的互补色:相对亮度恰为 1 - 底色亮度。
|
||||||
|
const std::uint32_t complement = 0xFF000000u | ((~background) & 0x00FFFFFFu);
|
||||||
|
|
||||||
|
UiInk ink;
|
||||||
|
ink.text = MixColor(darkText ? kInkDark : kInkLight, complement, kTint);
|
||||||
|
ink.text = WithAlpha(ink.text, 0xFFu);
|
||||||
|
ink.outline = MixColor(darkText ? kOutlineLight : kOutlineDark, complement, kTint);
|
||||||
|
ink.outline = WithAlpha(ink.outline, 0x99u);
|
||||||
|
return ink;
|
||||||
|
}
|
||||||
void UiDrawCheckBox(gd::Graphics& g, const gd::RectF& rect, int state, float radius)
|
void UiDrawCheckBox(gd::Graphics& g, const gd::RectF& rect, int state, float radius)
|
||||||
{
|
{
|
||||||
const Theme& theme = SolarizedLight();
|
const Theme& theme = SolarizedLight();
|
||||||
|
|||||||
Reference in new issue
Block a user