diff --git a/color-types/src/lib.rs b/color-types/src/lib.rs index 2f273c1f9..147e29233 100644 --- a/color-types/src/lib.rs +++ b/color-types/src/lib.rs @@ -221,6 +221,38 @@ impl SrgbaPixel { #[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))] pub struct SrgbaTuple(pub f32, pub f32, pub f32, pub f32); +impl SrgbaTuple { + pub fn premultiply(self) -> Self { + let SrgbaTuple(r, g, b, a) = self; + Self(r * a, g * a, b * a, a) + } + + pub fn demultiply(self) -> Self { + let SrgbaTuple(r, g, b, a) = self; + if a != 0. { + Self(r / a, g / a, b / a, a) + } else { + self + } + } + + pub fn interpolate(self, other: Self, k: f64) -> Self { + let k = k as f32; + + let SrgbaTuple(r0, g0, b0, a0) = self.premultiply(); + let SrgbaTuple(r1, g1, b1, a1) = other.premultiply(); + + let r = SrgbaTuple( + r0 + k * (r1 - r0), + g0 + k * (g1 - g0), + b0 + k * (b1 - b0), + a0 + k * (a1 - a0), + ); + + r.demultiply() + } +} + impl ToDynamic for SrgbaTuple { fn to_dynamic(&self) -> Value { self.to_string().to_dynamic() @@ -237,6 +269,13 @@ impl FromDynamic for SrgbaTuple { } } +impl From for SrgbaTuple { + fn from(pixel: SrgbaPixel) -> SrgbaTuple { + let (r, g, b, a) = pixel.as_srgba_tuple(); + SrgbaTuple(r, g, b, a) + } +} + impl From<(f32, f32, f32, f32)> for SrgbaTuple { fn from((r, g, b, a): (f32, f32, f32, f32)) -> SrgbaTuple { SrgbaTuple(r, g, b, a) diff --git a/wezterm-font/src/hbwrap.rs b/wezterm-font/src/hbwrap.rs index b07a8771e..e63fd8856 100644 --- a/wezterm-font/src/hbwrap.rs +++ b/wezterm-font/src/hbwrap.rs @@ -864,8 +864,12 @@ impl ColorLine { color_stops: color_stops .into_iter() .map(|stop| ColorStop { - offset: stop.offset, - color: hb_color_to_srgba_pixel(stop.color), + offset: stop.offset.into(), + color: if stop.is_foreground != 0 { + SrgbaPixel::rgba(0xff, 0xff, 0xff, 0xff) + } else { + hb_color_to_srgba_pixel(stop.color) + }, }) .collect(), extend: hb_extend_to_cairo(extend), diff --git a/wezterm-font/src/rasterizer/colr.rs b/wezterm-font/src/rasterizer/colr.rs index 6001c132a..e8f053ef7 100644 --- a/wezterm-font/src/rasterizer/colr.rs +++ b/wezterm-font/src/rasterizer/colr.rs @@ -1,9 +1,17 @@ -use cairo::{Context, Extend, LinearGradient, Matrix, Operator, RadialGradient}; -use wezterm_color_types::SrgbaPixel; +use cairo::{Context, Extend, LinearGradient, Matrix, Mesh, MeshCorner, Operator, RadialGradient}; +use wezterm_color_types::{SrgbaPixel, SrgbaTuple}; + +/* The gradient related routines in this file were ported from HarfBuzz, which + * were in turn ported from BlackRenderer by Black Foundry. + * Used by permission to relicense to HarfBuzz license, + * which is in turn compatible with wezterm's license. + * + * https://github.com/BlackFoundryCom/black-renderer + */ #[derive(Clone, Debug)] pub struct ColorStop { - pub offset: f32, + pub offset: f64, pub color: SrgbaPixel, } @@ -148,16 +156,346 @@ pub fn paint_radial_gradient( Ok(()) } +#[derive(Copy, Clone, Debug)] +struct Point { + x: f64, + y: f64, +} + +impl Point { + fn dot(&self, other: Self) -> f64 { + (self.x * other.x) + (self.y * other.y) + } + + fn normalize(self) -> Self { + let len = self.dot(self).sqrt(); + Self { + x: self.x / len, + y: self.y / len, + } + } + + pub fn sum(self, other: Self) -> Self { + Self { + x: self.x + other.x, + y: self.y + other.y, + } + } + + pub fn difference(self, other: Self) -> Self { + Self { + x: self.x - other.x, + y: self.y - other.y, + } + } + + pub fn scale(self, factor: f64) -> Self { + Self { + x: self.x * factor, + y: self.y * factor, + } + } + + /// Compute a vector from the supplied angle + pub fn from_angle(angle: f64) -> Self { + let (y, x) = angle.sin_cos(); + Self { x, y } + } +} + +fn interpolate(f0: f64, f1: f64, f: f64) -> f64 { + f0 + f * (f1 - f0) +} + +#[derive(Debug)] +struct Patch { + p0: Point, + c0: Point, + c1: Point, + p1: Point, + color0: SrgbaTuple, + color1: SrgbaTuple, +} + +impl Patch { + fn add_to_mesh(&self, center: Point, mesh: &Mesh) { + mesh.begin_patch(); + mesh.move_to(center.x, center.y); + mesh.line_to(self.p0.x, self.p0.y); + mesh.curve_to( + self.c0.x, self.c0.y, self.c1.x, self.c1.y, self.p1.x, self.p1.y, + ); + mesh.line_to(center.x, center.y); + + fn set_corner_color(mesh: &Mesh, corner: MeshCorner, color: SrgbaTuple) { + let SrgbaTuple(r, g, b, a) = color; + + mesh.set_corner_color_rgba(corner, r.into(), g.into(), b.into(), a.into()); + } + + set_corner_color(mesh, MeshCorner::MeshCorner0, self.color0); + set_corner_color(mesh, MeshCorner::MeshCorner1, self.color0); + set_corner_color(mesh, MeshCorner::MeshCorner2, self.color1); + set_corner_color(mesh, MeshCorner::MeshCorner3, self.color1); + + mesh.end_patch(); + } +} + +fn add_sweep_gradient_patches( + mesh: &Mesh, + center: Point, + radius: f64, + a0: f64, + c0: SrgbaTuple, + a1: f64, + c1: SrgbaTuple, +) { + const MAX_ANGLE: f64 = std::f64::consts::PI / 8.; + let num_splits = ((a1 - a0).abs() / MAX_ANGLE).ceil() as usize; + + let mut p0 = Point::from_angle(a0); + let mut color0 = c0; + + for idx in 0..num_splits { + let k = (idx as f64 + 1.) / num_splits as f64; + + let angle1 = interpolate(a0, a1, k); + let color1 = c0.interpolate(c1, k); + + let p1 = Point::from_angle(angle1); + + let a = p0.sum(p1).normalize(); + let u = Point { x: -a.y, y: a.x }; + + fn compute_control(a: Point, u: Point, p: Point, center: Point, radius: f64) -> Point { + let c = a.sum(u.scale(p.difference(a).dot(p) / u.dot(p))); + c.difference(p) + .scale(0.33333) + .sum(c) + .scale(radius) + .sum(center) + } + + let patch = Patch { + color0, + color1, + p0: center.sum(p0.scale(radius)), + p1: center.sum(p1.scale(radius)), + c0: compute_control(a, u, p0, center, radius), + c1: compute_control(a, u, p1, center, radius), + }; + + patch.add_to_mesh(center, mesh); + + p0 = p1; + color0 = color1; + } +} + +const PI_TIMES_2: f64 = std::f64::consts::PI * 2.; + +fn apply_sweep_gradient_patches( + mesh: &Mesh, + mut color_line: ColorLine, + center: Point, + radius: f64, + mut start_angle: f64, + mut end_angle: f64, +) { + if start_angle == end_angle { + if color_line.extend == Extend::Pad { + if start_angle > 0. { + let c = color_line.color_stops[0].color.into(); + add_sweep_gradient_patches(mesh, center, radius, 0., c, start_angle, c); + } + if end_angle < PI_TIMES_2 { + let c = color_line.color_stops.last().unwrap().color.into(); + add_sweep_gradient_patches(mesh, center, radius, end_angle, c, PI_TIMES_2, c); + } + } + return; + } + + if end_angle < start_angle { + std::mem::swap(&mut start_angle, &mut end_angle); + color_line.color_stops.reverse(); + for stop in &mut color_line.color_stops { + stop.offset = 1.0 - stop.offset; + } + } + + let angles: Vec = color_line + .color_stops + .iter() + .map(|stop| start_angle + stop.offset * (end_angle - start_angle)) + .collect(); + let colors: Vec = color_line + .color_stops + .iter() + .map(|stop| stop.color.into()) + .collect(); + + let n_stops = angles.len(); + + if color_line.extend == Extend::Pad { + let mut color0 = colors[0]; + let mut pos = 0; + while pos < n_stops { + if angles[pos] >= 0. { + if pos > 0 { + let k = (0. - angles[pos - 1]) / (angles[pos] - angles[pos - 1]); + + color0 = colors[pos - 1].interpolate(colors[pos], k); + } + break; + } + pos += 1; + } + if pos == n_stops { + /* everything is below 0 */ + color0 = colors[n_stops - 1]; + add_sweep_gradient_patches(mesh, center, radius, 0., color0, PI_TIMES_2, color0); + return; + } + + add_sweep_gradient_patches(mesh, center, radius, 0., color0, angles[pos], colors[pos]); + + pos += 1; + while pos < n_stops { + if angles[pos] <= PI_TIMES_2 { + add_sweep_gradient_patches( + mesh, + center, + radius, + angles[pos - 1], + colors[pos - 1], + angles[pos], + colors[pos], + ); + } else { + let k = (PI_TIMES_2 - angles[pos - 1]) / (angles[pos] - angles[pos - 1]); + let color1 = colors[pos - 1].interpolate(colors[pos], k); + add_sweep_gradient_patches( + mesh, + center, + radius, + angles[pos - 1], + colors[pos - 1], + PI_TIMES_2, + color1, + ); + break; + } + pos += 1; + } + + if pos == n_stops { + /* everything is below 2*M_PI */ + color0 = colors[n_stops - 1]; + add_sweep_gradient_patches( + mesh, + center, + radius, + angles[n_stops - 1], + color0, + PI_TIMES_2, + color0, + ); + return; + } + } else { + let span = angles[n_stops - 1] - angles[0]; + let mut k = 0isize; + if angles[0] >= 0. { + let mut ss = angles[0]; + while ss > 0. { + if span > 0. { + ss -= span; + k -= 1; + } else { + ss += span; + k += 1; + } + } + } else if angles[0] < 0. { + let mut ee = angles[n_stops - 1]; + while ee < 0. { + if span > 0. { + ee += span; + k += 1; + } else { + ee -= span; + k -= 1; + } + } + } + + debug_assert!( + angles[0] + (k as f64) * span <= 0. && 0. < angles[n_stops - 1] + (k as f64) * span + ); + let span = span.abs(); + + for l in k..k.min(1000) { + for i in 1..n_stops { + let (a0, a1, c0, c1); + + if l % 2 != 0 && color_line.extend == Extend::Reflect { + a0 = angles[0] + angles[n_stops - 1] - angles[n_stops - 1 - (i - 1)] + + (l as f64) * span; + a1 = angles[0] + angles[n_stops - 1] - angles[n_stops - 1 - i] + + (l as f64) * span; + c0 = colors[n_stops - 1 - (i - 1)]; + c1 = colors[n_stops - 1 - i]; + } else { + a0 = angles[i - 1] + (l as f64) * span; + a1 = angles[i] + (l as f64) * span; + c0 = colors[i - 1]; + c1 = colors[i]; + } + + if a1 < 0. { + continue; + } + + if a0 < 0. { + let f = (0. - a0) / (a1 - a0); + let color = c0.interpolate(c1, f); + add_sweep_gradient_patches(mesh, center, radius, 0., color, a1, c1); + } else if a1 >= PI_TIMES_2 { + let f = (PI_TIMES_2 - a0) / (a1 - a0); + let color = c0.interpolate(c1, f); + add_sweep_gradient_patches(mesh, center, radius, a0, c0, PI_TIMES_2, color); + return; + } else { + add_sweep_gradient_patches(mesh, center, radius, a0, c0, a1, c1); + } + } + } + } +} + pub fn paint_sweep_gradient( context: &Context, x0: f64, y0: f64, start_angle: f64, end_angle: f64, - mut color_line: ColorLine, + color_line: ColorLine, ) -> anyhow::Result<()> { - let (min_stop, max_stop) = normalize_color_line(&mut color_line); - anyhow::bail!("NOT IMPL: SweepGradient"); + let (x1, y1, x2, y2) = context.clip_extents()?; + + let max_x = ((x1 - x0) * (x1 - x0)).max((x2 - x0) * (x2 - x0)); + let max_y = ((y1 - y0) * (y1 - y0)).max((y2 - y0) * (y2 - y0)); + let radius = (max_x + max_y).sqrt(); + + let mesh = Mesh::new(); + let center = Point { x: x0, y: y0 }; + apply_sweep_gradient_patches(&mesh, color_line, center, radius, start_angle, end_angle); + context.set_source(mesh)?; + context.paint()?; + + Ok(()) } fn normalize_color_line(color_line: &mut ColorLine) -> (f64, f64) { @@ -179,12 +517,6 @@ fn normalize_color_line(color_line: &mut ColorLine) -> (f64, f64) { } } - // NOTE: hb-cairo-utils will call back out to some other state - // to fill in the color when is_foreground is true, defaulting - // to black with alpha varying by the alpha channel of the - // color value in the stop. Do we need to do something like - // that here? - (smallest as f64, largest as f64) }