use boa_engine::object::ObjectInitializer;
use boa_engine::property::Attribute;
use boa_engine::{Context, JsArgs, JsResult, JsValue};
use super::bind::{define_accessor, host, param_error, qualified};
const TYPE_ERROR: &str = "Incorrect parameter type.";
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
pub(crate) enum Color {
#[default]
Transparent,
Gray(f32),
Rgb(f32, f32, f32),
Cmyk(f32, f32, f32, f32),
}
fn rank(color: Color) -> u8 {
match color {
Color::Transparent => 0,
Color::Gray(_) => 1,
Color::Rgb(..) => 2,
Color::Cmyk(..) => 3,
}
}
fn in_range(component: f32) -> bool {
(0.0..=1.0).contains(&component)
}
impl Color {
pub(crate) fn convert(self, to: Space) -> Color {
match (self, to) {
(_, Space::Transparent) | (Color::Transparent, _) => Color::Transparent,
(Color::Gray(_), Space::Gray)
| (Color::Rgb(..), Space::Rgb)
| (Color::Cmyk(..), Space::Cmyk) => self,
(Color::Gray(g), Space::Rgb) => {
if in_range(g) {
Color::Rgb(g, g, g)
} else {
Color::Rgb(0.0, 0.0, 0.0)
}
}
(Color::Gray(g), Space::Cmyk) => {
if in_range(g) {
Color::Cmyk(0.0, 0.0, 0.0, 1.0 - g)
} else {
Color::Cmyk(0.0, 0.0, 0.0, 0.0)
}
}
(Color::Rgb(r, g, b), Space::Gray) => {
if in_range(r) && in_range(g) && in_range(b) {
Color::Gray(0.3 * r + 0.59 * g + 0.11 * b)
} else {
Color::Gray(0.0)
}
}
(Color::Rgb(r, g, b), Space::Cmyk) => {
if in_range(r) && in_range(g) && in_range(b) {
let inks = [1.0 - r, 1.0 - g, 1.0 - b];
let black = inks.iter().copied().fold(f32::INFINITY, f32::min);
Color::Cmyk(inks[0], inks[1], inks[2], black)
} else {
Color::Cmyk(0.0, 0.0, 0.0, 0.0)
}
}
(Color::Cmyk(c, m, y, k), Space::Gray) => {
if in_range(c) && in_range(m) && in_range(y) && in_range(k) {
Color::Gray(1.0 - (0.3 * c + 0.59 * m + 0.11 * y + k).min(1.0))
} else {
Color::Gray(0.0)
}
}
(Color::Cmyk(c, m, y, k), Space::Rgb) => {
if in_range(c) && in_range(m) && in_range(y) && in_range(k) {
Color::Rgb(
1.0 - (c + k).min(1.0),
1.0 - (m + k).min(1.0),
1.0 - (y + k).min(1.0),
)
} else {
Color::Rgb(0.0, 0.0, 0.0)
}
}
}
}
fn space(self) -> Space {
match self {
Color::Transparent => Space::Transparent,
Color::Gray(_) => Space::Gray,
Color::Rgb(..) => Space::Rgb,
Color::Cmyk(..) => Space::Cmyk,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Space {
Transparent,
Gray,
Rgb,
Cmyk,
}
impl Space {
fn parse(name: &str) -> Space {
match name {
"G" => Space::Gray,
"RGB" => Space::Rgb,
"CMYK" => Space::Cmyk,
_ => Space::Transparent,
}
}
}
fn read_color(value: &JsValue, context: &mut Context) -> JsResult<Color> {
let Some(object) = value.as_object() else {
return Ok(Color::Transparent);
};
let length = object
.get(boa_engine::js_string!("length"), context)?
.to_length(context)?;
if length == 0 {
return Ok(Color::Transparent);
}
let name = object.get(0u64, context)?;
let name = name.to_string(context)?.to_std_string_lossy();
let component = |index: u64, context: &mut Context| -> JsResult<f32> {
if length <= index {
return Ok(0.0);
}
#[allow(clippy::cast_possible_truncation)]
Ok(object.get(index, context)?.to_number(context)? as f32)
};
Ok(match Space::parse(&name) {
Space::Transparent => Color::Transparent,
Space::Gray => Color::Gray(component(1, context)?),
Space::Rgb => Color::Rgb(
component(1, context)?,
component(2, context)?,
component(3, context)?,
),
Space::Cmyk => Color::Cmyk(
component(1, context)?,
component(2, context)?,
component(3, context)?,
component(4, context)?,
),
})
}
fn write_color(color: Color, context: &mut Context) -> JsValue {
let parts: Vec<JsValue> = match color {
Color::Transparent => vec![JsValue::from(boa_engine::js_string!("T"))],
Color::Gray(g) => vec![
JsValue::from(boa_engine::js_string!("G")),
JsValue::from(f64::from(g)),
],
Color::Rgb(r, g, b) => vec![
JsValue::from(boa_engine::js_string!("RGB")),
JsValue::from(f64::from(r)),
JsValue::from(f64::from(g)),
JsValue::from(f64::from(b)),
],
Color::Cmyk(c, m, y, k) => vec![
JsValue::from(boa_engine::js_string!("CMYK")),
JsValue::from(f64::from(c)),
JsValue::from(f64::from(m)),
JsValue::from(f64::from(y)),
JsValue::from(f64::from(k)),
],
};
JsValue::from(boa_engine::object::builtins::JsArray::from_iter(
parts, context,
))
}
fn convert(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.len() < 2 {
return Err(param_error("color.convert"));
}
if !is_array(args.get_or_undefined(0)) {
return Err(qualified("color.convert", TYPE_ERROR));
}
let space = args
.get_or_undefined(1)
.clone()
.to_string(context)?
.to_std_string_lossy();
let color = read_color(&args.get_or_undefined(0).clone(), context)?;
Ok(write_color(color.convert(Space::parse(&space)), context))
}
fn equal(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.len() < 2 {
return Err(param_error("color.equal"));
}
if !is_array(args.get_or_undefined(0)) || !is_array(args.get_or_undefined(1)) {
return Err(qualified("color.equal", TYPE_ERROR));
}
let first = read_color(&args.get_or_undefined(0).clone(), context)?;
let second = read_color(&args.get_or_undefined(1).clone(), context)?;
let best = if rank(first) >= rank(second) {
first.space()
} else {
second.space()
};
Ok(JsValue::from(first.convert(best) == second.convert(best)))
}
fn is_array(value: &JsValue) -> bool {
value.as_object().is_some_and(|object| object.is_array())
}
const NAMED: [(&str, Color); 12] = [
("black", Color::Gray(0.0)),
("blue", Color::Rgb(0.0, 0.0, 1.0)),
("cyan", Color::Cmyk(1.0, 0.0, 0.0, 0.0)),
("dkGray", Color::Gray(0.25)),
("gray", Color::Gray(0.5)),
("green", Color::Rgb(0.0, 1.0, 0.0)),
("ltGray", Color::Gray(0.75)),
("magenta", Color::Cmyk(0.0, 1.0, 0.0, 0.0)),
("red", Color::Rgb(1.0, 0.0, 0.0)),
("transparent", Color::Transparent),
("white", Color::Gray(1.0)),
("yellow", Color::Cmyk(0.0, 0.0, 1.0, 0.0)),
];
macro_rules! named {
($get:ident, $set:ident, $slot:literal) => {
fn $get(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let color = host(context)
.and_then(|host| host.borrow().colors.get($slot).copied())
.unwrap_or_default();
Ok(write_color(color, context))
}
fn $set(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let incoming = args.get_or_undefined(0).clone();
if !is_array(&incoming) {
return Err(qualified(concat!("color.", $slot), TYPE_ERROR));
}
let color = read_color(&incoming, context)?;
if let Some(host) = host(context) {
host.borrow_mut().colors.insert($slot.to_owned(), color);
}
Ok(JsValue::undefined())
}
};
}
named!(get_black, set_black, "black");
named!(get_blue, set_blue, "blue");
named!(get_cyan, set_cyan, "cyan");
named!(get_dk_gray, set_dk_gray, "dkGray");
named!(get_gray, set_gray, "gray");
named!(get_green, set_green, "green");
named!(get_lt_gray, set_lt_gray, "ltGray");
named!(get_magenta, set_magenta, "magenta");
named!(get_red, set_red, "red");
named!(get_transparent, set_transparent, "transparent");
named!(get_white, set_white, "white");
named!(get_yellow, set_yellow, "yellow");
pub(crate) fn initial() -> std::collections::BTreeMap<String, Color> {
NAMED
.into_iter()
.map(|(name, color)| (name.to_owned(), color))
.collect()
}
pub(crate) fn install(context: &mut Context) -> JsResult<()> {
let color = {
let mut init = ObjectInitializer::new(context);
init.function(
super::bind::native(convert),
boa_engine::js_string!("convert"),
2,
)
.function(
super::bind::native(equal),
boa_engine::js_string!("equal"),
2,
);
init.build()
};
let properties: [(&str, super::af::Bound, super::af::Bound); 12] = [
("black", get_black, set_black),
("blue", get_blue, set_blue),
("cyan", get_cyan, set_cyan),
("dkGray", get_dk_gray, set_dk_gray),
("gray", get_gray, set_gray),
("green", get_green, set_green),
("ltGray", get_lt_gray, set_lt_gray),
("magenta", get_magenta, set_magenta),
("red", get_red, set_red),
("transparent", get_transparent, set_transparent),
("white", get_white, set_white),
("yellow", get_yellow, set_yellow),
];
for (name, get, set) in properties {
define_accessor(&color, context, name, get, set)?;
}
context.register_global_property(
boa_engine::js_string!("color"),
JsValue::from(color),
Attribute::all(),
)?;
Ok(())
}