@@ -88,15 +88,24 @@ var camera_active: bool = false;
8888
8989/// Convert screen-space pixel coordinates to NDC (-1..1) for sokol_gl.
9090/// When a camera is active, applies forward transform: (world - target) * zoom + offset.
91+ ///
92+ /// The camera offset is stored in design coordinates (e.g. design_w/2, design_h/2)
93+ /// so that the engine code is portable between backends. When physical screen
94+ /// dimensions differ from design dimensions (HiDPI / mobile), scale the offset to
95+ /// physical pixels before dividing by the physical screen extent.
9196fn toNdcX (px : f32 ) f32 {
9297 if (! camera_active ) {
9398 // Screen-space: map design coords directly to NDC so a design-width
9499 // quad fills exactly NDC -1..1 regardless of physical screen width.
95100 return (px / @as (f32 , @floatFromInt (design_w ))) * 2.0 - 1.0 ;
96101 }
97102 const cam = active_camera ;
98- const screen_x = (px - cam .target .x ) * cam .zoom + cam .offset .x ;
99- return (screen_x / @as (f32 , @floatFromInt (screen_w ))) * 2.0 - 1.0 ;
103+ const fw = @as (f32 , @floatFromInt (screen_w ));
104+ const fdw = @as (f32 , @floatFromInt (design_w ));
105+ // Scale offset from design-space to physical-space so the camera center
106+ // lands at the correct NDC position even when design != physical (HiDPI).
107+ const screen_x = (px - cam .target .x ) * cam .zoom + cam .offset .x * (fw / fdw );
108+ return (screen_x / fw ) * 2.0 - 1.0 ;
100109}
101110
102111fn toNdcY (py : f32 ) f32 {
@@ -107,10 +116,10 @@ fn toNdcY(py: f32) f32 {
107116 }
108117 const cam = active_camera ;
109118 const fh = @as (f32 , @floatFromInt (screen_h ));
119+ const fdh = @as (f32 , @floatFromInt (design_h ));
110120 // Positions arrive in screen-space Y-down (Y-flipped by renderer.toScreenY).
111- // Apply the camera in the same screen-Y-down convention as the raylib backend:
112- // screen_final = (py - target.y) * zoom + offset.y
113- const screen_y = (py - cam .target .y ) * cam .zoom + cam .offset .y ;
121+ // Scale offset from design-space to physical-space (same HiDPI correction as X).
122+ const screen_y = (py - cam .target .y ) * cam .zoom + cam .offset .y * (fh / fdh );
114123 return 1.0 - (screen_y / fh ) * 2.0 ;
115124}
116125
@@ -371,18 +380,24 @@ pub fn getScreenHeight() i32 {
371380}
372381
373382pub fn screenToWorld (pos : Vector2 , camera : Camera2D ) Vector2 {
383+ // Scale camera offset from design-space to physical-space (HiDPI correction).
384+ const off_x = camera .offset .x * @as (f32 , @floatFromInt (screen_w )) / @as (f32 , @floatFromInt (design_w ));
385+ const off_y = camera .offset .y * @as (f32 , @floatFromInt (screen_h )) / @as (f32 , @floatFromInt (design_h ));
374386 return .{
375- .x = (pos .x - camera . offset . x ) / camera .zoom + camera .target .x ,
387+ .x = (pos .x - off_x ) / camera .zoom + camera .target .x ,
376388 // Screen Y-down convention, same as raylib backend
377- .y = (pos .y - camera . offset . y ) / camera .zoom + camera .target .y ,
389+ .y = (pos .y - off_y ) / camera .zoom + camera .target .y ,
378390 };
379391}
380392
381393pub fn worldToScreen (pos : Vector2 , camera : Camera2D ) Vector2 {
394+ // Scale camera offset from design-space to physical-space (HiDPI correction).
395+ const off_x = camera .offset .x * @as (f32 , @floatFromInt (screen_w )) / @as (f32 , @floatFromInt (design_w ));
396+ const off_y = camera .offset .y * @as (f32 , @floatFromInt (screen_h )) / @as (f32 , @floatFromInt (design_h ));
382397 return .{
383- .x = (pos .x - camera .target .x ) * camera .zoom + camera . offset . x ,
398+ .x = (pos .x - camera .target .x ) * camera .zoom + off_x ,
384399 // Screen Y-down convention, same as raylib backend
385- .y = (pos .y - camera .target .y ) * camera .zoom + camera . offset . y ,
400+ .y = (pos .y - camera .target .y ) * camera .zoom + off_y ,
386401 };
387402}
388403
0 commit comments