475 lines
18 KiB
JavaScript
475 lines
18 KiB
JavaScript
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 = '<span class="spinner-border spinner-border-sm"></span> СОХРАНЯЕМ...';
|
||
|
||
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 = '<i class="fas fa-cloud-upload-alt me-2"></i> СОХРАНИТЬ В ГАЛЕРЕЮ';
|
||
}
|
||
});
|
||
|
||
// 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);
|
||
}
|
||
});
|
||
}); |