/** * Canvas editor for Scenar Creator v3. * Renders schedule blocks on a time-grid canvas with drag & drop via interact.js. */ const Canvas = { GRID_MINUTES: 15, PX_PER_SLOT: 30, // 30px per 15 min START_HOUR: 7, END_HOUR: 23, get pxPerMinute() { return this.PX_PER_SLOT / this.GRID_MINUTES; }, get totalSlots() { return ((this.END_HOUR - this.START_HOUR) * 60) / this.GRID_MINUTES; }, get totalHeight() { return this.totalSlots * this.PX_PER_SLOT; }, minutesToPx(minutes) { return (minutes - this.START_HOUR * 60) * this.pxPerMinute; }, pxToMinutes(px) { return Math.round(px / this.pxPerMinute + this.START_HOUR * 60); }, snapMinutes(minutes) { return Math.round(minutes / this.GRID_MINUTES) * this.GRID_MINUTES; }, formatTime(totalMinutes) { const h = Math.floor(totalMinutes / 60); const m = totalMinutes % 60; return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; }, parseTime(str) { const [h, m] = str.split(':').map(Number); return h * 60 + m; }, isLightColor(hex) { const h = hex.replace('#', ''); const r = parseInt(h.substring(0, 2), 16); const g = parseInt(h.substring(2, 4), 16); const b = parseInt(h.substring(4, 6), 16); return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.6; }, renderTimeAxis() { const axis = document.getElementById('timeAxis'); axis.innerHTML = ''; axis.style.height = this.totalHeight + 'px'; for (let slot = 0; slot <= this.totalSlots; slot++) { const minutes = this.START_HOUR * 60 + slot * this.GRID_MINUTES; const isHour = minutes % 60 === 0; const isHalf = minutes % 30 === 0; if (isHour || isHalf) { const label = document.createElement('div'); label.className = 'time-label'; label.style.top = (slot * this.PX_PER_SLOT) + 'px'; label.textContent = this.formatTime(minutes); if (!isHour) { label.style.fontSize = '9px'; label.style.opacity = '0.6'; } axis.appendChild(label); } } }, renderDayColumns(dates) { const header = document.getElementById('canvasHeader'); const columns = document.getElementById('dayColumns'); header.innerHTML = ''; columns.innerHTML = ''; if (dates.length === 0) dates = [new Date().toISOString().split('T')[0]]; dates.forEach(dateStr => { // Header const dh = document.createElement('div'); dh.className = 'day-header'; dh.textContent = dateStr; header.appendChild(dh); // Column const col = document.createElement('div'); col.className = 'day-column'; col.dataset.date = dateStr; col.style.height = this.totalHeight + 'px'; // Grid lines for (let slot = 0; slot <= this.totalSlots; slot++) { const minutes = this.START_HOUR * 60 + slot * this.GRID_MINUTES; const line = document.createElement('div'); line.className = 'grid-line ' + (minutes % 60 === 0 ? 'hour' : 'half'); line.style.top = (slot * this.PX_PER_SLOT) + 'px'; col.appendChild(line); } // Click area for creating blocks const clickArea = document.createElement('div'); clickArea.className = 'day-column-click-area'; clickArea.addEventListener('click', (e) => { if (e.target !== clickArea) return; const rect = col.getBoundingClientRect(); const y = e.clientY - rect.top; const minutes = this.snapMinutes(this.pxToMinutes(y)); const endMinutes = Math.min(minutes + 60, this.END_HOUR * 60); App.createBlock(dateStr, this.formatTime(minutes), this.formatTime(endMinutes)); }); col.appendChild(clickArea); columns.appendChild(col); }); }, renderBlocks(blocks, programTypes) { // Remove existing blocks document.querySelectorAll('.schedule-block').forEach(el => el.remove()); const typeMap = {}; programTypes.forEach(pt => { typeMap[pt.id] = pt; }); blocks.forEach(block => { const col = document.querySelector(`.day-column[data-date="${block.date}"]`); if (!col) return; const pt = typeMap[block.type_id] || { color: '#94a3b8', name: '?' }; const startMin = this.parseTime(block.start); const endMin = this.parseTime(block.end); const top = this.minutesToPx(startMin); const height = (endMin - startMin) * this.pxPerMinute; const isLight = this.isLightColor(pt.color); const el = document.createElement('div'); el.className = 'schedule-block' + (isLight ? ' light-bg' : ''); el.dataset.blockId = block.id; el.style.top = top + 'px'; el.style.height = Math.max(height, 20) + 'px'; el.style.backgroundColor = pt.color; el.innerHTML = `