ramadanproject/resources/js/components/GridCanvas.vue
Ahmed Darrazi 01c0996c71
Some checks failed
tests / ci (pull_request) Failing after 5m27s
linter / quality (pull_request) Successful in 58s
fix(vue): add lang attrs, fix ESLint issues in Vue components
2026-01-03 05:15:04 +01:00

227 lines
6.5 KiB
Vue

<template>
<div class="grid-canvas" ref="container">
<canvas ref="canvas" :width="width" :height="height" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
const props = defineProps({
masterImageUrl: { type: String, required: true },
cellSize: { type: Number, required: true },
});
const emit = defineEmits(['selection-changed']);
const container = ref(null);
const canvas = ref(null);
let ctx = null;
const width = 1200;
const height = 800;
let isPanning = false;
let start = { x: 0, y: 0 };
const offset = { x: 0, y: 0 };
let scale = 1;
let pinchActive = false;
let lastDistance = 0;
// selection in pixel coordinates relative to canvas (for drawing)
let selection = null; // {x,y,w,h} in pixels
let selecting = false; // whether we are drawing a selection rect
function drawGrid() {
if (!ctx) return;
ctx.clearRect(0, 0, width, height);
// Draw master image background if available
if (bgImage && bgImage.complete) {
ctx.drawImage(bgImage, offset.x, offset.y, width * scale, height * scale);
} else {
ctx.fillStyle = '#f3f3f3';
ctx.fillRect(0, 0, width, height);
}
// Draw grid lines
ctx.strokeStyle = 'rgba(0,0,0,0.08)';
const step = props.cellSize * scale;
for (let x = offset.x % step; x < width; x += step) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
for (let y = offset.y % step; y < height; y += step) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
// Draw selection (pixel coords)
if (selection) {
ctx.fillStyle = 'rgba(34,197,94,0.2)';
ctx.fillRect(selection.x, selection.y, selection.w, selection.h);
ctx.strokeStyle = 'rgba(34,197,94,0.8)';
ctx.strokeRect(selection.x, selection.y, selection.w, selection.h);
}
}
let bgImage = null;
watch(() => props.masterImageUrl, (v) => {
if (!v) return;
bgImage = new Image();
bgImage.src = v;
bgImage.onload = () => drawGrid();
});
onMounted(() => {
const c = canvas.value;
ctx = c.getContext('2d');
drawGrid();
c.addEventListener('pointerdown', (e) => {
// Determine if this is a selection or a pan.
// Desktop: hold Shift to select. Touch: treat as selection.
const rect = c.getBoundingClientRect();
const canvasX = e.clientX - rect.left;
const canvasY = e.clientY - rect.top;
if (e.pointerType === 'touch' || e.shiftKey) {
selecting = true;
selection = { x: canvasX, y: canvasY, w: 0, h: 0 };
// emit initial empty selection
emit('selection-changed', null);
} else {
isPanning = true;
start = { x: e.clientX, y: e.clientY };
}
});
// Touch pinch handlers (for older browsers this may be redundant with pointer events)
c.addEventListener('touchstart', (e) => {
if (e.touches && e.touches.length === 2) {
pinchActive = true;
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
lastDistance = Math.hypot(dx, dy);
}
}, { passive: false });
c.addEventListener('touchmove', (e) => {
if (!pinchActive || !e.touches || e.touches.length < 2) return;
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const dist = Math.hypot(dx, dy);
if (lastDistance <= 0) {
lastDistance = dist;
return;
}
const factor = dist / lastDistance;
const newScale = Math.min(4, Math.max(0.5, scale * factor));
// adjust offset to keep midpoint stable
const rect = c.getBoundingClientRect();
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left;
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top;
// compute world coords of midpoint
const worldX = (midX - offset.x) / scale;
const worldY = (midY - offset.y) / scale;
scale = newScale;
// recompute offset so world point stays under midpoint
offset.x = midX - worldX * scale;
offset.y = midY - worldY * scale;
lastDistance = dist;
drawGrid();
e.preventDefault();
}, { passive: false });
c.addEventListener('touchend', (e) => {
if (!e.touches || e.touches.length < 2) {
pinchActive = false;
lastDistance = 0;
}
});
c.addEventListener('pointermove', (e) => {
const rect = c.getBoundingClientRect();
const canvasX = e.clientX - rect.left;
const canvasY = e.clientY - rect.top;
if (selecting && selection) {
const x = Math.min(selection.x, canvasX);
const y = Math.min(selection.y, canvasY);
const w = Math.abs(canvasX - selection.x);
const h = Math.abs(canvasY - selection.y);
selection.x = x;
selection.y = y;
selection.w = w;
selection.h = h;
drawGrid();
// Map pixel selection to cell units and emit
const relX = selection.x - offset.x;
const relY = selection.y - offset.y;
const cellSize = props.cellSize;
const cellX = Math.max(0, Math.floor(relX / cellSize));
const cellY = Math.max(0, Math.floor(relY / cellSize));
const wCells = Math.max(1, Math.ceil(selection.w / cellSize));
const hCells = Math.max(1, Math.ceil(selection.h / cellSize));
emit('selection-changed', { x: cellX, y: cellY, w: wCells, h: hCells });
return;
}
if (!isPanning) return;
const dx = e.clientX - start.x;
const dy = e.clientY - start.y;
offset.x += dx;
offset.y += dy;
start = { x: e.clientX, y: e.clientY };
drawGrid();
});
c.addEventListener('pointerup', () => {
if (selecting && selection) {
// finalize selection and emit one last time
const relX = selection.x - offset.x;
const relY = selection.y - offset.y;
const cellSize = props.cellSize;
const cellX = Math.max(0, Math.floor(relX / cellSize));
const cellY = Math.max(0, Math.floor(relY / cellSize));
const wCells = Math.max(1, Math.ceil(selection.w / cellSize));
const hCells = Math.max(1, Math.ceil(selection.h / cellSize));
emit('selection-changed', { x: cellX, y: cellY, w: wCells, h: hCells });
selecting = false;
}
isPanning = false;
});
});
onBeforeUnmount(() => {
// cleanup if needed
});
// Note: This is a small scaffold: selection math and precise cell mapping
// will be expanded in future iterations. Emit selection-changed with cell units.
</script>
<style scoped>
.grid-canvas {
width: 100%;
height: 100%;
background: #fff;
}
canvas {
max-width: 100%;
border: 1px solid rgba(0,0,0,0.06);
}
</style>