@@ -1039,7 +1039,6 @@ fn find_input(blocks: &[RenderBlockInput], id: NodeId) -> &RenderBlockInput {
10391039 . expect ( "rendered_block must have matching input" )
10401040}
10411041
1042- /// Normalize a rotation angle to [0, 360).
10431042fn normalize_rotation ( deg : f32 ) -> f32 {
10441043 let mut r = deg % 360.0 ;
10451044 if r < 0.0 {
@@ -1057,8 +1056,6 @@ fn overlay_sprite_with_rotation(
10571056 let rotation_deg = normalize_rotation ( transform. rotation_deg ) ;
10581057
10591058 if rotation_deg. abs ( ) < 0.0001 || ( 360.0 - rotation_deg) . abs ( ) < 0.0001 {
1060- // Preserve legacy placement: expanded transforms were rounded,
1061- // non-expanded (original) transforms were truncated (cast to i64).
10621059 let ( ox, oy) = if is_expanded {
10631060 ( transform. x . round ( ) as i64 , transform. y . round ( ) as i64 )
10641061 } else {
@@ -1069,9 +1066,7 @@ fn overlay_sprite_with_rotation(
10691066 }
10701067
10711068 let ( rotated, _, _) = rotate_sprite_expand_top_left ( sprite, rotation_deg. to_radians ( ) ) ;
1072- // The unrotated sprite is centered at (transform.x + w/2, transform.y + h/2).
1073- // Align the rotated sprite's center to the same point so rotated text
1074- // stays visually centered within its layout box.
1069+ // Center the rotated sprite over the same anchor as the unrotated one.
10751070 let origin_x =
10761071 ( transform. x + transform. width * 0.5 - rotated. width ( ) as f32 * 0.5 ) . round ( ) as i64 ;
10771072 let origin_y =
@@ -1086,8 +1081,7 @@ fn rotate_sprite_expand_top_left(src: &RgbaImage, angle_rad: f32) -> (RgbaImage,
10861081 return ( RgbaImage :: new ( 0 , 0 ) , 0.0 , 0.0 ) ;
10871082 }
10881083
1089- // Fast path: exact 90° multiples — direct integer pixel rearrangement,
1090- // no resampling. The layout matches the bilinear path exactly.
1084+ // 90° multiples → direct pixel rearrangement, no resampling.
10911085 let deg = angle_rad. to_degrees ( ) ;
10921086 let remainder = deg % 90.0 ;
10931087 if remainder. abs ( ) < 0.001 || ( 90.0 - remainder. abs ( ) ) . abs ( ) < 0.001 {
@@ -1129,10 +1123,10 @@ fn rotate_sprite_expand_top_left(src: &RgbaImage, angle_rad: f32) -> (RgbaImage,
11291123 let cos = angle_rad. cos ( ) ;
11301124 let sin = angle_rad. sin ( ) ;
11311125 let corners = [
1132- rotate_point_top_left ( 0.0 , 0.0 , cos, sin) ,
1133- rotate_point_top_left ( src_w as f32 , 0.0 , cos, sin) ,
1134- rotate_point_top_left ( 0.0 , src_h as f32 , cos, sin) ,
1135- rotate_point_top_left ( src_w as f32 , src_h as f32 , cos, sin) ,
1126+ rotate_point ( 0.0 , 0.0 , cos, sin) ,
1127+ rotate_point ( src_w as f32 , 0.0 , cos, sin) ,
1128+ rotate_point ( 0.0 , src_h as f32 , cos, sin) ,
1129+ rotate_point ( src_w as f32 , src_h as f32 , cos, sin) ,
11361130 ] ;
11371131
11381132 let min_x = corners
@@ -1171,18 +1165,11 @@ fn rotate_sprite_expand_top_left(src: &RgbaImage, angle_rad: f32) -> (RgbaImage,
11711165 ( dst, min_x, min_y)
11721166}
11731167
1174- fn rotate_point_top_left ( x : f32 , y : f32 , cos : f32 , sin : f32 ) -> ( f32 , f32 ) {
1175- // Matches CSS rotate(theta) matrix.
1168+ fn rotate_point ( x : f32 , y : f32 , cos : f32 , sin : f32 ) -> ( f32 , f32 ) {
11761169 ( cos * x - sin * y, sin * x + cos * y)
11771170}
11781171
1179- /// Bilinear sample of an RGBA sprite.
1180- ///
1181- /// Interpolates RGB in premultiplied-alpha space and alpha in straight-alpha
1182- /// space, then un-premultiplies once at the end. This is mathematically
1183- /// equivalent to the standard "premultiply all four channels, interpolate,
1184- /// un-premultiply" approach but avoids premultiplying alpha (which is
1185- /// invariant under premultiplication) through the interpolation step.
1172+ /// Bilinear sample in premultiplied-alpha space, un-premultiply once at the end.
11861173fn sample_bilinear_rgba ( src : & RgbaImage , x : f32 , y : f32 ) -> Rgba < u8 > {
11871174 let max_x = src. width ( ) as f32 - 1.0 ;
11881175 let max_y = src. height ( ) as f32 - 1.0 ;
@@ -1226,7 +1213,7 @@ fn sample_bilinear_rgba(src: &RgbaImage, x: f32, y: f32) -> Rgba<u8> {
12261213 // Alpha is interpolated in straight-alpha space.
12271214 let top_a = lerp ( p00[ 3 ] , p10[ 3 ] , wx) ;
12281215 let bottom_a = lerp ( p01[ 3 ] , p11[ 3 ] , wx) ;
1229- let alpha = lerp ( top_a, bottom_a, wy) . clamp ( 0.0 , 255.0 ) ;
1216+ let alpha = lerp ( top_a, bottom_a, wy) ;
12301217
12311218 let mut out = [ 0u8 ; 4 ] ;
12321219 let alpha_u8 = alpha. round ( ) as u8 ;
@@ -1573,7 +1560,6 @@ mod tests {
15731560
15741561 fn make_test_sprite ( w : u32 , h : u32 ) -> RgbaImage {
15751562 let mut img = RgbaImage :: new ( w, h) ;
1576- // Paint a solid red pixel so we can verify the output is not empty.
15771563 img. put_pixel ( 0 , 0 , Rgba ( [ 255 , 0 , 0 , 255 ] ) ) ;
15781564 img
15791565 }
@@ -1594,7 +1580,6 @@ mod tests {
15941580 let sprite = make_test_sprite ( 20 , 20 ) ;
15951581 let transform = make_transform ( 50.0 , 50.0 , 0.0 ) ;
15961582 overlay_sprite_with_rotation ( & mut canvas, & sprite, & transform, false ) ;
1597- // An unrotated sprite at (50, 50) should leave a red pixel there.
15981583 assert_eq ! ( canvas. get_pixel( 50 , 50 ) , & Rgba ( [ 255 , 0 , 0 , 255 ] ) ) ;
15991584 }
16001585
@@ -1608,12 +1593,13 @@ mod tests {
16081593 }
16091594
16101595 #[ test]
1611- fn overlay_sprite_180deg_rotates ( ) {
1612- let mut canvas = RgbaImage :: new ( 600 , 600 ) ;
1613- let sprite = make_test_sprite ( 100 , 100 ) ;
1614- let transform = make_transform ( 250 .0, 250 .0, 180.0 ) ;
1596+ fn overlay_sprite_180deg_is_centered ( ) {
1597+ let mut canvas = RgbaImage :: new ( 300 , 300 ) ;
1598+ let sprite = make_test_sprite ( 20 , 20 ) ;
1599+ let transform = make_transform ( 50 .0, 50 .0, 180.0 ) ;
16151600 overlay_sprite_with_rotation ( & mut canvas, & sprite, & transform, false ) ;
1616- assert ! ( canvas. pixels( ) . any( |p| p. 0 [ 3 ] != 0 ) ) ;
1601+ // (0,0) pixel flipped 180° → (19,19) in rotated sprite, centered on transform.
1602+ assert_eq ! ( canvas. get_pixel( 109 , 84 ) , & Rgba ( [ 255 , 0 , 0 , 255 ] ) ) ;
16171603 }
16181604
16191605 #[ test]
0 commit comments