Compare commits

...

4 Commits

Author SHA1 Message Date
Nate Butler
8274b5e9c5 Try juicing AA as quads rotate 2025-05-14 09:18:00 +02:00
Nate Butler
34f3ef3d24 animate! 2025-05-13 11:43:21 +02:00
Nate Butler
a257b59ec0 Add transform example 2025-05-13 10:27:21 +02:00
Nate Butler
972ff1582e Set up initial transform example 2025-05-13 09:20:14 +02:00
6 changed files with 516 additions and 195 deletions

View File

@@ -292,3 +292,7 @@ path = "examples/window_shadow.rs"
[[example]]
name = "on_window_close_quit"
path = "examples/on_window_close_quit.rs"
[[example]]
name = "transform"
path = "examples/transform.rs"

View File

@@ -0,0 +1,152 @@
use gpui::{
AnyElement, App, Application, Bounds, Context, Hsla, IntoElement, Radians, Render,
TransformationMatrix, Window, WindowBounds, WindowOptions, canvas, div, fill, point,
prelude::*, px, rgb, scaled_px, size,
};
use smol::Timer;
use std::{f32::consts::PI, time::Duration};
#[derive(IntoElement)]
struct ExampleTile {
fg_color: Hsla,
bg_color: Hsla,
example: AnyElement,
}
impl ExampleTile {
fn new(bg_color: Hsla, fg_color: Hsla, example: impl IntoElement) -> Self {
Self {
bg_color,
fg_color,
example: example.into_any_element(),
}
}
}
impl RenderOnce for ExampleTile {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.relative()
// .overflow_hidden()
.w(px(200.))
.h(px(200.))
.bg(self.bg_color)
.border_1()
.border_color(self.fg_color)
.child(self.example)
}
}
struct TransformExample {
epoch: u64,
}
impl Render for TransformExample {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let grid = div().flex().flex_wrap().size_full();
// Create a 5x3 grid with 15 example slots
let mut grid_with_examples = grid;
// grid_with_examples = grid_with_examples.child(ExampleTile::new(
// gpui::green(),
// gpui::white(),
// render_slow_rotation_scaling_example(self.epoch, window, cx),
// ));
grid_with_examples = grid_with_examples.child(ExampleTile::new(
gpui::blue(),
gpui::white(),
render_rotation_scaling_example(self.epoch, window, cx),
));
div()
.bg(rgb(0x121212))
.size_full()
.child(grid_with_examples)
}
}
impl TransformExample {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
cx.spawn_in(window, async move |example, cx| {
loop {
Timer::after(Duration::from_millis(16)).await;
example
.update(cx, |example, cx| {
example.update_epoch(cx);
})
.ok();
}
})
.detach();
Self { epoch: 0 }
}
fn update_epoch(&mut self, cx: &mut Context<Self>) {
const MAX_EPOCH: u64 = 360;
let direction = if (self.epoch / MAX_EPOCH) % 2 == 0 {
1
} else {
-1
};
self.epoch = (self.epoch as i64 + direction).rem_euclid(MAX_EPOCH as i64) as u64;
cx.notify();
}
}
fn render_rotation_scaling_example(
epoch: u64,
_window: &mut Window,
_cx: &mut App,
) -> impl IntoElement {
canvas(
|_, _, _| {},
move |canvas_bounds, _, window, _| {
let canvas_center = canvas_bounds.center();
let quad_size = 80.0;
// Quad centered on the origin.
let quad_bounds =
Bounds::new(point(px(0.), px(0.)), size(px(quad_size), px(quad_size)));
let scale_factor = 0.75 + 0.5 * ((epoch as f32 / 180.0) * PI).sin().abs();
let rotation_angle = (epoch as f32 / 360.0) * 2.0 * PI;
let matrix = TransformationMatrix::unit()
.translate(point(
scaled_px(canvas_center.x.0, window),
scaled_px(canvas_center.y.0, window),
))
.rotate(Radians(rotation_angle))
.scale(size(scale_factor, scale_factor))
.translate(point(
scaled_px(quad_size / -2.0, window),
scaled_px(quad_size / -2.0, window),
));
let quad = fill(quad_bounds, rgb(0xFFFF00));
window.paint_quad(quad.transformation(matrix));
// window.paint_quad(quad);
},
)
.size_full()
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(980.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| cx.new(|cx| TransformExample::new(window, cx)),
)
.unwrap();
cx.activate(true);
});
}

View File

