#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmotionColor {
Anger,
Disgust,
Surprise,
Joy,
Interest,
Sadness,
Fear,
Neutral,
Inactive,
}
impl EmotionColor {
pub const fn hex(self) -> u32 {
match self {
Self::Anger => 0xff0000,
Self::Disgust => 0xff5b00,
Self::Surprise => 0xfcf000,
Self::Joy => 0x00fe00,
Self::Interest => 0x00acff,
Self::Sadness => 0x304ffe,
Self::Fear => 0xd500f9,
Self::Neutral => 0xffffff,
Self::Inactive => 0x4b4b4d,
}
}
pub const fn rgb(self) -> (u8, u8, u8) {
let h = self.hex();
(((h >> 16) & 0xff) as u8, ((h >> 8) & 0xff) as u8, (h & 0xff) as u8)
}
}
pub fn polarity(v: f64) -> EmotionColor {
if v > 0.0 {
EmotionColor::Joy
} else if v < 0.0 {
EmotionColor::Anger
} else {
EmotionColor::Neutral
}
}
pub fn threshold(v: f64, low: f64, high: f64) -> EmotionColor {
if v >= high {
EmotionColor::Joy
} else if v >= low {
EmotionColor::Surprise
} else {
EmotionColor::Anger
}
}
pub fn threshold_multi(v: f64, cutoffs: &[f64], colors: &[EmotionColor]) -> EmotionColor {
debug_assert_eq!(cutoffs.len(), colors.len(), "cutoffs and colors must align");
if cutoffs.is_empty() {
return EmotionColor::Neutral;
}
let mut idx = 0usize;
for (j, &c) in cutoffs.iter().enumerate() {
if v >= c {
idx = j;
} else {
break;
}
}
colors[idx]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemanticAction {
Navigate,
Confirm,
Danger,
Caution,
Explore,
}
pub fn semantic(action: SemanticAction) -> EmotionColor {
match action {
SemanticAction::Navigate => EmotionColor::Neutral,
SemanticAction::Confirm => EmotionColor::Joy,
SemanticAction::Danger => EmotionColor::Anger,
SemanticAction::Caution => EmotionColor::Surprise,
SemanticAction::Explore => EmotionColor::Interest,
}
}
pub fn continuous(v: f64, min: f64, max: f64) -> EmotionColor {
let range = max - min;
if range <= 0.0 {
return EmotionColor::Neutral;
}
let t = ((v - min) / range).clamp(0.0, 1.0);
let palette = [
EmotionColor::Anger, EmotionColor::Disgust, EmotionColor::Surprise, EmotionColor::Joy, EmotionColor::Interest, EmotionColor::Sadness, EmotionColor::Fear, ];
let idx = (t * 6.0).round() as usize;
palette[idx.min(6)]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Freshness {
Fresh,
Stale,
Unavailable,
}
pub fn apply_freshness(emotion: EmotionColor, freshness: Freshness) -> (EmotionColor, f32) {
match freshness {
Freshness::Fresh => (emotion, 1.0),
Freshness::Stale => (emotion, 0.5),
Freshness::Unavailable => (EmotionColor::Neutral, 1.0),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn polarity_three_branches() {
assert_eq!(polarity(1.0), EmotionColor::Joy);
assert_eq!(polarity(0.0), EmotionColor::Neutral);
assert_eq!(polarity(-1.0), EmotionColor::Anger);
}
#[test]
fn threshold_three_zones() {
assert_eq!(threshold(0.8, 0.2, 0.5), EmotionColor::Joy);
assert_eq!(threshold(0.3, 0.2, 0.5), EmotionColor::Surprise);
assert_eq!(threshold(0.1, 0.2, 0.5), EmotionColor::Anger);
assert_eq!(threshold(0.5, 0.2, 0.5), EmotionColor::Joy);
assert_eq!(threshold(0.2, 0.2, 0.5), EmotionColor::Surprise);
}
#[test]
fn threshold_multi_picks_highest_satisfied() {
let cutoffs = [0.0, 10.0, 20.0, 30.0];
let colors = [
EmotionColor::Anger,
EmotionColor::Disgust,
EmotionColor::Surprise,
EmotionColor::Joy,
];
assert_eq!(threshold_multi(35.0, &cutoffs, &colors), EmotionColor::Joy);
assert_eq!(threshold_multi(25.0, &cutoffs, &colors), EmotionColor::Surprise);
assert_eq!(threshold_multi(15.0, &cutoffs, &colors), EmotionColor::Disgust);
assert_eq!(threshold_multi(5.0, &cutoffs, &colors), EmotionColor::Anger);
assert_eq!(threshold_multi(-5.0, &cutoffs, &colors), EmotionColor::Anger);
}
#[test]
fn semantic_returns_category_color() {
assert_eq!(semantic(SemanticAction::Confirm), EmotionColor::Joy);
assert_eq!(semantic(SemanticAction::Danger), EmotionColor::Anger);
assert_eq!(semantic(SemanticAction::Navigate), EmotionColor::Neutral);
}
#[test]
fn continuous_endpoints() {
assert_eq!(continuous(0.0, 0.0, 1.0), EmotionColor::Anger);
assert_eq!(continuous(1.0, 0.0, 1.0), EmotionColor::Fear);
assert_eq!(continuous(0.5, 0.0, 1.0), EmotionColor::Joy);
}
#[test]
fn continuous_degenerate_range_yields_neutral() {
assert_eq!(continuous(5.0, 10.0, 10.0), EmotionColor::Neutral);
}
#[test]
fn freshness_fresh_passes_through() {
let (e, op) = apply_freshness(EmotionColor::Joy, Freshness::Fresh);
assert_eq!(e, EmotionColor::Joy);
assert_eq!(op, 1.0);
}
#[test]
fn freshness_stale_dims_opacity() {
let (e, op) = apply_freshness(EmotionColor::Joy, Freshness::Stale);
assert_eq!(e, EmotionColor::Joy);
assert_eq!(op, 0.5);
}
#[test]
fn freshness_unavailable_replaces_with_neutral() {
let (e, _) = apply_freshness(EmotionColor::Joy, Freshness::Unavailable);
assert_eq!(e, EmotionColor::Neutral);
}
#[test]
fn hex_round_trip() {
for c in [
EmotionColor::Anger,
EmotionColor::Disgust,
EmotionColor::Surprise,
EmotionColor::Joy,
EmotionColor::Interest,
EmotionColor::Sadness,
EmotionColor::Fear,
EmotionColor::Neutral,
EmotionColor::Inactive,
] {
let (r, g, b) = c.rgb();
let reconstructed = (r as u32) << 16 | (g as u32) << 8 | b as u32;
assert_eq!(reconstructed, c.hex());
}
}
}