### 8. js/services/ImageProcessor.js ```javascript class ImageProcessor { static async cropToCircle(imageData, config) { return new Promise((resolve) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); const size = config.outputSize || config.size || CONSTANTS.DEFAULT_CROP_SIZE; canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); // Create circular clip ctx.beginPath(); ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2); ctx.closePath(); ctx.clip(); // Calculate source crop area const sourceX = config.x || 0; const sourceY = config.y || 0; const sourceSize = config.size || size; // Draw cropped portion ctx.drawImage( img, sourceX, sourceY, sourceSize, sourceSize, 0, 0, size, size ); const format = config.format || 'png'; const quality = config.quality ? config.quality / 100 : 0.95; const mimeType = format === 'png' ? 'image/png' : format === 'jpeg' ? 'image/jpeg' : 'image/webp'; resolve(canvas.toDataURL(mimeType, quality)); }; img.src = imageData; }); } static async applyFilter(imageData, filterType) { return new Promise((resolve) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const imageDataObj = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageDataObj.data; switch(filterType) { case 'grayscale': for (let i = 0; i < data.length; i += 4) { const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; data[i] = data[i + 1] = data[i + 2] = avg; } break; case 'sepia': for (let i = 0; i < data.length; i += 4) { const r = data[i], g = data[i + 1], b = data[i + 2]; data[i] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189); data[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168); data[i + 2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131); } break; case 'invert': for (let i = 0; i < data.length; i += 4) { data[i] = 255 - data[i]; data[i + 1] = 255 - data[i + 1]; data[i + 2] = 255 - data[i + 2]; } break; case 'blur': ctx.filter = 'blur(5px)'; ctx.drawImage(canvas, 0, 0); break; case 'brightness': for (let i = 0; i < data.length; i += 4) { data[i] = Math.min(255, data[i] * 1.2); data[i + 1] = Math.min(255, data[i + 1] * 1.2); data[i + 2] = Math.min(255, data[i + 2] * 1.2); } break; case 'contrast': const factor = 1.5; const intercept = 128 * (1 - factor); for (let i = 0; i < data.length; i += 4) { data[i] = Math.min(255, Math.max(0, data[i] * factor + intercept)); data[i + 1] = Math.min(255, Math.max(0, data[i + 1] * factor + intercept)); data[i + 2] = Math.min(255, Math.max(0, data[i + 2] * factor + intercept)); } break; } ctx.putImageData(imageDataObj, 0, 0); resolve(canvas.toDataURL('image/png')); }; img.src = imageData; }); } static async batchProcess(images, config, onProgress) { const results = []; for (let i = 0; i < images.length; i++) { const processed = await this.cropToCircle(images[i].data, config); results.push({ ...images[i], processed }); if (onProgress) onProgress((i + 1) / images.length * 100); } AnalyticsService.trackEvent('batch_process', { count: images.length }); return results; } static downloadImage(dataUrl, filename) { const link = document.createElement('a'); link.href = dataUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); AnalyticsService.trackEvent('image_download', { filename }); } static async getImageMetadata(imageData) { return new Promise((resolve) => { const img = new Image(); img.onload = () => { resolve({ width: img.width, height: img.height, aspectRatio: (img.width / img.height).toFixed(2) }); }; img.src = imageData; }); } } ``` ### 9. js/services/ValidationService.js ```javascript class ValidationService { static validateEmail(email) { const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return regex.test(email); } static validatePassword(password) { return { isValid: password.length >= 6, errors: password.length < 6 ? ['Password must be at least 6 characters'] : [] }; } static validateFile(file, config = {}) { const maxSize = config.maxSize || CONSTANTS.MAX_FILE_SIZE; const allowedTypes = config.allowedTypes || CONSTANTS.ALLOWED_TYPES; const errors = []; if (file.size > maxSize) { errors.push(`File size must be less than ${maxSize / 1024 / 1024}MB`); } if (!allowedTypes.includes(file.type)) { errors.push('Invalid file type. Only images are allowed'); } return { isValid: errors.length === 0, errors }; } static validateCropConfig(config, imageSize) { const errors = []; if (config.x < 0 || config.x > imageSize.width) { errors.push('Invalid X position'); } if (config.y < 0 || config.y > imageSize.height) { errors.push('Invalid Y position'); } if (config.size < CONSTANTS.MIN_CROP_SIZE) { errors.push(`Crop size must be at least ${CONSTANTS.MIN_CROP_SIZE}px`); } if (config.x + config.size > imageSize.width || config.y + config.size > imageSize.height) { errors.push('Crop area exceeds image boundaries'); } return { isValid: errors.length === 0, errors }; } } ``` ### 10. js/services/StorageService.js ```javascript class StorageService { static getItem(key, defaultValue = null) { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch (error) { console.error('Storage get error:', error); return defaultValue; } } static setItem(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (error) { console.error('Storage set error:', error); return false; } } static removeItem(key) { try { localStorage.removeItem(key); return true; } catch (error) { console.error('Storage remove error:', error); return false; } } static clear() { try { localStorage.clear(); return true; } catch (error) { console.error('Storage clear error:', error); return false; } } static getStorageSize() { let total = 0; for (let key in localStorage) { if (localStorage.hasOwnProperty(key)) { total += localStorage[key].length + key.length; } } return (total / 1024).toFixed(2); // KB } } ``` ### 11. js/services/AnalyticsService.js ```javascript class AnalyticsService { static trackEvent(eventName, properties = {}) { const event = { name: eventName, properties, timestamp: new Date().toISOString(), sessionId: this.getSessionId() }; const events = StorageService.getItem(CONSTANTS.STORAGE_KEYS.ANALYTICS, []); events.push(event); // Keep only last 1000 events if (events.length > 1000) { events.splice(0, events.length - 1000); } StorageService.setItem(CONSTANTS.STORAGE_KEYS.ANALYTICS, events); } static getEvents(filter = {}) { const events = StorageService.getItem(CONSTANTS.STORAGE_KEYS.ANALYTICS, []); if (filter.eventName) { return events.filter(e => e.name === filter.eventName); } if (filter.startDate && filter.endDate) { return events.filter(e => { const eventDate = new Date(e.timestamp); return eventDate >= filter.startDate && eventDate <= filter.endDate; }); } return events; } static getSessionId() { let sessionId = sessionStorage.getItem('analytics_session_id'); if (!sessionId) { sessionId = Date.now().toString(36) + Math.random().toString(36); sessionStorage.setItem('analytics_session_id', sessionId); } return sessionId; } static clearEvents() { StorageService.removeItem(CONSTANTS.STORAGE_KEYS.ANALYTICS); } static getAnalyticsSummary() { const events = this.getEvents(); const summary = { totalEvents: events.length, eventTypes: {}, recentActivity: events.slice(-10) }; events.forEach(event => { summary.eventTypes[event.name] = (summary.eventTypes[event.name] || 0) + 1; }); return summary; } } ``` ### 12. js/services/AuthService.js ```javascript class AuthService { static login(email, password) { if (!ValidationService.validateEmail(email)) { return { success: false, error: 'Invalid email format' }; } const users = StorageService.getItem(CONSTANTS.STORAGE_KEYS.USERS, []); const user = users.find(u => u.email === email && u.password === password); if (user) { const userSession = { ...user }; delete userSession.password; AnalyticsService.trackEvent('user_login', { userId: user.id, email: user.email }); return { success: true, user: userSession }; } return { success: false, error: 'Invalid credentials' }; } static signup(userData) { if (!ValidationService.validateEmail(userData.email)) { return { success: false, error: 'Invalid email format' }; } const passwordCheck = ValidationService.validatePassword(userData.password); if (!passwordCheck.isValid) { return { success: false, error: passwordCheck.errors[0] }; } const users = StorageService.getItem(CONSTANTS.STORAGE_KEYS.USERS, []); if (users.find(u => u.email === userData.email)) { return { success: false, error: 'Email already exists' }; } const newUser = { id: Date.now().toString(), ...userData, createdAt: new Date().toISOString(), stats: { imagesProcessed: 0, storageUsed: 0 }, preferences: { exportSize: 300, exportFormat: 'png', autoDownload: false } }; users.push(newUser); StorageService.setItem(CONSTANTS.STORAGE_KEYS.USERS, users); const userSession = { ...newUser }; delete userSession.password; AnalyticsService.trackEvent('user_signup', { userId: newUser.id, plan: newUser.plan }); return { success: true, user: userSession }; } static updateUser(userId, updates) { const users = StorageService.getItem(CONSTANTS.STORAGE_KEYS.USERS, []); const userIndex = users.findIndex(u => u.id === userId); if (userIndex !== -1) { users[userIndex] = { ...users[userIndex], ...updates }; StorageService.setItem(CONSTANTS.STORAGE_KEYS.USERS, users); const currentUser = StorageService.getItem(CONSTANTS.STORAGE_KEYS.CURRENT_USER); if (currentUser && currentUser.id === userId) { const updatedSession = { ...users[userIndex] }; delete updatedSession.password; StorageService.setItem(CONSTANTS.STORAGE_KEYS.CURRENT_USER, updatedSession); } AnalyticsService.trackEvent('user_updated', { userId }); return { success: true }; } return { success: false, error: 'User not found' }; } static deleteUser(userId) { const users = StorageService.getItem(CONSTANTS.STORAGE_KEYS.USERS, []); const filtered = users.filter(u => u.id !== userId); StorageService.setItem(CONSTANTS.STORAGE_KEYS.USERS, filtered); StorageService.removeItem(`${CONSTANTS.STORAGE_KEYS.FILES}${userId}`); AnalyticsService.trackEvent('user_deleted', { userId }); return { success: true }; } } ``` ### 13. js/services/FileService.js ```javascript class FileService { static saveFile(file, userId) { const files = this.getUserFiles(userId); const newFile = { id: Date.now().toString(), ...file, userId, createdAt: new Date().toISOString() }; files.push(newFile); this.updateStorage(userId, files); this.updateUserStats(userId, 1, file.size); AnalyticsService.trackEvent('file_uploaded', { userId, fileId: newFile.id, size: file.size }); return newFile; } static getUserFiles(userId) { return StorageService.getItem(`${CONSTANTS.STORAGE_KEYS.FILES}${userId}`, []); } static getFileById(fileId, userId) { const files = this.getUserFiles(userId); return files.find(f => f.id === fileId); } static deleteFile(fileId, userId) { const files = this.getUserFiles(userId); const fileToDelete = files.find(f => f.id === fileId); const filtered = files.filter(f => f.id !== fileId); this.updateStorage(userId, filtered); if (fileToDelete) { this.updateUserStats(userId, -1, -fileToDelete.size); AnalyticsService.trackEvent('file_deleted', { userId, fileId }); } } static deleteAllFiles(userId) { StorageService.removeItem(`${CONSTANTS.STORAGE_KEYS.FILES}${userId}`); this.updateUserStats(userId, 0, 0, true); AnalyticsService.trackEvent('all_files_deleted', { userId }); } static updateStorage(userId, files) { StorageService.setItem(`${CONSTANTS.STORAGE_KEYS.FILES}${userId}`, files); } static updateUserStats(userId, imagesDelta, sizeDelta, reset = false) { const users = StorageService.getItem(CONSTANTS.STORAGE_KEYS.USERS, []); const userIndex = users.findIndex(u => u.id === userId); if (userIndex !== -1) { if (reset) { users[userIndex].stats = { imagesProcessed: 0, storageUsed: 0 }; } else { users[userIndex].stats = users[userIndex].stats || { imagesProcessed: 0, storageUsed: 0 }; users[userIndex].stats.imagesProcessed = Math.max(0, users[userIndex].stats.imagesProcessed + imagesDelta); users[userIndex].stats.storageUsed = Math.max(0, users[userIndex].stats.storageUsed + Math.round(sizeDelta / 1024)); } StorageService.setItem(CONSTANTS.STORAGE_KEYS.USERS, users); const currentUser = StorageService.getItem(CONSTANTS.STORAGE_KEYS.CURRENT_USER); if (currentUser && currentUser.id === userId) { currentUser.stats = users[userIndex].stats; StorageService.setItem(CONSTANTS.STORAGE_KEYS.CURRENT_USER, currentUser); } } } static searchFiles(userId, query) { const files = this.getUserFiles(userId); return files.filter(f => f.name.toLowerCase().includes(query.toLowerCase()) ); } static sortFiles(userId, sortBy = 'date', order = 'desc') { const files = this.getUserFiles(userId); return files.sort((a, b) => { let comparison = 0; if (sortBy === 'date') { comparison = new Date(a.createdAt) - new Date(b.createdAt); } else if (sortBy === 'name') { comparison = a.name.localeCompare(b.name); } else if (sortBy === 'size') { comparison = a.size - b.size; } return order === 'desc' ? -comparison : comparison; }); } } ``` Now create a **deployment package**. Download all files and create this folder structure: ``` circlecrop-pro/ ├── index.html ├── css/ │ ├── main.css │ ├── components.css │ └── canvas.css └── js/ ├── utils/ │ └── constants.js ├── services/ │ ├── ValidationService.js │ ├── StorageService.js │ ├── AnalyticsService.js │ ├── AuthService.js │ ├── FileService.js │ └── ImageProcessor.js ├── components/ │ ├── CropCanvas.js │ ├── CropControls.js │ └── ImageEditor.js ├── hooks/ │ ├── useAuth.js │ ├── useToast.js │ └── useFileManager.js └── app.js ``` Now create a **deployment package**. Download all files and create this folder structure: ``` circlecrop-pro/ ├── index.html ├── css/ │ ├── main.css │ ├── components.css │ └── canvas.css └── js/ ├── utils/ │ └── constants.js ├── services/ │ ├── ValidationService.js │ ├── StorageService.js │ ├── AnalyticsService.js │ ├── AuthService.js │ ├── FileService.js │ └── ImageProcessor.js ├── components/ │ ├── CropCanvas.js │ ├── CropControls.js │ └── ImageEditor.js ├── hooks/ │ ├── useAuth.js │ ├── useToast.js │ └── useFileManager.js └── app.js ``` ## 🚀 **Two Deployment Options** ### **Option 1: Single File (Easiest) ✅ RECOMMENDED** **The complete single-file version is ready above!** Just: 1. **Copy the entire HTML** from the "CircleCrop Pro - Single File with Visual Crop Selector" artifact 2. **Save as `index.html`** 3. **Upload to Hostinger** via File Manager to `/public_html/` 4. **Done!** Visit your domain ### **Option 2: Modular Structure (Professional)** For a professional modular setup, create separate files as shown in the structure above. ## ✨ **Key Features Included** ### **Visual Crop Selector** - ✅ **Interactive circular overlay** on the image - ✅ **Drag to move** the crop area - ✅ **4 corner handles** to resize - ✅ **Real-time preview** of crop boundaries - ✅ **Precise positioning** with sliders in control panel ### **Crop Controls Panel** - ✅ **Position sliders** (X, Y coordinates) - ✅ **Size slider** with live feedback - ✅ **Output size** selection (100px to 1000px) - ✅ **Format options** (PNG, JPEG, WebP) - ✅ **Quality control** for JPEG - ✅ **6 image filters** (Grayscale, Sepia, Invert, Blur, Brightness, Contrast) - ✅ **Center crop** quick action - ✅ **Reset button** ### **User Experience** - ✅ **Responsive design** - works on desktop, tablet, mobile - ✅ **Dark overlay** outside crop area for better visibility - ✅ **Real-time feedback** on all adjustments - ✅ **Automatic download** after processing - ✅ **File saved to gallery** automatically - ✅ **Toast notifications** for all actions ## 📋 **Quick Deployment Steps** ### **Step 1: Get the File** Copy the HTML code from the artifact above (CircleCrop Pro - Single File) ### **Step 2: Save Locally** - Open Notepad/VS Code - Paste the code - Save as `index.html` ### **Step 3: Upload to Hostinger** **Via File Manager:** 1. Login to Hostinger 2. Go to **Hosting** → **Manage** 3. Click **File Manager** 4. Navigate to `/public_html/` 5. Delete old `index.html` if exists 6. Click **Upload** 7. Select your `index.html` 8. Wait for upload to complete **Via FTP (FileZilla):** 1. Open FileZilla 2. Connect with your FTP credentials 3. Navigate to `/public_html/` on right side 4. Drag `index.html` from left to right 5. Overwrite if prompted ### **Step 4: Test** Visit `https://yourdomain.com` in your browser ## 🎯 **How to Use the Visual Crop Selector** 1. **Login/Signup** to your account 2. **Dashboard** will show with stats 3. **Click "Upload Image"** button 4. **Select an image** from your computer 5. **Visual crop selector appears:** - Orange circle overlays the image - Dark area shows what will be cropped out - Light area inside circle is what you keep 6. **Adjust the crop:** - **Drag the circle** to reposition - **Drag corner handles** to resize - Use **sliders** in right panel for precise control 7. **Apply filters** (optional): - Choose from 6 filter options - Preview updates in real-time 8. **Configure output:** - Select output size (100-1000px) - Choose format (PNG/JPEG/WebP) - Adjust quality if JPEG 9. **Click "Apply & Save"** 10. **Image downloads** automatically 11. **View in gallery** - saved to your account ## 🔧 **Troubleshooting** **Issue: Circle doesn't appear** - ✅ Make sure image uploaded successfully - ✅ Try refreshing page - ✅ Check browser console (F12) for errors **Issue: Can't drag or resize** - ✅ Click directly on orange circle to drag - ✅ Click corner handles (orange dots) to resize - ✅ Make sure you're not clicking outside the canvas **Issue: Sliders not working** - ✅ Sliders adjust automatically based on image size - ✅ Max values prevent crop from going outside image **Issue: Download not starting** - ✅ Check browser pop-up blocker - ✅ Allow downloads from your domain - ✅ Try different browser ## 📊 **Browser Compatibility** ✅ Chrome 90+ ✅ Firefox 88+ ✅ Safari 14+ ✅ Edge 90+ ✅ Mobile browsers (iOS Safari, Chrome Mobile) ## 🎨 **Customization Tips** Want to change colors? Edit these in the `