Skip to content

Commit

Permalink
Add 514 rgb() support (#254)
Browse files Browse the repository at this point in the history
Allows for `rgb()` with arguments of form 
`[r,g,b|h,s,[v|l|y]],(a),(space)`

Also allows the full names of the arguments. Technically, BYOND 
supports just the first letter so `halifax` works for `hue` but fuck 
that.

Also now lints to see if the values are in the valid ranges.

Co-authored-by: Tad Hardesty <tad@platymuus.com>
  • Loading branch information
ZeWaka and SpaceManiac authored Mar 19, 2021
1 parent 0a108b6 commit 7e3731e
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 21 deletions.
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/dreammaker/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/dreammaker/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
202 changes: 182 additions & 20 deletions src/dreammaker/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -856,4 +841,181 @@ impl<'a> ConstantFolder<'a> {
}
Err(self.error(format!("unknown variable: {}", ident)))
}

fn rgb(&mut self, args: Vec<Expression>) -> Result<String, DMError> {
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<i32>,
}

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<f64> = 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))
}
}
}
Loading

0 comments on commit 7e3731e

Please sign in to comment.