Skip to content

Commit c764478

Browse files
committed
PIX-60 Enhance canvas rendering and interaction to support dynamic dimensions and initial fit-to-screen, and improve code clarity.
1 parent 0faaa7f commit c764478

2 files changed

Lines changed: 63 additions & 70 deletions

File tree

client/src/components/editor/PixelCanvas.vue

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const props = withDefaults(defineProps<{
1212
initialZoom: 10
1313
});
1414
15-
// Emits stroke events
15+
1616
const 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<{
2222
const canvasRef = ref<HTMLCanvasElement | null>(null);
2323
const viewportRef = ref<HTMLDivElement | null>(null);
2424
25-
// Navigation State
2625
const scale = ref(props.initialZoom);
2726
const pan = ref({ x: 0, y: 0 });
2827
const 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
9096
const 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
97103
const 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
112118
const 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
125130
const 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 = () => {
159159
const 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
200194
const 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+
215220
const 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
230233
onMounted(() => {
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
239246
onUnmounted(() => {
@@ -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
248261
watch(() => props.pixelUpdateEvent, (event) => {
249262
if (!event) return;
250263
@@ -257,7 +270,7 @@ watch(() => props.pixelUpdateEvent, (event) => {
257270
258271
defineExpose({
259272
updatePixel,
260-
drawAll
273+
renderFullCanvas
261274
});
262275
263276
</script>

0 commit comments

Comments
 (0)