document.addEventListener('DOMContentLoaded', () => {
// --- Core UI Elements ---
const form = document.getElementById('generation-form');
const generateBtn = document.getElementById('generate-btn');
const placeholderText = document.getElementById('placeholder-text');
const loadingState = document.getElementById('loading-state');
const contentContainer = document.getElementById('content-container');
const actionButtons = document.getElementById('action-buttons');
const downloadBtn = document.getElementById('download-btn');
const editBtn = document.getElementById('edit-btn');
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 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');
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');
// --- Editor State ---
let canvas = null;
let cropper = null;
let fabricImage = null; // background image
let currentResultUrl = '';
let originalPrompt = '';
let isDrawing = false;
// --- Generation Logic ---
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
originalPrompt = formData.get('prompt');
showLoading();
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();
}
});
function showLoading() {
placeholderText.classList.add('d-none');
contentContainer.classList.add('d-none');
actionButtons.classList.add('d-none');
infoOverlay.classList.add('d-none');
statusMessage.classList.add('d-none');
loadingState.classList.remove('d-none');
generateBtn.disabled = true;
}
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;
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';
contentContainer.appendChild(img);
editBtn.classList.remove('d-none');
} else {
const video = document.createElement('video');
video.src = result.url;
video.controls = 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');
}
}
// --- 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();
}
});
}
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();
}
};
function openEditor(url) {
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', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: action,
original_prompt: originalPrompt,
edit_prompt: customPrompt,
image_url: currentDataUrl
})
});
const result = await response.json();
if (result.success) {
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');
}, { crossOrigin: 'anonymous' });
} else {
alert('Ошибка ИИ: ' + result.error);
editorLoading.classList.add('d-none');
}
} catch (e) {
alert('Ошибка связи с сервером');
editorLoading.classList.add('d-none');
}
}
applyAiMagicBtn.addEventListener('click', () => {
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);
}
});
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();
});
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 interaction
document.addEventListener('click', (e) => {
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);
}
});
});