Checklists in the body, colour from tags, and commit-derived CalVer #4

Merged
bvandeusen merged 73 commits from dev into main 2026-08-29 13:39:45 -04:00
3 changed files with 66 additions and 24 deletions
Showing only changes of commit 3d490bb6f3 - Show all commits
@@ -158,7 +158,7 @@ fun noteColorIsChosen(
// at most a 1.03 contrast ratio, which is to say not at all. Nine tints that look
// like three is exactly the wall the tint was added to break up.
//
// So this hashes to a colour directly rather than to a key. 324 distinct fills in
// So this hashes to a colour directly rather than to a key. 338 distinct fills in
// dark, 193 in light, against nine.
//
// TWO AXES, AND THE SECOND ONE IS THE FIX. The old ramp varied hue while pinning
@@ -185,12 +185,23 @@ private const val DARK_SATURATION = 0.25
private const val LIGHT_SATURATION = 0.60
/**
* Dark starts at 0.090 — a hair under `neutral-900`, the plain card surface — and
* climbs. Nothing is ever darker than an untinted card, so no note recedes into the
* board; they only ever rise off it. Top of the range measures 1.54 against the
* board where the old single level managed 1.14.
* HSL LIGHTNESS IS NOT LUMINANCE, and the floor here is set by the difference.
*
* The obvious floor is `neutral-900`'s own lightness, 0.090 — "start at the plain card
* surface and climb, so no note ever recedes into the board". That was the first
* attempt and it was wrong, because at a FIXED HSL lightness the eye sees wildly
* different brightnesses depending on hue: green carries 71% of the luminance formula
* and blue only 7%, so at L=0.090 a yellow measures 0.0118 and a blue 0.0061 — the
* blue landing 1.41x DARKER than the card it was supposed to match. A sixth of the
* board would have been holes rather than variety.
*
* 0.113 is the lowest floor at which EVERY hue clears the card surface, solved for
* rather than guessed. The range then measures 1.111.71 against the board where the
* old single level managed 1.14, so the floor is unchanged and the ceiling is much
* higher. `no generated fill sinks below the card surface` in DerivedTintTest is what
* caught the original mistake and what holds this.
*/
private val DARK_LIGHTNESS = doubleArrayOf(0.090, 0.104, 0.118, 0.132, 0.146, 0.160)
private val DARK_LIGHTNESS = doubleArrayOf(0.113, 0.127, 0.141, 0.155, 0.169, 0.183)
/**
* Light runs the other way, from white down toward the `neutral-50` board and just
@@ -196,10 +196,10 @@ class DerivedTintTest {
* actually compared. The same four ids and hexes are a comment in colors.ts. */
@Test
fun `generated fills match the fixture shared with the web`() {
assertEquals("#192a29", hex(derivedFillArgb("00000000-0000-0000-0000-000000000000", dark = true)))
assertEquals("#152114", hex(derivedFillArgb("11111111-1111-1111-1111-111111111111", dark = true)))
assertEquals("#111d14", hex(derivedFillArgb("6ba7b810-9dad-11d1-80b4-00c04fd430c8", dark = true)))
assertEquals("#2a191b", hex(derivedFillArgb("f47ac10b-58cc-4372-a567-0e02b2c3d479", dark = true)))
assertEquals("#1e3130", hex(derivedFillArgb("00000000-0000-0000-0000-000000000000", dark = true)))
assertEquals("#1a2818", hex(derivedFillArgb("11111111-1111-1111-1111-111111111111", dark = true)))
assertEquals("#162419", hex(derivedFillArgb("6ba7b810-9dad-11d1-80b4-00c04fd430c8", dark = true)))
assertEquals("#311e20", hex(derivedFillArgb("f47ac10b-58cc-4372-a567-0e02b2c3d479", dark = true)))
assertEquals("#f3fcfb", hex(derivedFillArgb("00000000-0000-0000-0000-000000000000", dark = false)))
assertEquals("#fbfefb", hex(derivedFillArgb("11111111-1111-1111-1111-111111111111", dark = false)))
@@ -234,14 +234,35 @@ class DerivedTintTest {
assertEquals(true, levels.max() / levels.min() > 2.0)
}
/** Nothing may be darker than the plain card surface (`neutral-900`, #171717) in
* dark, or the note recedes into the near-black board instead of sitting on it. */
/**
* Nothing may be darker than the plain card surface (`neutral-900`, #171717) in
* dark, or the note reads as a hole in the board rather than a card on it.
*
* THIS TEST HAS ALREADY EARNED ITS KEEP. The first floor was 0.090 — `neutral-900`'s
* own HSL lightness — on the reasoning that starting at the card surface and
* climbing could not possibly go below it. That reasoning confuses HSL lightness
* with luminance. At one fixed lightness the eye sees very different brightnesses
* by hue, because green carries 71% of the luminance formula and blue 7%: at 0.090
* a yellow measures 0.0118 and a blue 0.0061, so every blue-ish untagged note was
* 1.41x darker than the card it was supposed to match. Solved for, the floor is
* 0.113.
*
* Which is why this asserts on LUMINANCE and not on the input lightness — testing
* the number that was put in would have agreed with the bug.
*/
@Test
fun `no generated fill sinks below the card surface`() {
val surface = luminance(0xFF171717.toInt())
for (n in 0 until 500) {
val l = luminance(derivedFillArgb("note-$n", dark = true))
assertEquals(true, l >= surface * 0.98)
assertEquals(true, luminance(derivedFillArgb("note-$n", dark = true)) >= surface)
}
// The worst case is a HUE — blue, around 240 — and 500 ids are not enough to
// be sure of having visited it at the darkest level. There are 360 x 6 = 2160
// reachable combinations; by the coupon-collector bound ~16,600 draws covers
// them, so 40,000 makes a miss vanishingly unlikely. Cheap on the JVM, and the
// alternative is a test that would have PASSED against the original bug.
for (n in 0 until 40_000) {
assertEquals(true, luminance(derivedFillArgb("hue-sweep-$n", dark = true)) >= surface)
}
}
+20 -10
View File
@@ -154,7 +154,7 @@ export function derivedTint(id: string): NoteColor {
// at most a 1.03 contrast ratio, which is to say not at all. Nine tints that look
// like three is exactly the wall the tint was added to break up.
//
// So this hashes to a colour directly rather than to a key. 324 distinct fills in
// So this hashes to a colour directly rather than to a key. 338 distinct fills in
// dark, 193 in light, against nine.
//
// TWO AXES, AND THE SECOND ONE IS THE FIX. The old ramp varied hue while pinning
@@ -181,13 +181,23 @@ const TINT_LEVELS = 6;
const DARK_SATURATION = 0.25;
const LIGHT_SATURATION = 0.6;
// Dark starts a hair under `neutral-900`, the plain card surface, and climbs — so
// nothing is ever darker than an untinted card and no note recedes into the board.
// The top of the range measures 1.54 against the board where the old single level
// HSL LIGHTNESS IS NOT LUMINANCE, and the dark floor is set by the difference.
//
// The obvious floor is `neutral-900`'s own lightness, 0.090 — start at the plain card
// surface and climb, so no note ever recedes into the board. That was the first
// attempt and it was wrong: at a FIXED HSL lightness the eye sees wildly different
// brightnesses by hue, because green carries 71% of the luminance formula and blue
// only 7%. At L=0.090 a yellow measures 0.0118 and a blue 0.0061 — the blue landing
// 1.41x DARKER than the card it was meant to match, so a sixth of the board would
// have been holes rather than variety.
//
// 0.113 is the lowest floor at which every hue clears the card surface, solved for
// rather than guessed: 1.11-1.71 against the board, where the old single level
// managed 1.14. Light runs the other way, from white down past the `neutral-50`
// board; a card slightly darker than the board still reads as one because the edge
// says so.
const DARK_LIGHTNESS = [0.09, 0.104, 0.118, 0.132, 0.146, 0.16];
// says so, and near white the hue barely moves luminance at all so it needs no
// equivalent correction.
const DARK_LIGHTNESS = [0.113, 0.127, 0.141, 0.155, 0.169, 0.183];
const LIGHT_LIGHTNESS = [1.0, 0.99, 0.98, 0.97, 0.96, 0.95];
/**
@@ -282,10 +292,10 @@ export function resolveLabelColor(label: { name: string; color?: string | null }
//
// And for derivedFill, which uses the same hash on two axes (dark / light):
//
// 00000000-0000-0000-0000-000000000000 hue 177 lev 3 #192a29 #f3fcfb
// 11111111-1111-1111-1111-111111111111 hue 113 lev 1 #152114 #fbfefb
// 6ba7b810-9dad-11d1-80b4-00c04fd430c8 hue 136 lev 0 #111d14 #ffffff
// f47ac10b-58cc-4372-a567-0e02b2c3d479 hue 352 lev 3 #2a191b #fcf3f4
// 00000000-0000-0000-0000-000000000000 hue 177 lev 3 #1e3130 #f3fcfb
// 11111111-1111-1111-1111-111111111111 hue 113 lev 1 #1a2818 #fbfefb
// 6ba7b810-9dad-11d1-80b4-00c04fd430c8 hue 136 lev 0 #162419 #ffffff
// f47ac10b-58cc-4372-a567-0e02b2c3d479 hue 352 lev 3 #311e20 #fcf3f4
//
// Note `work`/`ideas` and `home`/`reading` collide. Nine keys makes that unavoidable
// and it is not a bug: colour hints that two notes are related, it never claims they