Skip to content
- Choosing a selection results in a full page refresh.
- Opens in a new window.
const SNAP_PX = 10; // Snap grid size in pixels
let resizing = false;
let resizeData = null;
let dragging = false;
let dragData = null;
let selectedItem = null;
function pxToCm(px) {
const DPI = 96; // Assume screen DPI
return (px / DPI * 2.54).toFixed(2);
}
function renderAllItems() {
// Clear old bounding boxes
canvasWrapper.querySelectorAll('.bounding-box').forEach((el) => el.remove());
// Sort items by zIndex for layering
items.sort((a, b) => a.zIndex - b.zIndex);
items.forEach((item, index) => {
const bb = document.createElement('div');
bb.classList.add('bounding-box');
const scaledWidth = item.width * item.scale;
const scaledHeight = item.height * item.scale;
bb.style.left = item.x + 'px';
bb.style.top = item.y + 'px';
bb.style.width = scaledWidth + 'px';
bb.style.height = scaledHeight + 'px';
bb.style.zIndex = 1000 + item.zIndex;
bb.style.transform = `rotate(${item.rotation}deg)`;
bb.dataset.index = index;
if (item === selectedItem) {
bb.style.borderColor = 'red';
bb.style.boxShadow = '0 0 10px red';
} else {
bb.style.borderColor = 'rgba(255,0,0,0.5)';
bb.style.boxShadow = 'none';
}
if (item.type === 'text') {
bb.textContent = item.text;
bb.style.fontFamily = item.font;
bb.style.fontSize = item.fontSize * item.scale + 'px';
bb.style.display = 'flex';
bb.style.alignItems = 'center';
bb.style.justifyContent = 'center';
bb.style.userSelect = 'none';
bb.style.whiteSpace = 'nowrap';
} else if (item.type === 'image') {
bb.style.backgroundImage = `url(${item.image.src})`;
bb.style.backgroundSize = 'contain';
bb.style.backgroundRepeat = 'no-repeat';
bb.style.backgroundPosition = 'center';
}
bb.addEventListener('mousedown', (e) => {
e.stopPropagation();
selectItem(item);
startDrag(e, item, bb);
});
bb.addEventListener('touchstart', (e) => {
e.stopPropagation();
selectItem(item);
startDrag(e.touches[0], item, bb);
}, { passive: false });
if (item === selectedItem) {
['tl', 'tr', 'bl', 'br'].forEach((pos) => {
const handle = document.createElement('div');
handle.classList.add('handle', pos);
bb.appendChild(handle);
handle.addEventListener('mousedown', (e) => {
e.stopPropagation();
startResize(e, item, bb, pos);
});
handle.addEventListener('touchstart', (e) => {
e.stopPropagation();
startResize(e.touches[0], item, bb, pos);
}, { passive: false });
});
}
const label = document.createElement('div');
label.className = 'dimension-label';
label.textContent = `${pxToCm(scaledWidth)} cm × ${pxToCm(scaledHeight)} cm`;
bb.appendChild(label);
canvasWrapper.appendChild(bb);
});
}
function startDrag(e, item, bb) {
dragging = true;
dragData = {
item,
bb,
startX: e.clientX,
startY: e.clientY,
origX: item.x,
origY: item.y,
};
window.addEventListener('mousemove', doDrag);
window.addEventListener('mouseup', stopDrag);
window.addEventListener('touchmove', doDrag, { passive: false });
window.addEventListener('touchend', stopDrag);
}
function doDrag(e) {
if (!dragging) return;
e.preventDefault();
const { item, startX, startY, origX, origY } = dragData;
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
let dx = clientX - startX;
let dy = clientY - startY;
// Snap position to grid
let newX = Math.round((origX + dx) / SNAP_PX) * SNAP_PX;
let newY = Math.round((origY + dy) / SNAP_PX) * SNAP_PX;
item.x = newX;
item.y = newY;
renderAllItems();
}
function stopDrag() {
dragging = false;
dragData = null;
window.removeEventListener('mousemove', doDrag);
window.removeEventListener('mouseup', stopDrag);
window.removeEventListener('touchmove', doDrag);
window.removeEventListener('touchend', stopDrag);
}
function startResize(e, item, bb, handlePos) {
resizing = true;
resizeData = {
item,
bb,
handlePos,
startX: e.clientX,
startY: e.clientY,
origX: item.x,
origY: item.y,
origWidth: item.width * item.scale,
origHeight: item.height * item.scale,
};
window.addEventListener('mousemove', doResize);
window.addEventListener('mouseup', stopResize);
window.addEventListener('touchmove', doResize, { passive: false });
window.addEventListener('touchend', stopResize);
}
function doResize(e) {
if (!resizing) return;
e.preventDefault();
const { item, bb, handlePos, startX, startY, origX, origY, origWidth, origHeight } = resizeData;
const currentX = e.clientX || (e.touches && e.touches[0].clientX);
const currentY = e.clientY || (e.touches && e.touches[0].clientY);
let dx = currentX - startX;
let dy = currentY - startY;
// Determine scale change based on handle position and mouse movement
let scaleChange = 0;
if (handlePos.includes('r')) {
scaleChange = dx;
} else if (handlePos.includes('l')) {
scaleChange = -dx;
}
if (handlePos.includes('b')) {
if (Math.abs(dy) > Math.abs(scaleChange)) scaleChange = dy;
} else if (handlePos.includes('t')) {
if (Math.abs(dy) > Math.abs(scaleChange)) scaleChange = -dy;
}
let newSize = origWidth + scaleChange;
if (newSize < 20) newSize = 20; // minimum size
// Snap newSize to grid
newSize = Math.round(newSize / SNAP_PX) * SNAP_PX;
// Calculate new scale based on uniform scaling
const newScale = newSize / item.width;
// Adjust position if resizing from left or top handles
let newX = origX;
let newY = origY;
if (handlePos.includes('l')) {
newX = origX + (origWidth - newSize);
newX = Math.round(newX / SNAP_PX) * SNAP_PX;
}
if (handlePos.includes('t')) {
newY = origY + (origHeight - newSize);
newY = Math.round(newY / SNAP_PX) * SNAP_PX;
}
// Update item properties
item.scale = newScale;
item.x = newX;
item.y = newY;
// Update bounding box style immediately
bb.style.width = newSize + 'px';
bb.style.height = newSize + 'px';
bb.style.left = newX + 'px';
bb.style.top = newY + 'px';
// Update dimension label
const label = bb.querySelector('.dimension-label');
if (label) {
label.textContent = `${pxToCm(newSize)} cm × ${pxToCm(newSize)} cm`;
}
}
function stopResize() {
resizing = false;
resizeData = null;
window.removeEventListener('mousemove', doResize);
window.removeEventListener('mouseup', stopResize);
window.removeEventListener('touchmove', doResize);
window.removeEventListener('touchend', stopResize);
// Re-render to update UI fully after resize ends
renderAllItems();
}
function selectItem(item) {
selectedItem = item;
renderAllItems();
}