586 lines
22 KiB
PHP
586 lines
22 KiB
PHP
@push('scripts')
|
||
<script>
|
||
class UniversalCameraEditor {
|
||
constructor() {
|
||
// Initialize elements
|
||
this.video = document.getElementById('video');
|
||
this.canvas = document.getElementById('canvas');
|
||
this.drawingCanvas = document.getElementById('drawingCanvas');
|
||
this.ctx = this.drawingCanvas.getContext('2d');
|
||
|
||
// Buttons
|
||
this.startButton = document.getElementById('startButton');
|
||
this.takePhotoButton = document.getElementById('takePhotoButton');
|
||
this.switchButton = document.getElementById('switchButton');
|
||
this.backButton = document.getElementById('backButton');
|
||
this.saveButton = document.getElementById('saveButton');
|
||
this.undoButton = document.getElementById('undoButton');
|
||
this.resetButton = document.getElementById('resetButton');
|
||
|
||
// Drawing controls
|
||
this.colorPicker = document.getElementById('colorPicker');
|
||
this.brushSize = document.getElementById('brushSize');
|
||
this.brushSizeValue = document.getElementById('brushSizeValue');
|
||
|
||
// Text modal elements
|
||
this.textModal = document.getElementById('textModal');
|
||
this.textInput = document.getElementById('textInput');
|
||
this.confirmTextBtn = document.getElementById('confirmTextBtn');
|
||
this.cancelTextBtn = document.getElementById('cancelTextBtn');
|
||
|
||
// Initialize state
|
||
this.stream = null;
|
||
this.currentFacingMode = 'environment';
|
||
this.isDrawing = false;
|
||
this.currentTool = 'brush';
|
||
this.drawingHistory = [];
|
||
this.historyIndex = -1;
|
||
|
||
// Drawing coordinates
|
||
this.startX = 0;
|
||
this.startY = 0;
|
||
this.lastX = 0;
|
||
this.lastY = 0;
|
||
|
||
// Setup
|
||
this.setupCanvas();
|
||
this.setupEventListeners();
|
||
this.setupDrawingContext();
|
||
|
||
|
||
// Add text overlay container
|
||
this.textOverlay = document.getElementById('textOverlay');
|
||
this.activeTextElement = null;
|
||
this.isDraggingText = false;
|
||
this.textOffset = {
|
||
x: 0,
|
||
y: 0
|
||
};
|
||
|
||
|
||
// Current input field reference
|
||
this.currentInputField = null;
|
||
|
||
}
|
||
|
||
setupCanvas() {
|
||
// Set initial canvas size
|
||
this.canvas.width = 1280;
|
||
this.canvas.height = 960;
|
||
this.drawingCanvas.width = 1280;
|
||
this.drawingCanvas.height = 960;
|
||
}
|
||
|
||
setupDrawingContext() {
|
||
this.ctx.strokeStyle = this.colorPicker.value;
|
||
this.ctx.lineWidth = this.brushSize.value;
|
||
this.ctx.lineCap = 'round';
|
||
this.ctx.lineJoin = 'round';
|
||
}
|
||
|
||
async startCamera() {
|
||
try {
|
||
if (this.stream) {
|
||
this.stream.getTracks().forEach(track => track.stop());
|
||
}
|
||
|
||
const constraints = {
|
||
video: {
|
||
facingMode: this.currentFacingMode,
|
||
width: {
|
||
ideal: 1280
|
||
},
|
||
height: {
|
||
ideal: 960
|
||
}
|
||
},
|
||
audio: false
|
||
};
|
||
|
||
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||
this.video.srcObject = this.stream;
|
||
|
||
// Enable buttons after camera starts
|
||
this.takePhotoButton.disabled = false;
|
||
this.switchButton.disabled = false;
|
||
|
||
// Show video, hide canvas
|
||
this.video.style.display = 'block';
|
||
this.canvas.style.display = 'none';
|
||
this.drawingCanvas.style.display = 'none';
|
||
|
||
// Hide editor controls
|
||
document.getElementById('editorControls').classList.add('hidden');
|
||
document.getElementById('cameraControls').classList.remove('hidden');
|
||
|
||
} catch (error) {
|
||
console.error('Error accessing camera:', error);
|
||
alert('Error accessing camera. Please make sure you have granted camera permissions.');
|
||
}
|
||
}
|
||
|
||
takePhoto() {
|
||
// Draw video frame to canvas
|
||
const context = this.canvas.getContext('2d');
|
||
context.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
|
||
|
||
// Hide video, show canvases
|
||
this.video.style.display = 'none';
|
||
this.canvas.style.display = 'block';
|
||
this.drawingCanvas.style.display = 'block';
|
||
|
||
// Show editor controls, hide camera controls
|
||
document.getElementById('editorControls').classList.remove('hidden');
|
||
document.getElementById('cameraControls').classList.add('hidden');
|
||
|
||
// Clear drawing history
|
||
this.drawingHistory = [];
|
||
this.historyIndex = -1;
|
||
this.saveDrawingState();
|
||
}
|
||
|
||
switchCamera() {
|
||
this.currentFacingMode = this.currentFacingMode === 'environment' ? 'user' : 'environment';
|
||
this.startCamera();
|
||
}
|
||
|
||
resetToCamera() {
|
||
// Clear canvases
|
||
const context = this.canvas.getContext('2d');
|
||
context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||
this.ctx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height);
|
||
|
||
// Reset history
|
||
this.drawingHistory = [];
|
||
this.historyIndex = -1;
|
||
|
||
// Restart camera
|
||
this.startCamera();
|
||
}
|
||
|
||
saveDrawingState() {
|
||
// Trim history if we're not at the end
|
||
if (this.historyIndex < this.drawingHistory.length - 1) {
|
||
this.drawingHistory = this.drawingHistory.slice(0, this.historyIndex + 1);
|
||
}
|
||
|
||
// Save current state
|
||
this.drawingHistory.push(this.drawingCanvas.toDataURL());
|
||
this.historyIndex++;
|
||
}
|
||
|
||
undo() {
|
||
if (this.historyIndex > 0) {
|
||
this.historyIndex--;
|
||
this.redrawHistory();
|
||
}
|
||
}
|
||
|
||
|
||
|
||
resetDrawing() {
|
||
this.ctx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height);
|
||
this.saveDrawingState();
|
||
}
|
||
|
||
// Implement all event listeners from previous code here
|
||
setupEventListeners() {
|
||
// Camera controls
|
||
this.startButton.onclick = () => this.startCamera();
|
||
this.takePhotoButton.onclick = () => this.takePhoto();
|
||
this.switchButton.onclick = () => this.switchCamera();
|
||
this.backButton.onclick = () => this.resetToCamera();
|
||
|
||
// Drawing controls
|
||
this.setupDrawingEvents();
|
||
this.setupToolButtons();
|
||
|
||
// Settings changes
|
||
this.colorPicker.oninput = (e) => this.ctx.strokeStyle = e.target.value;
|
||
this.brushSize.oninput = (e) => {
|
||
this.ctx.lineWidth = e.target.value;
|
||
this.brushSizeValue.textContent = `${e.target.value}px`;
|
||
};
|
||
|
||
// History controls
|
||
this.undoButton.onclick = () => this.undo();
|
||
this.resetButton.onclick = () => this.resetDrawing();
|
||
|
||
// Save functionality
|
||
this.saveButton.onclick = () => this.saveAndUpload();
|
||
|
||
|
||
// Text tool modal controls
|
||
this.confirmTextBtn.onclick = () => {
|
||
const text = this.textInput.value.trim();
|
||
if (text) {
|
||
this.addDraggableText(text);
|
||
this.textInput.value = '';
|
||
|
||
}
|
||
};
|
||
}
|
||
|
||
setupToolButtons() {
|
||
const toolButtons = document.querySelectorAll('.tool-btn');
|
||
toolButtons.forEach(button => {
|
||
button.onclick = () => {
|
||
toolButtons.forEach(btn => btn.classList.remove('active'));
|
||
button.classList.add('active');
|
||
this.currentTool = button.dataset.tool;
|
||
};
|
||
});
|
||
}
|
||
|
||
// Implement drawing events from previous code here
|
||
setupDrawingEvents() {
|
||
// Mouse events
|
||
this.drawingCanvas.addEventListener('mousedown', (e) => this.startDrawing(e));
|
||
this.drawingCanvas.addEventListener('mousemove', (e) => this.draw(e));
|
||
this.drawingCanvas.addEventListener('mouseup', () => this.stopDrawing());
|
||
this.drawingCanvas.addEventListener('mouseout', () => this.stopDrawing());
|
||
|
||
// Touch events
|
||
this.drawingCanvas.addEventListener('touchstart', (e) => {
|
||
if (this.shouldPreventDefault(e)) {
|
||
e.preventDefault();
|
||
}
|
||
const touch = e.touches[0];
|
||
const mouseEvent = new MouseEvent('mousedown', {
|
||
clientX: touch.clientX,
|
||
clientY: touch.clientY
|
||
});
|
||
this.startDrawing(mouseEvent);
|
||
}, {
|
||
passive: false
|
||
});
|
||
|
||
this.drawingCanvas.addEventListener('touchmove', (e) => {
|
||
if (this.shouldPreventDefault(e)) {
|
||
e.preventDefault();
|
||
}
|
||
const touch = e.touches[0];
|
||
const mouseEvent = new MouseEvent('mousemove', {
|
||
clientX: touch.clientX,
|
||
clientY: touch.clientY
|
||
});
|
||
this.draw(mouseEvent);
|
||
}, {
|
||
passive: false
|
||
});
|
||
|
||
this.drawingCanvas.addEventListener('touchend', () => {
|
||
this.stopDrawing();
|
||
}, {
|
||
passive: true
|
||
});
|
||
}
|
||
|
||
// Implement drawing methods from previous code here
|
||
startDrawing(e) {
|
||
this.isDrawing = true;
|
||
const rect = this.drawingCanvas.getBoundingClientRect();
|
||
const scaleX = this.drawingCanvas.width / rect.width;
|
||
const scaleY = this.drawingCanvas.height / rect.height;
|
||
|
||
this.startX = (e.clientX - rect.left) * scaleX;
|
||
this.startY = (e.clientY - rect.top) * scaleY;
|
||
this.lastX = this.startX;
|
||
this.lastY = this.startY;
|
||
|
||
if (this.currentTool === 'brush') {
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(this.startX, this.startY);
|
||
}
|
||
}
|
||
|
||
addDraggableText(text) {
|
||
const textElement = document.createElement('div');
|
||
textElement.className = 'draggable-text';
|
||
textElement.style.cssText = `
|
||
position: absolute;
|
||
cursor: move;
|
||
user-select: none;
|
||
color: ${this.colorPicker.value};
|
||
font-size: ${this.brushSize.value * 2}px;
|
||
padding: 5px;
|
||
border-radius: 3px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
`;
|
||
|
||
// Create text span
|
||
const textSpan = document.createElement('span');
|
||
textSpan.textContent = text;
|
||
textElement.appendChild(textSpan);
|
||
|
||
// Create delete button
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.innerHTML = '×';
|
||
deleteBtn.style.cssText = `
|
||
border: none;
|
||
background: none;
|
||
color: #ff4444;
|
||
font-size: 20px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
line-height: 1;
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
`;
|
||
textElement.appendChild(deleteBtn);
|
||
|
||
// Show/hide delete button on hover
|
||
textElement.addEventListener('mouseenter', () => {
|
||
deleteBtn.style.opacity = '1';
|
||
});
|
||
textElement.addEventListener('mouseleave', () => {
|
||
deleteBtn.style.opacity = '0';
|
||
});
|
||
|
||
// Delete functionality
|
||
deleteBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
textElement.remove();
|
||
this.saveDrawingState();
|
||
});
|
||
|
||
// Center the text initially
|
||
textElement.style.left = '50%';
|
||
textElement.style.top = '50%';
|
||
textElement.style.transform = 'translate(-50%, -50%)';
|
||
|
||
// Add drag functionality
|
||
this.setupDraggableText(textElement);
|
||
|
||
this.textOverlay.appendChild(textElement);
|
||
this.saveDrawingState();
|
||
}
|
||
|
||
setupDraggableText(element) {
|
||
let isDragging = false;
|
||
let currentX;
|
||
let currentY;
|
||
let initialX;
|
||
let initialY;
|
||
let xOffset = 0;
|
||
let yOffset = 0;
|
||
|
||
const dragStart = (e) => {
|
||
if (this.currentTool === 'text') {
|
||
const event = e.type === 'mousedown' ? e : e.touches[0];
|
||
initialX = event.clientX - xOffset;
|
||
initialY = event.clientY - yOffset;
|
||
|
||
if (e.target === element || e.target.parentNode === element) {
|
||
isDragging = true;
|
||
}
|
||
}
|
||
};
|
||
|
||
const drag = (e) => {
|
||
if (isDragging) {
|
||
e.preventDefault();
|
||
const event = e.type === 'mousemove' ? e : e.touches[0];
|
||
|
||
currentX = event.clientX - initialX;
|
||
currentY = event.clientY - initialY;
|
||
|
||
xOffset = currentX;
|
||
yOffset = currentY;
|
||
|
||
setTranslate(currentX, currentY, element);
|
||
}
|
||
};
|
||
|
||
const dragEnd = () => {
|
||
if (isDragging) {
|
||
isDragging = false;
|
||
this.saveDrawingState();
|
||
}
|
||
};
|
||
|
||
const setTranslate = (xPos, yPos, el) => {
|
||
el.style.transform = `translate(${xPos}px, ${yPos}px)`;
|
||
};
|
||
|
||
// Mouse events
|
||
element.addEventListener('mousedown', dragStart);
|
||
document.addEventListener('mousemove', drag);
|
||
document.addEventListener('mouseup', dragEnd);
|
||
|
||
// Touch events
|
||
element.addEventListener('touchstart', dragStart);
|
||
document.addEventListener('touchmove', drag);
|
||
document.addEventListener('touchend', dragEnd);
|
||
}
|
||
|
||
// Add this CSS to your stylesheet
|
||
|
||
|
||
draw(e) {
|
||
if (!this.isDrawing) return;
|
||
|
||
const rect = this.drawingCanvas.getBoundingClientRect();
|
||
const scaleX = this.drawingCanvas.width / rect.width;
|
||
const scaleY = this.drawingCanvas.height / rect.height;
|
||
const x = (e.clientX - rect.left) * scaleX;
|
||
const y = (e.clientY - rect.top) * scaleY;
|
||
|
||
switch (this.currentTool) {
|
||
case 'arrow':
|
||
// Restore the previous state before drawing new arrow
|
||
if (this.historyIndex >= 0) {
|
||
this.redrawHistory();
|
||
} else {
|
||
this.ctx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height);
|
||
}
|
||
this.drawArrow(this.startX, this.startY, x, y);
|
||
break;
|
||
|
||
case 'brush':
|
||
this.ctx.lineTo(x, y);
|
||
this.ctx.stroke();
|
||
break;
|
||
|
||
case 'rectangle':
|
||
// Restore the previous state before drawing new rectangle
|
||
if (this.historyIndex >= 0) {
|
||
this.redrawHistory();
|
||
} else {
|
||
this.ctx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height);
|
||
}
|
||
this.ctx.beginPath();
|
||
this.ctx.strokeRect(this.startX, this.startY, x - this.startX, y - this.startY);
|
||
break;
|
||
|
||
case 'circle':
|
||
// Restore the previous state before drawing new circle
|
||
if (this.historyIndex >= 0) {
|
||
this.redrawHistory();
|
||
} else {
|
||
this.ctx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height);
|
||
}
|
||
const radius = Math.sqrt(Math.pow(x - this.startX, 2) + Math.pow(y - this.startY, 2));
|
||
this.ctx.beginPath();
|
||
this.ctx.arc(this.startX, this.startY, radius, 0, Math.PI * 2);
|
||
this.ctx.stroke();
|
||
break;
|
||
}
|
||
|
||
this.lastX = x;
|
||
this.lastY = y;
|
||
}
|
||
|
||
drawArrow(fromX, fromY, toX, toY) {
|
||
const headLength = 20;
|
||
const headAngle = Math.PI / 6;
|
||
|
||
// Calculate angle
|
||
const angle = Math.atan2(toY - fromY, toX - fromX);
|
||
|
||
// Draw main line
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(fromX, fromY);
|
||
this.ctx.lineTo(toX, toY);
|
||
this.ctx.stroke();
|
||
|
||
// Draw arrowhead
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(toX, toY);
|
||
this.ctx.lineTo(
|
||
toX - headLength * Math.cos(angle - headAngle),
|
||
toY - headLength * Math.sin(angle - headAngle)
|
||
);
|
||
this.ctx.moveTo(toX, toY);
|
||
this.ctx.lineTo(
|
||
toX - headLength * Math.cos(angle + headAngle),
|
||
toY - headLength * Math.sin(angle + headAngle)
|
||
);
|
||
this.ctx.stroke();
|
||
}
|
||
|
||
startDrawing(e) {
|
||
this.isDrawing = true;
|
||
const rect = this.drawingCanvas.getBoundingClientRect();
|
||
const scaleX = this.drawingCanvas.width / rect.width;
|
||
const scaleY = this.drawingCanvas.height / rect.height;
|
||
|
||
this.startX = (e.clientX - rect.left) * scaleX;
|
||
this.startY = (e.clientY - rect.top) * scaleY;
|
||
this.lastX = this.startX;
|
||
this.lastY = this.startY;
|
||
|
||
if (this.currentTool === 'brush') {
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(this.startX, this.startY);
|
||
}
|
||
}
|
||
|
||
stopDrawing() {
|
||
if (this.isDrawing) {
|
||
this.isDrawing = false;
|
||
if (this.currentTool === 'brush') {
|
||
this.ctx.closePath();
|
||
}
|
||
this.saveDrawingState();
|
||
}
|
||
}
|
||
|
||
redrawHistory() {
|
||
if (this.historyIndex >= 0 && this.drawingHistory[this.historyIndex]) {
|
||
const img = new Image();
|
||
img.src = this.drawingHistory[this.historyIndex];
|
||
this.ctx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height);
|
||
this.ctx.drawImage(img, 0, 0);
|
||
} else {
|
||
this.ctx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height);
|
||
}
|
||
}
|
||
closeEditor() {
|
||
if (this.stream) {
|
||
this.stream.getTracks().forEach(track => track.stop());
|
||
}
|
||
|
||
// Clear canvases and text overlay
|
||
this.ctx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height);
|
||
const mainCtx = this.canvas.getContext('2d');
|
||
mainCtx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||
this.textOverlay.innerHTML = '';
|
||
|
||
// Reset state
|
||
this.drawingHistory = [];
|
||
this.historyIndex = -1;
|
||
this.currentInputField = null;
|
||
|
||
// Close modals
|
||
this.cameraModal.hide();
|
||
this.textInputModal.hide();
|
||
}
|
||
|
||
// Override this method in your implementation
|
||
saveAndUpload() {
|
||
|
||
}
|
||
|
||
closeCamera() {
|
||
// Stop the video stream
|
||
if (this.stream) {
|
||
this.stream.getTracks().forEach(track => track.stop());
|
||
}
|
||
|
||
// Hide video and canvas elements
|
||
this.video.style.display = 'none';
|
||
this.canvas.style.display = 'none';
|
||
this.drawingCanvas.style.display = 'none';
|
||
|
||
|
||
// Reset any additional state if needed
|
||
this.stream = null;
|
||
}
|
||
|
||
}
|
||
</script>
|
||
@endpush
|