Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented some additional API functions related to crate::core_type… #729

Merged
merged 2 commits into from Apr 30, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
109 changes: 109 additions & 0 deletions gdnative-core/src/core_types/color.rs
Expand Up @@ -2,6 +2,7 @@ use crate::private::get_api;
use crate::sys;
use std::mem::transmute;

use crate::core_types::GodotString;
/// RGBA color with 32 bits floating point components.
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -47,6 +48,68 @@ impl Color {
a: self.a + (weight * (other.a - self.a)),
}
}
#[inline]
pub fn blend(&self, other: &Color) -> Color {
Color::from_sys(unsafe { (get_api().godot_color_blend)(self.sys(), other.sys()) })
}

#[inline]
pub fn contrasted(&self) -> Color {
Color::from_sys(unsafe { (get_api().godot_color_contrasted)(self.sys()) })
}
#[inline]
Bromeon marked this conversation as resolved.
Show resolved Hide resolved
pub fn darkened(&self, amount: f32) -> Color {
Color::from_sys(unsafe { (get_api().godot_color_darkened)(self.sys(), amount) })
}
#[inline]
pub fn from_hsv(h: f32, s: f32, v: f32, a: f32) -> Color {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this maybe be named hsv(), consistent with the rgb() constructor -- or rename the latter?

Also, I would add overloads for HSV and HSVA.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go ahead and add those.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to rename rgb() here: from_thing is a common naming pattern for constructors in Rust.

Copy link
Contributor Author

@jacobsky jacobsky Apr 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my latest commit, I changed the name to hsv() and hsva() in in 41969dd.

I can change it to follow either pattern as they both make sense to me.

Copy link
Contributor Author

@jacobsky jacobsky Apr 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@toasteater @Bromeon after thinking this over, I was wondering if it would be possible to use the from_* naming pattern, doing so would likely break compatibility with anyone that is currently using color.

To avoid breaking compability, I could mark leave the rgb() and rgba() methods in while marking them as deprecated. Does that sound reasonable?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jacobsky That sounds reasonable! FWIW, we're working on a 0.10 release on master, so breaking compatibility is acceptable here, although this is useful to note since we might want to make a 0.9.4 release too. I think adding from_* but deprecating the old methods is good for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@toasteater Thanks for the feedback. I'll get to work on that. I noticed that there were a few clippy lints that failed when I added the deprecated to rgb() and rgba(), so I want to take a little more time to ensure that I don't miss any of those changes.

let color = Color::rgba(0.0, 0.0, 0.0, 0.0);
Color::from_sys(unsafe { (get_api().godot_color_from_hsv)(color.sys(), h, s, v, a) })
}
#[inline]
pub fn gray(&self) -> f32 {
// Implemented as described in godot docs
(self.r + self.b + self.g) / 3.0
}

