Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23fd2da91e | ||
|
|
3d490bb6f3 |
@@ -158,7 +158,7 @@ fun noteColorIsChosen(
|
|||||||
// at most a 1.03 contrast ratio, which is to say not at all. Nine tints that look
|
// 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.
|
// 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.
|
// dark, 193 in light, against nine.
|
||||||
//
|
//
|
||||||
// TWO AXES, AND THE SECOND ONE IS THE FIX. The old ramp varied hue while pinning
|
// 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
|
private const val LIGHT_SATURATION = 0.60
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dark starts at 0.090 — a hair under `neutral-900`, the plain card surface — and
|
* HSL LIGHTNESS IS NOT LUMINANCE, and the floor here is set by the difference.
|
||||||
* 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
|
* The obvious floor is `neutral-900`'s own lightness, 0.090 — "start at the plain card
|
||||||
* board where the old single level managed 1.14.
|
* 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.11–1.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
|
* Light runs the other way, from white down toward the `neutral-50` board and just
|
||||||
|
|||||||
@@ -58,9 +58,23 @@ fun NoteCard(
|
|||||||
.border(1.dp, if (dark) CARD_EDGE_DARK else CARD_EDGE_LIGHT, RoundedCornerShape(CARD_RADIUS))
|
.border(1.dp, if (dark) CARD_EDGE_DARK else CARD_EDGE_LIGHT, RoundedCornerShape(CARD_RADIUS))
|
||||||
.padding(12.dp),
|
.padding(12.dp),
|
||||||
) {
|
) {
|
||||||
// Body then checklist, in order — a note can carry both (M13 step 2), and
|
// TAGS FIRST. They used to sit under everything else, which on a tall note put
|
||||||
// nothing above them: the first line of the body IS the note's name, at the
|
// the one thing that says what a note IS below the fold of a glance. A board is
|
||||||
// same weight as the rest of it (M13 steps 3 and 4).
|
// scanned, not read, and the answer to "which of these is about the thing I am
|
||||||
|
// looking for" should be the first thing the eye lands on rather than the last.
|
||||||
|
//
|
||||||
|
// Above the body rather than beside it, because the body's first line is the
|
||||||
|
// note's NAME (M13 steps 3 and 4) and a chip floated next to it would compete
|
||||||
|
// with the thing that identifies the note. A row of its own costs one line and
|
||||||
|
// only on notes that have tags at all.
|
||||||
|
if (note.labels.isNotEmpty()) {
|
||||||
|
LabelChips(labels = note.labels)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body then checklist, in order — a note can carry both (M13 step 2). The
|
||||||
|
// first line of the body IS the note's name, at the same weight as the rest of
|
||||||
|
// it (M13 steps 3 and 4).
|
||||||
if (note.body.isNotBlank()) {
|
if (note.body.isNotBlank()) {
|
||||||
NoteBody(note = note, onToggleItem = onToggleItem)
|
NoteBody(note = note, onToggleItem = onToggleItem)
|
||||||
}
|
}
|
||||||
@@ -76,11 +90,6 @@ fun NoteCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.labels.isNotEmpty()) {
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
LabelChips(labels = note.labels)
|
|
||||||
}
|
|
||||||
|
|
||||||
note.remindAt?.let { at ->
|
note.remindAt?.let { at ->
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
ReminderChip(instant = at, recurrence = note.recurrence)
|
ReminderChip(instant = at, recurrence = note.recurrence)
|
||||||
|
|||||||
@@ -196,10 +196,10 @@ class DerivedTintTest {
|
|||||||
* actually compared. The same four ids and hexes are a comment in colors.ts. */
|
* actually compared. The same four ids and hexes are a comment in colors.ts. */
|
||||||
@Test
|
@Test
|
||||||
fun `generated fills match the fixture shared with the web`() {
|
fun `generated fills match the fixture shared with the web`() {
|
||||||
assertEquals("#192a29", hex(derivedFillArgb("00000000-0000-0000-0000-000000000000", dark = true)))
|
assertEquals("#1e3130", hex(derivedFillArgb("00000000-0000-0000-0000-000000000000", dark = true)))
|
||||||
assertEquals("#152114", hex(derivedFillArgb("11111111-1111-1111-1111-111111111111", dark = true)))
|
assertEquals("#1a2818", hex(derivedFillArgb("11111111-1111-1111-1111-111111111111", dark = true)))
|
||||||
assertEquals("#111d14", hex(derivedFillArgb("6ba7b810-9dad-11d1-80b4-00c04fd430c8", dark = true)))
|
assertEquals("#162419", hex(derivedFillArgb("6ba7b810-9dad-11d1-80b4-00c04fd430c8", dark = true)))
|
||||||
assertEquals("#2a191b", hex(derivedFillArgb("f47ac10b-58cc-4372-a567-0e02b2c3d479", 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("#f3fcfb", hex(derivedFillArgb("00000000-0000-0000-0000-000000000000", dark = false)))
|
||||||
assertEquals("#fbfefb", hex(derivedFillArgb("11111111-1111-1111-1111-111111111111", 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)
|
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
|
@Test
|
||||||
fun `no generated fill sinks below the card surface`() {
|
fun `no generated fill sinks below the card surface`() {
|
||||||
val surface = luminance(0xFF171717.toInt())
|
val surface = luminance(0xFF171717.toInt())
|
||||||
for (n in 0 until 500) {
|
for (n in 0 until 500) {
|
||||||
val l = luminance(derivedFillArgb("note-$n", dark = true))
|
assertEquals(true, luminance(derivedFillArgb("note-$n", dark = true)) >= surface)
|
||||||
assertEquals(true, l >= surface * 0.98)
|
}
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -259,6 +259,24 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
|
|||||||
|
|
||||||
`shadow-sm`, back down from `shadow`: the border is the boundary again, so the
|
`shadow-sm`, back down from `shadow`: the border is the boundary again, so the
|
||||||
shadow is only depth. -->
|
shadow is only depth. -->
|
||||||
|
<!-- TAGS FIRST. They used to sit under everything else, which on a tall note put
|
||||||
|
the one thing that says what a note IS below the fold of a glance. A board is
|
||||||
|
scanned, not read, and the answer to "which of these is about the thing I am
|
||||||
|
looking for" should be the first thing the eye lands on rather than the last.
|
||||||
|
|
||||||
|
Above the image and the body rather than beside them, because the body's first
|
||||||
|
line is the note's NAME (M13 steps 3 and 4) and a chip floated next to it would
|
||||||
|
compete with the thing that identifies the note. -->
|
||||||
|
<div v-if="note.labels.length" class="mb-2 flex flex-wrap gap-1">
|
||||||
|
<span
|
||||||
|
v-for="lb in note.labels"
|
||||||
|
:key="lb.id"
|
||||||
|
class="rounded-full px-2 py-0.5 text-xs"
|
||||||
|
:class="labelChip(lb)"
|
||||||
|
>{{ lb.via_tag ? "#" + lb.name : lb.name }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-if="firstImage"
|
v-if="firstImage"
|
||||||
:src="firstImage.url"
|
:src="firstImage.url"
|
||||||
@@ -317,16 +335,6 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
|
|||||||
two paragraphs instead of always after them. Rendering both would have shown
|
two paragraphs instead of always after them. Rendering both would have shown
|
||||||
every list twice. -->
|
every list twice. -->
|
||||||
|
|
||||||
<div v-if="note.labels.length" class="mt-2 flex flex-wrap gap-1">
|
|
||||||
<span
|
|
||||||
v-for="lb in note.labels"
|
|
||||||
:key="lb.id"
|
|
||||||
class="rounded-full px-2 py-0.5 text-xs"
|
|
||||||
:class="labelChip(lb)"
|
|
||||||
>{{ lb.via_tag ? "#" + lb.name : lb.name }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="note.remind_at" class="mt-2 flex flex-wrap items-center gap-1.5">
|
<div v-if="note.remind_at" class="mt-2 flex flex-wrap items-center gap-1.5">
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs"
|
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs"
|
||||||
|
|||||||
@@ -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
|
// 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.
|
// 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.
|
// dark, 193 in light, against nine.
|
||||||
//
|
//
|
||||||
// TWO AXES, AND THE SECOND ONE IS THE FIX. The old ramp varied hue while pinning
|
// 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 DARK_SATURATION = 0.25;
|
||||||
const LIGHT_SATURATION = 0.6;
|
const LIGHT_SATURATION = 0.6;
|
||||||
|
|
||||||
// Dark starts a hair under `neutral-900`, the plain card surface, and climbs — so
|
// HSL LIGHTNESS IS NOT LUMINANCE, and the dark floor is set by the difference.
|
||||||
// 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
|
// 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`
|
// 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
|
// board; a card slightly darker than the board still reads as one because the edge
|
||||||
// says so.
|
// says so, and near white the hue barely moves luminance at all so it needs no
|
||||||
const DARK_LIGHTNESS = [0.09, 0.104, 0.118, 0.132, 0.146, 0.16];
|
// 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];
|
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):
|
// And for derivedFill, which uses the same hash on two axes (dark / light):
|
||||||
//
|
//
|
||||||
// 00000000-0000-0000-0000-000000000000 hue 177 lev 3 #192a29 #f3fcfb
|
// 00000000-0000-0000-0000-000000000000 hue 177 lev 3 #1e3130 #f3fcfb
|
||||||
// 11111111-1111-1111-1111-111111111111 hue 113 lev 1 #152114 #fbfefb
|
// 11111111-1111-1111-1111-111111111111 hue 113 lev 1 #1a2818 #fbfefb
|
||||||
// 6ba7b810-9dad-11d1-80b4-00c04fd430c8 hue 136 lev 0 #111d14 #ffffff
|
// 6ba7b810-9dad-11d1-80b4-00c04fd430c8 hue 136 lev 0 #162419 #ffffff
|
||||||
// f47ac10b-58cc-4372-a567-0e02b2c3d479 hue 352 lev 3 #2a191b #fcf3f4
|
// f47ac10b-58cc-4372-a567-0e02b2c3d479 hue 352 lev 3 #311e20 #fcf3f4
|
||||||
//
|
//
|
||||||
// Note `work`/`ideas` and `home`/`reading` collide. Nine keys makes that unavoidable
|
// 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
|
// and it is not a bug: colour hints that two notes are related, it never claims they
|
||||||
|
|||||||
Reference in New Issue
Block a user