/* ──────────────────────────────────────── Ghali System - Version Saisie Précise + Déplacement + Export ──────────────────────────────────────── */ const Ghali = { canvas: null, ctx: null, drawing: false, mode: 'mouse', color: '#dc3545', thickness: 4, strokes: [], redoStack: [], startPos: null, lastMovePos: null, isVisible: false, isEditingText: false, // Propriétés pour le déplacement selectedStroke: null, isDragging: false, dragOffset: { x: 0, y: 0 }, init() { this.createUI(); this.canvas = document.getElementById('annotationCanvas'); this.ctx = this.canvas.getContext('2d'); this.resize(); window.addEventListener('resize', () => this.resize()); const c = this.canvas; c.addEventListener('mousedown', e => this.start(e)); c.addEventListener('mousemove', e => this.draw(e)); window.addEventListener('mouseup', () => this.stop()); c.addEventListener('touchstart', e => { e.preventDefault(); this.start(e); }, { passive: false }); c.addEventListener('touchmove', e => { e.preventDefault(); this.draw(e); }, { passive: false }); c.addEventListener('touchend', () => this.stop()); document.addEventListener('click', (e) => { if (!e.target.closest('.g-group') && !e.target.closest('.g-toggle-btn')) { document.querySelectorAll('.g-menu, .thickness-popover').forEach(m => m.classList.remove('show')); } }); }, createUI() { if (!document.getElementById('annotationCanvas')) { const container = document.getElementById('ghali-overlay-container') || document.body; if (container.id !== 'ghali-overlay-container') { const wrapper = document.createElement('div'); wrapper.id = 'ghali-overlay-container'; while (document.body.firstChild) wrapper.appendChild(document.body.firstChild); document.body.appendChild(wrapper); } const canvas = document.createElement('canvas'); canvas.id = 'annotationCanvas'; document.getElementById('ghali-overlay-container').prepend(canvas); } if (!document.querySelector('.g-toggle-btn')) { const toggleBtn = document.createElement('button'); toggleBtn.className = 'g-toggle-btn'; toggleBtn.innerHTML = '✏️'; toggleBtn.onclick = () => this.toggleUI(); document.body.appendChild(toggleBtn); } if (!document.querySelector('.ghali-controls')) { const controls = document.createElement('div'); controls.className = 'ghali-controls hidden'; controls.innerHTML = `
`; document.body.appendChild(controls); } }, toggleUI() { this.isVisible = !this.isVisible; const controls = document.querySelector('.ghali-controls'); const toggleBtn = document.querySelector('.g-toggle-btn'); if (this.isVisible) { controls.classList.remove('hidden'); toggleBtn.innerHTML = '✖️'; toggleBtn.classList.add('active'); } else { controls.classList.add('hidden'); toggleBtn.innerHTML = '✏️'; toggleBtn.classList.remove('active'); this.setMode('mouse'); } }, resize() { const container = document.getElementById('ghali-overlay-container'); this.canvas.width = container.offsetWidth; this.canvas.height = Math.max(document.body.scrollHeight, window.innerHeight); this.redraw(); }, toggleMenu(id) { const menu = document.getElementById(id); const isShow = menu.classList.contains('show'); document.querySelectorAll('.g-menu, .thickness-popover').forEach(m => m.classList.remove('show')); if (!isShow) menu.classList.add('show'); }, setMode(m) { this.mode = m; document.querySelectorAll('.g-btn').forEach(b => b.classList.remove('active')); const btn = document.getElementById(`g-mode-${m}`); if (btn) btn.classList.add('active'); const container = document.getElementById('ghali-overlay-container'); if (m === 'mouse') container.classList.remove('drawing-active'); else container.classList.add('drawing-active'); }, setColor(c) { this.color = c; document.querySelectorAll('.g-menu').forEach(menu => menu.classList.remove('show')); }, setThickness(t) { this.thickness = parseInt(t); }, getPosition(e) { const r = this.canvas.getBoundingClientRect(); const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; return { x: clientX - r.left, y: clientY - r.top }; }, // Détection de collision pour les formes géométriques isPointInStroke(pos, s) { if (!s.start || !s.end) return false; const minX = Math.min(s.start.x, s.end.x) - 10; const maxX = Math.max(s.start.x, s.end.x) + 10; const minY = Math.min(s.start.y, s.end.y) - 10; const maxY = Math.max(s.start.y, s.end.y) + 10; return pos.x >= minX && pos.x <= maxX && pos.y >= minY && pos.y <= maxY; }, start(e) { if (this.mode === 'mouse' || this.isEditingText) return; const pos = this.getPosition(e); this.startPos = pos; // Si on clique sur une figure existante en mode géo -> Déplacement if (['line', 'rect', 'circle', 'triangle'].includes(this.mode)) { const found = [...this.strokes].reverse().find(s => s.start && this.isPointInStroke(pos, s)); if (found) { this.isDragging = true; this.selectedStroke = found; this.dragOffset = { x: pos.x - found.start.x, y: pos.y - found.start.y }; return; } } if (this.mode === 'text') { this.drawInput(this.startPos); return; } this.drawing = true; if (['pen', 'high', 'eraser'].includes(this.mode)) { this.strokes.push({ mode: this.mode, color: this.color, thickness: this.thickness, points: [this.startPos] }); } }, drawInput(pos) { this.isEditingText = true; const input = document.createElement('input'); input.className = 'ghali-dynamic-input'; const size = this.thickness * 6; Object.assign(input.style, { left: `${pos.x}px`, top: `${pos.y - (size/2)}px`, color: this.color, fontSize: `${size}px`, fontStyle: 'italic', fontWeight: 'bold' }); document.getElementById('ghali-overlay-container').appendChild(input); setTimeout(() => input.focus(), 50); const commit = () => { if (input.value.trim() !== "") { this.strokes.push({ mode: 'text', color: this.color, thickness: this.thickness, points: [pos], content: input.value }); this.redraw(); } input.remove(); setTimeout(() => { this.isEditingText = false; }, 200); }; const cancel = () => { input.remove(); this.isEditingText = false; this.setMode('mouse'); }; input.onkeydown = (e) => { if(e.key === 'Enter') { e.preventDefault(); commit(); } else if (e.key === 'Escape') { cancel(); } }; input.onblur = () => { cancel(); }; }, draw(e) { if (this.isEditingText) return; const currentPos = this.getPosition(e); if (this.isDragging && this.selectedStroke) { const dx = this.selectedStroke.end.x - this.selectedStroke.start.x; const dy = this.selectedStroke.end.y - this.selectedStroke.start.y; this.selectedStroke.start = { x: currentPos.x - this.dragOffset.x, y: currentPos.y - this.dragOffset.y }; this.selectedStroke.end = { x: this.selectedStroke.start.x + dx, y: this.selectedStroke.start.y + dy }; this.redraw(); return; } if (!this.drawing || this.mode === 'mouse') return; this.lastMovePos = currentPos; if (['pen', 'high', 'eraser'].includes(this.mode)) { this.strokes[this.strokes.length - 1].points.push(currentPos); } this.redraw(); if (['line', 'rect', 'circle', 'triangle'].includes(this.mode)) { this.drawShape(this.ctx, this.startPos, currentPos, this.mode, this.color, this.thickness); } }, stop() { if (this.isDragging) { this.isDragging = false; this.selectedStroke = null; return; } if (!this.drawing) return; this.drawing = false; if (['line', 'rect', 'circle', 'triangle'].includes(this.mode) && this.lastMovePos) { this.strokes.push({ mode: this.mode, color: this.color, thickness: this.thickness, start: this.startPos, end: this.lastMovePos }); this.redraw(); } }, drawShape(ctx, start, end, mode, color, thickness) { ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = thickness; ctx.lineCap = 'round'; if (mode === 'line') { ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); } else if (mode === 'rect') { ctx.strokeRect(start.x, start.y, end.x - start.x, end.y - start.y); } else if (mode === 'circle') { const r = Math.sqrt(Math.pow(end.x-start.x,2)+Math.pow(end.y-start.y,2)); ctx.arc(start.x, start.y, r, 0, 2*Math.PI); } else if (mode === 'triangle') { ctx.moveTo(start.x + (end.x - start.x)/2, start.y); ctx.lineTo(start.x, end.y); ctx.lineTo(end.x, end.y); ctx.closePath(); } ctx.stroke(); }, redraw() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); for (const s of this.strokes) { this.ctx.globalAlpha = (s.mode === 'high') ? 0.3 : 1.0; this.ctx.strokeStyle = (s.mode === 'eraser') ? '#f8f9fa' : s.color; this.ctx.fillStyle = s.color; this.ctx.lineWidth = (s.mode === 'high') ? s.thickness * 4 : s.thickness; this.ctx.globalCompositeOperation = (s.mode === 'eraser') ? 'destination-out' : 'source-over'; if (s.mode === 'text') { this.ctx.font = `italic bold ${s.thickness * 6}px sans-serif`; this.ctx.textBaseline = 'middle'; this.ctx.fillText(s.content, s.points[0].x, s.points[0].y); } else if (s.points) { this.ctx.beginPath(); this.ctx.moveTo(s.points[0].x, s.points[0].y); for (let i = 1; i < s.points.length; i++) { this.ctx.lineTo(s.points[i].x, s.points[i].y); } this.ctx.stroke(); } else if (s.start) { this.drawShape(this.ctx, s.start, s.end, s.mode, s.color, s.thickness); } } this.ctx.globalCompositeOperation = 'source-over'; }, download() { const link = document.createElement('a'); link.download = `ghali-annotation-${Date.now()}.png`; link.href = this.canvas.toDataURL(); link.click(); }, undo() { if (this.strokes.length > 0) { this.redoStack.push(this.strokes.pop()); this.redraw(); } }, clear() { if(confirm("هل أنت متأكد من مسح جميع التعديلات؟")) { this.strokes = []; this.redraw(); } } }; (function() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => Ghali.init()); } else { Ghali.init(); } })();