@@ -44,8 +44,24 @@ const NeedleCanvas = forwardRef<NeedleCanvasHandle, NeedleCanvasProps>(function
4444
4545 // ── helpers ──────────────────────────────────────────────────────────────
4646
47+ /** Returns context already scaled for the current devicePixelRatio. */
4748 function getCtx ( ) {
48- return canvasRef . current ?. getContext ( "2d" ) ?? null ;
49+ const canvas = canvasRef . current ;
50+ if ( ! canvas ) return null ;
51+ return canvas . getContext ( "2d" ) ?? null ;
52+ }
53+
54+ /** Scale the canvas backing store to match devicePixelRatio. */
55+ function applyDpr ( ) {
56+ const canvas = canvasRef . current ;
57+ if ( ! canvas ) return ;
58+ const dpr = window . devicePixelRatio || 1 ;
59+ canvas . width = Math . round ( width * dpr ) ;
60+ canvas . height = Math . round ( height * dpr ) ;
61+ const ctx = canvas . getContext ( "2d" ) ;
62+ if ( ctx ) {
63+ ctx . setTransform ( dpr , 0 , 0 , dpr , 0 , 0 ) ;
64+ }
4965 }
5066
5167 function drawBackground ( ctx : CanvasRenderingContext2D ) {
@@ -84,6 +100,7 @@ const NeedleCanvas = forwardRef<NeedleCanvasHandle, NeedleCanvasProps>(function
84100 // ── full redraw ───────────────────────────────────────────────────────────
85101
86102 const fullRedraw = useCallback ( ( ) => {
103+ applyDpr ( ) ;
87104 const ctx = getCtx ( ) ;
88105 if ( ! ctx ) return ;
89106 ctx . clearRect ( 0 , 0 , width , height ) ;
@@ -126,19 +143,21 @@ const NeedleCanvas = forwardRef<NeedleCanvasHandle, NeedleCanvasProps>(function
126143 const canvas = canvasRef . current ;
127144 if ( ! canvas ) return ;
128145 const rect = canvas . getBoundingClientRect ( ) ;
129- const scaleX = width / rect . width ;
130- const scaleY = height / rect . height ;
131- onCanvasClick ( ( e . clientX - rect . left ) * scaleX , ( e . clientY - rect . top ) * scaleY ) ;
146+ // coords are in CSS pixels — no DPR correction needed here because
147+ // the canvas transform maps CSS px → logical px automatically
148+ onCanvasClick ( e . clientX - rect . left , e . clientY - rect . top ) ;
132149 }
133150
134151 return (
135152 < canvas
136153 ref = { canvasRef }
154+ // Physical size is set dynamically via applyDpr(); these attrs are just
155+ // initial placeholders — style controls the visible CSS size.
137156 width = { width }
138157 height = { height }
139158 onClick = { handleClick }
140159 className = "rounded-xl cursor-crosshair"
141- style = { { display : "block" } }
160+ style = { { display : "block" , width , height } }
142161 />
143162 ) ;
144163} ) ;
0 commit comments