diff --git a/Cargo.lock b/Cargo.lock index 1c68d1e41..f53e0c64f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "addr2line" version = "0.14.1" @@ -319,6 +321,12 @@ dependencies = [ "objc", ] +[[package]] +name = "color_space" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3776b2bcc4e914db501bb9be9572dd706e344b9eb8f882894f3daa651d281381" + [[package]] name = "const_fn" version = "0.4.5" @@ -533,6 +541,7 @@ version = "0.1.0" dependencies = [ "bitflags", "builtins-proc-macro", + "color_space", "guard", "interval-tree", "linked-hash-map", diff --git a/src/dreammaker/Cargo.toml b/src/dreammaker/Cargo.toml index 09d28d4e9..53dd9a280 100644 --- a/src/dreammaker/Cargo.toml +++ b/src/dreammaker/Cargo.toml @@ -19,6 +19,7 @@ serde_derive = "1.0.103" toml = "0.5.5" guard = "0.5.0" phf = { version = "0.8.0", features = ["macros"] } +color_space = "0.5.3" [dependencies.linked-hash-map] git = "https://github.com/SpaceManiac/linked-hash-map" diff --git a/src/dreammaker/builtins.rs b/src/dreammaker/builtins.rs index 17e28d744..fed686620 100644 --- a/src/dreammaker/builtins.rs +++ b/src/dreammaker/builtins.rs @@ -513,7 +513,7 @@ pub fn register_builtins(tree: &mut ObjectTree) { proc/REGEX_QUOTE_REPLACEMENT(text); proc/replacetext(Haystack,Needle,Replacement,Start=1,End=0); proc/replacetextEx(Haystack,Needle,Replacement,Start=1,End=0); - proc/rgb(R,G,B,A=null,space); // special form? - [r,g,b|x,y,z],(a),(space) + proc/rgb(R,G,B,A=null,space,red,blue,green,alpha,h,hue,s,saturation,c,chroma,v,value,l,y,luminance); // [r,g,b|h,s,[v|l|y]],(a),(space) proc/rgb2num(color, space); proc/roll(ndice=1,sides); // +1 form proc/round(A,B=null); diff --git a/src/dreammaker/constants.rs b/src/dreammaker/constants.rs index c7a3a7ad7..343c4472e 100644 --- a/src/dreammaker/constants.rs +++ b/src/dreammaker/constants.rs @@ -5,11 +5,12 @@ use std::path::Path; use linked_hash_map::LinkedHashMap; use ordered_float::OrderedFloat; +use color_space::{Hsl, Hsv, Lch, Rgb}; -use super::{DMError, Location, HasLocation, Context}; -use super::objtree::*; use super::ast::*; +use super::objtree::*; use super::preprocessor::DefineMap; +use super::{Context, DMError, HasLocation, Location, Severity}; /// An absolute typepath and optional variables. /// @@ -478,7 +479,7 @@ fn constant_ident_lookup( .cloned() { Some(decl) => decl, - None => return Ok(ConstLookup::Continue(None)), // definitely doesn't exist + None => return Ok(ConstLookup::Continue(None)), // definitely doesn't exist }; let type_ = &mut tree[ty]; @@ -742,23 +743,7 @@ impl<'a> ConstantFolder<'a> { "cos" => self.trig_op(args, f32::cos)?, "arcsin" => self.trig_op(args, f32::asin)?, "arccos" => self.trig_op(args, f32::acos)?, - "rgb" => { - use std::fmt::Write; - if args.len() != 3 && args.len() != 4 { - return Err(self.error(format!("malformed rgb() call, must have 3 or 4 arguments and instead has {}", args.len()))); - } - let mut result = String::with_capacity(7); - result.push('#'); - for each in args { - if let Some(i) = self.expr(each, None)?.to_int() { - let clamped = std::cmp::max(::std::cmp::min(i, 255), 0); - let _ = write!(result, "{:02x}", clamped); - } else { - return Err(self.error("malformed rgb() call, argument wasn't an int")); - } - } - Constant::String(result) - }, + "rgb" => Constant::String(self.rgb(args)?), "defined" if self.defines.is_some() => { let defines = self.defines.unwrap(); // annoying, but keeps the match clean if args.len() != 1 { @@ -856,4 +841,181 @@ impl<'a> ConstantFolder<'a> { } Err(self.error(format!("unknown variable: {}", ident))) } + + fn rgb(&mut self, args: Vec) -> Result { + enum ColorSpace { + Rgb = 0, + Hsv = 1, + Hsl = 2, + Hcy = 3, + } + + #[derive(Default)] + struct ColorArgs { + r: bool, + g: bool, + b: bool, + h: bool, + s: bool, + v: bool, + l: bool, + c: bool, + y: bool, + a: Option, + } + + if args.len() != 3 && args.len() != 4 && args.len() != 5 { + return Err(self.error(format!("malformed rgb() call, must have 3, 4, or 5 arguments and instead has {}", args.len()))); + } + + let arguments = self.arguments(args)?; + let mut space = None; + let mut color_args = ColorArgs::default(); + + // Get the value of the `space` kwarg if present, or collect which kwargs are set to automatically determine the color space. + for (value, potential_kwarg_value) in &arguments { + // Check for kwargs if we're in the right form + if let Some(kwarg_value) = potential_kwarg_value { + if let Some(kwarg) = value.as_str() { + match kwarg { + "r" | "red" => color_args.r = true, + "g" | "green" => color_args.g = true, + "b" | "blue" => color_args.b = true, + "h" | "hue" => color_args.h = true, + "s" | "saturation" => color_args.s = true, + "v" | "value" => color_args.v = true, + "l" | "luminance" => color_args.l = true, + "c" | "chroma" => color_args.c = true, + "y" => color_args.y = true, + "a" | "alpha" => color_args.a = kwarg_value.to_int(), + "space" => match kwarg_value.to_int() { // Do we have an actual colorspace specified? Set the values. + Some(0) => space = Some(ColorSpace::Rgb), + Some(1) => space = Some(ColorSpace::Hsv), + Some(2) => space = Some(ColorSpace::Hsl), + Some(3) => space = Some(ColorSpace::Hcy), + _ => { + return Err(self.error(format!("malformed rgb() call, bad color space: {}", kwarg_value))) + } + } + _ => { + return Err(self.error(format!("malformed rgb() call, bad kwarg passed: {}", kwarg))) + } + } + } else { + return Err(self.error(format!("malformed rgb() call, kwarg is not string: {}", value))); + } + } + } + + // Only set space if it wasn't set manually by the space arg + let space = if let Some(space) = space { + space + } else { + if color_args.r || color_args.g || color_args.b { + // TODO: Add hint here for useless r/g/b kwarg + ColorSpace::Rgb + } else if color_args.h { + if color_args.c && color_args.y { + ColorSpace::Hcy + } else if color_args.s { + if color_args.v { + ColorSpace::Hsv + } else if color_args.l { + ColorSpace::Hsl + } else { + return Err(self.error("malformed rgb() call, could not determine space: only h & s specified")); + } + } else { + return Err(self.error("malformed rgb() call, could not determine space: only h specified")); + } + } else { + ColorSpace::Rgb // Default + } + }; + + let mut value_vec: Vec = vec![]; + + for (arg_pos, (value, potential_kwarg_value)) in arguments.iter().enumerate() { + let mut to_check = value; + + // Determines the range based on predetermined colorspace + let mut range = match arg_pos { + 0 => match space { + ColorSpace::Rgb => 0..=255, //r + ColorSpace::Hsv => 0..=360, //h + ColorSpace::Hsl => 0..=360, //h + ColorSpace::Hcy => 0..=360, //h + }, + 1 => match space { + ColorSpace::Rgb => 0..=255, //g + ColorSpace::Hsv => 0..=100, //s + ColorSpace::Hsl => 0..=100, //s + ColorSpace::Hcy => 0..=100, //c + }, + 2 => match space { + ColorSpace::Rgb => 0..=255, //b + ColorSpace::Hsv => 0..=100, //v + ColorSpace::Hsl => 0..=100, //l + ColorSpace::Hcy => 0..=100, //y + }, + _ => 0..=255, + }; + + // If we have a secondary value, it's a kwarg, we need to get the actual value. If this fails, it's normal. + if let Some(kwarg_value) = potential_kwarg_value { + if let Some(kwarg) = value.as_str() { + to_check = kwarg_value; // Set the value to actually check to be our associated vaue + + range = match kwarg { + "r" | "red" | "g" | "green" | "b" | "blue" => 0..=255, + "h" | "hue" => 0..=360, + "s" | "saturation" => 0..=100, + "v" | "value" => 0..=100, + "c" | "chroma" => 0..=100, + "l" | "y" | "luminance" => 0..=100, + "a" | "alpha" => 0..=255, + "space" => continue, // Don't range-check the value of the space + _ => { + return Err(self.error(format!("malformed rgb() call, bad kwarg passed: {}", kwarg))) + } + }; + } else { + return Err(self.error(format!("malformed rgb() call, kwarg is not string: {}", value))); + } + } + + if let Some(i) = to_check.to_int() { + if !range.contains(&i) { + return Err(self.error(format!("malformed rgb() call, {} is not within the valid range ({}..{})", i, range.start(), range.end())) + .set_severity(Severity::Warning) + .with_location(self.location) + ); + } + let clamped = std::cmp::max(::std::cmp::min(i, *range.end()), *range.start()); + value_vec.push(clamped.into()); + } else { + return Err(self.error("malformed rgb() call, value wasn't an int")); + } + } + + assert!(value_vec.len() >= 3); // Make sure we got 3+ values + + // Convert our color given a space to a rgb hexcode + let color: Rgb = match space { + ColorSpace::Rgb => Rgb::new(value_vec[0], value_vec[1], value_vec[2]), + ColorSpace::Hsv => Hsv::new(value_vec[0], value_vec[1] * 0.01, value_vec[2] * 0.01).into(), + ColorSpace::Hsl => Hsl::new(value_vec[0], value_vec[1] * 0.01, value_vec[2] * 0.01).into(), + ColorSpace::Hcy => Lch::new(value_vec[2], value_vec[1], value_vec[0]).into(), + }; + + // Extract the raw 4th alpha positional argument if it wasn't a kwarg + let alpha = color_args.a.or(value_vec.get(3).map(|&x| x as i32)); + + // APPARENTLY the author thinks fractional rgb is a thing, hence the rounding + if let Some(alpha) = alpha { + Ok(format!("#{:02x}{:02x}{:02x}{:02x}", color.r.round() as u8, color.g.round() as u8, color.b.round() as u8, alpha)) + } else { + Ok(format!("#{:02x}{:02x}{:02x}", color.r.round() as u8, color.g.round() as u8, color.b.round() as u8)) + } + } } diff --git a/src/dreammaker/tests/constants_tests.rs b/src/dreammaker/tests/constants_tests.rs index 88be97898..dd304ea2d 100644 --- a/src/dreammaker/tests/constants_tests.rs +++ b/src/dreammaker/tests/constants_tests.rs @@ -13,3 +13,131 @@ fn floating_point_rgb() { Constant::String("#7f7f7f".to_owned()), ); } + +#[test] +fn rgb_base() { + let code_good = "rgb(0, 255, 0)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_good.as_bytes()) + .expect("evaluation failed"), + Constant::String("#00ff00".to_owned()), + ); + let code_good2 = "rgb(50, 50, 50)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_good2.as_bytes()) + .expect("evaluation failed"), + Constant::String("#323232".to_owned()), + ); +} + +#[test] +fn rgb_range() { + let code_rgb = "rgb(0, 300, 0)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_rgb.as_bytes()) + .unwrap_err().description(), + "malformed rgb() call, 300 is not within the valid range (0..255)", + ); + let code_hsv = "rgb(361, 0, 0, space=1)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_hsv.as_bytes()) + .unwrap_err().description(), + "malformed rgb() call, 361 is not within the valid range (0..360)", + ); +} + +#[test] +fn rgb_args() { + let code = "rgb(50)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code.as_bytes()) + .unwrap_err().description(), + "malformed rgb() call, must have 3, 4, or 5 arguments and instead has 1", + ); +} + +#[test] +fn rgb_alpha() { + let code_hsv = "rgb(h=0, s=0, v=100, a=50, space=1)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_hsv.as_bytes()) + .expect("evaluation failed"), + Constant::String("#ffffff32".to_owned()), + ); + let code_hsv2 = "rgb(h=0, s=0, v=100, 50)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_hsv2.as_bytes()) + .expect("evaluation failed"), + Constant::String("#ffffff32".to_owned()), + ); +} + +#[test] +fn rgb_hsv() { + let code_hsv = "rgb(h=0, s=0, v=100)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_hsv.as_bytes()) + .expect("evaluation failed"), + Constant::String("#ffffff".to_owned()), + ); + let code_hsv2 = "rgb(h=50, s=50, v=50)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_hsv2.as_bytes()) + .expect("evaluation failed"), + Constant::String("#807540".to_owned()), + ); + + let code_hsv_space = "rgb(360, 0, 0, space=1)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_hsv_space.as_bytes()) + .expect("evaluation failed"), + Constant::String("#000000".to_owned()), + ); +} + +#[test] +fn rgb_hsl() { + let code_hsl = "rgb(h=0, s=0, l=100)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_hsl.as_bytes()) + .expect("evaluation failed"), + Constant::String("#ffffff".to_owned()), + ); + let code_hsl2 = "rgb(h=50, s=50, l=50)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_hsl2.as_bytes()) + .expect("evaluation failed"), + Constant::String("#bfaa40".to_owned()), + ); + + let code_hsl_space = "rgb(360, 0, 0, space=2)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_hsl_space.as_bytes()) + .expect("evaluation failed"), + Constant::String("#000000".to_owned()), + ); +} + + +#[test] +fn rgb_hcy() { + let code_hsl = "rgb(h=0, c=0, y=100)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_hsl.as_bytes()) + .expect("evaluation failed"), + Constant::String("#ffffff".to_owned()), + ); + let code_hsl2 = "rgb(h=50, c=50, y=50)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_hsl2.as_bytes()) + .expect("evaluation failed"), + Constant::String("#b65f37".to_owned()), + ); + + let code_hsl_space = "rgb(360, 0, 0, space=3)"; + assert_eq!( + dm::constants::evaluate_str(Default::default(), code_hsl_space.as_bytes()) + .expect("evaluation failed"), + Constant::String("#000000".to_owned()), + ); +}