From 28596db6d9d5d3b53c2b17a5fbd6128602c6bbaa Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 26 Jan 2026 01:57:49 +0000 Subject: [PATCH] Nano Media AI --- api/generate.php | 116 ++----- api/save.php | 40 +++ assets/css/custom.css | 274 +++++++++++----- assets/js/main.js | 557 +++++++++++++++++++++----------- index.php | 727 +++++++++++++++++++++--------------------- 5 files changed, 989 insertions(+), 725 deletions(-) create mode 100644 api/save.php diff --git a/api/generate.php b/api/generate.php index e99983c..6f25e15 100644 --- a/api/generate.php +++ b/api/generate.php @@ -11,46 +11,25 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $type = $_POST['type'] ?? 'photo'; $prompt = $_POST['prompt'] ?? ''; $style = $_POST['style'] ?? ''; -$aspect_ratio = $_POST['aspect_ratio'] ?? '1:1'; -$rapidapi_key = $_POST['rapidapi_key'] ?? ''; -$video_provider = $_POST['video_provider'] ?? 'pexels'; // 'pexels' or 'ai' if (empty($prompt)) { echo json_encode(['success' => false, 'error' => 'Запрос не может быть пустым']); exit; } -// Map aspect ratios to dimensions -$dimensions = [ - '1:1' => [1024, 1024], - '16:9' => [1280, 720], - '9:16' => [720, 1280], - '4:3' => [1024, 768], - '3:2' => [1080, 720] -]; +$width = 1024; +$height = 1024; -$width = $dimensions[$aspect_ratio][0] ?? 1024; -$height = $dimensions[$aspect_ratio][1] ?? 1024; - -// Enhance prompt with style $enhanced_prompt = $prompt; if (!empty($style)) { $style_map = [ 'anime' => 'in anime style, high quality, vibrant colors', 'realism' => 'photorealistic, highly detailed, 8k, masterpiece', 'cyberpunk' => 'cyberpunk style, neon lights, futuristic, highly detailed', - 'vaporwave' => 'vaporwave aesthetic, pink and blue colors, retro-futuristic', - 'pixel-art' => 'pixel art style, 8-bit, retro game aesthetic', - 'fantasy' => 'fantasy art, magical atmosphere, epic, highly detailed', - '3d-render' => '3d render, octane render, cinematic lighting, unreal engine 5', - 'steampunk' => 'steampunk style, gears, brass, Victorian aesthetic', - 'oil-painting' => 'oil painting, brush strokes, artistic, classic masterpiece', - 'sketch' => 'pencil sketch, hand drawn, artistic, charcoal', - 'pop-art' => 'pop art style, Andy Warhol aesthetic, vibrant, bold colors', + '3d-render' => '3d render, soft aesthetic, cinematic lighting, high quality', 'minimalism' => 'minimalist style, clean lines, simple, elegant', - 'cinematic' => 'cinematic shot, dramatic lighting, movie still, high contrast' + 'cinematic' => 'cinematic shot, dramatic lighting, movie still' ]; - if (isset($style_map[$style])) { $enhanced_prompt .= ", " . $style_map[$style]; } @@ -64,8 +43,7 @@ try { if ($type === 'photo') { $is_ai = true; - $provider_name = 'Pollinations AI (Flux)'; - // Real AI Generation (Truly free) + $provider_name = 'Pollinations Flux'; $seed = rand(1000, 99999); $encoded_prompt = urlencode($enhanced_prompt); $api_url = "https://image.pollinations.ai/prompt/{$encoded_prompt}?width={$width}&height={$height}&seed={$seed}&model=flux&nologo=true"; @@ -86,94 +64,38 @@ try { file_put_contents($target, $image_data); $result_url = $filename; } else { - // Fallback to direct URL if saving failed $result_url = $api_url; } } else { - // Video Logic - if ($video_provider === 'ai') { - if (!empty($rapidapi_key)) { - $is_ai = true; - $provider_name = 'RapidAPI AI'; - // Attempt real AI Video generation - // Note: Different APIs have different formats, this is a generic attempt - $ch = curl_init(); - curl_setopt_array($ch, [ - CURLOPT_URL => "https://text-to-video2.p.rapidapi.com/generate", - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => json_encode(["prompt" => $enhanced_prompt]), - CURLOPT_HTTPHEADER => [ - "Content-Type: application/json", - "X-RapidAPI-Host: text-to-video2.p.rapidapi.com", - "X-RapidAPI-Key: " . $rapidapi_key - ], - ]); - $response = curl_exec($ch); - $err = curl_error($ch); - curl_close($ch); - - if (!$err) { - $res_data = json_decode($response, true); - if (isset($res_data['video_url'])) { - $result_url = $res_data['video_url']; - } else if (isset($res_data['url'])) { - $result_url = $res_data['url']; - } - } - } - - if (empty($result_url)) { - // Fallback to Pexels search if no key or API failed - $is_ai = false; - $provider_name = 'Pexels Stock (Fallback)'; - if (empty($rapidapi_key)) { - $message = 'Ключ RapidAPI не найден. Используется качественный стоковый поиск вместо ИИ-генерации.'; - } else { - $message = 'Ошибка API генерации. Используется стоковый поиск.'; - } - - $url = 'https://api.pexels.com/videos/search?query=' . urlencode($prompt) . '&per_page=1&page=1'; - $data = pexels_get($url); - if ($data && !empty($data['videos'])) { - foreach ($data['videos'][0]['video_files'] as $file) { - if ($file['quality'] === 'hd' || $file['quality'] === 'sd') { - $result_url = $file['link']; - break; - } - } - } - } - } else { - // Standard Pexels Stock - $is_ai = false; - $provider_name = 'Pexels Stock'; - $url = 'https://api.pexels.com/videos/search?query=' . urlencode($prompt) . '&per_page=1&page=1'; - $data = pexels_get($url); - if ($data && !empty($data['videos'])) { - foreach ($data['videos'][0]['video_files'] as $file) { - if ($file['quality'] === 'hd' || $file['quality'] === 'sd') { - $result_url = $file['link']; - break; - } + // Video always from Pexels + $is_ai = false; + $provider_name = 'Pexels Stock'; + $url = 'https://api.pexels.com/videos/search?query=' . urlencode($prompt) . '&per_page=1&page=1'; + $data = pexels_get($url); + if ($data && !empty($data['videos'])) { + foreach ($data['videos'][0]['video_files'] as $file) { + if ($file['quality'] === 'hd' || $file['quality'] === 'sd') { + $result_url = $file['link']; + break; } } } if (empty($result_url)) { $result_url = 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4'; + $message = 'Видео не найдено по вашему запросу. Показываем пример.'; } } - // Save to history + $history_prompt = $prompt . ($style ? " ($style)" : ""); $stmt = db()->prepare("INSERT INTO media_history (type, prompt, result_url) VALUES (?, ?, ?)"); - $stmt->execute([$type, $prompt . ($style ? " ($style)" : ""), $result_url]); + $stmt->execute([$type, $history_prompt, $result_url]); echo json_encode([ 'success' => true, 'type' => $type, 'url' => $result_url, - 'prompt' => $prompt, + 'prompt' => $history_prompt, 'is_ai' => $is_ai, 'provider' => $provider_name, 'message' => $message diff --git a/api/save.php b/api/save.php new file mode 100644 index 0000000..a1bd919 --- /dev/null +++ b/api/save.php @@ -0,0 +1,40 @@ + false, 'error' => 'No image data provided']); + exit; +} + +try { + $img_data = $data['image']; + $img_data = str_replace('data:image/png;base64,', '', $img_data); + $img_data = str_replace(' ', '+', $img_data); + $decoded_data = base64_decode($img_data); + + $filename = 'assets/images/pexels/edited_' . md5(uniqid()) . '.png'; + $target = __DIR__ . '/../' . $filename; + + if (!is_dir(__DIR__ . '/../assets/images/pexels/')) { + mkdir(__DIR__ . '/../assets/images/pexels/', 0775, true); + } + + if (file_put_contents($target, $decoded_data)) { + // Save to database + $stmt = db()->prepare("INSERT INTO media_history (type, prompt, result_url) VALUES (?, ?, ?)"); + $stmt->execute(['photo', 'Edited in Nano Editor', $filename]); + + echo json_encode([ + 'success' => true, + 'url' => $filename, + 'message' => 'Сохранено в галерее!' + ]); + } else { + echo json_encode(['success' => false, 'error' => 'Failed to save image file']); + } +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/assets/css/custom.css b/assets/css/custom.css index 0b7e4cf..fcea7ea 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,120 +1,240 @@ -/* Custom Styles for AI Media Forge */ +/* Nano Banana Aesthetic CSS - Pro Edition */ :root { - --primary-color: #000000; - --accent-color: #007bff; - --bg-light: #f8f9fa; - --card-radius: 16px; + --nano-yellow: #FFDE59; + --nano-yellow-soft: #FFF9E0; + --nano-black: #1A1A1A; + --nano-white: #FFFFFF; + --nano-gray: #F5F5F5; + --nano-radius: 24px; } body { font-family: 'Inter', sans-serif; - color: #333; + background-color: var(--nano-gray); + color: var(--nano-black); } +.fw-black { font-weight: 900; } +.bg-yellow-soft { background-color: var(--nano-yellow-soft); } + /* Navbar */ .navbar { - backdrop-filter: blur(10px); - background-color: rgba(33, 37, 41, 0.95) !important; + border-bottom: 3px solid var(--nano-black); } /* Cards */ -.card { - border: none; - transition: transform 0.2s ease, box-shadow 0.2s ease; +.card-nano { + background: var(--nano-white); + border: 3px solid var(--nano-black); + border-radius: var(--nano-radius); + box-shadow: 8px 8px 0px var(--nano-black); + transition: all 0.2s ease; + overflow: hidden; } .history-card:hover { - transform: translateY(-5px); - box-shadow: 0 10px 20px rgba(0,0,0,0.1) !important; + transform: translate(-2px, -2px); + box-shadow: 12px 12px 0px var(--nano-black); } /* Buttons */ -.btn-primary { - background-color: var(--primary-color); - padding: 10px 24px; - transition: all 0.3s ease; +.btn-nano { + background-color: var(--nano-yellow); + color: var(--nano-black); + border: 3px solid var(--nano-black); + border-radius: var(--nano-radius); + font-weight: 800; + padding: 12px 24px; + text-transform: uppercase; + letter-spacing: 1px; + transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); + display: inline-flex; + align-items: center; + justify-content: center; } -.btn-primary:hover { - background-color: #333; - transform: translateY(-1px); +.btn-nano:hover { + background-color: var(--nano-black); + color: var(--nano-yellow); + transform: scale(1.05); } -/* Editor */ +.btn-nano:active { + transform: scale(0.95); +} + +/* Form Controls */ +.form-control-nano { + border: 3px solid var(--nano-black); + border-radius: 16px; + padding: 12px 16px; + font-weight: 600; +} + +.form-control-nano:focus { + box-shadow: none; + background-color: #FFFBE6; + border-color: var(--nano-black); +} + +/* Editor Specifics */ .editor-preview-container { - box-shadow: inset 0 0 20px rgba(0,0,0,0.5); + border: 3px solid var(--nano-black); + border-radius: 20px; + background-image: radial-gradient(#ccc 1px, transparent 1px); + background-size: 20px 20px; + background-color: #f0f0f0; + box-shadow: inset 0 0 20px rgba(0,0,0,0.05); +} + +#editor-canvas { + max-width: 100%; + max-height: 70vh; +} + +.canvas-container { + box-shadow: 0 10px 40px rgba(0,0,0,0.2); + border-radius: 4px; + overflow: hidden; +} + +/* Tabs */ +.nav-tabs { + border-bottom: 2px solid var(--nano-black) !important; +} + +.nav-tabs .nav-link { + border: none; + border-radius: 12px 12px 0 0; + margin-right: 2px; + padding: 8px 15px; + font-size: 0.8rem; + background: transparent; + transition: all 0.2s; +} + +.nav-tabs .nav-link.active { + background-color: var(--nano-yellow) !important; + border: 2px solid var(--nano-black) !important; + border-bottom: none !important; +} + +/* Range Input */ +.filter-range { + height: 10px; + -webkit-appearance: none; + background: transparent; } .filter-range::-webkit-slider-runnable-track { - background: #ddd; - height: 4px; - border-radius: 2px; + width: 100%; + height: 8px; + cursor: pointer; + background: #E0E0E0; + border-radius: 4px; + border: 2px solid var(--nano-black); } .filter-range::-webkit-slider-thumb { - background: var(--primary-color); - margin-top: -6px; + height: 24px; + width: 24px; + border-radius: 50%; + background: var(--nano-yellow); + cursor: pointer; + -webkit-appearance: none; + margin-top: -10px; + border: 3px solid var(--nano-black); + box-shadow: 2px 2px 0px var(--nano-black); } -/* AI Magic Box */ -.ai-magic-box { - border: 1px solid #e0e0e0; - transition: border-color 0.3s ease; +/* Custom Color Picker */ +.form-control-color { + width: 50px; + height: 45px; + padding: 5px; + border-radius: 12px; } -.ai-magic-box:focus-within { - border-color: var(--accent-color); +/* Cropper Overrides */ +.cropper-view-box, +.cropper-face { + border-radius: 0; +} + +.cropper-line, .cropper-point { + background-color: var(--nano-yellow); } /* Badges */ -.badge-ai { - font-weight: 600; - letter-spacing: 0.5px; - padding: 6px 12px; -} - -/* Transitions */ -.fade-in { - animation: fadeIn 0.5s ease-in; -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -/* Responsiveness */ -@media (max-width: 768px) { - .display-5 { - font-size: 2rem; - } -} - -/* Nano Editor Specifics */ -.letter-spacing-1 { - letter-spacing: 1px; -} - -.nav-pills .nav-link { - border: 1px solid transparent; -} - -.nav-pills .nav-link:not(.active):hover { - background-color: #f0f0f0; -} - -#editor-loading { +.badge-nano { + background: var(--nano-yellow); + color: var(--nano-black); + border: 2px solid var(--nano-black); border-radius: 8px; + padding: 4px 10px; + font-weight: 800; + font-size: 0.75rem; + text-transform: uppercase; } -textarea#ai-edit-prompt { - resize: none; - border-radius: 8px; - font-size: 0.9rem; +/* Tool Buttons */ +.tool-btn { + border: 2px solid var(--nano-black); + border-radius: 12px; + font-weight: 700; + transition: all 0.2s; } -.btn-outline-dark { - border-radius: 8px; - font-weight: 500; +.tool-btn.active { + background-color: var(--nano-yellow); + box-shadow: 3px 3px 0px var(--nano-black); + transform: translate(-1px, -1px); +} + +/* Sticker Items */ +.sticker-item { + font-size: 1.5rem; + width: 45px; + height: 45px; + display: flex; + align-items: center; + justify-content: center; + background: #f8f9fa; + border: 2px solid #ddd; + border-radius: 10px; + transition: all 0.2s; +} + +.sticker-item:hover { + background: var(--nano-yellow-soft); + border-color: var(--nano-yellow); + transform: scale(1.1) rotate(5deg); +} + +/* Animations */ +@keyframes banana-float { + 0% { transform: translateY(0) rotate(0deg); } + 50% { transform: translateY(-10px) rotate(10deg); } + 100% { transform: translateY(0) rotate(0deg); } +} + +.fas.fa-banana { + animation: banana-float 2s ease-in-out infinite; + display: inline-block; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; +} +::-webkit-scrollbar-track { + background: #f1f1f1; +} +::-webkit-scrollbar-thumb { + background: var(--nano-black); + border-radius: 10px; +} +::-webkit-scrollbar-thumb:hover { + background: #333; } \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index 03a23f7..25a29a1 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,7 +1,6 @@ document.addEventListener('DOMContentLoaded', () => { + // --- Core UI Elements --- const form = document.getElementById('generation-form'); - const mediaType = document.getElementById('media-type'); - const videoProviderContainer = document.getElementById('video-provider-container'); const generateBtn = document.getElementById('generate-btn'); const placeholderText = document.getElementById('placeholder-text'); const loadingState = document.getElementById('loading-state'); @@ -12,53 +11,70 @@ document.addEventListener('DOMContentLoaded', () => { const providerBadge = document.getElementById('provider-badge'); const infoOverlay = document.getElementById('info-overlay'); const statusMessage = document.getElementById('status-message'); + const uploadInput = document.getElementById('upload-image'); - // Editor Elements - const editorModal = new bootstrap.Modal(document.getElementById('editorModal')); - const editorCanvas = document.getElementById('editor-canvas'); - const ctx = editorCanvas.getContext('2d'); - const filterRanges = document.querySelectorAll('.filter-range'); - const resetFiltersBtn = document.getElementById('reset-filters'); - const saveEditedBtn = document.getElementById('save-edited-btn'); + // --- Editor Elements --- + const editorModalEl = document.getElementById('editorModal'); + const editorModal = new bootstrap.Modal(editorModalEl); const editorLoading = document.getElementById('editor-loading'); + const cropperImg = document.getElementById('cropper-image'); + const fabricWrapper = document.getElementById('fabric-wrapper'); + const transformBar = document.getElementById('transform-bar'); + + // Controls + const filterRanges = document.querySelectorAll('.filter-range'); + const brushToggle = document.getElementById('brush-toggle'); + const brushColor = document.getElementById('brush-color'); + const brushSize = document.getElementById('brush-size'); + const textInput = document.getElementById('text-input'); + const addTextBtn = document.getElementById('add-text-btn'); + const textColor = document.getElementById('text-color'); + const fontFamily = document.getElementById('font-family'); + const stickerItems = document.querySelectorAll('.sticker-item'); + + const startCropBtn = document.getElementById('start-crop-btn'); + const applyCropBtn = document.getElementById('apply-crop-btn'); + const resetEditorBtn = document.getElementById('reset-editor'); + const saveEditedBtn = document.getElementById('save-edited-btn'); + const saveToGalleryBtn = document.getElementById('save-to-gallery-btn'); - // AI Magic Elements const aiEditPrompt = document.getElementById('ai-edit-prompt'); const applyAiMagicBtn = document.getElementById('apply-ai-magic'); const removeBgBtn = document.getElementById('remove-bg-btn'); const upscaleBtn = document.getElementById('upscale-btn'); - - let currentImage = null; + + // --- Editor State --- + let canvas = null; + let cropper = null; + let fabricImage = null; // background image + let currentResultUrl = ''; let originalPrompt = ''; + let isDrawing = false; - // Local storage for settings - const getRapidKey = () => localStorage.getItem('rapidapi_key') || ''; - const setRapidKey = (key) => localStorage.setItem('rapidapi_key', key); + // --- Generation Logic --- + form.addEventListener('submit', async (e) => { + e.preventDefault(); + const formData = new FormData(form); + originalPrompt = formData.get('prompt'); - // Sync input with local storage - const keyInput = document.getElementById('rapidapi-key-input'); - if (keyInput) { - keyInput.value = getRapidKey(); - keyInput.addEventListener('change', (e) => setRapidKey(e.target.value)); - } + showLoading(); - // Toggle video provider field - mediaType.addEventListener('change', () => { - if (mediaType.value === 'video') { - videoProviderContainer.style.display = 'block'; - } else { - videoProviderContainer.style.display = 'none'; + try { + const response = await fetch('api/generate.php', { + method: 'POST', + body: formData + }); + const result = await response.json(); + if (result.success) renderResult(result); + else alert('Ошибка: ' + (result.error || 'Сбой генерации')); + } catch (error) { + alert('Сетевая ошибка'); + } finally { + hideLoading(); } }); - form.addEventListener('submit', async (e) => { - e.preventDefault(); - - const formData = new FormData(form); - formData.append('rapidapi_key', getRapidKey()); - originalPrompt = formData.get('prompt'); - - // UI State: Loading + function showLoading() { placeholderText.classList.add('d-none'); contentContainer.classList.add('d-none'); actionButtons.classList.add('d-none'); @@ -66,155 +82,303 @@ document.addEventListener('DOMContentLoaded', () => { statusMessage.classList.add('d-none'); loadingState.classList.remove('d-none'); generateBtn.disabled = true; + } - try { - const response = await fetch('api/generate.php', { - method: 'POST', - body: formData - }); - - const result = await response.json(); - - if (result.success) { - renderResult(result); - } else { - alert('Ошибка: ' + (result.error || 'Что-то пошло не так')); - resetUI(); - } - } catch (error) { - console.error('Generation error:', error); - alert('Сетевая ошибка при генерации'); - resetUI(); - } finally { - loadingState.classList.add('d-none'); - generateBtn.disabled = false; - } - }); + function hideLoading() { + loadingState.classList.add('d-none'); + generateBtn.disabled = false; + } function renderResult(result) { contentContainer.innerHTML = ''; contentContainer.classList.remove('d-none'); actionButtons.classList.remove('d-none'); infoOverlay.classList.remove('d-none'); - + currentResultUrl = result.url; providerBadge.textContent = result.provider; - if (result.is_ai) { - providerBadge.className = 'badge badge-ai shadow-sm'; - } else { - providerBadge.className = 'badge badge-stock shadow-sm'; - } - - if (result.message) { - statusMessage.textContent = result.message; - statusMessage.classList.remove('d-none'); - } + providerBadge.className = result.is_ai ? 'badge-nano bg-info' : 'badge-nano bg-warning'; if (result.type === 'photo') { const img = document.createElement('img'); img.src = result.url; img.className = 'img-fluid shadow-sm rounded mx-auto d-block'; img.style.maxHeight = '480px'; - img.style.objectFit = 'contain'; - img.id = 'active-result-img'; contentContainer.appendChild(img); - editBtn.classList.remove('d-none'); - editBtn.onclick = () => { - originalPrompt = result.prompt || originalPrompt; - openEditor(result.url); - }; } else { const video = document.createElement('video'); video.src = result.url; video.controls = true; - video.autoplay = true; video.className = 'rounded mx-auto d-block shadow-sm'; video.style.maxWidth = '100%'; video.style.maxHeight = '480px'; contentContainer.appendChild(video); editBtn.classList.add('d-none'); } + } - downloadBtn.onclick = () => { - const a = document.createElement('a'); - a.href = result.url; - a.download = `generation_${Date.now()}.${result.type === 'photo' ? 'jpg' : 'mp4'}`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); + // --- Upload Handler --- + uploadInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (f) => { + originalPrompt = "Uploaded image"; + openEditor(f.target.result); }; + reader.readAsDataURL(file); + }); + + // --- Editor Initialization --- + function initFabric() { + if (canvas) return; + canvas = new fabric.Canvas('editor-canvas', { + isDrawingMode: false, + preserveObjectStacking: true + }); + + // Handle object deletion + window.addEventListener('keydown', (e) => { + if (e.key === 'Delete' || e.key === 'Backspace') { + deleteObject(); + } + }); } - function resetUI() { - placeholderText.classList.remove('d-none'); - contentContainer.classList.add('d-none'); - actionButtons.classList.add('d-none'); - infoOverlay.classList.add('d-none'); - } + window.deleteObject = () => { + const activeObjects = canvas.getActiveObjects(); + if (activeObjects.length && !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) { + canvas.remove(...activeObjects); + canvas.discardActiveObject(); + canvas.requestRenderAll(); + } + }; + + window.bringToFront = () => { + const active = canvas.getActiveObject(); + if (active) { + active.bringToFront(); + canvas.requestRenderAll(); + } + }; + + window.sendToBack = () => { + const active = canvas.getActiveObject(); + if (active) { + // Keep background image at the very bottom + active.sendToBack(); + if (fabricImage) fabricImage.sendToBack(); + canvas.requestRenderAll(); + } + }; - // Editor Logic function openEditor(url) { - currentImage = new Image(); - currentImage.crossOrigin = "Anonymous"; - currentImage.src = url; - currentImage.onload = () => { - editorCanvas.width = currentImage.width; - editorCanvas.height = currentImage.height; - applyFilters(); - editorModal.show(); - }; - } - - function applyFilters() { - if (!currentImage) return; - - let filters = ''; - filterRanges.forEach(range => { - const filter = range.dataset.filter; - const value = range.value; - - if (filter === 'brightness' || filter === 'contrast' || filter === 'saturate') { - filters += `${filter}(${value}%) `; - } else if (filter === 'blur') { - filters += `${filter}(${value}px) `; - } else { - filters += `${filter}(${value}%) `; - } - }); - - ctx.filter = filters; - ctx.clearRect(0, 0, editorCanvas.width, editorCanvas.height); - ctx.drawImage(currentImage, 0, 0); - } - - filterRanges.forEach(range => { - range.addEventListener('input', applyFilters); - }); - - resetFiltersBtn.addEventListener('click', () => { - filterRanges.forEach(range => { - if (range.dataset.filter === 'brightness' || range.dataset.filter === 'contrast' || range.dataset.filter === 'saturate') { - range.value = 100; - } else { - range.value = 0; - } - }); - applyFilters(); - }); - - saveEditedBtn.addEventListener('click', () => { - const link = document.createElement('a'); - link.download = `edited_${Date.now()}.png`; - link.href = editorCanvas.toDataURL('image/png'); - link.click(); - }); - - // AI Magic Handlers - async function performAiEdit(action, customPrompt = '') { - if (!currentImage) return; - + initFabric(); editorLoading.classList.remove('d-none'); editorLoading.classList.add('d-flex'); + + // Reset State + canvas.clear(); + isDrawing = false; + brushToggle.classList.remove('active'); + canvas.isDrawingMode = false; + + fabric.Image.fromURL(url, (img) => { + fabricImage = img; + const maxDimension = 1000; + if (img.width > maxDimension || img.height > maxDimension) { + const scale = maxDimension / Math.max(img.width, img.height); + img.scale(scale); + } + + canvas.setWidth(img.getScaledWidth()); + canvas.setHeight(img.getScaledHeight()); + canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + + // Sync ranges + filterRanges.forEach(r => { + const f = r.dataset.filter; + r.value = (f === 'brightness' || f === 'contrast' || f === 'saturate') ? 100 : 0; + const vDisplay = document.getElementById(`val-${f}`); + if (vDisplay) vDisplay.textContent = r.value; + }); + + editorLoading.classList.add('d-none'); + editorLoading.classList.remove('d-flex'); + editorModal.show(); + }, { crossOrigin: 'anonymous' }); + } + + editBtn.addEventListener('click', () => currentResultUrl && openEditor(currentResultUrl)); + downloadBtn.addEventListener('click', () => { + if (!currentResultUrl) return; + const a = document.createElement('a'); + a.href = currentResultUrl; + a.download = `nano_${Date.now()}.${currentResultUrl.includes('.mp4') ? 'mp4' : 'jpg'}`; + a.click(); + }); + + // --- Filter Logic --- + filterRanges.forEach(range => { + range.addEventListener('input', () => { + const filterType = range.dataset.filter; + const val = parseFloat(range.value); + const vDisplay = document.getElementById(`val-${filterType}`); + if (vDisplay) vDisplay.textContent = val; + applyFabricFilters(); + }); + }); + + function applyFabricFilters() { + if (!fabricImage) return; + + const f = fabric.Image.filters; + fabricImage.filters = []; + + filterRanges.forEach(r => { + const type = r.dataset.filter; + const v = parseFloat(r.value); + + if (type === 'brightness' && v !== 100) fabricImage.filters.push(new f.Brightness({ brightness: (v - 100) / 100 })); + if (type === 'contrast' && v !== 100) fabricImage.filters.push(new f.Contrast({ contrast: (v - 100) / 100 })); + if (type === 'saturate' && v !== 100) fabricImage.filters.push(new f.Saturation({ saturation: (v - 100) / 100 })); + if (type === 'blur' && v > 0) fabricImage.filters.push(new f.Blur({ blur: v / 20 })); + if (type === 'sepia' && v > 0) fabricImage.filters.push(new f.Sepia()); + if (type === 'grayscale' && v > 0) fabricImage.filters.push(new f.Grayscale()); + if (type === 'hue-rotate' && v > 0) fabricImage.filters.push(new f.HueRotation({ rotation: v })); + if (type === 'noise' && v > 0) fabricImage.filters.push(new f.Noise({ noise: v * 2 })); + if (type === 'vignette' && v > 0) fabricImage.filters.push(new f.Vignette({ brightness: v / 100 })); + }); + + fabricImage.applyFilters(); + canvas.requestRenderAll(); + } + + // --- Transform Logic --- + window.rotateLeft = () => rotateCanvas(-90); + window.rotateRight = () => rotateCanvas(90); + window.flipH = () => { + if (!fabricImage) return; + fabricImage.set('flipX', !fabricImage.flipX); + canvas.requestRenderAll(); + }; + window.flipV = () => { + if (!fabricImage) return; + fabricImage.set('flipY', !fabricImage.flipY); + canvas.requestRenderAll(); + }; + + function rotateCanvas(degrees) { + if (!fabricImage) return; + const angle = (fabricImage.angle + degrees) % 360; + fabricImage.set('angle', angle); + + if (Math.abs(degrees) % 180 !== 0) { + const w = canvas.width; + const h = canvas.height; + canvas.setDimensions({ width: h, height: w }); + } + + canvas.centerObject(fabricImage); + fabricImage.setCoords(); + canvas.requestRenderAll(); + } + + // --- Cropper Logic --- + startCropBtn.addEventListener('click', () => { + const dataUrl = canvas.toDataURL({ format: 'png' }); + fabricWrapper.style.display = 'none'; + cropperImg.src = dataUrl; + cropperImg.style.display = 'block'; + transformBar.classList.remove('d-none'); + + if (cropper) cropper.destroy(); + cropper = new Cropper(cropperImg, { + viewMode: 1, + autoCropArea: 0.8, + responsive: true + }); + }); + + applyCropBtn.addEventListener('click', () => { + if (!cropper) return; + const croppedCanvas = cropper.getCroppedCanvas(); + const croppedDataUrl = croppedCanvas.toDataURL(); + + cropper.destroy(); + cropper = null; + cropperImg.style.display = 'none'; + fabricWrapper.style.display = 'block'; + transformBar.classList.add('d-none'); + + fabric.Image.fromURL(croppedDataUrl, (img) => { + fabricImage = img; + canvas.clear(); + canvas.setWidth(img.width); + canvas.setHeight(img.height); + canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + }); + }); + + // --- Decor Logic --- + brushToggle.addEventListener('click', () => { + isDrawing = !isDrawing; + canvas.isDrawingMode = isDrawing; + brushToggle.classList.toggle('active', isDrawing); + canvas.freeDrawingBrush = new fabric.PencilBrush(canvas); + canvas.freeDrawingBrush.color = brushColor.value; + canvas.freeDrawingBrush.width = parseInt(brushSize.value, 10); + }); + + brushColor.addEventListener('input', () => { + if (canvas.freeDrawingBrush) canvas.freeDrawingBrush.color = brushColor.value; + }); + brushSize.addEventListener('input', () => { + if (canvas.freeDrawingBrush) canvas.freeDrawingBrush.width = parseInt(brushSize.value, 10); + }); + + addTextBtn.addEventListener('click', () => { + const textStr = textInput.value.trim() || 'Nano!'; + const text = new fabric.IText(textStr, { + left: canvas.width / 2, + top: canvas.height / 2, + fontFamily: fontFamily.value, + fill: textColor.value, + fontSize: 50, + originX: 'center', + originY: 'center', + cornerStyle: 'circle', + cornerColor: '#FFDE59', + cornerStrokeColor: '#000', + transparentCorners: false + }); + canvas.add(text); + canvas.setActiveObject(text); + textInput.value = ''; + }); + + stickerItems.forEach(item => { + item.addEventListener('click', () => { + const char = item.dataset.sticker; + const sticker = new fabric.Text(char, { + fontSize: 100, + left: canvas.width / 2, + top: canvas.height / 2, + originX: 'center', + originY: 'center', + cornerStyle: 'circle' + }); + canvas.add(sticker); + canvas.setActiveObject(sticker); + }); + }); + + // --- AI Magic --- + async function performAiEdit(action, customPrompt = '') { + editorLoading.classList.remove('d-none'); + editorLoading.classList.add('d-flex'); + + const currentDataUrl = canvas.toDataURL({ format: 'png' }); try { const response = await fetch('api/edit.php', { @@ -224,62 +388,87 @@ document.addEventListener('DOMContentLoaded', () => { action: action, original_prompt: originalPrompt, edit_prompt: customPrompt, - image_url: currentImage.src + image_url: currentDataUrl }) }); const result = await response.json(); - if (result.success) { - // Load new image into canvas - const newImg = new Image(); - newImg.crossOrigin = "Anonymous"; - newImg.src = result.url + '?t=' + Date.now(); - newImg.onload = () => { - currentImage = newImg; - editorCanvas.width = newImg.width; - editorCanvas.height = newImg.height; - applyFilters(); + fabric.Image.fromURL(result.url + '?t=' + Date.now(), (img) => { + fabricImage = img; + canvas.clear(); + canvas.setWidth(img.getScaledWidth()); + canvas.setHeight(img.getScaledHeight()); + canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); editorLoading.classList.add('d-none'); - editorLoading.classList.remove('d-flex'); - }; + }, { crossOrigin: 'anonymous' }); } else { - alert('Ошибка ИИ: ' + (result.error || 'Не удалось применить изменения')); + alert('Ошибка ИИ: ' + result.error); editorLoading.classList.add('d-none'); - editorLoading.classList.remove('d-flex'); } - } catch (error) { - console.error('AI Edit error:', error); - alert('Сетевая ошибка при работе с ИИ'); + } catch (e) { + alert('Ошибка связи с сервером'); editorLoading.classList.add('d-none'); - editorLoading.classList.remove('d-flex'); } } applyAiMagicBtn.addEventListener('click', () => { - const prompt = aiEditPrompt.value.trim(); - if (!prompt) { - alert('Введите описание изменений'); - return; + const p = aiEditPrompt.value.trim(); + if (!p) return alert('Опишите изменения'); + performAiEdit('magic', p); + }); + removeBgBtn.addEventListener('click', () => performAiEdit('remove_bg')); + upscaleBtn.addEventListener('click', () => performAiEdit('upscale')); + + // --- Finalize & Save --- + resetEditorBtn.addEventListener('click', () => { + if (confirm('Сбросить все изменения?')) { + openEditor(fabricImage._originalElement.src); } - performAiEdit('magic', prompt); }); - removeBgBtn.addEventListener('click', () => { - performAiEdit('remove_bg'); + saveEditedBtn.addEventListener('click', () => { + const dataUrl = canvas.toDataURL({ format: 'png', quality: 1.0 }); + const link = document.createElement('a'); + link.download = `nano_edit_${Date.now()}.png`; + link.href = dataUrl; + link.click(); }); - upscaleBtn.addEventListener('click', () => { - performAiEdit('upscale'); + saveToGalleryBtn.addEventListener('click', async () => { + saveToGalleryBtn.disabled = true; + saveToGalleryBtn.innerHTML = ' СОХРАНЯЕМ...'; + + const dataUrl = canvas.toDataURL({ format: 'png', quality: 1.0 }); + + try { + const response = await fetch('api/save.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ image: dataUrl }) + }); + const result = await response.json(); + if (result.success) { + alert(result.message); + location.reload(); // Refresh to see in gallery + } else { + alert('Ошибка: ' + result.error); + } + } catch (e) { + alert('Ошибка сети'); + } finally { + saveToGalleryBtn.disabled = false; + saveToGalleryBtn.innerHTML = ' СОХРАНИТЬ В ГАЛЕРЕЮ'; + } }); - // History Edit Buttons + // History interaction document.addEventListener('click', (e) => { - if (e.target.closest('.history-edit-btn')) { - const btn = e.target.closest('.history-edit-btn'); - const url = btn.dataset.url; - const card = btn.closest('.card'); - originalPrompt = card.querySelector('.card-text').textContent; + const hBtn = e.target.closest('.history-edit-btn'); + if (hBtn) { + const url = hBtn.dataset.url; + const prompt = hBtn.closest('.history-card').querySelector('.history-prompt').textContent; + originalPrompt = prompt; openEditor(url); } }); diff --git a/index.php b/index.php index 20140fa..ea7d7e3 100644 --- a/index.php +++ b/index.php @@ -1,7 +1,7 @@ @@ -13,381 +13,436 @@ $project_description = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Generate unique AI ph - - + + + + + + + + - + -