mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e25be21376a772d15f97ae789845340a9651d3c4246ff5ebb6a2b35f9c37bd31"
|
checksum = "e25be21376a772d15f97ae789845340a9651d3c4246ff5ebb6a2b35f9c37bd31"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oklab"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "467e40ada50d13bab19019e3707862b5076ca15841f31ee1474c40397c1b9f11"
|
||||||
|
dependencies = [
|
||||||
|
"rgb",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.17.1"
|
version = "1.17.1"
|
||||||
@ -2340,6 +2349,7 @@ dependencies = [
|
|||||||
"indexmap",
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
|
"oklab",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pdf-writer",
|
"pdf-writer",
|
||||||
"pixglyph",
|
"pixglyph",
|
||||||
|
@ -40,6 +40,11 @@ static FONTS: Lazy<(Prehashed<FontBook>, Vec<Font>)> = Lazy::new(|| {
|
|||||||
|
|
||||||
static LIBRARY: Lazy<Prehashed<Library>> = Lazy::new(|| {
|
static LIBRARY: Lazy<Prehashed<Library>> = Lazy::new(|| {
|
||||||
let mut lib = typst_library::build();
|
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
|
lib.styles
|
||||||
.set(PageElem::set_width(Smart::Custom(Abs::pt(240.0).into())));
|
.set(PageElem::set_width(Smart::Custom(Abs::pt(240.0).into())));
|
||||||
lib.styles.set(PageElem::set_height(Smart::Auto));
|
lib.styles.set(PageElem::set_height(Smart::Auto));
|
||||||
|
@ -3,7 +3,7 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use time::{Month, PrimitiveDateTime};
|
use time::{Month, PrimitiveDateTime};
|
||||||
|
|
||||||
use typst::eval::{Datetime, Regex};
|
use typst::eval::{Datetime, Module, Regex};
|
||||||
|
|
||||||
use crate::prelude::*;
|
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.
|
/// Creates a custom symbol with modifiers.
|
||||||
///
|
///
|
||||||
/// ## Example { #example }
|
/// ## Example { #example }
|
||||||
|
@ -23,6 +23,7 @@ pub(super) fn define(global: &mut Scope) {
|
|||||||
global.define("luma", luma_func());
|
global.define("luma", luma_func());
|
||||||
global.define("rgb", rgb_func());
|
global.define("rgb", rgb_func());
|
||||||
global.define("cmyk", cmyk_func());
|
global.define("cmyk", cmyk_func());
|
||||||
|
global.define("color", color_module());
|
||||||
global.define("datetime", datetime_func());
|
global.define("datetime", datetime_func());
|
||||||
global.define("symbol", symbol_func());
|
global.define("symbol", symbol_func());
|
||||||
global.define("str", str_func());
|
global.define("str", str_func());
|
||||||
|
@ -28,6 +28,7 @@ image = { version = "0.24", default-features = false, features = ["png", "jpeg",
|
|||||||
indexmap = "1.9.3"
|
indexmap = "1.9.3"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
miniz_oxide = "0.7"
|
miniz_oxide = "0.7"
|
||||||
|
oklab = "1"
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
pdf-writer = "0.7.1"
|
pdf-writer = "0.7.1"
|
||||||
pixglyph = "0.1"
|
pixglyph = "0.1"
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::diag::bail;
|
||||||
|
use crate::eval::{cast, Array, Cast};
|
||||||
|
|
||||||
/// A color in a dynamic format.
|
/// A color in a dynamic format.
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
@ -68,6 +70,31 @@ impl Color {
|
|||||||
Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()),
|
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 {
|
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.
|
/// An 8-bit grayscale color.
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct LumaColor(pub u8);
|
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::align::{Align, GenAlign, HorizontalAlign, VerticalAlign};
|
||||||
pub use self::angle::{Angle, AngleUnit};
|
pub use self::angle::{Angle, AngleUnit};
|
||||||
pub use self::axes::{Axes, Axis};
|
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::corners::{Corner, Corners};
|
||||||
pub use self::dir::Dir;
|
pub use self::dir::Dir;
|
||||||
pub use self::ellipse::ellipse;
|
pub use self::ellipse::ellipse;
|
||||||
|
@ -14,6 +14,26 @@
|
|||||||
#test(rgb("#133337").negate(), rgb(236, 204, 200))
|
#test(rgb("#133337").negate(), rgb(236, 204, 200))
|
||||||
#test(white.lighten(100%), white)
|
#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.
|
// Test gray color conversion.
|
||||||
// Ref: true
|
// Ref: true
|
||||||
@ -40,6 +60,18 @@
|
|||||||
// Error: 21-26 expected integer or ratio, found boolean
|
// Error: 21-26 expected integer or ratio, found boolean
|
||||||
#rgb(10%, 20%, 30%, false)
|
#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
|
// Ref: true
|
||||||
#let envelope = symbol(
|
#let envelope = symbol(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user