authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 21:13:19-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 07:05:30-07:00
log91ad7c52e4867559ba209810469eea16612dcca8
tree0b015255736a1ee993dfef721c0a5122fbc524ce
parent2bfd2c325dbad48b099155c8aee21fcd5b3a5ee5
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: grey section colours shade grey

Section shading took only a colour's hue at the theme's saturation, which turned OneNote's Silver (#9595AA) indigo and its mists and Cyan vivid. A colour greyer than OneNote's bright ones now takes the saturation in proportion, in the tabs, the frame, sidebar icons, notebook covers and the section's pen. Assisted-by: claude-opus-5.5

3 files changed, 45 insertions(+), 4 deletions(-)

crates/canvas/src/interaction/ink.rs+5-1
...@@ -20,7 +20,11 @@ use onestore::{...@@ -20,7 +20,11 @@ use onestore::{
20pub fn pens(section: [f32; 4]) -> [Pen; 15] {20pub fn pens(section: [f32; 4]) -> [Pen; 15] {
21 let [red, green, blue] = draw::srgb_bytes({21 let [red, green, blue] = draw::srgb_bytes({
22 let [saturation, lightness] = draw::LIGHT_ACCENT;22 let [saturation, lightness] = draw::LIGHT_ACCENT;
23 draw::hsl(draw::hue(section), saturation, lightness)23 draw::hsl(
24 draw::hue(section),
25 saturation * draw::vividness(section),
26 lightness,
27 )
24 });28 });
25 let accent = Pen::new(35.0, Some(u32::from_le_bytes([red, green, blue, 0])));29 let accent = Pen::new(35.0, Some(u32::from_le_bytes([red, green, blue, 0])));
26 std::array::from_fn(|place| place.checked_sub(1).map_or(accent, |at| FAVORITES[at]))30 std::array::from_fn(|place| place.checked_sub(1).map_or(accent, |at| FAVORITES[at]))
crates/draw/src/render.rs+9
...@@ -1762,6 +1762,15 @@ pub fn hue(color: [f32; 4]) -> f32 {...@@ -1762,6 +1762,15 @@ pub fn hue(color: [f32; 4]) -> f32 {
1762 to_hsl([color[0], color[1], color[2]].map(crate::encode))[0]1762 to_hsl([color[0], color[1], color[2]].map(crate::encode))[0]
1763}1763}
17641764
1765/// How much of a shade's saturation a linear colour's hue takes, from 0 to 1: all of it from
1766/// a saturation of `VIVID` up, as OneNote 2010's bright section colours have, less as the
1767/// colour greys, so its Silver and mists stay grey rather than turn their faint hue vivid.
1768pub fn vividness(color: [f32; 4]) -> f32 {
1769 const VIVID: f32 = 0.35;
1770 let [_, saturation, _] = to_hsl([color[0], color[1], color[2]].map(crate::encode));
1771 (saturation / VIVID).min(1.0)
1772}
1773
1765/// Hue in degrees, saturation and lightness of an encoded sRGB colour.1774/// Hue in degrees, saturation and lightness of an encoded sRGB colour.
1766fn to_hsl([r, g, b]: [f32; 3]) -> [f32; 3] {1775fn to_hsl([r, g, b]: [f32; 3]) -> [f32; 3] {
1767 let [max, min] = [r.max(g).max(b), r.min(g).min(b)];1776 let [max, min] = [r.max(g).max(b), r.min(g).min(b)];
crates/ui/src/theme.rs+31-3
...@@ -290,10 +290,12 @@ impl Theme {...@@ -290,10 +290,12 @@ impl Theme {
290 }290 }
291 }291 }
292292
293 /// The colours a section takes from the hue of `color`, linear RGBA.293 /// The colours a section takes from the hue of `color`, linear RGBA, as grey as it is.
294 pub fn section(&self, color: [f32; 4]) -> Section {294 pub fn section(&self, color: [f32; 4]) -> Section {
295 let hue = hue(color);295 let (hue, vividness) = (hue(color), draw::vividness(color));
296 let shade = |hue: f32, [saturation, lightness]: [f32; 2]| hsl(hue, saturation, lightness);296 let shade = |hue: f32, [saturation, lightness]: [f32; 2]| {
297 hsl(hue, saturation * vividness, lightness)
298 };
297 let Shades {299 let Shades {
298 frame,300 frame,
299 tab,301 tab,
...@@ -326,6 +328,32 @@ mod tests {...@@ -326,6 +328,32 @@ mod tests {
326 assert_eq!(hsl(240.0, 0.0, 1.0), srgb(0xff, 0xff, 0xff));328 assert_eq!(hsl(240.0, 0.0, 1.0), srgb(0xff, 0xff, 0xff));
327 }329 }
328330
331 /// OneNote 2010's Silver (`corpus/section-color/native/colors.txt`) shades grey, as
332 /// faint in hue as it is, while its Blue takes the theme's full saturation.
333 #[test]
334 fn grey_sections_stay_grey() {
335 // HSL saturation, as sRGB shows it.
336 let saturation = |color: [f32; 4]| {
337 let [r, g, b] = draw::srgb_bytes(color).map(|byte| f32::from(byte) / 255.0);
338 let [max, min] = [r.max(g).max(b), r.min(g).min(b)];
339 (max - min) / (1.0 - (max + min - 1.0).abs())
340 };
341 for theme in [Theme::light(), Theme::dark()] {
342 let silver = theme.section(srgb(0x95, 0x95, 0xaa));
343 let blue = theme.section(srgb(0x8a, 0xa8, 0xe4));
344 for (silver, blue) in [
345 (silver.tab, blue.tab),
346 (silver.accent, blue.accent),
347 (silver.frame[0], blue.frame[0]),
348 ] {
349 assert!(
350 saturation(silver) < saturation(blue) * 0.4,
351 "{silver:?} {blue:?}"
352 );
353 }
354 }
355 }
356
329 #[test]357 #[test]
330 fn icon_badges_stand_clear_of_the_accent() {358 fn icon_badges_stand_clear_of_the_accent() {
331 let badge = |accent_hue: f32| {359 let badge = |accent_hue: f32| {