Add Color::brighter/darker functions

These are exposed in .60 as well as in Rust and C++ and implemented by
converting to HSV color space and adjusting the brightness (value).
This commit is contained in:
Simon Hausmann 2021-02-22 11:05:46 +01:00
parent a42c3a0b01
commit 391d0152f0
13 changed files with 271 additions and 3 deletions

View file

@ -43,6 +43,7 @@ All notable changes to this project will be documented in this file.
- `return` statements - `return` statements
- `Text` word wrap and elide - `Text` word wrap and elide
- `drop-shadow-*` properties (limited to `Rectangle` at the moment) - `drop-shadow-*` properties (limited to `Rectangle` at the moment)
- `Color.brighter` / `Color.darker`
## [0.0.4] - 2020-12-14 ## [0.0.4] - 2020-12-14

View file

@ -133,6 +133,9 @@ public:
/// Returns the alpha channel of the color as u8 in the range 0..255. /// Returns the alpha channel of the color as u8 in the range 0..255.
uint8_t alpha() const { return inner.alpha; } uint8_t alpha() const { return inner.alpha; }
inline Color brighter(float factor) const;
inline Color darker(float factor) const;
/// Returns true if \a lhs has the same values for the individual color channels as \rhs; false /// Returns true if \a lhs has the same values for the individual color channels as \rhs; false
/// otherwise. /// otherwise.
friend bool operator==(const Color &lhs, const Color &rhs) friend bool operator==(const Color &lhs, const Color &rhs)
@ -156,7 +159,7 @@ public:
} }
// FIXME: we need this to create GradientStop // FIXME: we need this to create GradientStop
operator const cbindgen_private::types::Color&() { return inner; } operator const cbindgen_private::types::Color &() { return inner; }
private: private:
cbindgen_private::types::Color inner; cbindgen_private::types::Color inner;
@ -164,6 +167,20 @@ private:
friend class Brush; friend class Brush;
}; };
inline Color Color::brighter(float factor) const
{
Color result;
cbindgen_private::types::sixtyfps_color_brighter(&inner, factor, &result.inner);
return result;
}
inline Color Color::darker(float factor) const
{
Color result;
cbindgen_private::types::sixtyfps_color_darker(&inner, factor, &result.inner);
return result;
}
template<> template<>
RgbaColor<uint8_t>::RgbaColor(const Color &color) RgbaColor<uint8_t>::RgbaColor(const Color &color)
{ {

View file

@ -437,6 +437,21 @@ In addition to plain colors, many elements have properties that are of type `bru
A brush is a type that can be either a color or gradient. The brush is then used to fill an element or A brush is a type that can be either a color or gradient. The brush is then used to fill an element or
draw the outline. draw the outline.
#### Methods
All colors have methods that can be called on them:
* **`brighter(factor: float) -> Color`**
Returns a new color that is derived from this color but has its brightness increased by the specified factor.
For example if the factor is 0.5 (or for example 50%) the returned color is 50% brighter. Negative factors
decrease the brightness.
* **`darker(factor: float) -> Color`**
Returns a new color that is derived from this color but has its brightness decreased by the specified factor.
For example if the factor is .5 (or for example 50%) the returned color is 50% darker. Negative factors
increase the brightness.
#### Gradients #### Gradients
Gradients allow creating smooth colorful surfaces. They are specified using an angle and a series of Gradients allow creating smooth colorful surfaces. They are specified using an angle and a series of

View file

@ -86,6 +86,8 @@ pub enum BuiltinFunction {
StringToFloat, StringToFloat,
/// the "42".is_float() /// the "42".is_float()
StringIsFloat, StringIsFloat,
ColorBrighter,
ColorDarker,
ImplicitItemSize, ImplicitItemSize,
} }
@ -149,6 +151,14 @@ impl BuiltinFunction {
}), }),
args: vec![Type::ElementReference], args: vec![Type::ElementReference],
}, },
BuiltinFunction::ColorBrighter => Type::Function {
return_type: Box::new(Type::Color),
args: vec![Type::Color, Type::Float32],
},
BuiltinFunction::ColorDarker => Type::Function {
return_type: Box::new(Type::Color),
args: vec![Type::Color, Type::Float32],
},
} }
} }
} }

View file

@ -1434,6 +1434,12 @@ fn compile_expression(
BuiltinFunction::ImplicitItemSize => { BuiltinFunction::ImplicitItemSize => {
unreachable!() unreachable!()
} }
BuiltinFunction::ColorBrighter => {
"[](const auto &color, float factor) {{ return color.brighter(factor); }}".into()
}
BuiltinFunction::ColorDarker => {
"[](const auto &color, float factor) {{ return color.darker(factor); }}".into()
}
}, },
Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"), Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"),
Expression::MemberFunction { .. } => panic!("member function expressions must not appear in the code generator anymore"), Expression::MemberFunction { .. } => panic!("member function expressions must not appear in the code generator anymore"),

View file

@ -1137,6 +1137,12 @@ fn compile_expression(expr: &Expression, component: &Rc<Component>) -> TokenStre
BuiltinFunction::StringIsFloat => { BuiltinFunction::StringIsFloat => {
quote!((|x: SharedString| { <f64 as ::core::str::FromStr>::from_str(x.as_str()).is_ok() } )) quote!((|x: SharedString| { <f64 as ::core::str::FromStr>::from_str(x.as_str()).is_ok() } ))
} }
BuiltinFunction::ColorBrighter => {
quote!((|x: Color, factor| -> Color { x.brighter(factor as f32) }))
}
BuiltinFunction::ColorDarker => {
quote!((|x: Color, factor| -> Color { x.darker(factor as f32) }))
}
}, },
Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"), Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"),
Expression::MemberFunction{ .. } => panic!("member function expressions must not appear in the code generator anymore"), Expression::MemberFunction{ .. } => panic!("member function expressions must not appear in the code generator anymore"),

View file

@ -1304,6 +1304,24 @@ fn maybe_lookup_object(
}), }),
}; };
} }
Type::Color => {
return Expression::MemberFunction {
base: Box::new(base),
base_node: next.clone().into(), // Note that this is not the base_node, but the function's node
member: Box::new(match next_str.as_str() {
"lighter" => {
Expression::BuiltinFunctionReference(BuiltinFunction::ColorBrighter)
}
"darker" => {
Expression::BuiltinFunctionReference(BuiltinFunction::ColorDarker)
}
_ => {
ctx.diag.push_error("Cannot access fields of color".into(), &next);
return Expression::Invalid;
}
}),
};
}
_ => { _ => {
ctx.diag.push_error("Cannot access fields of property".into(), &next); ctx.diag.push_error("Cannot access fields of property".into(), &next);
return Expression::Invalid; return Expression::Invalid;

View file

@ -33,7 +33,7 @@ pub type Size = euclid::default::Size2D<f32>;
/// 2D Transform /// 2D Transform
pub type Transform = euclid::default::Transform2D<f32>; pub type Transform = euclid::default::Transform2D<f32>;
mod color; pub(crate) mod color;
pub use color::*; pub use color::*;
mod path; mod path;

View file

@ -166,6 +166,33 @@ impl Color {
pub fn alpha(self) -> u8 { pub fn alpha(self) -> u8 {
self.alpha self.alpha
} }
/// Returns a new version of this color that has the brightness increased
/// by the specified factor. This is done by converting the color to the HSV
/// color space and multiplying the brightness (value) with (1 + factor).
/// The result is converted back to RGB and the alpha channel is unchanged.
/// So for example `brighter(0.2)` will increase the brightness by 20%, and
/// calling `brighter(-0.5)` will return a color that's 50% darker.
pub fn brighter(&self, factor: f32) -> Self {
let rgbaf: RgbaColor<f32> = self.clone().into();
let mut hsva: HsvaColor = rgbaf.into();
hsva.v *= 1. + factor;
let rgbaf: RgbaColor<f32> = hsva.into();
rgbaf.into()
}
/// Returns a new version of this color that has the brightness decreased
/// by the specified factor. This is done by converting the color to the HSV
/// color space and dividing the brightness (value) by (1 + factor). The
/// result is converted back to RGB and the alpha channel is unchanged.
/// So for example `darker(0.3)` will decrease the brightness by 30%.
pub fn darker(&self, factor: f32) -> Self {
let rgbaf: RgbaColor<f32> = self.clone().into();
let mut hsva: HsvaColor = rgbaf.into();
hsva.v /= 1. + factor;
let rgbaf: RgbaColor<f32> = hsva.into();
rgbaf.into()
}
} }
impl InterpolatedPropertyValue for Color { impl InterpolatedPropertyValue for Color {
@ -198,3 +225,109 @@ impl From<Color> for femtovg::Color {
Self::rgba(col.red, col.green, col.blue, col.alpha) Self::rgba(col.red, col.green, col.blue, col.alpha)
} }
} }
#[derive(Debug, Clone, Copy, PartialEq)]
struct HsvaColor {
h: f32,
s: f32,
v: f32,
alpha: f32,
}
impl From<RgbaColor<f32>> for HsvaColor {
fn from(col: RgbaColor<f32>) -> Self {
// RGB to HSL conversion from https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
let red = col.red;
let green = col.green;
let blue = col.blue;
let min = red.min(green).min(blue);
let max = red.max(green).max(blue);
let chroma = max - min;
let hue = 60.
* if chroma == 0. {
0.0
} else if max == red {
((green - blue) / chroma) % 6.0
} else if max == green {
2. + (blue - red) / chroma
} else {
4. + (red - green) / chroma
};
let saturation = if max == 0. { 0. } else { chroma / max };
Self { h: hue, s: saturation, v: max, alpha: col.alpha }
}
}
impl From<HsvaColor> for RgbaColor<f32> {
fn from(col: HsvaColor) -> Self {
// RGB to HSL conversion from https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
let chroma = col.s * col.v;
let x = chroma * (1. - ((col.h / 60.) % 2. - 1.).abs());
let (red, green, blue) = match (col.h / 60.0) as usize {
0 => (chroma, x, 0.),
1 => (x, chroma, 0.),
2 => (0., chroma, x),
3 => (0., x, chroma),
4 => (x, 0., chroma),
5 => (chroma, 0., x),
_ => (0., 0., 0.),
};
let m = col.v - chroma;
Self { red: red + m, green: green + m, blue: blue + m, alpha: col.alpha }
}
}
#[test]
fn test_rgb_to_hsv() {
// White
assert_eq!(
HsvaColor::from(RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.5 }),
HsvaColor { h: 0., s: 0., v: 1., alpha: 0.5 }
);
assert_eq!(
RgbaColor::<f32>::from(HsvaColor { h: 0., s: 0., v: 1., alpha: 0.3 }),
RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.3 }
);
// Bright greenish, verified via colorizer.org
assert_eq!(
HsvaColor::from(RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 }),
HsvaColor { h: 120., s: 1., v: 0.9, alpha: 1.0 }
);
assert_eq!(
RgbaColor::<f32>::from(HsvaColor { h: 120., s: 1., v: 0.9, alpha: 1.0 }),
RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 }
);
}
#[test]
fn test_brighter_darker() {
let blue = Color::from_rgb_u8(0, 0, 128);
assert_eq!(blue.brighter(0.5), Color::from_rgb_u8(0, 0, 192));
assert_eq!(blue.darker(0.5), Color::from_rgb_u8(0, 0, 85));
}
pub(crate) mod ffi {
#![allow(unsafe_code)]
use super::*;
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_color_brighter(col: &Color, factor: f32, out: *mut Color) {
core::ptr::write(out, col.brighter(factor))
}
#[no_mangle]
pub unsafe extern "C" fn sixtyfps_color_darker(col: &Color, factor: f32, out: *mut Color) {
core::ptr::write(out, col.darker(factor))
}
}

View file

@ -86,4 +86,5 @@ pub fn use_modules() -> usize {
+ window::ffi::sixtyfps_component_window_drop as usize + window::ffi::sixtyfps_component_window_drop as usize
+ component::ffi::sixtyfps_component_init_items as usize + component::ffi::sixtyfps_component_init_items as usize
+ timers::ffi::sixtyfps_timer_start as usize + timers::ffi::sixtyfps_timer_start as usize
+ graphics::color::ffi::sixtyfps_color_brighter as usize
} }

View file

@ -549,6 +549,34 @@ pub fn eval_expression(e: &Expression, local_context: &mut EvalLocalContext) ->
panic!("Argument not a string"); panic!("Argument not a string");
} }
} }
Expression::BuiltinFunctionReference(BuiltinFunction::ColorBrighter) => {
if arguments.len() != 2 {
panic!("internal error: incorrect argument count to ColorBrighter")
}
if let Value::Color(col) = eval_expression(&arguments[0], local_context) {
if let Value::Number(factor) = eval_expression(&arguments[1], local_context) {
Value::Color(col.brighter(factor as _))
} else {
panic!("Second argument not a number");
}
} else {
panic!("First argument not a color");
}
}
Expression::BuiltinFunctionReference(BuiltinFunction::ColorDarker) => {
if arguments.len() != 2 {
panic!("internal error: incorrect argument count to ColorDarker")
}
if let Value::Color(col) = eval_expression(&arguments[0], local_context) {
if let Value::Number(factor) = eval_expression(&arguments[1], local_context) {
Value::Color(col.darker(factor as _))
} else {
panic!("Second argument not a number");
}
} else {
panic!("First argument not a color");
}
}
Expression::BuiltinFunctionReference(BuiltinFunction::ImplicitItemSize) => { Expression::BuiltinFunctionReference(BuiltinFunction::ImplicitItemSize) => {
if arguments.len() != 1 { if arguments.len() != 1 {
panic!("internal error: incorrect argument count to ImplicitItemSize") panic!("internal error: incorrect argument count to ImplicitItemSize")

View file

@ -0,0 +1,25 @@
/* LICENSE BEGIN
This file is part of the SixtyFPS Project -- https://sixtyfps.io
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
SPDX-License-Identifier: GPL-3.0-only
This file is also available under commercial licensing terms.
Please contact info@sixtyfps.io for more information.
LICENSE END */
Win := Window {
property <color> base: #00007F;
GridLayout {
r := Rectangle {
background: base;
}
Rectangle {
background: base.lighter(50%);
}
Rectangle {
background: base.darker(50%);
}
}
}

View file

@ -91,6 +91,8 @@ fn gen_corelib(root_dir: &Path, include_dir: &Path) -> anyhow::Result<()> {
"ComponentWindow", "ComponentWindow",
"VoidArg", "VoidArg",
"KeyEventArg", "KeyEventArg",
"sixtyfps_color_brighter",
"sixtyfps_color_darker",
] ]
.iter() .iter()
.map(|x| x.to_string()) .map(|x| x.to_string())
@ -138,7 +140,11 @@ fn gen_corelib(root_dir: &Path, include_dir: &Path) -> anyhow::Result<()> {
for (rust_types, extra_excluded_types, internal_header) in [ for (rust_types, extra_excluded_types, internal_header) in [
(vec!["Resource"], vec![], "sixtyfps_resource_internal.h"), (vec!["Resource"], vec![], "sixtyfps_resource_internal.h"),
(vec!["Color"], vec![], "sixtyfps_color_internal.h"), (
vec!["Color", "sixtyfps_color_brighter", "sixtyfps_color_darker"],
vec![],
"sixtyfps_color_internal.h",
),
( (
vec![ vec![
"PathData", "PathData",
@ -173,6 +179,8 @@ fn gen_corelib(root_dir: &Path, include_dir: &Path) -> anyhow::Result<()> {
"sixtyfps_component_window_show_popup", "sixtyfps_component_window_show_popup",
"sixtyfps_new_path_elements", "sixtyfps_new_path_elements",
"sixtyfps_new_path_events", "sixtyfps_new_path_events",
"sixtyfps_color_brighter",
"sixtyfps_color_darker",
] ]
.iter() .iter()
.filter(|exclusion| rust_types.iter().find(|inclusion| inclusion == exclusion).is_none()) .filter(|exclusion| rust_types.iter().find(|inclusion| inclusion == exclusion).is_none())