@@ -13,7 +13,7 @@ use std::{
ops::{Add, Div, Mul, MulAssign, Neg, Sub},
};
use crate::{App, DisplayId};
use crate::{App, DisplayId, Window};
/// Axis in a 2D cartesian space.
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
@@ -2825,6 +2825,12 @@ impl From<usize> for DevicePixels {
}
}
/// Converts a value in logical pixels to scaled pixels based on
/// the scale factor for the current window.
pub fn scaled_px(value: f32, window: &Window) -> ScaledPixels {
px(value).scale(window.scale_factor())
}
/// Represents scaled pixels that take into account the device's scale factor.
///
/// `ScaledPixels` are used to ensure that UI elements appear at the correct size on devices

View File

@@ -10,9 +10,10 @@ float4 srgb_to_oklab(float4 color);
float4 oklab_to_srgb(float4 color);
float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
constant Size_DevicePixels *viewport_size);
float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds,
TransformationMatrix transformation,
constant Size_DevicePixels *input_viewport_size);
float4
to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds,
TransformationMatrix transformation,
constant Size_DevicePixels *input_viewport_size);
float2 to_tile_position(float2 unit_vertex, AtlasTile tile,
constant Size_DevicePixels *atlas_size);
@@ -22,7 +23,9 @@ float corner_dash_velocity(float dv1, float dv2);
float dash_alpha(float t, float period, float length, float dash_velocity,
float antialias_threshold);
float quarter_ellipse_sdf(float2 point, float2 radii);
float pick_corner_radius(float2 center_to_point, Corners_ScaledPixels corner_radii);
float pick_corner_radius(float2 center_to_point,
Corners_ScaledPixels corner_radii);
float improved_aa_alpha(float sdf, float threshold, float gradient);
float quad_sdf(float2 point, Bounds_ScaledPixels bounds,
Corners_ScaledPixels corner_radii);
float quad_sdf_impl(float2 center_to_point, float corner_radius);
@@ -32,15 +35,17 @@ float blur_along_x(float x, float y, float sigma, float corner,
float2 half_size);
float4 over(float4 below, float4 above);
float radians(float degrees);
float4 fill_color(Background background, float2 position, Bounds_ScaledPixels bounds,
float4 solid_color, float4 color0, float4 color1);
float4 fill_color(Background background, float2 position,
Bounds_ScaledPixels bounds, float4 solid_color, float4 color0,
float4 color1);
struct GradientColor {
float4 solid;
float4 color0;
float4 color1;
};
GradientColor prepare_fill_color(uint tag, uint color_space, Hsla solid, Hsla color0, Hsla color1);
GradientColor prepare_fill_color(uint tag, uint color_space, Hsla solid,
Hsla color0, Hsla color1);
struct QuadVertexOutput {
uint quad_id [[flat]];
@@ -71,19 +76,15 @@ vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
[[buffer(QuadInputIndex_ViewportSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
Quad quad = quads[quad_id];
float4 device_position =
to_device_position(unit_vertex, quad.bounds, viewport_size);
float4 device_position = to_device_position_transformed(
unit_vertex, quad.bounds, quad.transformation, viewport_size);
float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds,
quad.content_mask.bounds);
float4 border_color = hsla_to_rgba(quad.border_color);
GradientColor gradient = prepare_fill_color(
quad.background.tag,
quad.background.color_space,
quad.background.solid,
quad.background.colors[0].color,
quad.background.colors[1].color
);
quad.background.tag, quad.background.color_space, quad.background.solid,
quad.background.colors[0].color, quad.background.colors[1].color);
return QuadVertexOutput{
quad_id,
@@ -99,46 +100,88 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
constant Quad *quads
[[buffer(QuadInputIndex_Quads)]]) {
Quad quad = quads[input.quad_id];
float4 background_color = fill_color(quad.background, input.position.xy, quad.bounds,
input.background_solid, input.background_color0, input.background_color1);
float4 background_color = fill_color(
quad.background, input.position.xy, quad.bounds, input.background_solid,
input.background_color0, input.background_color1);
bool unrounded = quad.corner_radii.top_left == 0.0 &&
quad.corner_radii.bottom_left == 0.0 &&
quad.corner_radii.top_right == 0.0 &&
quad.corner_radii.bottom_right == 0.0;
quad.corner_radii.bottom_left == 0.0 &&
quad.corner_radii.top_right == 0.0 &&
quad.corner_radii.bottom_right == 0.0;
// Fast path when the quad is not rounded and doesn't have any border
if (quad.border_widths.top == 0.0 &&
quad.border_widths.left == 0.0 &&
quad.border_widths.right == 0.0 &&
quad.border_widths.bottom == 0.0 &&
unrounded) {
return background_color;
}
// // Fast path when the quad is not rounded and doesn't have any border
// if (quad.border_widths.top == 0.0 && quad.border_widths.left == 0.0 &&
// quad.border_widths.right == 0.0 && quad.border_widths.bottom == 0.0 &&
// unrounded) {
// return background_color;
// }
float2 size = float2(quad.bounds.size.width, quad.bounds.size.height);
float2 half_size = size / 2.0;
float2 point = input.position.xy - float2(quad.bounds.origin.x, quad.bounds.origin.y);
TransformationMatrix transformation = quad.transformation;
// Invert the transformation matrix if it's going to be applied
float det =
transformation.rotation_scale[0][0] *
transformation.rotation_scale[1][1] -
transformation.rotation_scale[0][1] * transformation.rotation_scale[1][0];
float2x2 inverted_rotation_scale;
if (abs(det) > 0.0001) {
inverted_rotation_scale[0][0] = transformation.rotation_scale[1][1] / det;
inverted_rotation_scale[0][1] = -transformation.rotation_scale[0][1] / det;
inverted_rotation_scale[1][0] = -transformation.rotation_scale[1][0] / det;
inverted_rotation_scale[1][1] = transformation.rotation_scale[0][0] / det;
} else {
// If determinant is close to zero, use identity matrix as fallback
inverted_rotation_scale[0][0] = 1.0;
inverted_rotation_scale[0][1] = 0.0;
inverted_rotation_scale[1][0] = 0.0;
inverted_rotation_scale[1][1] = 1.0;
}
// Translate point back to origin, then apply inverse rotation/scale
float2 translated_position =
input.position.xy -
float2(transformation.translation[0], transformation.translation[1]);
float2 transformed_position;
transformed_position[0] =
translated_position[0] * inverted_rotation_scale[0][0] +
translated_position[1] * inverted_rotation_scale[0][1];
transformed_position[1] =
translated_position[0] * inverted_rotation_scale[1][0] +
translated_position[1] * inverted_rotation_scale[1][1];
float2 point =
transformed_position - float2(quad.bounds.origin.x, quad.bounds.origin.y);
float2 center_to_point = point - half_size;
// Signed distance field threshold for inclusion of pixels. 0.5 is the
// minimum distance between the center of the pixel and the edge.
const float antialias_threshold = 0.5;
// Base antialiasing threshold
float base_antialias_threshold = 0.5;
// Calculate rotation magnitude from transformation matrix
float rotation_factor = max(
abs(transformation.rotation_scale[0][1]),
abs(transformation.rotation_scale[1][0])
);
// Increase threshold when rotation is present (up to 1.5x for 45-degree rotations)
float antialias_threshold = base_antialias_threshold * (1.0 + rotation_factor);
// Radius of the nearest corner
float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
// Width of the nearest borders
float2 border = float2(
center_to_point.x < 0.0 ? quad.border_widths.left : quad.border_widths.right,
center_to_point.y < 0.0 ? quad.border_widths.top : quad.border_widths.bottom
);
float2 border = float2(center_to_point.x < 0.0 ? quad.border_widths.left
: quad.border_widths.right,
center_to_point.y < 0.0 ? quad.border_widths.top
: quad.border_widths.bottom);
// 0-width borders are reduced so that `inner_sdf >= antialias_threshold`.
// The purpose of this is to not draw antialiasing pixels in this case.
float2 reduced_border = float2(
border.x == 0.0 ? -antialias_threshold : border.x,
border.y == 0.0 ? -antialias_threshold : border.y);
float2 reduced_border =
float2(border.x == 0.0 ? -antialias_threshold : border.x,
border.y == 0.0 ? -antialias_threshold : border.y);
// Vector from the corner of the quad bounds to the point, after mirroring
// the point into the bottom right quadrant. Both components are <= 0.
@@ -150,26 +193,25 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
// Whether the nearest point on the border is rounded
bool is_near_rounded_corner =
corner_center_to_point.x >= 0.0 &&
corner_center_to_point.y >= 0.0;
corner_center_to_point.x >= 0.0 && corner_center_to_point.y >= 0.0;
// Vector from straight border inner corner to point.
//
// 0-width borders are turned into width -1 so that inner_sdf is > 1.0 near
// the border. Without this, antialiasing pixels would be drawn.
float2 straight_border_inner_corner_to_point = corner_to_point + reduced_border;
float2 straight_border_inner_corner_to_point =
corner_to_point + reduced_border;
// Whether the point is beyond the inner edge of the straight border
bool is_beyond_inner_straight_border =
straight_border_inner_corner_to_point.x > 0.0 ||
straight_border_inner_corner_to_point.y > 0.0;
straight_border_inner_corner_to_point.x > 0.0 ||
straight_border_inner_corner_to_point.y > 0.0;
// Whether the point is far enough inside the quad, such that the pixels are
// not affected by the straight border.
bool is_within_inner_straight_border =
straight_border_inner_corner_to_point.x < -antialias_threshold &&
straight_border_inner_corner_to_point.y < -antialias_threshold;
straight_border_inner_corner_to_point.x < -antialias_threshold &&
straight_border_inner_corner_to_point.y < -antialias_threshold;
// Fast path for points that must be part of the background
if (is_within_inner_straight_border && !is_near_rounded_corner) {
@@ -199,7 +241,8 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
// Fast path for circular inner edge.
inner_sdf = -(outer_sdf + reduced_border.x);
} else {
float2 ellipse_radii = max(float2(0.0), float2(corner_radius) - reduced_border);
float2 ellipse_radii =
max(float2(0.0), float2(corner_radius) - reduced_border);
inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii);
}
@@ -227,7 +270,8 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
// Dash pattern: (2 * border width) dash, (1 * border width) gap
const float dash_length_per_width = 2.0;
const float dash_gap_per_width = 1.0;
const float dash_period_per_width = dash_length_per_width + dash_gap_per_width;
const float dash_period_per_width =
dash_length_per_width + dash_gap_per_width;
// Since the dash size is determined by border width, the density of
// dashes varies. Multiplying a pixel distance by this returns a
@@ -242,7 +286,8 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
// When corners aren't rounded, the dashes are separately laid
// out on each straight line, rather than around the whole
// perimeter. This way each line starts and ends with a dash.
bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y;
bool is_horizontal =
corner_center_to_point.x < corner_center_to_point.y;
float border_width = is_horizontal ? border.x : border.y;
dash_velocity = dv_numerator / border_width;
t = is_horizontal ? point.x : point.y;
@@ -297,7 +342,8 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
max_t = upto_tl + c_tl;
if (is_near_rounded_corner) {
float radians = atan2(corner_center_to_point.y, corner_center_to_point.x);
float radians =
atan2(corner_center_to_point.y, corner_center_to_point.x);
float corner_t = radians * corner_radius;
if (center_to_point.x >= 0.0) {
@@ -330,7 +376,8 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
}
} else {
// Straight borders
bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y;
bool is_horizontal =
corner_center_to_point.x < corner_center_to_point.y;
if (is_horizontal) {
if (center_to_point.y < 0.0) {
dash_velocity = dv_t;
@@ -370,8 +417,8 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
float dash_gap = max_t - dash_length;
if (dash_gap > 0.0) {
float dash_period = dash_length + dash_gap;
border_color.a *= dash_alpha(t, dash_period, dash_length, dash_velocity,
antialias_threshold);
border_color.a *= dash_alpha(t, dash_period, dash_length,
dash_velocity, antialias_threshold);
}
}
}
@@ -379,11 +426,44 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
// Blend the border on top of the background and then linearly interpolate
// between the two as we slide inside the background.
float4 blended_border = over(background_color, border_color);
// Calculate analytical derivatives for better border antialiasing
float ddx_inner = dfdx(inner_sdf);
float ddy_inner = dfdy(inner_sdf);
float grad_inner = sqrt(ddx_inner * ddx_inner + ddy_inner * ddy_inner);
// Adjust inner antialiasing based on the gradient
float adjusted_inner_aa = max(antialias_threshold, grad_inner * 0.5);
color = mix(background_color, blended_border,
saturate(antialias_threshold - inner_sdf));
improved_aa_alpha(inner_sdf, adjusted_inner_aa, grad_inner));
}
return color * float4(1.0, 1.0, 1.0, saturate(antialias_threshold - outer_sdf));
// Calculate analytical derivatives for the outer edge
float ddx_outer = dfdx(outer_sdf);
float ddy_outer = dfdy(outer_sdf);
float grad_outer = sqrt(ddx_outer * ddx_outer + ddy_outer * ddy_outer);
// Adjust outer antialiasing based on gradient and rotation
float adjusted_outer_aa = max(antialias_threshold, grad_outer * 0.5);
// Apply antialiasing to the outer edge with improved quality
float alpha = improved_aa_alpha(outer_sdf, adjusted_outer_aa, grad_outer);
return color * float4(1.0, 1.0, 1.0, alpha);
}
// Improved antialiasing alpha calculation that adapts to edge gradients
// For better quality on transformed and rotated shapes
float improved_aa_alpha(float sdf, float threshold, float gradient) {
// Use smoothstep for a more natural falloff at the edges
// Scale by gradient to adapt to edge angle and transformation
float scale = max(1.0, gradient * 0.5);
// Prevent division by zero with an epsilon
float safe_scale = max(scale, 0.001);
// Apply smoothstep with gradient-aware scaling for better antialiasing on rotated shapes
return smoothstep(0.0, threshold, threshold - sdf) / safe_scale;
}
// Returns the dash velocity of a corner given the dash velocity of the two
@@ -406,9 +486,8 @@ float corner_dash_velocity(float dv1, float dv2) {
// Returns alpha used to render antialiased dashes.
// `t` is within the dash when `fmod(t, period) < length`.
float dash_alpha(
float t, float period, float length, float dash_velocity,
float antialias_threshold) {
float dash_alpha(float t, float period, float length, float dash_velocity,
float antialias_threshold) {
float half_period = period / 2.0;
float half_length = length / 2.0;
// Value in [-half_period, half_period]
@@ -507,7 +586,8 @@ fragment float4 shadow_fragment(ShadowFragmentInput input [[stage_in]],
float alpha;
if (shadow.blur_radius == 0.) {
float distance = quad_sdf(input.position.xy, shadow.bounds, shadow.corner_radii);
float distance =
quad_sdf(input.position.xy, shadow.bounds, shadow.corner_radii);
alpha = saturate(0.5 - distance);
} else {
// The signal is only non-zero in a limited range, so don't waste samples
@@ -613,8 +693,8 @@ vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex(
[[buffer(SpriteInputIndex_AtlasTextureSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
MonochromeSprite sprite = sprites[sprite_id];
float4 device_position =
to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation, viewport_size);
float4 device_position = to_device_position_transformed(
unit_vertex, sprite.bounds, sprite.transformation, viewport_size);
float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds,
sprite.content_mask.bounds);
float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
@@ -770,21 +850,12 @@ vertex PathSpriteVertexOutput path_sprite_vertex(
float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
GradientColor gradient = prepare_fill_color(
sprite.color.tag,
sprite.color.color_space,
sprite.color.solid,
sprite.color.colors[0].color,
sprite.color.colors[1].color
);
sprite.color.tag, sprite.color.color_space, sprite.color.solid,
sprite.color.colors[0].color, sprite.color.colors[1].color);
return PathSpriteVertexOutput{
device_position,
tile_position,
sprite_id,
gradient.solid,
gradient.color0,
gradient.color1
};
return PathSpriteVertexOutput{device_position, tile_position,
sprite_id, gradient.solid,
gradient.color0, gradient.color1};
}
fragment float4 path_sprite_fragment(
@@ -799,7 +870,7 @@ fragment float4 path_sprite_fragment(
PathSprite sprite = sprites[input.sprite_id];
Background background = sprite.color;
float4 color = fill_color(background, input.position.xy, sprite.bounds,
input.solid_color, input.color0, input.color1);
input.solid_color, input.color0, input.color1);
color.a *= mask;
return color;
}
@@ -904,34 +975,32 @@ float4 hsla_to_rgba(Hsla hsla) {
return rgba;
}
float3 srgb_to_linear(float3 color) {
return pow(color, float3(2.2));
}
float3 srgb_to_linear(float3 color) { return pow(color, float3(2.2)); }
float3 linear_to_srgb(float3 color) {
return pow(color, float3(1.0 / 2.2));
}
float3 linear_to_srgb(float3 color) { return pow(color, float3(1.0 / 2.2)); }
// Converts a sRGB color to the Oklab color space.
// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
// Reference:
// https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
float4 srgb_to_oklab(float4 color) {
// Convert non-linear sRGB to linear sRGB
color = float4(srgb_to_linear(color.rgb), color.a);
float l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
float m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
float s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
float l =
0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
float m =
0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
float s =
0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
float l_ = pow(l, 1.0/3.0);
float m_ = pow(m, 1.0/3.0);
float s_ = pow(s, 1.0/3.0);
float l_ = pow(l, 1.0 / 3.0);
float m_ = pow(m, 1.0 / 3.0);
float s_ = pow(s, 1.0 / 3.0);
return float4(
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
color.a
);
return float4(0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
color.a);
}
// Converts an Oklab color to the sRGB color space.
@@ -944,11 +1013,10 @@ float4 oklab_to_srgb(float4 color) {
float m = m_ * m_ * m_;
float s = s_ * s_ * s_;
float3 linear_rgb = float3(
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
);
float3 linear_rgb =
float3(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s);
// Convert linear sRGB to non-linear sRGB
return float4(linear_to_srgb(linear_rgb), color.a);
@@ -966,17 +1034,20 @@ float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
return float4(device_position, 0., 1.);
}
float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds,
TransformationMatrix transformation,
constant Size_DevicePixels *input_viewport_size) {
float4 to_device_position_transformed(
float2 unit_vertex, Bounds_ScaledPixels bounds,
TransformationMatrix transformation,
constant Size_DevicePixels *input_viewport_size) {
float2 position =
unit_vertex * float2(bounds.size.width, bounds.size.height) +
float2(bounds.origin.x, bounds.origin.y);
// Apply the transformation matrix to the position via matrix multiplication.
float2 transformed_position = float2(0, 0);
transformed_position[0] = position[0] * transformation.rotation_scale[0][0] + position[1] * transformation.rotation_scale[0][1];
transformed_position[1] = position[0] * transformation.rotation_scale[1][0] + position[1] * transformation.rotation_scale[1][1];
transformed_position[0] = position[0] * transformation.rotation_scale[0][0] +
position[1] * transformation.rotation_scale[0][1];
transformed_position[1] = position[0] * transformation.rotation_scale[1][0] +
position[1] * transformation.rotation_scale[1][1];
// Add in the translation component of the transformation matrix.
transformed_position[0] += transformation.translation[0];
@@ -989,7 +1060,6 @@ float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bo
return float4(device_position, 0., 1.);
}
float2 to_tile_position(float2 unit_vertex, AtlasTile tile,
constant Size_DevicePixels *atlas_size) {
float2 tile_origin = float2(tile.bounds.origin.x, tile.bounds.origin.y);
@@ -999,7 +1069,8 @@ float2 to_tile_position(float2 unit_vertex, AtlasTile tile,
}
// Selects corner radius based on quadrant.
float pick_corner_radius(float2 center_to_point, Corners_ScaledPixels corner_radii) {
float pick_corner_radius(float2 center_to_point,
Corners_ScaledPixels corner_radii) {
if (center_to_point.x < 0.) {
if (center_to_point.y < 0.) {
return corner_radii.top_left;
@@ -1019,31 +1090,31 @@ float pick_corner_radius(float2 center_to_point, Corners_ScaledPixels corner_rad
// border, and negative inside.
float quad_sdf(float2 point, Bounds_ScaledPixels bounds,
Corners_ScaledPixels corner_radii) {
float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.0;
float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
float2 center_to_point = point - center;
float corner_radius = pick_corner_radius(center_to_point, corner_radii);
float2 corner_to_point = fabs(center_to_point) - half_size;
float2 corner_center_to_point = corner_to_point + corner_radius;
return quad_sdf_impl(corner_center_to_point, corner_radius);
float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.0;
float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
float2 center_to_point = point - center;
float corner_radius = pick_corner_radius(center_to_point, corner_radii);
float2 corner_to_point = fabs(center_to_point) - half_size;
float2 corner_center_to_point = corner_to_point + corner_radius;
return quad_sdf_impl(corner_center_to_point, corner_radius);
}
// Implementation of quad signed distance field
float quad_sdf_impl(float2 corner_center_to_point, float corner_radius) {
if (corner_radius == 0.0) {
// Fast path for unrounded corners
return max(corner_center_to_point.x, corner_center_to_point.y);
} else {
// Signed distance of the point from a quad that is inset by corner_radius
// It is negative inside this quad, and positive outside
float signed_distance_to_inset_quad =
// 0 inside the inset quad, and positive outside
length(max(float2(0.0), corner_center_to_point)) +
// 0 outside the inset quad, and negative inside
min(0.0, max(corner_center_to_point.x, corner_center_to_point.y));
if (corner_radius == 0.0) {
// Fast path for unrounded corners
return max(corner_center_to_point.x, corner_center_to_point.y);
} else {
// Signed distance of the point from a quad that is inset by corner_radius
// It is negative inside this quad, and positive outside
float signed_distance_to_inset_quad =
// 0 inside the inset quad, and positive outside
length(max(float2(0.0), corner_center_to_point)) +
// 0 outside the inset quad, and negative inside
min(0.0, max(corner_center_to_point.x, corner_center_to_point.y));
return signed_distance_to_inset_quad - corner_radius;
}
return signed_distance_to_inset_quad - corner_radius;
}
}
// A standard gaussian function, used for weighting samples
@@ -1055,7 +1126,8 @@ float gaussian(float x, float sigma) {
float2 erf(float2 x) {
float2 s = sign(x);
float2 a = abs(x);
float2 r1 = 1. + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a;
float2 r1 =
1. + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a;
float2 r2 = r1 * r1;
return s - s / (r2 * r2);
}
@@ -1091,7 +1163,7 @@ float4 over(float4 below, float4 above) {
}
GradientColor prepare_fill_color(uint tag, uint color_space, Hsla solid,
Hsla color0, Hsla color1) {
Hsla color0, Hsla color1) {
GradientColor out;
if (tag == 0 || tag == 2) {
out.solid = hsla_to_rgba(solid);
@@ -1112,80 +1184,83 @@ GradientColor prepare_fill_color(uint tag, uint color_space, Hsla solid,
}
float2x2 rotate2d(float angle) {
float s = sin(angle);
float c = cos(angle);
return float2x2(c, -s, s, c);
float s = sin(angle);
float c = cos(angle);
return float2x2(c, -s, s, c);
}
float4 fill_color(Background background,
float2 position,
Bounds_ScaledPixels bounds,
float4 solid_color, float4 color0, float4 color1) {
float4 fill_color(Background background, float2 position,
Bounds_ScaledPixels bounds, float4 solid_color, float4 color0,
float4 color1) {
float4 color;
switch (background.tag) {
case 0:
color = solid_color;
break;
case 1: {
// -90 degrees to match the CSS gradient angle.
float gradient_angle = background.gradient_angle_or_pattern_height;
float radians = (fmod(gradient_angle, 360.0) - 90.0) * (M_PI_F / 180.0);
float2 direction = float2(cos(radians), sin(radians));
// Expand the short side to be the same as the long side
if (bounds.size.width > bounds.size.height) {
direction.y *= bounds.size.height / bounds.size.width;
} else {
direction.x *= bounds.size.width / bounds.size.height;
}
// Get the t value for the linear gradient with the color stop percentages.
float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.;
float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
float2 center_to_point = position - center;
float t = dot(center_to_point, direction) / length(direction);
// Check the direction to determine whether to use x or y
if (abs(direction.x) > abs(direction.y)) {
t = (t + half_size.x) / bounds.size.width;
} else {
t = (t + half_size.y) / bounds.size.height;
}
// Adjust t based on the stop percentages
t = (t - background.colors[0].percentage) /
(background.colors[1].percentage - background.colors[0].percentage);
t = clamp(t, 0.0, 1.0);
switch (background.color_space) {
case 0:
color = solid_color;
color = mix(color0, color1, t);
break;
case 1: {
// -90 degrees to match the CSS gradient angle.
float gradient_angle = background.gradient_angle_or_pattern_height;
float radians = (fmod(gradient_angle, 360.0) - 90.0) * (M_PI_F / 180.0);
float2 direction = float2(cos(radians), sin(radians));
// Expand the short side to be the same as the long side
if (bounds.size.width > bounds.size.height) {
direction.y *= bounds.size.height / bounds.size.width;
} else {
direction.x *= bounds.size.width / bounds.size.height;
}
// Get the t value for the linear gradient with the color stop percentages.
float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.;
float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
float2 center_to_point = position - center;
float t = dot(center_to_point, direction) / length(direction);
// Check the direction to determine whether to use x or y
if (abs(direction.x) > abs(direction.y)) {
t = (t + half_size.x) / bounds.size.width;
} else {
t = (t + half_size.y) / bounds.size.height;
}
// Adjust t based on the stop percentages
t = (t - background.colors[0].percentage)
/ (background.colors[1].percentage
- background.colors[0].percentage);
t = clamp(t, 0.0, 1.0);
switch (background.color_space) {
case 0:
color = mix(color0, color1, t);
break;
case 1: {
float4 oklab_color = mix(color0, color1, t);
color = oklab_to_srgb(oklab_color);
break;
}
}
float4 oklab_color = mix(color0, color1, t);
color = oklab_to_srgb(oklab_color);
break;
}
case 2: {
float gradient_angle_or_pattern_height = background.gradient_angle_or_pattern_height;
float pattern_width = (gradient_angle_or_pattern_height / 65535.0f) / 255.0f;
float pattern_interval = fmod(gradient_angle_or_pattern_height, 65535.0f) / 255.0f;
float pattern_height = pattern_width + pattern_interval;
float stripe_angle = M_PI_F / 4.0;
float pattern_period = pattern_height * sin(stripe_angle);
float2x2 rotation = rotate2d(stripe_angle);
float2 relative_position = position - float2(bounds.origin.x, bounds.origin.y);
float2 rotated_point = rotation * relative_position;
float pattern = fmod(rotated_point.x, pattern_period);
float distance = min(pattern, pattern_period - pattern) - pattern_period * (pattern_width / pattern_height) / 2.0f;
color = solid_color;
color.a *= saturate(0.5 - distance);
break;
}
break;
}
case 2: {
float gradient_angle_or_pattern_height =
background.gradient_angle_or_pattern_height;
float pattern_width =
(gradient_angle_or_pattern_height / 65535.0f) / 255.0f;
float pattern_interval =
fmod(gradient_angle_or_pattern_height, 65535.0f) / 255.0f;
float pattern_height = pattern_width + pattern_interval;
float stripe_angle = M_PI_F / 4.0;
float pattern_period = pattern_height * sin(stripe_angle);
float2x2 rotation = rotate2d(stripe_angle);
float2 relative_position =
position - float2(bounds.origin.x, bounds.origin.y);
float2 rotated_point = rotation * relative_position;
float pattern = fmod(rotated_point.x, pattern_period);
float distance = min(pattern, pattern_period - pattern) -
pattern_period * (pattern_width / pattern_height) / 2.0f;
color = solid_color;
color.a *= saturate(0.5 - distance);
break;
}
}
return color;

View File

@@ -462,6 +462,7 @@ pub(crate) struct Quad {
pub border_color: Hsla,
pub corner_radii: Corners<ScaledPixels>,
pub border_widths: Edges<ScaledPixels>,
pub transformation: TransformationMatrix,
}
impl From<Quad> for Primitive {

View File

@@ -7,7 +7,7 @@ use crate::{
KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Modifiers,
ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent,
Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Radians, Render, RenderGlyphParams,
RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR,
SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
@@ -2388,6 +2388,7 @@ impl Window {
corner_radii: quad.corner_radii.scale(scale_factor),
border_widths: quad.border_widths.scale(scale_factor),
border_style: quad.border_style,
transformation: quad.transformation,
});
}
@@ -4196,6 +4197,8 @@ pub struct PaintQuad {
pub border_color: Hsla,
/// The style of the quad's borders.
pub border_style: BorderStyle,
/// The transformation to apply to the quad.
pub transformation: TransformationMatrix,
}
impl PaintQuad {
@@ -4230,6 +4233,48 @@ impl PaintQuad {
..self
}
}
/// Sets the transformation matrix to apply to the quad.
pub fn transformation(self, transformation: impl Into<TransformationMatrix>) -> Self {
PaintQuad {
transformation: transformation.into(),
..self
}
}
/// Rotates the quad around its origin by the given angle (in radians, clockwise).
pub fn rotate(self, angle: impl Into<Radians>) -> Self {
PaintQuad {
transformation: self.transformation.rotate(angle.into()),
..self
}
}
/// Scales the quad around its origin by the given factor.
pub fn scale(self, scale: impl Into<Size<f32>>) -> Self {
PaintQuad {
transformation: self.transformation.scale(scale.into()),
..self
}
}
/// Translates the quad by the given offset.
pub fn translate(self, offset: impl Into<Point<ScaledPixels>>) -> Self {
PaintQuad {
transformation: self.transformation.translate(offset.into()),
..self
}
}
/// Applies multiple transformations in sequence: first rotation, then scaling, then translation.
pub fn transform(
self,
angle: impl Into<Radians>,
scale: impl Into<Size<f32>>,
offset: impl Into<Point<ScaledPixels>>,
) -> Self {
self.rotate(angle).scale(scale).translate(offset)
}
}
/// Creates a quad with the given parameters.
@@ -4248,9 +4293,45 @@ pub fn quad(
border_widths: border_widths.into(),
border_color: border_color.into(),
border_style,
transformation: TransformationMatrix::default(),
}
}
/// Applies a transformation matrix to a quad.
pub fn transform_quad(quad: PaintQuad, transformation: TransformationMatrix) -> PaintQuad {
PaintQuad {
transformation,
..quad
}
}
/// Creates a filled quad that is rotated by the given angle.
pub fn rotated_fill(
bounds: impl Into<Bounds<Pixels>>,
background: impl Into<Background>,
angle: impl Into<Radians>,
) -> PaintQuad {
fill(bounds, background).rotate(angle)
}
/// Creates a filled quad that is scaled by the given factor.
pub fn scaled_fill(
bounds: impl Into<Bounds<Pixels>>,
background: impl Into<Background>,
scale: impl Into<Size<f32>>,
) -> PaintQuad {
fill(bounds, background).scale(scale)
}
/// Creates a filled quad that is translated by the given offset.
pub fn translated_fill(
bounds: impl Into<Bounds<Pixels>>,
background: impl Into<Background>,
offset: impl Into<Point<ScaledPixels>>,
) -> PaintQuad {
fill(bounds, background).translate(offset)
}
/// Creates a filled quad with the given bounds and background color.
pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Background>) -> PaintQuad {
PaintQuad {
@@ -4260,6 +4341,7 @@ pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Background>
border_widths: (0.).into(),
border_color: transparent_black(),
border_style: BorderStyle::default(),
transformation: TransformationMatrix::default(),
}
}
@@ -4276,5 +4358,6 @@ pub fn outline(
border_widths: (1.).into(),
border_color: border_color.into(),
border_style,
transformation: TransformationMatrix::default(),
}
}