#[inline]
pub fn inverted(&self) -> Color {
// Implementation as described in godot docs.
Color {
r: 1.0f32 - self.r,
g: 1.0f32 - self.g,
b: 1.0f32 - self.b,
a: self.a,
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two methods have a simple implementation, but is there a reason to not call the GDNative API? Or where do we draw the border?

For the case with to_abgr64, where the wrong type is returned by GDNative, it clearly makes sense.

Copy link

@ghost ghost Apr 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to inline simple implementations on small types to avoid the FFI overhead. We do this for Vector2 and Vector3, and Color is essentially a Vector4. I don't see how this is problematic. That does not mean that we need to actively go port everything -- it only means that it's fine and welcome to do so. Let's say that we draw the border at Color / Vector4, maybe?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think that's a good boundary 👍🏼
And yes, let's leave the other methods as is for now, no need to change what's not broken.

The only small risk is that if a Godot method ever got a small change in semantics or bugfix, it wouldn't be carried over, but for most of the methods this is unlikely.

One subtlety I could imagine is how the [0.0, 1.0] float range is mapped to the integer interval [0, 255] -- just multiplying by 255 does not create even bins. All values in [0.0, 1/255] are mapped to 0, but only the exact value 1.0 is mapped to 255. In other words, in a uniform distribution, it is infinitely improbable for a color to have a component with value 255 (in theory at least).

Copy link

@ghost ghost Apr 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Bromeon Uniform [0, 1] distributions do exist. The distribution used by Godot's RandomNumberGenerator class is one: https://github.com/godotengine/godot/blob/28f56e2cbf03a164741f2eade17f9515f887482c/core/math/random_pcg.h#L90 Nevermind, I understand what you mean now. Still, multiplying by 255 is a common way to do this, and if it's consistent with the Godot implementation I think it should be fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely. My point was more that if Godot ever decides to change how this is implemented, we will be deviating slightly and could cause subtle bugs for users.

But I don't think it's something worthwhile to worry about, we could even add tests for such cases (later, not in this PR).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. I implemented these by hand initially because they were trivial and I wasn't sure how to call the API. If there's no significant performance hit, it'd probably be better to call the api. I'll test it later when I'm back at my PC.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I didn't mean you should change it. As toasteater said, it's OK in these cases 🙂

I think you've had enough obstacles in this PR already -- if we really feel this could become a problem one day, we can simply add regression tests where we compare the built-in vs. the API function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all of the information/discussion. I'll work on implementing a basic regression test for the custom implementation for this type and include it in another PR a little later.


pub fn to_abgr32(&self) -> i32 {
jacobsky marked this conversation as resolved.
Show resolved Hide resolved
unsafe { (get_api().godot_color_to_abgr32)(self.sys()) }
}

pub fn to_abgr64(&self) -> i32 {
unsafe { (get_api().godot_color_to_abgr64)(self.sys()) }
}

pub fn to_argb32(&self) -> i32 {
unsafe { (get_api().godot_color_to_argb32)(self.sys()) }
}

pub fn to_argb64(&self) -> i32 {
jacobsky marked this conversation as resolved.
Show resolved Hide resolved
unsafe { (get_api().godot_color_to_argb64)(self.sys()) }
}

pub fn to_html(&self, with_alpha: bool) -> GodotString {
GodotString::from_sys(unsafe { (get_api().godot_color_to_html)(self.sys(), with_alpha) })
}

pub fn to_rgba32(&self) -> i32 {
unsafe { (get_api().godot_color_to_rgba32)(self.sys()) }
}

pub fn to_rgba64(&self) -> i32 {
unsafe { (get_api().godot_color_to_rgba64)(self.sys()) }
}

#[doc(hidden)]
#[inline]
Expand All @@ -72,3 +135,49 @@ fn color_repr() {
use std::mem::size_of;
assert_eq!(size_of::<Color>(), size_of::<sys::godot_color>());
}

godot_test!(test_color {
// Test to_html
assert_eq!("ffffffff", Color::rgba(1.0, 1.0, 1.0, 1.0).to_html(true).to_string());
assert_eq!("ffffff", Color::rgba(1.0, 1.0, 1.0, 1.0).to_html(false).to_string());
assert_eq!("80ffffff", Color::rgba(1.0, 1.0, 1.0, 0.5).to_html(true).to_string());
assert_eq!("ffffff", Color::rgba(1.0, 1.0, 1.0, 0.5).to_html(false).to_string());
assert_eq!("ff8000", Color::rgb(1.0, 0.5, 0.0).to_html(false).to_string());
assert_eq!("ff0080ff", Color::rgb(0.0, 0.5, 1.0).to_html(true).to_string());
// Test Gray
// String comparison due to non-trivial way to truncate floats
use crate::core_types::IsEqualApprox;
assert!(0.4f32.is_equal_approx(Color::rgb(0.2, 0.4, 0.6).gray()));
assert!(0.5f32.is_equal_approx(Color::rgb(0.1, 0.5, 0.9).gray()));
assert!(0.9f32.is_equal_approx(Color::rgb(1.0, 1.0, 0.7).gray()));
assert!(0.42f32.is_equal_approx(Color::rgb(0.6, 0.6, 0.06).gray()));
// Test invert
let inverted = Color::rgb(1.0, 1.0,1.0).inverted();
assert!(0f32.is_equal_approx(inverted.r));
assert!(0f32.is_equal_approx(inverted.g));
assert!(0f32.is_equal_approx(inverted.b));

let inverted = Color::rgb(0.95, 0.95,0.95).inverted();
assert!(0.05f32.is_equal_approx(inverted.r));
assert!(0.05f32.is_equal_approx(inverted.g));
assert!(0.05f32.is_equal_approx(inverted.b));

let inverted = Color::rgb(0.05, 0.95,0.55).inverted();
jacobsky marked this conversation as resolved.
Show resolved Hide resolved
assert!(0.95f32.is_equal_approx(inverted.r));
assert!(0.05f32.is_equal_approx(inverted.g));
assert!(0.45f32.is_equal_approx(inverted.b));

// This is a series of sanity checks to test that the API bounds work properly.
let color = Color::from_hsv(1.0, 1.0, 1.0, 1.0);
color.darkened(0.20);
color.contrasted();
color.inverted();
jacobsky marked this conversation as resolved.
Show resolved Hide resolved
color.to_rgba32();
color.to_rgba64();
color.to_abgr32();
color.to_abgr64();
color.to_argb32();
color.to_argb64();
let other_color = Color::rgba(1.0, 1.0, 1.0, 1.0);
color.blend(&other_color);
});
2 changes: 1 addition & 1 deletion test/src/lib.rs
Expand Up @@ -22,7 +22,7 @@ pub extern "C" fn run_tests(

status &= gdnative::core_types::dictionary::test_dictionary();
// status &= gdnative::test_dictionary_clone_clear();

status &= gdnative::core_types::test_color();
status &= gdnative::core_types::test_array();
status &= gdnative::core_types::test_array_debug();
// status &= gdnative::test_array_clone_clear();
Expand Down