diff --git a/crates/typst-pdf/src/color.rs b/crates/typst-pdf/src/color.rs index a758d935..3d90926f 100644 --- a/crates/typst-pdf/src/color.rs +++ b/crates/typst-pdf/src/color.rs @@ -10,20 +10,12 @@ use crate::page::{PageContext, Transforms}; pub const SRGB: Name<'static> = Name(b"srgb"); pub const D65_GRAY: Name<'static> = Name(b"d65gray"); pub const OKLAB: Name<'static> = Name(b"oklab"); -pub const HSV: Name<'static> = Name(b"hsv"); -pub const HSL: Name<'static> = Name(b"hsl"); pub const LINEAR_SRGB: Name<'static> = Name(b"linearrgb"); // The names of the color components. const OKLAB_L: Name<'static> = Name(b"L"); const OKLAB_A: Name<'static> = Name(b"A"); const OKLAB_B: Name<'static> = Name(b"B"); -const HSV_H: Name<'static> = Name(b"H"); -const HSV_S: Name<'static> = Name(b"S"); -const HSV_V: Name<'static> = Name(b"V"); -const HSL_H: Name<'static> = Name(b"H"); -const HSL_S: Name<'static> = Name(b"S"); -const HSL_L: Name<'static> = Name(b"L"); // The ICC profiles. static SRGB_ICC_DEFLATED: Lazy> = @@ -34,10 +26,6 @@ static GRAY_ICC_DEFLATED: Lazy> = // The PostScript functions for color spaces. static OKLAB_DEFLATED: Lazy> = Lazy::new(|| deflate(minify(include_str!("postscript/oklab.ps")).as_bytes())); -static HSV_DEFLATED: Lazy> = - Lazy::new(|| deflate(minify(include_str!("postscript/hsv.ps")).as_bytes())); -static HSL_DEFLATED: Lazy> = - Lazy::new(|| deflate(minify(include_str!("postscript/hsl.ps")).as_bytes())); /// The color spaces present in the PDF document #[derive(Default)] @@ -45,8 +33,6 @@ pub struct ColorSpaces { oklab: Option, srgb: Option, d65_gray: Option, - hsv: Option, - hsl: Option, use_linear_rgb: bool, } @@ -70,24 +56,6 @@ impl ColorSpaces { *self.d65_gray.get_or_insert_with(|| alloc.bump()) } - /// Get a reference to the hsv color space. - /// - /// # Warning - /// The Hue component of the color must be in degrees and must be divided - /// by 360.0 before being encoded into the PDF file. - pub fn hsv(&mut self, alloc: &mut Ref) -> Ref { - *self.hsv.get_or_insert_with(|| alloc.bump()) - } - - /// Get a reference to the hsl color space. - /// - /// # Warning - /// The Hue component of the color must be in degrees and must be divided - /// by 360.0 before being encoded into the PDF file. - pub fn hsl(&mut self, alloc: &mut Ref) -> Ref { - *self.hsl.get_or_insert_with(|| alloc.bump()) - } - /// Mark linear RGB as used. pub fn linear_rgb(&mut self) { self.use_linear_rgb = true; @@ -101,7 +69,7 @@ impl ColorSpaces { alloc: &mut Ref, ) { match color_space { - ColorSpace::Oklab => { + ColorSpace::Oklab | ColorSpace::Hsl | ColorSpace::Hsv => { let mut oklab = writer.device_n([OKLAB_L, OKLAB_A, OKLAB_B]); self.write(ColorSpace::LinearRgb, oklab.alternate_color_space(), alloc); oklab.tint_ref(self.oklab(alloc)); @@ -121,18 +89,6 @@ impl ColorSpaces { ]), ); } - ColorSpace::Hsl => { - let mut hsl = writer.device_n([HSL_H, HSL_S, HSL_L]); - self.write(ColorSpace::Srgb, hsl.alternate_color_space(), alloc); - hsl.tint_ref(self.hsl(alloc)); - hsl.attrs().subtype(DeviceNSubtype::DeviceN); - } - ColorSpace::Hsv => { - let mut hsv = writer.device_n([HSV_H, HSV_S, HSV_V]); - self.write(ColorSpace::Srgb, hsv.alternate_color_space(), alloc); - hsv.tint_ref(self.hsv(alloc)); - hsv.attrs().subtype(DeviceNSubtype::DeviceN); - } ColorSpace::Cmyk => writer.device_cmyk(), } } @@ -151,14 +107,6 @@ impl ColorSpaces { self.write(ColorSpace::D65Gray, spaces.insert(D65_GRAY).start(), alloc); } - if self.hsv.is_some() { - self.write(ColorSpace::Hsv, spaces.insert(HSV).start(), alloc); - } - - if self.hsl.is_some() { - self.write(ColorSpace::Hsl, spaces.insert(HSL).start(), alloc); - } - if self.use_linear_rgb { self.write(ColorSpace::LinearRgb, spaces.insert(LINEAR_SRGB).start(), alloc); } @@ -176,24 +124,6 @@ impl ColorSpaces { .filter(Filter::FlateDecode); } - // Write the HSV function & color space. - if let Some(hsv) = self.hsv { - chunk - .post_script_function(hsv, &HSV_DEFLATED) - .domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .filter(Filter::FlateDecode); - } - - // Write the HSL function & color space. - if let Some(hsl) = self.hsl { - chunk - .post_script_function(hsl, &HSL_DEFLATED) - .domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .filter(Filter::FlateDecode); - } - // Write the sRGB color space. if let Some(srgb) = self.srgb { chunk @@ -255,7 +185,7 @@ pub trait ColorEncode { impl ColorEncode for ColorSpace { fn encode(&self, color: Color) -> [f32; 4] { match self { - ColorSpace::Oklab | ColorSpace::Oklch => { + ColorSpace::Oklab | ColorSpace::Oklch | ColorSpace::Hsl | ColorSpace::Hsv => { let [l, c, h, alpha] = color.to_oklch().to_vec4(); // Clamp on Oklch's chroma, not Oklab's a\* and b\* as to not distort hue. let c = c.clamp(0.0, 0.5); @@ -264,15 +194,7 @@ impl ColorEncode for ColorSpace { let b = c * h.to_radians().sin(); [l, a + 0.5, b + 0.5, alpha] } - ColorSpace::Hsl => { - let [h, s, l, _] = color.to_hsl().to_vec4(); - [h / 360.0, s, l, 0.0] - } - ColorSpace::Hsv => { - let [h, s, v, _] = color.to_hsv().to_vec4(); - [h / 360.0, s, v, 0.0] - } - _ => color.to_vec4(), + _ => color.to_space(*self).to_vec4(), } } } @@ -315,7 +237,7 @@ impl PaintEncode for Color { ctx.content.set_fill_color([l]); } // Oklch is converted to Oklab. - Color::Oklab(_) | Color::Oklch(_) => { + Color::Oklab(_) | Color::Oklch(_) | Color::Hsl(_) | Color::Hsv(_) => { ctx.parent.colors.oklab(&mut ctx.parent.alloc); ctx.set_fill_color_space(OKLAB); @@ -342,20 +264,6 @@ impl PaintEncode for Color { let [c, m, y, k] = ColorSpace::Cmyk.encode(*self); ctx.content.set_fill_cmyk(c, m, y, k); } - Color::Hsl(_) => { - ctx.parent.colors.hsl(&mut ctx.parent.alloc); - ctx.set_fill_color_space(HSL); - - let [h, s, l, _] = ColorSpace::Hsl.encode(*self); - ctx.content.set_fill_color([h, s, l]); - } - Color::Hsv(_) => { - ctx.parent.colors.hsv(&mut ctx.parent.alloc); - ctx.set_fill_color_space(HSV); - - let [h, s, v, _] = ColorSpace::Hsv.encode(*self); - ctx.content.set_fill_color([h, s, v]); - } } } @@ -369,7 +277,7 @@ impl PaintEncode for Color { ctx.content.set_stroke_color([l]); } // Oklch is converted to Oklab. - Color::Oklab(_) | Color::Oklch(_) => { + Color::Oklab(_) | Color::Oklch(_) | Color::Hsl(_) | Color::Hsv(_) => { ctx.parent.colors.oklab(&mut ctx.parent.alloc); ctx.set_stroke_color_space(OKLAB); @@ -396,20 +304,6 @@ impl PaintEncode for Color { let [c, m, y, k] = ColorSpace::Cmyk.encode(*self); ctx.content.set_stroke_cmyk(c, m, y, k); } - Color::Hsl(_) => { - ctx.parent.colors.hsl(&mut ctx.parent.alloc); - ctx.set_stroke_color_space(HSL); - - let [h, s, l, _] = ColorSpace::Hsl.encode(*self); - ctx.content.set_stroke_color([h, s, l]); - } - Color::Hsv(_) => { - ctx.parent.colors.hsv(&mut ctx.parent.alloc); - ctx.set_stroke_color_space(HSV); - - let [h, s, v, _] = ColorSpace::Hsv.encode(*self); - ctx.content.set_stroke_color([h, s, v]); - } } } } diff --git a/crates/typst-pdf/src/gradient.rs b/crates/typst-pdf/src/gradient.rs index b12ac53f..0882a70e 100644 --- a/crates/typst-pdf/src/gradient.rs +++ b/crates/typst-pdf/src/gradient.rs @@ -8,7 +8,7 @@ use pdf_writer::{Filter, Finish, Name, Ref}; use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform}; use typst::util::Numeric; use typst::visualize::{ - Color, ColorSpace, ConicGradient, Gradient, RelativeTo, WeightedColor, + Color, ColorSpace, Gradient, RatioOrAngle, RelativeTo, WeightedColor, }; use crate::color::{ColorSpaceExt, PaintEncode, QuantizedColor}; @@ -49,7 +49,13 @@ pub(crate) fn write_gradients(ctx: &mut PdfContext) { ctx.colors .write(gradient.space(), shading.color_space(), &mut ctx.alloc); - let (sin, cos) = (angle.sin(), angle.cos()); + let (mut sin, mut cos) = (angle.sin(), angle.cos()); + + // Scale to edges of unit square. + let factor = cos.abs() + sin.abs(); + sin *= factor; + cos *= factor; + let (x1, y1, x2, y2): (f64, f64, f64, f64) = match angle.quadrant() { Quadrant::First => (0.0, 0.0, cos, sin), Quadrant::Second => (1.0, 0.0, cos + 1.0, sin), @@ -57,12 +63,6 @@ pub(crate) fn write_gradients(ctx: &mut PdfContext) { Quadrant::Fourth => (0.0, 1.0, cos, sin + 1.0), }; - let clamp = |i: f64| if i < 1e-4 { 0.0 } else { i.clamp(0.0, 1.0) }; - let x1 = clamp(x1); - let y1 = clamp(y1); - let x2 = clamp(x2); - let y2 = clamp(y2); - shading .anti_alias(gradient.anti_alias()) .function(shading_function) @@ -100,7 +100,7 @@ pub(crate) fn write_gradients(ctx: &mut PdfContext) { shading_pattern } Gradient::Conic(conic) => { - let vertices = compute_vertex_stream(conic, aspect_ratio); + let vertices = compute_vertex_stream(&gradient, aspect_ratio); let stream_shading_id = ctx.alloc.bump(); let mut stream_shading = @@ -148,73 +148,20 @@ fn shading_function(ctx: &mut PdfContext, gradient: &Gradient) -> Ref { for window in gradient.stops_ref().windows(2) { let (first, second) = (window[0], window[1]); - // Skip stops with the same position. - if first.1.get() == second.1.get() { - continue; - } + // If we have a hue index, we will create several stops in-between + // to make the gradient smoother without interpolation issues with + // native color spaces. + let mut last_c = first.0; + if gradient.space().hue_index().is_some() { + for i in 0..=32 { + let t = i as f64 / 32.0; + let real_t = first.1.get() * (1.0 - t) + second.1.get() * t; - // If the color space is HSL or HSV, and we cross the 0°/360° boundary, - // we need to create two separate stops. - if gradient.space() == ColorSpace::Hsl || gradient.space() == ColorSpace::Hsv { - let t1 = first.1.get() as f32; - let t2 = second.1.get() as f32; - let [h1, s1, x1, _] = first.0.to_space(gradient.space()).to_vec4(); - let [h2, s2, x2, _] = second.0.to_space(gradient.space()).to_vec4(); - - // Compute the intermediary stop at 360°. - if (h1 - h2).abs() > 180.0 { - let h1 = if h1 < h2 { h1 + 360.0 } else { h1 }; - let h2 = if h2 < h1 { h2 + 360.0 } else { h2 }; - - // We compute where the crossing happens between zero and one - let t = (360.0 - h1) / (h2 - h1); - // We then map it back to the original range. - let t_prime = t * (t2 - t1) + t1; - - // If the crossing happens between the two stops, - // we need to create an extra stop. - if t_prime <= t2 && t_prime >= t1 { - bounds.push(t_prime); - bounds.push(t_prime); - bounds.push(t2); - encode.extend([0.0, 1.0]); - encode.extend([0.0, 1.0]); - encode.extend([0.0, 1.0]); - - // These need to be individual function to encode 360.0 correctly. - let func1 = ctx.alloc.bump(); - ctx.pdf - .exponential_function(func1) - .range(gradient.space().range()) - .c0(gradient.space().convert(first.0)) - .c1([1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) - .domain([0.0, 1.0]) - .n(1.0); - - let func2 = ctx.alloc.bump(); - ctx.pdf - .exponential_function(func2) - .range(gradient.space().range()) - .c0([1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) - .c1([0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) - .domain([0.0, 1.0]) - .n(1.0); - - let func3 = ctx.alloc.bump(); - ctx.pdf - .exponential_function(func3) - .range(gradient.space().range()) - .c0([0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) - .c1(gradient.space().convert(second.0)) - .domain([0.0, 1.0]) - .n(1.0); - - functions.push(func1); - functions.push(func2); - functions.push(func3); - - continue; - } + let c = gradient.sample(RatioOrAngle::Ratio(Ratio::new(real_t))); + functions.push(single_gradient(ctx, last_c, c, ColorSpace::Oklab)); + bounds.push(real_t as f32); + encode.extend([0.0, 1.0]); + last_c = c; } } @@ -427,108 +374,76 @@ fn control_point(c: Point, r: f32, angle_start: f32, angle_end: f32) -> (Point, } #[comemo::memoize] -fn compute_vertex_stream(conic: &ConicGradient, aspect_ratio: Ratio) -> Arc> { +fn compute_vertex_stream(gradient: &Gradient, aspect_ratio: Ratio) -> Arc> { + let Gradient::Conic(conic) = gradient else { unreachable!() }; + // Generated vertices for the Coons patches let mut vertices = Vec::new(); // Correct the gradient's angle let angle = Gradient::correct_aspect_ratio(conic.angle, aspect_ratio); - // We want to generate a vertex based on some conditions, either: - // - At the boundary of a stop - // - At the boundary of a quadrant - // - When we cross the boundary of a hue turn (for HSV and HSL only) for window in conic.stops.windows(2) { let ((c0, t0), (c1, t1)) = (window[0], window[1]); - // Skip stops with the same position + // Precision: + // - On an even color, insert a stop every 90deg + // - For a hue-based color space, insert 200 stops minimum + // - On any other, insert 20 stops minimum + let max_dt = if c0 == c1 { + 0.25 + } else if conic.space.hue_index().is_some() { + 0.005 + } else { + 0.05 + }; + let encode_space = conic + .space + .hue_index() + .map(|_| ColorSpace::Oklab) + .unwrap_or(conic.space); + let mut t_x = t0.get(); + let dt = (t1.get() - t0.get()).min(max_dt); + + // Special casing for sharp gradients. if t0 == t1 { + write_patch( + &mut vertices, + t0.get() as f32, + t1.get() as f32, + encode_space.convert(c0), + encode_space.convert(c1), + angle, + ); continue; } - // If the angle between the two stops is greater than 90 degrees, we need to - // generate a vertex at the boundary of the quadrant. - // However, we add more stops in-between to make the gradient smoother, so we - // need to generate a vertex at least every 5 degrees. - // If the colors are the same, we do it every quadrant only. - let slope = 1.0 / (t1.get() - t0.get()); - let mut t_x = t0.get(); - let dt = (t1.get() - t0.get()).min(0.25); while t_x < t1.get() { let t_next = (t_x + dt).min(t1.get()); - let t1 = slope * (t_x - t0.get()); - let t2 = slope * (t_next - t0.get()); - - // We don't use `Gradient::sample` to avoid issues with sharp gradients. + // The current progress in the current window. + let t = |t| (t - t0.get()) / (t1.get() - t0.get()); let c = Color::mix_iter( - [WeightedColor::new(c0, 1.0 - t1), WeightedColor::new(c1, t1)], + [WeightedColor::new(c0, 1.0 - t(t_x)), WeightedColor::new(c1, t(t_x))], conic.space, ) .unwrap(); let c_next = Color::mix_iter( - [WeightedColor::new(c0, 1.0 - t2), WeightedColor::new(c1, t2)], + [ + WeightedColor::new(c0, 1.0 - t(t_next)), + WeightedColor::new(c1, t(t_next)), + ], conic.space, ) .unwrap(); - // If the color space is HSL or HSV, and we cross the 0°/360° boundary, - // we need to create two separate stops. - if conic.space == ColorSpace::Hsl || conic.space == ColorSpace::Hsv { - let [h1, s1, x1, _] = c.to_space(conic.space).to_vec4(); - let [h2, s2, x2, _] = c_next.to_space(conic.space).to_vec4(); - - // Compute the intermediary stop at 360°. - if (h1 - h2).abs() > 180.0 { - let h1 = if h1 < h2 { h1 + 360.0 } else { h1 }; - let h2 = if h2 < h1 { h2 + 360.0 } else { h2 }; - - // We compute where the crossing happens between zero and one - let t = (360.0 - h1) / (h2 - h1); - // We then map it back to the original range. - let t_prime = t * (t_next as f32 - t_x as f32) + t_x as f32; - - // If the crossing happens between the two stops, - // we need to create an extra stop. - if t_prime <= t_next as f32 && t_prime >= t_x as f32 { - let c0 = [1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]; - let c1 = [0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]; - let c0 = c0.map(|c| u16::quantize(c, [0.0, 1.0])); - let c1 = c1.map(|c| u16::quantize(c, [0.0, 1.0])); - - write_patch( - &mut vertices, - t_x as f32, - t_prime, - conic.space.convert(c), - c0, - angle, - ); - - write_patch(&mut vertices, t_prime, t_prime, c0, c1, angle); - - write_patch( - &mut vertices, - t_prime, - t_next as f32, - c1, - conic.space.convert(c_next), - angle, - ); - - t_x = t_next; - continue; - } - } - } - write_patch( &mut vertices, t_x as f32, t_next as f32, - conic.space.convert(c), - conic.space.convert(c_next), + encode_space.convert(c), + encode_space.convert(c_next), angle, ); diff --git a/crates/typst-pdf/src/postscript/hsl.ps b/crates/typst-pdf/src/postscript/hsl.ps deleted file mode 100644 index 740bc3ed..00000000 --- a/crates/typst-pdf/src/postscript/hsl.ps +++ /dev/null @@ -1,63 +0,0 @@ - -{ - % Starting stack: H, S, L - % /!\ WARNING: The hue component **MUST** be encoded - % in the range [0, 1] before calling this function. - % This is because the function assumes that the - % hue component are divided by a factor of 360 - % in order to meet the range requirements of the - % PDF specification. - - % First we do H = (H * 360.0) % 360 - 3 2 roll 360 mul 3 1 roll - - % Compute C = (1 - |2 * L - 1|) * S - dup 1 exch 2 mul 1 sub abs sub 3 2 roll mul - - % P = (H / 60) % 2 - 3 2 roll dup 60 div 2 - 2 copy div cvi mul exch sub abs - - % X = C * (1 - |P - 1|) - 1 exch 1 sub abs sub 3 2 roll dup 3 1 roll mul - - % Compute m = L - C / 2 - exch dup 2 div 5 4 roll exch sub - - % Rotate so H is top - 4 3 roll exch 4 1 roll - - % Construct the RGB stack - dup 60 lt { - % We need to build: (C, X, 0) - pop 0 3 1 roll - } { - dup 120 lt { - % We need to build: (X, C, 0) - pop exch 0 3 1 roll - } { - dup 180 lt { - % We need to build: (0, C, X) - pop 0 - } { - dup 240 lt { - % We need to build: (0, X, C) - pop exch 0 - } { - 300 lt { - % We need to build: (X, 0, C) - 0 3 2 roll - } { - % We need to build: (C, 0, X) - 0 exch - } ifelse - } ifelse - } ifelse - } ifelse - } ifelse - - 4 3 roll - - % Add m to each component - dup dup 6 2 roll add 5 2 roll add exch 4 3 roll add exch -} \ No newline at end of file diff --git a/crates/typst-pdf/src/postscript/hsv.ps b/crates/typst-pdf/src/postscript/hsv.ps deleted file mode 100644 index b29adf11..00000000 --- a/crates/typst-pdf/src/postscript/hsv.ps +++ /dev/null @@ -1,62 +0,0 @@ -{ - % Starting stack: H, S, V - % /!\ WARNING: The hue component **MUST** be encoded - % in the range [0, 1] before calling this function. - % This is because the function assumes that the - % hue component are divided by a factor of 360 - % in order to meet the range requirements of the - % PDF specification. - - % First we do H = (H * 360.0) % 360 - 3 2 roll 360 mul 3 1 roll - - % Compute C = V * S - dup 3 1 roll mul - - % P = (H / 60) % 2 - 3 2 roll dup 60 div 2 - 2 copy div cvi mul exch sub abs - - % X = C * (1 - |P - 1|) - 1 exch 1 sub abs sub 3 2 roll dup 3 1 roll mul - - % Compute m = V - C - exch dup 5 4 roll exch sub - - % Rotate so H is top - 4 3 roll exch 4 1 roll - - % Construct the RGB stack - dup 60 lt { - % We need to build: (C, X, 0) - pop 0 3 1 roll - } { - dup 120 lt { - % We need to build: (X, C, 0) - pop exch 0 3 1 roll - } { - dup 180 lt { - % We need to build: (0, C, X) - pop 0 - } { - dup 240 lt { - % We need to build: (0, X, C) - pop exch 0 - } { - 300 lt { - % We need to build: (X, 0, C) - 0 3 2 roll - } { - % We need to build: (C, 0, X) - 0 exch - } ifelse - } ifelse - } ifelse - } ifelse - } ifelse - - 4 3 roll - - % Add m to each component - dup dup 6 2 roll add 5 2 roll add exch 4 3 roll add exch -} \ No newline at end of file diff --git a/crates/typst/src/visualize/gradient.rs b/crates/typst/src/visualize/gradient.rs index 3848b499..623cc368 100644 --- a/crates/typst/src/visualize/gradient.rs +++ b/crates/typst/src/visualize/gradient.rs @@ -161,6 +161,20 @@ use crate::visualize::{Color, ColorSpace, WeightedColor}; /// # Presets /// Typst predefines color maps that you can use with your gradients. See the /// [`color`]($color/#predefined-color-maps) documentation for more details. +/// +/// # Note on file sizes +/// +/// Gradients can be quite large, especially if they have many stops. This is +/// because gradients are stored as a list of colors and offsets, which can +/// take up a lot of space. If you are concerned about file sizes, you should +/// consider the following: +/// - SVG gradients are currently inefficiently encoded. This will be improved +/// in the future. +/// - PDF gradients in the [`color.hsv`]($color.hsv), [`color.hsl`]($color.hsl), +/// and [`color.oklch`]($color.oklch) color spaces are stored as a list of +/// [`color.oklab`]($color.oklab) colors with extra stops in between. This +/// avoids needing to encode these color spaces in your PDF file, but it does +/// add extra stops to your gradient, which can increase the file size. #[ty(scope)] #[derive(Clone, PartialEq, Eq, Hash)] pub enum Gradient { diff --git a/tests/ref/visualize/gradient-hue-rotation.png b/tests/ref/visualize/gradient-hue-rotation.png new file mode 100644 index 00000000..2d786f71 Binary files /dev/null and b/tests/ref/visualize/gradient-hue-rotation.png differ diff --git a/tests/typ/visualize/gradient-hue-rotation.typ b/tests/typ/visualize/gradient-hue-rotation.typ new file mode 100644 index 00000000..2cc6f9a6 --- /dev/null +++ b/tests/typ/visualize/gradient-hue-rotation.typ @@ -0,0 +1,66 @@ +// Tests whether hue rotation works correctly. + +--- +// Test in Oklab space for reference. +#set page( + width: 100pt, + height: 30pt, + fill: gradient.linear(red, purple, space: oklab) +) + +--- +// Test in OkLCH space. +#set page( + width: 100pt, + height: 30pt, + fill: gradient.linear(red, purple, space: oklch) +) + +--- +// Test in HSV space. +#set page( + width: 100pt, + height: 30pt, + fill: gradient.linear(red, purple, space: color.hsv) +) + +--- +// Test in HSL space. +#set page( + width: 100pt, + height: 30pt, + fill: gradient.linear(red, purple, space: color.hsl) +) + + +--- +// Test in Oklab space for reference. +#set page( + width: 100pt, + height: 100pt, + fill: gradient.conic(red, purple, space: oklab) +) + +--- +// Test in OkLCH space. +#set page( + width: 100pt, + height: 100pt, + fill: gradient.conic(red, purple, space: oklch) +) + +--- +// Test in HSV space. +#set page( + width: 100pt, + height: 100pt, + fill: gradient.conic(red, purple, space: color.hsv) +) + +--- +// Test in HSL space. +#set page( + width: 100pt, + height: 100pt, + fill: gradient.conic(red, purple, space: color.hsl) +)