@@ -12,7 +12,7 @@ const props = withDefaults(defineProps<{
1212 initialZoom: 10
1313});
1414
15- // Emits stroke events
15+
1616const emit = defineEmits <{
1717 (e : ' stroke-start' , payload : { x: number , y: number }): void ;
1818 (e : ' stroke-move' , payload : { x: number , y: number }): void ;
@@ -22,7 +22,6 @@ const emit = defineEmits<{
2222const canvasRef = ref <HTMLCanvasElement | null >(null );
2323const viewportRef = ref <HTMLDivElement | null >(null );
2424
25- // Navigation State
2625const scale = ref (props .initialZoom );
2726const pan = ref ({ x: 0 , y: 0 });
2827const isPanning = ref (false );
@@ -41,11 +40,17 @@ const updatePixel = (x: number, y: number, colorIndex: number) => {
4140};
4241
4342// Redraw the entire canvas (1:1 scaling)
44- const drawAll = () => {
43+ const renderFullCanvas = () => {
4544 const canvas = canvasRef .value ;
4645 if (! canvas ) return ;
4746 const ctx = canvas .getContext (' 2d' );
4847 if (! ctx ) return ;
48+
49+ if (props .width === 0 || props .height === 0 ) {
50+ canvas .width = 0 ;
51+ canvas .height = 0 ;
52+ return ;
53+ }
4954
5055 canvas .width = props .width ;
5156 canvas .height = props .height ;
@@ -63,13 +68,14 @@ const drawAll = () => {
6368 }
6469};
6570
66- const getCoords = (e : MouseEvent ) => {
71+ const getGridCoordinates = (e : MouseEvent ) => {
6772 const canvas = canvasRef .value ;
6873 if (! canvas ) return null ;
74+
75+ if (props .width === 0 || props .height === 0 ) return null ;
6976
7077 const rect = canvas .getBoundingClientRect ();
7178
72- // Calculate relative position within the canvas element
7379 const relativeX = e .clientX - rect .left ;
7480 const relativeY = e .clientY - rect .top ;
7581
@@ -85,15 +91,15 @@ const getCoords = (e: MouseEvent) => {
8591 return null ;
8692};
8793
88- // --- Interaction Handlers ---
94+
8995
9096const getMinScale = () => {
9197 if (! viewportRef .value ) return 1 ;
98+ if (props .width === 0 || props .height === 0 ) return 1 ;
9299 const { width : vw, height : vh } = viewportRef .value .getBoundingClientRect ();
93100 return Math .min (vw / props .width , vh / props .height );
94101};
95102
96- // Helper: Centers content if smaller than view, otherwise clamps to edges
97103const clampAxis = (val : number , viewSize : number , contentSize : number ) => {
98104 if (contentSize < viewSize ) return (viewSize - contentSize ) / 2 ;
99105 return Math .min (Math .max (val , viewSize - contentSize ), 0 );
@@ -110,39 +116,33 @@ const clampPan = (x: number, y: number, scale: number) => {
110116};
111117
112118const handleMouseDown = (e : MouseEvent ) => {
113- // Robustness: Check event flag directly
114119 if (e .altKey || isAltPressed .value ) {
115120 isPanning .value = true ;
116121 isAltPressed .value = true ; // Sync state
117122 return ;
118123 }
119- const coords = getCoords (e );
124+ const coords = getGridCoordinates (e );
120125 if (coords ) {
121126 emit (' stroke-start' , coords );
122127 }
123128};
124129
125130const handleMouseMove = (e : MouseEvent ) => {
126- // Robustness: Self-correct state if Alt is released outside
127131 isAltPressed .value = e .altKey ;
128132
129- // Panning Logic
130133 if (isPanning .value ) {
131134 const newX = pan .value .x + e .movementX ;
132135 const newY = pan .value .y + e .movementY ;
133136
134- // Apply clamping
135137 const clamped = clampPan (newX , newY , scale .value );
136138 pan .value .x = clamped .x ;
137139 pan .value .y = clamped .y ;
138140 return ;
139141 }
140142
141- // Drawing Logic
142- // Check if primary button is pressed
143143 if (e .buttons !== 1 ) return ;
144144
145- const coords = getCoords (e );
145+ const coords = getGridCoordinates (e );
146146 if (coords ) {
147147 emit (' stroke-move' , coords );
148148 }
@@ -159,34 +159,29 @@ const handleMouseUp = () => {
159159const handleWheel = (e : WheelEvent ) => {
160160 e .preventDefault ();
161161
162- // Standard zoom logic:
163- // newScale = oldScale * (1 + delta)
164162 const delta = - e .deltaY ;
165163 const zoomFactor = 1.1 ;
166164 const newScale = delta > 0 ? scale .value * zoomFactor : scale .value / zoomFactor ;
167165
168- // Clamp scale
169- const MIN_SCALE = getMinScale (); // Dynamic min scale
166+ const MIN_SCALE = getMinScale ();
170167 const MAX_SCALE = 100 ;
171168 const clampedScale = Math .min (Math .max (newScale , MIN_SCALE ), MAX_SCALE );
172169
173170 if (clampedScale === scale .value ) return ;
174171
175- // Zoom towards mouse cursor
172+ if (clampedScale === scale .value ) return ;
173+
176174 if (viewportRef .value ) {
177175 const rect = viewportRef .value .getBoundingClientRect ();
178176 const mouseX = e .clientX - rect .left ;
179177 const mouseY = e .clientY - rect .top ;
180178
181- // Calculate mouse position relative to the transform origin (top-left of content)
182- // Current World Pos = (MouseScreen - Pan) / OldScale
183179 const worldX = (mouseX - pan .value .x ) / scale .value ;
184180 const worldY = (mouseY - pan .value .y ) / scale .value ;
185181
186182 let targetX = mouseX - worldX * clampedScale ;
187183 let targetY = mouseY - worldY * clampedScale ;
188184
189- // Apply strict Pan Clamping on Zoom to prevent going out of bounds
190185 const clampedPan = clampPan (targetX , targetY , clampedScale );
191186
192187 pan .value .x = clampedPan .x ;
@@ -196,7 +191,6 @@ const handleWheel = (e: WheelEvent) => {
196191 scale .value = clampedScale ;
197192};
198193
199- // Global Input Listeners for Alt key
200194const handleKeyDown = (e : KeyboardEvent ) => {
201195 if (e .key === ' Alt' && ! e .repeat ) {
202196 e .preventDefault ();
@@ -211,29 +205,42 @@ const handleKeyUp = (e: KeyboardEvent) => {
211205 }
212206};
213207
214- // Handle window resizing to keep constraints valid
208+ const fitToScreen = () => {
209+ if (! viewportRef .value ) return ;
210+ if (props .width === 0 || props .height === 0 ) return ;
211+
212+ const minScale = getMinScale ();
213+ scale .value = minScale ;
214+
215+ const clamped = clampPan (0 , 0 , scale .value );
216+ pan .value .x = clamped .x ;
217+ pan .value .y = clamped .y ;
218+ };
219+
215220const handleResize = () => {
216221 if (! viewportRef .value ) return ;
217222
218- // Ensure zoom is at least minScale
219223 const minScale = getMinScale ();
220224 if (scale .value < minScale ) {
221225 scale .value = minScale ;
222226 }
223227
224- // Re-clamp pan
225228 const clamped = clampPan (pan .value .x , pan .value .y , scale .value );
226229 pan .value .x = clamped .x ;
227230 pan .value .y = clamped .y ;
228231};
229232
230233onMounted (() => {
231- drawAll ();
234+ renderFullCanvas ();
232235 window .addEventListener (' keydown' , handleKeyDown );
233236 window .addEventListener (' keyup' , handleKeyUp );
234237 window .addEventListener (' resize' , handleResize );
235238
236- setTimeout (handleResize , 0 );
239+ window .addEventListener (' resize' , handleResize );
240+
241+ setTimeout (() => {
242+ fitToScreen ();
243+ }, 0 );
237244});
238245
239246onUnmounted (() => {
@@ -242,9 +249,15 @@ onUnmounted(() => {
242249 window .removeEventListener (' resize' , handleResize );
243250});
244251
245- watch (() => [props .width , props .height , props .pixels , props .palette ], drawAll );
252+ watch (() => [props .width , props .height ], () => {
253+ renderFullCanvas ();
254+ fitToScreen ();
255+ });
256+
257+ watch (() => [props .pixels , props .palette ], renderFullCanvas );
258+
259+ watch (() => [props .pixels , props .palette ], renderFullCanvas );
246260
247- // Efficiently handle remote pixel updates - only redraw the changed pixel
248261watch (() => props .pixelUpdateEvent , (event ) => {
249262 if (! event ) return ;
250263
@@ -257,7 +270,7 @@ watch(() => props.pixelUpdateEvent, (event) => {
257270
258271defineExpose ({
259272 updatePixel ,
260- drawAll
273+ renderFullCanvas
261274});
262275
263276 </script >
0 commit comments