/* ────────────────────────────────────────
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();
}
})();