|
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | +// SPDX-FileCopyrightText: 2025-2026 hyperpolymath |
| 3 | +// |
| 4 | +// AlertPalette -- the accessibility alert-colour lookup co-processor, the |
| 5 | +// pure-integer core extracted from src/app/utils/ColorPalette.res `alertColor`. |
| 6 | +// The existing ColorPalette co-processor (colorpalette.wasm, bound by |
| 7 | +// ColorPaletteCoprocessor.res) deliberately captured only the RGB-channel and |
| 8 | +// WCAG luminance/contrast KERNELS and left the accessibility palette LOOKUP |
| 9 | +// TABLES on the ReScript side (FeaturePacks documents this: "the live HUD |
| 10 | +// call-sites alertColor/deviceColor are accessibility palette lookup tables with |
| 11 | +// no corresponding kernel"). This brain captures exactly that residual: the |
| 12 | +// alert-level colour table, which is wholly pure-integer (enum bands in, a 24-bit |
| 13 | +// RGB integer out) with no host delegation -- unlike deviceColor/securityColor |
| 14 | +// whose Normal arm delegates to DeviceTypes host getters. |
| 15 | +// |
| 16 | +// Per the DESIGN-VISION the JS host keeps the AccessibilitySettings refs (the |
| 17 | +// high-contrast flag and colour-blind mode); AffineScript owns only the integer |
| 18 | +// table: given the high-contrast flag, the colour-blind mode and the alert level |
| 19 | +// it returns the packed colour. |
| 20 | +// |
| 21 | +//## Alert level encoding (levelInt, the header contract for the JS host) |
| 22 | +// 0 = Clear 1 = Noticed 2 = Caution 3 = Alert 4 = Danger |
| 23 | +// The ReScript switch has explicit arms for 0..4 and a `_` catch-all that takes |
| 24 | +// the Danger-row's "max" colour; we reproduce that fold (level >= 4 OR level < 0 |
| 25 | +// uses the catch-all row), so the table is total over all Int and matches the |
| 26 | +// ReScript exactly. |
| 27 | +// |
| 28 | +//## Colour-blind mode encoding (mode, mirrors AccessibilitySettings.colorBlindMode) |
| 29 | +// 0 = Normal 3 = Tritanopia |
| 30 | +// 1 = Protanopia 4 = Achromatopsia |
| 31 | +// 2 = Deuteranopia |
| 32 | +// Protanopia and Deuteranopia share one table (the ReScript `Protanopia | |
| 33 | +// Deuteranopia =>` combined arm). An out-of-band mode folds onto Normal (0), |
| 34 | +// matching AccessibilitySettings.colorBlindModeFromString's `_ => Normal`. |
| 35 | +// |
| 36 | +// high_contrast is a host boolean crossed as 0 (off) or 1 (on); any non-zero is |
| 37 | +// treated as on. When on, the mode is ignored (the ReScript tests high contrast |
| 38 | +// first), so the high-contrast table is returned regardless of mode. |
| 39 | +// |
| 40 | +// All outputs are 24-bit RGB integers (0..0xFFFFFF = 16777215), well inside i32; |
| 41 | +// no value collides with another row at the same level, and -1 is never emitted, |
| 42 | +// so this is a pure total lookup (assail stays clean: no sentinel collapse). |
| 43 | + |
| 44 | +// Fold an arbitrary colour-blind mode index onto the valid 0..4 band; unknown -> |
| 45 | +// Normal (0). Mirrors colorBlindModeFromString's `_ => Normal`. |
| 46 | +pub fn clamp_mode(mode: Int) -> Int { |
| 47 | + if mode < 0 { 0 } else { if mode > 4 { 0 } else { mode } } |
| 48 | +} |
| 49 | + |
| 50 | +// The catch-all alert row index used for level < 0 or level > 4: the ReScript |
| 51 | +// `_` arm coincides with the Danger row visually but uses a DISTINCT "max" |
| 52 | +// colour, so it is modelled as its own row index 5. |
| 53 | +fn alert_row(level: Int) -> Int { |
| 54 | + if level < 0 { 5 } else { if level > 4 { 5 } else { level } } |
| 55 | +} |
| 56 | + |
| 57 | +//## High-contrast alert table (isHighContrastEnabled() == true) |
| 58 | +// row 0 0x00ff00 1 0xffff00 2 0xffaa00 3 0xff4400 4 0xff0000 _ 0xff0000 |
| 59 | +fn high_contrast_colour(level: Int) -> Int { |
| 60 | + let r = alert_row(level); |
| 61 | + if r == 0 { 65280 } else { |
| 62 | + if r == 1 { 16776960 } else { |
| 63 | + if r == 2 { 16755200 } else { |
| 64 | + if r == 3 { 16729088 } else { |
| 65 | + 16711680 } } } } |
| 66 | +} |
| 67 | + |
| 68 | +//## Normal-mode alert table |
| 69 | +// 0 0x00ff00 1 0x88ff00 2 0xffff00 3 0xff8800 4 0xff4400 _ 0xff0000 |
| 70 | +fn normal_colour(level: Int) -> Int { |
| 71 | + let r = alert_row(level); |
| 72 | + if r == 0 { 65280 } else { |
| 73 | + if r == 1 { 8978176 } else { |
| 74 | + if r == 2 { 16776960 } else { |
| 75 | + if r == 3 { 16746496 } else { |
| 76 | + if r == 4 { 16729088 } else { |
| 77 | + 16711680 } } } } } |
| 78 | +} |
| 79 | + |
| 80 | +//## Protanopia/Deuteranopia alert table (shared arm) |
| 81 | +// 0 0x0088ff 1 0x44aaff 2 0xffdd44 3 0xffaa00 4 0xff8800 _ 0xff6600 |
| 82 | +fn protan_deuter_colour(level: Int) -> Int { |
| 83 | + let r = alert_row(level); |
| 84 | + if r == 0 { 35071 } else { |
| 85 | + if r == 1 { 4500223 } else { |
| 86 | + if r == 2 { 16768324 } else { |
| 87 | + if r == 3 { 16755200 } else { |
| 88 | + if r == 4 { 16746496 } else { |
| 89 | + 16737792 } } } } } |
| 90 | +} |
| 91 | + |
| 92 | +//## Tritanopia alert table |
| 93 | +// 0 0x00ee88 1 0x66dd66 2 0xdddd00 3 0xee6688 4 0xdd2266 _ 0xcc0044 |
| 94 | +fn tritan_colour(level: Int) -> Int { |
| 95 | + let r = alert_row(level); |
| 96 | + if r == 0 { 61064 } else { |
| 97 | + if r == 1 { 6741350 } else { |
| 98 | + if r == 2 { 14540032 } else { |
| 99 | + if r == 3 { 15623816 } else { |
| 100 | + if r == 4 { 14492262 } else { |
| 101 | + 13369412 } } } } } |
| 102 | +} |
| 103 | + |
| 104 | +//## Achromatopsia alert table (brightness-only) |
| 105 | +// 0 0xffffff 1 0xcccccc 2 0x999999 3 0x777777 4 0x444444 _ 0x222222 |
| 106 | +fn achroma_colour(level: Int) -> Int { |
| 107 | + let r = alert_row(level); |
| 108 | + if r == 0 { 16777215 } else { |
| 109 | + if r == 1 { 13421772 } else { |
| 110 | + if r == 2 { 10066329 } else { |
| 111 | + if r == 3 { 7829367 } else { |
| 112 | + if r == 4 { 4473924 } else { |
| 113 | + 2236962 } } } } } |
| 114 | +} |
| 115 | + |
| 116 | +// The packed RGB alert colour for a high-contrast flag, a colour-blind mode and |
| 117 | +// an alert level. High contrast wins first (mode ignored); otherwise the mode |
| 118 | +// selects the table. This is the full ColorPalette.res `alertColor` decision. |
| 119 | +pub fn alert_colour(high_contrast: Int, mode: Int, level: Int) -> Int { |
| 120 | + if high_contrast != 0 { high_contrast_colour(level) } else { |
| 121 | + let m = clamp_mode(mode); |
| 122 | + if m == 0 { normal_colour(level) } else { |
| 123 | + if m == 1 { protan_deuter_colour(level) } else { |
| 124 | + if m == 2 { protan_deuter_colour(level) } else { |
| 125 | + if m == 3 { tritan_colour(level) } else { |
| 126 | + achroma_colour(level) } } } } |
| 127 | + } |
| 128 | +} |
0 commit comments