Skip to content

Commit d726ab7

Browse files
zachelnetDeepSeek V4
andcommitted
fix(renderer): correctly center rotated sprites, align collision check
Two fixes for rotated text rendering: 1. Overlay centering: revert pre-compensation in centred_sprite_transform and instead compute the overlay origin in overlay_sprite_with_rotation so the rotated output image's center lands where the unrotated sprite's center would be. Formula: origin = (x + w/2 - dst_w/2, y + h/2 - dst_h/2) 2. Collision check: rotate the sprite via rotate_sprite_expand_top_left before checking against the bubble mask, matching the bilinear- resampled pixels used by the actual render overlay path. Previously rotated source pixels directly, which could miss edge pixels introduced by bilinear interpolation. Co-authored-by: DeepSeek V4 <deepseek@v4.ai>
1 parent e05d614 commit d726ab7

1 file changed

Lines changed: 22 additions & 24 deletions

File tree

crates/koharu-app/src/renderer.rs

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -707,23 +707,26 @@ fn sprite_collides_with_bubble_mask(
707707
mask: &GrayImage,
708708
bubble_id: u8,
709709
) -> bool {
710-
let origin_x = transform.x.round() as i32;
711-
let origin_y = transform.y.round() as i32;
712710
let mask_w = mask.width() as i32;
713711
let mask_h = mask.height() as i32;
714712

713+
// Rotate the sprite first so the collision check uses the same
714+
// bilinear-resampled pixels as the actual render overlay path.
715715
let rotation_rad = transform.rotation_deg.to_radians();
716-
let cos = rotation_rad.cos();
717-
let sin = rotation_rad.sin();
716+
let (rotated, _, _) = rotate_sprite_expand_top_left(sprite, rotation_rad);
718717

719-
for (x, y, pixel) in sprite.enumerate_pixels() {
718+
// Match the overlay origin computed by overlay_sprite_with_rotation.
719+
let origin_x =
720+
(transform.x + transform.width * 0.5 - rotated.width() as f32 * 0.5).round() as i32;
721+
let origin_y =
722+
(transform.y + transform.height * 0.5 - rotated.height() as f32 * 0.5).round() as i32;
723+
724+
for (x, y, pixel) in rotated.enumerate_pixels() {
720725
if pixel.0[3] <= MASK_COLLISION_ALPHA_THRESHOLD {
721726
continue;
722727
}
723-
let fx = x as f32;
724-
let fy = y as f32;
725-
let mask_x = origin_x + (cos * fx - sin * fy).round() as i32;
726-
let mask_y = origin_y + (sin * fx + cos * fy).round() as i32;
728+
let mask_x = origin_x + x as i32;
729+
let mask_y = origin_y + y as i32;
727730
if mask_x < 0 || mask_y < 0 || mask_x >= mask_w || mask_y >= mask_h {
728731
return true;
729732
}
@@ -999,19 +1002,9 @@ fn centred_sprite_transform(
9991002
let sprite_h = sprite_height as f32;
10001003
let cx = anchor_box.x + anchor_box.width * 0.5;
10011004
let cy = anchor_box.y + anchor_box.height * 0.5;
1002-
1003-
// The sprite is rotated around its top-left corner (CSS transform-origin model).
1004-
// Compute the top-left such that the sprite's visual center lands at (cx, cy).
1005-
let hw = sprite_w * 0.5;
1006-
let hh = sprite_h * 0.5;
1007-
let cos = rotation_deg.to_radians().cos();
1008-
let sin = rotation_deg.to_radians().sin();
1009-
let rotated_center_x = cos * hw - sin * hh;
1010-
let rotated_center_y = sin * hw + cos * hh;
1011-
10121005
Transform {
1013-
x: (cx - rotated_center_x).round(),
1014-
y: (cy - rotated_center_y).round(),
1006+
x: (cx - sprite_w * 0.5).round(),
1007+
y: (cy - sprite_h * 0.5).round(),
10151008
width: sprite_w,
10161009
height: sprite_h,
10171010
rotation_deg,
@@ -1048,9 +1041,14 @@ fn overlay_sprite_with_rotation(
10481041
return;
10491042
}
10501043

1051-
let (rotated, min_x, min_y) = rotate_sprite_expand_top_left(sprite, rotation_deg.to_radians());
1052-
let origin_x = (transform.x + min_x).round() as i64;
1053-
let origin_y = (transform.y + min_y).round() as i64;
1044+
let (rotated, _, _) = rotate_sprite_expand_top_left(sprite, rotation_deg.to_radians());
1045+
// The unrotated sprite is centered at (transform.x + w/2, transform.y + h/2).
1046+
// Align the rotated sprite's center to the same point so rotated text
1047+
// stays visually centered within its layout box.
1048+
let origin_x =
1049+
(transform.x + transform.width * 0.5 - rotated.width() as f32 * 0.5).round() as i64;
1050+
let origin_y =
1051+
(transform.y + transform.height * 0.5 - rotated.height() as f32 * 0.5).round() as i64;
10541052
imageops::overlay(canvas, &rotated, origin_x, origin_y);
10551053
}
10561054

0 commit comments

Comments
 (0)