Color mixing function (#1332)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
parent
5fdd62141f
commit
9a9da80665
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -1311,6 +1311,15 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e25be21376a772d15f97ae789845340a9651d3c4246ff5ebb6a2b35f9c37bd31"
|
||||
|
||||
[[package]]
|
||||
name = "oklab"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "467e40ada50d13bab19019e3707862b5076ca15841f31ee1474c40397c1b9f11"
|
||||
dependencies = [
|
||||
"rgb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.1"
|
||||
@ -2340,6 +2349,7 @@ dependencies = [
|
||||
"indexmap",
|
||||
"log",
|
||||
"miniz_oxide",
|
||||
"oklab",
|
||||
"once_cell",
|
||||
"pdf-writer",
|
||||
"pixglyph",
|
||||
|
@ -40,6 +40,11 @@ static FONTS: Lazy<(Prehashed<FontBook>, Vec<Font>)> = Lazy::new(|| {
|
||||
|
||||
static LIBRARY: Lazy<Prehashed<Library>> = Lazy::new(|| {
|
||||
let mut lib = typst_library::build();
|
||||
// Hack for documenting the `mix` function in the color module.
|
||||
// Will be superseded by proper associated functions.
|
||||
lib.global
|
||||
.scope_mut()
|
||||
.define("mix", typst_library::compute::mix_func());
|
||||
lib.styles
|
||||
.set(PageElem::set_width(Smart::Custom(Abs::pt(240.0).into())));
|
||||
lib.styles.set(PageElem::set_height(Smart::Auto));
|
||||
|
@ -3,7 +3,7 @@ use std::str::FromStr;
|
||||
|
||||
use time::{Month, PrimitiveDateTime};
|
||||
|
||||
use typst::eval::{Datetime, Regex};
|
||||
use typst::eval::{Datetime, Module, Regex};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
@ -379,6 +379,44 @@ cast! {
|
||||
},
|
||||
}
|
||||
|
||||
/// A module with functions operating on colors.
|
||||
pub fn color_module() -> Module {
|
||||
let mut scope = Scope::new();
|
||||
scope.define("mix", mix_func());
|
||||
Module::new("color").with_scope(scope)
|
||||
}
|
||||
|
||||
/// Create a color by mixing two or more colors.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```example
|
||||
/// #color.mix(red, green)
|
||||
/// #color.mix(red, green, white)
|
||||
/// #color.mix(red, green, space: "srgb")
|
||||
/// #color.mix((red, 30%), (green, 70%))
|
||||
/// ````
|
||||
///
|
||||
/// _Note:_ This function must be specified as `color.mix`, not just `mix`.
|
||||
/// Currently, `color` is a module, but it is designed to be forward compatible
|
||||
/// with a future `color` type.
|
||||
///
|
||||
/// Display: Mix
|
||||
/// Category: construct
|
||||
#[func]
|
||||
pub fn mix(
|
||||
/// The colors, optionally with weights, specified as a pair (array of
|
||||
/// length two) of color and weight (float or ratio).
|
||||
#[variadic]
|
||||
colors: Vec<WeightedColor>,
|
||||
/// The color space to mix in. By default, this happens in a perceptual
|
||||
/// color space (Oklab).
|
||||
#[named]
|
||||
#[default(ColorSpace::Oklab)]
|
||||
space: ColorSpace,
|
||||
) -> StrResult<Color> {
|
||||
Color::mix(colors, space)
|
||||
}
|
||||
|
||||
/// Creates a custom symbol with modifiers.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
|
@ -23,6 +23,7 @@ pub(super) fn define(global: &mut Scope) {
|
||||
global.define("luma", luma_func());
|
||||
global.define("rgb", rgb_func());
|
||||
global.define("cmyk", cmyk_func());
|
||||
global.define("color", color_module());
|
||||
global.define("datetime", datetime_func());
|
||||
global.define("symbol", symbol_func());
|
||||
global.define("str", str_func());
|
||||
|
@ -28,6 +28,7 @@ image = { version = "0.24", default-features = false, features = ["png", "jpeg",
|
||||
indexmap = "1.9.3"
|
||||
log = "0.4"
|
||||
miniz_oxide = "0.7"
|
||||
oklab = "1"
|
||||
once_cell = "1"
|
||||
pdf-writer = "0.7.1"
|
||||
pixglyph = "0.1"
|
||||
|
@ -1,6 +1,8 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use crate::diag::bail;
|
||||
use crate::eval::{cast, Array, Cast};
|
||||
|
||||
/// A color in a dynamic format.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
@ -68,6 +70,31 @@ impl Color {
|
||||
Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mixes multiple colors through weight.
|
||||
pub fn mix(
|
||||
colors: impl IntoIterator<Item = WeightedColor>,
|
||||
space: ColorSpace,
|
||||
) -> StrResult<Color> {
|
||||
let mut total = 0.0;
|
||||
let mut acc = [0.0; 4];
|
||||
|
||||
for WeightedColor(color, weight) in colors.into_iter() {
|
||||
let v = rgba_to_vec4(color.to_rgba(), space);
|
||||
acc[0] += weight * v[0];
|
||||
acc[1] += weight * v[1];
|
||||
acc[2] += weight * v[2];
|
||||
acc[3] += weight * v[3];
|
||||
total += weight;
|
||||
}
|
||||
|
||||
if total <= 0.0 {
|
||||
bail!("sum of weights must be positive");
|
||||
}
|
||||
|
||||
let mixed = acc.map(|v| v / total);
|
||||
Ok(vec4_to_rgba(mixed, space).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Color {
|
||||
@ -80,6 +107,74 @@ impl Debug for Color {
|
||||
}
|
||||
}
|
||||
|
||||
/// A color with a weight.
|
||||
pub struct WeightedColor(Color, f32);
|
||||
|
||||
cast! {
|
||||
WeightedColor,
|
||||
v: Color => Self(v, 1.0),
|
||||
v: Array => {
|
||||
let mut iter = v.into_iter();
|
||||
match (iter.next(), iter.next(), iter.next()) {
|
||||
(Some(c), Some(w), None) => Self(c.cast()?, w.cast::<Weight>()?.0),
|
||||
_ => bail!("expected a color or color-weight pair"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A weight for color mixing.
|
||||
struct Weight(f32);
|
||||
|
||||
cast! {
|
||||
Weight,
|
||||
v: f64 => Self(v as f32),
|
||||
v: Ratio => Self(v.get() as f32),
|
||||
}
|
||||
|
||||
/// Convert an RGBA color to four components in the given color space.
|
||||
fn rgba_to_vec4(color: RgbaColor, space: ColorSpace) -> [f32; 4] {
|
||||
match space {
|
||||
ColorSpace::Oklab => {
|
||||
let RgbaColor { r, g, b, a } = color;
|
||||
let oklab = oklab::srgb_to_oklab(oklab::RGB { r, g, b });
|
||||
[oklab.l, oklab.a, oklab.b, a as f32 / 255.0]
|
||||
}
|
||||
ColorSpace::Srgb => {
|
||||
let RgbaColor { r, g, b, a } = color;
|
||||
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert four components in the given color space to RGBA.
|
||||
fn vec4_to_rgba(vec: [f32; 4], space: ColorSpace) -> RgbaColor {
|
||||
match space {
|
||||
ColorSpace::Oklab => {
|
||||
let [l, a, b, alpha] = vec;
|
||||
let oklab::RGB { r, g, b } = oklab::oklab_to_srgb(oklab::Oklab { l, a, b });
|
||||
RgbaColor { r, g, b, a: (alpha * 255.0).round() as u8 }
|
||||
}
|
||||
ColorSpace::Srgb => {
|
||||
let [r, g, b, a] = vec;
|
||||
RgbaColor {
|
||||
r: (r * 255.0).round() as u8,
|
||||
g: (g * 255.0).round() as u8,
|
||||
b: (b * 255.0).round() as u8,
|
||||
a: (a * 255.0).round() as u8,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A color space for mixing.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||
pub enum ColorSpace {
|
||||
/// A perceptual color space.
|
||||
Oklab,
|
||||
/// The standard RGB color space.
|
||||
Srgb,
|
||||
}
|
||||
|
||||
/// An 8-bit grayscale color.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct LumaColor(pub u8);
|
||||
|
@ -31,7 +31,9 @@ pub use self::abs::{Abs, AbsUnit};
|
||||
pub use self::align::{Align, GenAlign, HorizontalAlign, VerticalAlign};
|
||||
pub use self::angle::{Angle, AngleUnit};
|
||||
pub use self::axes::{Axes, Axis};
|
||||
pub use self::color::{CmykColor, Color, LumaColor, RgbaColor};
|
||||
pub use self::color::{
|
||||
CmykColor, Color, ColorSpace, LumaColor, RgbaColor, WeightedColor,
|
||||
};
|
||||
pub use self::corners::{Corner, Corners};
|
||||
pub use self::dir::Dir;
|
||||
pub use self::ellipse::ellipse;
|
||||
|
@ -14,6 +14,26 @@
|
||||
#test(rgb("#133337").negate(), rgb(236, 204, 200))
|
||||
#test(white.lighten(100%), white)
|
||||
|
||||
// Color mixing, in Oklab space by default.
|
||||
#test(color.mix(rgb("#ff0000"), rgb("#00ff00")), rgb("#d0a800"))
|
||||
#test(color.mix(rgb("#ff0000"), rgb("#00ff00"), space: "oklab"), rgb("#d0a800"))
|
||||
#test(color.mix(rgb("#ff0000"), rgb("#00ff00"), space: "srgb"), rgb("#808000"))
|
||||
|
||||
#test(color.mix(red, green, blue), rgb("#909282"))
|
||||
#test(color.mix(red, blue, green), rgb("#909282"))
|
||||
#test(color.mix(blue, red, green), rgb("#909282"))
|
||||
|
||||
// Mix with weights.
|
||||
#test(color.mix((red, 50%), (green, 50%)), rgb("#c0983b"))
|
||||
#test(color.mix((red, 0.5), (green, 0.5)), rgb("#c0983b"))
|
||||
#test(color.mix((red, 5), (green, 5)), rgb("#c0983b"))
|
||||
#test(color.mix((green, 5), (white, 0), (red, 5)), rgb("#c0983b"))
|
||||
#test(color.mix((red, 100%), (green, 0%)), red)
|
||||
#test(color.mix((red, 0%), (green, 100%)), green)
|
||||
#test(color.mix((rgb("#aaff00"), 25%), (rgb("#aa00ff"), 75%), space: "srgb"), rgb("#aa40bf"))
|
||||
#test(color.mix((rgb("#aaff00"), 50%), (rgb("#aa00ff"), 50%), space: "srgb"), rgb("#aa8080"))
|
||||
#test(color.mix((rgb("#aaff00"), 75%), (rgb("#aa00ff"), 25%), space: "srgb"), rgb("#aabf40"))
|
||||
|
||||
---
|
||||
// Test gray color conversion.
|
||||
// Ref: true
|
||||
@ -40,6 +60,18 @@
|
||||
// Error: 21-26 expected integer or ratio, found boolean
|
||||
#rgb(10%, 20%, 30%, false)
|
||||
|
||||
---
|
||||
// Error: 12-24 expected float or ratio, found string
|
||||
#color.mix((red, "yes"), (green, "no"))
|
||||
|
||||
---
|
||||
// Error: 12-23 expected a color or color-weight pair
|
||||
#color.mix((red, 1, 2))
|
||||
|
||||
---
|
||||
// Error: 31-38 expected "oklab" or "srgb"
|
||||
#color.mix(red, green, space: "cyber")
|
||||
|
||||
---
|
||||
// Ref: true
|
||||
#let envelope = symbol(
|
||||
|
Loading…
Reference in New Issue
Block a user