Интерактивная планировка Зала 1 этажа

Один палец — перетащить стол. Клик — меню и поворот ↻. Два пальца — масштабировать схему (строго по центру).

Всего объектов: 0
Приглашено гостей: 0
Зал свободен
function draw() { checkSafetyCollisions(); updateAnalytics(); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.translate(VIRTUAL_WIDTH / 2, VIRTUAL_HEIGHT / 2); ctx.scale(cameraZoom, cameraZoom); ctx.translate(-VIRTUAL_WIDTH / 2, -VIRTUAL_HEIGHT / 2); ctx.fillStyle = '#111827'; ctx.fillRect(-500, -500, VIRTUAL_WIDTH + 1000, VIRTUAL_HEIGHT + 1000); // Паркетный пол ctx.save(); ctx.beginPath(); ctx.moveTo(120, 140); ctx.lineTo(880, 140); ctx.lineTo(880, 360); ctx.lineTo(700, 540); ctx.lineTo(300, 540); ctx.lineTo(120, 360); ctx.lineTo(120, 140); ctx.closePath(); ctx.shadowColor = 'rgba(0, 0, 0, 0.4)'; ctx.shadowBlur = 20; ctx.shadowOffsetY = 6; const floorGrad = ctx.createLinearGradient(120, 140, 880, 540); floorGrad.addColorStop(0, '#8f5b30'); floorGrad.addColorStop(0.5, '#ad7542'); floorGrad.addColorStop(1, '#7a4e25'); ctx.fillStyle = floorGrad; ctx.fill(); ctx.restore(); // 3 Массивные темные двери по чертежу ctx.save(); ctx.fillStyle = '#2b190e'; ctx.strokeStyle = '#e1d7c6'; ctx.lineWidth = 1.5; ctx.fillRect(280, 134, 55, 6); ctx.strokeRect(280, 134, 55, 6); // левая ctx.fillRect(460, 134, 80, 6); ctx.strokeRect(460, 134, 80, 6); // центр ctx.fillRect(670, 134, 50, 6); ctx.strokeRect(670, 134, 50, 6); // правая ctx.restore(); // Колонны (выступы) ctx.save(); ctx.fillStyle = '#1e293b'; ctx.strokeStyle = '#cbd5e1'; ctx.lineWidth = 1; ctx.fillRect(120, 200, 15, 110); ctx.strokeRect(120, 200, 15, 110); ctx.fillRect(865, 200, 15, 110); ctx.strokeRect(865, 200, 15, 110); ctx.restore(); // СТАЦИОНАРНЫЙ БАР RESTAURANT (ВЕРХНИЙ ПРАВЫЙ УГОЛ) ctx.save(); ctx.fillStyle = 'rgba(225, 215, 198, 0.15)'; ctx.strokeStyle = '#e1d7c6'; ctx.lineWidth = 2.5; ctx.fillRect(720, 140, 160, 35); ctx.strokeRect(720, 140, 160, 35); ctx.fillStyle = '#e1d7c6'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('БАР / ФУРШЕТ', 800, 162); ctx.restore(); // Окна ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 5.5; ctx.beginPath(); ctx.moveTo(880, 360); ctx.lineTo(700, 540); ctx.lineTo(300, 540); ctx.lineTo(120, 360); ctx.stroke(); ctx.strokeStyle = '#2d543a'; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(420, 540); ctx.lineTo(580, 540); ctx.stroke(); ctx.beginPath(); ctx.moveTo(170, 410); ctx.lineTo(250, 490); ctx.stroke(); ctx.beginPath(); ctx.moveTo(830, 410); ctx.lineTo(750, 490); ctx.stroke(); ctx.strokeStyle = '#334155'; ctx.lineWidth = 6; ctx.beginPath(); ctx.moveTo(120, 360); ctx.lineTo(120, 140); ctx.lineTo(880, 140); ctx.lineTo(880, 360); ctx.stroke(); tables.forEach(table => { ctx.save(); ctx.translate(table.x, table.y); ctx.rotate(table.angle || 0); if (table.type === "circle") { const r = (table.d * SCALE) / 2; const safetyR = getObjectSafetyRadius(table); ctx.beginPath(); ctx.arc(0, 0, safetyR, 0, 2 * Math.PI); ctx.strokeStyle = table.hasCollision ? 'rgba(239, 68, 68, 0.8)' : 'rgba(148, 163, 184, 0.35)'; if (table.hasCollision) { ctx.fillStyle = 'rgba(239, 68, 68, 0.08)'; ctx.fill(); } ctx.lineWidth = 1.5; ctx.stroke(); // ОТРИСОВКА ИЗУМРУДНЫХ БАРХАТНЫХ СТУЛЬЕВ И СЕРВИРОВКИ С ТАРЕЛКАМИ ПО ВАШЕМУ ПРИМЕРУ for (let i = 0; i < table.seats; i++) { const angle = (i * 2 * Math.PI) / table.seats; const chairX = (r + 10) * Math.cos(angle); const chairY = (r + 10) * Math.sin(angle); const plateX = (r - 12) * Math.cos(angle); const plateY = (r - 12) * Math.sin(angle); const glassX = (r - 15) * Math.cos(angle + 0.18); const glassY = (r - 15) * Math.sin(angle + 0.18); // Изумрудный бархатный стул с золотистой канвой Green 19 ctx.save(); ctx.translate(chairX, chairY); ctx.rotate(angle + Math.PI/2); ctx.beginPath(); ctx.rect(-7, -4, 14, 8); ctx.fillStyle = '#1e3f20'; ctx.strokeStyle = '#e1d7c6'; ctx.lineWidth = 1.2; ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.rect(-5, -6, 10, 3); ctx.fillStyle = '#142c16'; ctx.fill(); ctx.restore(); // Белые ресторанные тарелки ctx.beginPath(); ctx.arc(plateX, plateY, 4.5, 0, 2 * Math.PI); ctx.fillStyle = '#ffffff'; ctx.strokeStyle = '#cbd5e1'; ctx.lineWidth = 0.8; ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.arc(plateX, plateY, 3, 0, 2 * Math.PI); ctx.stroke(); // Хрустальные бокалы ctx.beginPath(); ctx.arc(glassX, glassY, 1.2, 0, 2 * Math.PI); ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; ctx.strokeStyle = 'rgba(225, 215, 198, 0.8)'; ctx.fill(); ctx.stroke(); } // Столешница ctx.beginPath(); ctx.arc(0, 0, r, 0, 2 * Math.PI); ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; ctx.shadowBlur = 10; ctx.shadowOffsetY = 4; const grad = ctx.createRadialGradient(0, 0, 2, 0, 0, r); grad.addColorStop(0, '#ffffff'); grad.addColorStop(0.85, '#fcfcfc'); grad.addColorStop(1, '#f1f3f5'); ctx.fillStyle = grad; ctx.strokeStyle = '#e1d7c6'; ctx.lineWidth = 1.5; ctx.fill(); ctx.stroke(); ctx.shadowColor = 'transparent'; ctx.fillStyle = '#0f172a'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(table.name, 0, -6); ctx.font = '10px sans-serif'; ctx.fillStyle = '#64748b'; ctx.fillText(table.seats + ' мест', 0, 10); } else { const w = table.w * SCALE; const h = table.h * SCALE; const safetyPad = (table.type === "zone" ? 0.2 : SAFETY_ZONE_METERS) * SCALE; ctx.beginPath(); ctx.rect(-w/2 - safetyPad, -h/2 - safetyPad, w + safetyPad*2, h + safetyPad*2); ctx.strokeStyle = table.hasCollision ? 'rgba(239, 68, 68, 0.8)' : 'rgba(148, 163, 184, 0.35)'; if (table.hasCollision) { ctx.fillStyle = 'rgba(239, 68, 68, 0.08)'; ctx.fill(); } ctx.lineWidth = 1.5; ctx.stroke(); ctx.beginPath(); ctx.rect(-w/2, -h/2, w, h); ctx.shadowColor = 'rgba(0, 0, 0, 0.2)'; ctx.shadowBlur = 8; ctx.shadowOffsetY = 3; ctx.fillStyle = table.type === "presidium" ? '#f8fafc' : 'rgba(225, 215, 198, 0.15)'; ctx.strokeStyle = '#e1d7c6'; ctx.lineWidth = 2; ctx.fill(); ctx.stroke(); ctx.fillStyle = '#e1d7c6'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(table.name, 0, 0); } if (activeTableMenu && activeTableMenu.id === table.id) { ctx.save(); const offsetRadius = (table.type === "circle" ? (table.d * SCALE)/2 : (table.h * SCALE)/2) + 25; ctx.translate(0, -offsetRadius); ctx.beginPath(); ctx.arc(0, 0, 14, 0, 2 * Math.PI); ctx.fillStyle = '#e1d7c6'; ctx.fill(); ctx.strokeStyle = '#111827'; ctx.lineWidth = 2; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#111827'; ctx.fillText('↻', 0, 0); ctx.restore(); } ctx.restore(); }); ctx.restore(); } function checkRotationButtonClick(vPos) { if (!activeTableMenu) return false; const t = activeTableMenu; const offsetRadius = (t.type === "circle" ? (t.d * SCALE)/2 : (t.h * SCALE)/2) + 25; const buttonWorldX = t.x + offsetRadius * Math.sin(t.angle || 0); const buttonWorldY = t.y - offsetRadius * Math.cos(t.angle || 0); if (Math.sqrt((vPos.x - buttonWorldX)**2 + (vPos.y - buttonWorldY)**2) <= 20) { t.angle = (t.angle || 0) + (45 * Math.PI / 180); draw(); return true; } return false; } function deleteTable(table) { if (!table) return; tables = tables.filter(t => t.id !== table.id); if(activeTableMenu && activeTableMenu.id === table.id) activeTableMenu = null; draw(); } function renameTable(table) { if (!table) return; const newName = prompt("Введите название объекта:", table.name); if (newName !== null && newName.trim() !== "") table.name = newName.trim(); if (table.type === "presidium") { const seatsInput = prompt("Мест Президиума (2-6):", table.seats); if (seatsInput !== null) { let seatsNum = parseInt(seatsInput, 10); if (!isNaN(seatsNum)) table.seats = Math.max(2, Math.min(6, seatsNum)); } } draw(); } function startDrag(e) { const isTouch = e.touches && e.touches.length > 0; if (isTouch && e.touches.length === 2) { selectedTable = null; isDragging = false; const dx = e.touches[1].clientX - e.touches[0].clientX; const dy = e.touches[1].clientY - e.touches[0].clientY; initialPinchDistance = Math.sqrt(dx * dx + dy * dy); return; } const touchObj = isTouch ? e.touches[0] : e; const vPos = getVirtualPos(touchObj.clientX, touchObj.clientY); if (checkRotationButtonClick(vPos)) { if (e.cancelable) e.preventDefault(); return; } const clicked = getTableAt(vPos); const now = new Date().getTime(); const timesince = now - lastTap; if (clicked && e.cancelable) e.preventDefault(); if (timesince < 300 && timesince > 0 && clicked) { deleteTable(clicked); endDrag(); lastTap = 0; return; } lastTap = now; if (clicked) { selectedTable = clicked; activeTableMenu = clicked; isDragging = true; dragMoved = false; offsetX = vPos.x - clicked.x; offsetY = vPos.y - clicked.y; } else { activeTableMenu = null; draw(); } } function moveDrag(e) { const isTouch = e.touches && e.touches.length > 0; if (isTouch && e.touches.length === 2) { if (e.cancelable) e.preventDefault(); const dx = e.touches[1].clientX - e.touches[0].clientX; const dy = e.touches[1].clientY - e.touches[0].clientY; const currentDistance = Math.sqrt(dx * dx + dy * dy); if (initialPinchDistance && initialPinchDistance > 0) { cameraZoom = Math.max(0.6, Math.min(2.5, cameraZoom * (currentDistance / initialPinchDistance))); initialPinchDistance = currentDistance; draw(); } return; } if (!isDragging || !selectedTable) return; if (e.cancelable) e.preventDefault(); const touchObj = isTouch ? e.touches[0] : e; const vPos = getVirtualPos(touchObj.clientX, touchObj.clientY); dragMoved = true; let newX = Math.round((vPos.x - offsetX) / SNAP_GRID_SIZE) * SNAP_GRID_SIZE; let newY = Math.round((vPos.y - offsetY) / SNAP_GRID_SIZE) * SNAP_GRID_SIZE; const w = (selectedTable.w ? selectedTable.w * SCALE : selectedTable.d * SCALE) / 2; if (newX < w) newX = w; if (newX > VIRTUAL_WIDTH - w) newX = VIRTUAL_WIDTH - w; if (newY < w) newY = w; if (newY > VIRTUAL_HEIGHT - w) newY = VIRTUAL_HEIGHT - w; selectedTable.x = newX; selectedTable.y = newY; draw(); } function endDrag() { if (isDragging && !dragMoved && selectedTable) renameTable(selectedTable); isDragging = false; initialPinchDistance = null; selectedTable = null; } window.addEventListener('wheel', (e) => { const rect = canvas.getBoundingClientRect(); if (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) { e.preventDefault(); cameraZoom = Math.max(0.6, Math.min(2.5, cameraZoom * (e.deltaY > 0 ? 0.92 : 1.08))); draw(); } }, { passive: false }); canvas.addEventListener('mousedown', startDrag); canvas.addEventListener('mousemove', moveDrag); window.addEventListener('mouseup', endDrag); canvas.addEventListener('touchstart', startDrag, { passive: false }); canvas.addEventListener('touchmove', moveDrag, { passive: false }); window.addEventListener('touchend', endDrag); document.getElementById('addTable10').addEventListener('click', () => { tables.push({ id: generateId(), type: "circle", x: 500, y: 300, d: 1.8, seats: 10, name: "Стол №" + (tables.length + 1), angle: 0 }); draw(); }); document.getElementById('addTable8').addEventListener('click', () => { tables.push({ id: generateId(), type: "circle", x: 500, y: 300, d: 1.5, seats: 8, name: "Стол №" + (tables.length + 1), angle: 0 }); draw(); }); document.getElementById('addPresidium').addEventListener('click', () => { tables.push({ id: generateId(), type: "presidium", x: 500, y: 210, w: 2.2, h: 1.0, seats: 4, name: "Президиум", angle: 0 }); draw(); }); document.getElementById('addScene').addEventListener('click', () => { tables.push({ id: generateId(), type: "zone", x: 500, y: 300, w: 3.5, h: 1.5, seats: 0, name: "Сцена / DJ", angle: 0 }); draw(); }); document.getElementById('saveLayout').addEventListener('click', () => { localStorage.setItem('canvas_restaurant_layout_premium_floor1', JSON.stringify(tables)); alert('Проект 1 этажа сохранен!'); }); document.getElementById('loadLayout').addEventListener('click', () => { const saved = localStorage.getItem('canvas_restaurant_layout_premium_floor1'); if (!saved) { alert('Сохранений не найдено.'); return; } tables = JSON.parse(saved); draw(); }); document.getElementById('exportPNG').addEventListener('click', () => { const dataURL = canvas.toDataURL('image/png'); const downloadLink = document.createElement('a'); downloadLink.href = dataURL; downloadLink.download = 'Plan_Floor1_Green19.png'; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); }); window.addEventListener('keydown', (e) => { if ((e.key === 'Delete' || e.key === 'Backspace') && selectedTable) { deleteTable(selectedTable); selectedTable = null; } }); window.addEventListener('resize', () => { draw(); }); initCanvas();