Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28596db6d9 | ||
|
|
c2f7887af4 |
90
api/edit.php
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$data) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid request data']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $data['action'] ?? 'magic';
|
||||||
|
$original_prompt = $data['original_prompt'] ?? '';
|
||||||
|
$edit_prompt = $data['edit_prompt'] ?? '';
|
||||||
|
$image_url = $data['image_url'] ?? '';
|
||||||
|
|
||||||
|
if (empty($original_prompt) && empty($edit_prompt)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Prompt is required']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$new_prompt = '';
|
||||||
|
|
||||||
|
// Step 1: Use AI to merge prompts or handle actions
|
||||||
|
if ($action === 'magic') {
|
||||||
|
$ai_payload = [
|
||||||
|
'input' => [
|
||||||
|
['role' => 'system', 'content' => 'You are an AI Image Prompt Engineer. Your task is to combine an original image description with a new edit instruction into a single, highly detailed, and cohesive new image prompt. Maintain the core subject and style of the original but incorporate the changes naturally. Output ONLY the new prompt, no explanation.'],
|
||||||
|
['role' => 'user', 'content' => "Original description: {$original_prompt}\nEdit instruction: {$edit_prompt}\nNew Prompt:"]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$ai_response = LocalAIApi::createResponse($ai_payload);
|
||||||
|
if (!empty($ai_response['success'])) {
|
||||||
|
$new_prompt = LocalAIApi::extractText($ai_response);
|
||||||
|
} else {
|
||||||
|
// Fallback: simple concatenation
|
||||||
|
$new_prompt = $original_prompt . ", " . $edit_prompt;
|
||||||
|
}
|
||||||
|
} else if ($action === 'remove_bg') {
|
||||||
|
$new_prompt = "{$original_prompt}, isolated on a pure white background, studio lighting, professional product photography";
|
||||||
|
} else if ($action === 'upscale') {
|
||||||
|
$new_prompt = "{$original_prompt}, extremely detailed, 8k resolution, masterpiece, sharp focus, hyperrealistic";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($new_prompt)) {
|
||||||
|
$new_prompt = $original_prompt . " " . $edit_prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Generate new image using Pollinations (Flux)
|
||||||
|
$seed = rand(1000, 99999);
|
||||||
|
$encoded_prompt = urlencode($new_prompt);
|
||||||
|
$api_url = "https://image.pollinations.ai/prompt/{$encoded_prompt}?width=1024&height=1024&seed={$seed}&model=flux&nologo=true";
|
||||||
|
|
||||||
|
$filename = 'assets/images/pexels/gen_edit_' . md5($new_prompt . $seed) . '.jpg';
|
||||||
|
$target = __DIR__ . '/../' . $filename;
|
||||||
|
|
||||||
|
// Download the image
|
||||||
|
$ch = curl_init($api_url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
|
||||||
|
$image_data = curl_exec($ch);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($http_code === 200 && $image_data) {
|
||||||
|
if (!is_dir(__DIR__ . '/../assets/images/pexels/')) {
|
||||||
|
mkdir(__DIR__ . '/../assets/images/pexels/', 0775, true);
|
||||||
|
}
|
||||||
|
file_put_contents($target, $image_data);
|
||||||
|
|
||||||
|
// Save to history
|
||||||
|
$stmt = db()->prepare("INSERT INTO media_history (type, prompt, result_url) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute(['photo', "[Edit] " . $new_prompt, $filename]);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'url' => $filename,
|
||||||
|
'new_prompt' => $new_prompt
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Failed to generate image from AI: HTTP ' . $http_code]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
106
api/generate.php
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/../includes/pexels.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid request method']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $_POST['type'] ?? 'photo';
|
||||||
|
$prompt = $_POST['prompt'] ?? '';
|
||||||
|
$style = $_POST['style'] ?? '';
|
||||||
|
|
||||||
|
if (empty($prompt)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Запрос не может быть пустым']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$width = 1024;
|
||||||
|
$height = 1024;
|
||||||
|
|
||||||
|
$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',
|
||||||
|
'3d-render' => '3d render, soft aesthetic, cinematic lighting, high quality',
|
||||||
|
'minimalism' => 'minimalist style, clean lines, simple, elegant',
|
||||||
|
'cinematic' => 'cinematic shot, dramatic lighting, movie still'
|
||||||
|
];
|
||||||
|
if (isset($style_map[$style])) {
|
||||||
|
$enhanced_prompt .= ", " . $style_map[$style];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result_url = '';
|
||||||
|
$is_ai = false;
|
||||||
|
$provider_name = '';
|
||||||
|
$message = '';
|
||||||
|
|
||||||
|
if ($type === 'photo') {
|
||||||
|
$is_ai = true;
|
||||||
|
$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";
|
||||||
|
|
||||||
|
$filename = 'assets/images/pexels/gen_' . md5($enhanced_prompt . $seed) . '.jpg';
|
||||||
|
$target = __DIR__ . '/../' . $filename;
|
||||||
|
|
||||||
|
$ch = curl_init($api_url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||||
|
$image_data = curl_exec($ch);
|
||||||
|
$content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($http_code === 200 && strpos($content_type, 'image') !== false && $image_data) {
|
||||||
|
file_put_contents($target, $image_data);
|
||||||
|
$result_url = $filename;
|
||||||
|
} else {
|
||||||
|
$result_url = $api_url;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 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 = 'Видео не найдено по вашему запросу. Показываем пример.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$history_prompt = $prompt . ($style ? " ($style)" : "");
|
||||||
|
$stmt = db()->prepare("INSERT INTO media_history (type, prompt, result_url) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$type, $history_prompt, $result_url]);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'type' => $type,
|
||||||
|
'url' => $result_url,
|
||||||
|
'prompt' => $history_prompt,
|
||||||
|
'is_ai' => $is_ai,
|
||||||
|
'provider' => $provider_name,
|
||||||
|
'message' => $message
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
40
api/save.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$data || empty($data['image'])) {
|
||||||
|
echo json_encode(['success' => 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()]);
|
||||||
|
}
|
||||||
@ -1,346 +1,240 @@
|
|||||||
|
/* Nano Banana Aesthetic CSS - Pro Edition */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-bg: #ffffff;
|
--nano-yellow: #FFDE59;
|
||||||
--color-text: #1a1a1a;
|
--nano-yellow-soft: #FFF9E0;
|
||||||
--color-primary: #2563EB; /* Vibrant Blue */
|
--nano-black: #1A1A1A;
|
||||||
--color-secondary: #000000;
|
--nano-white: #FFFFFF;
|
||||||
--color-accent: #A3E635; /* Lime Green */
|
--nano-gray: #F5F5F5;
|
||||||
--color-surface: #f8f9fa;
|
--nano-radius: 24px;
|
||||||
--font-heading: 'Space Grotesk', sans-serif;
|
|
||||||
--font-body: 'Inter', sans-serif;
|
|
||||||
--border-width: 2px;
|
|
||||||
--shadow-hard: 5px 5px 0px #000;
|
|
||||||
--shadow-hover: 8px 8px 0px #000;
|
|
||||||
--radius-pill: 50rem;
|
|
||||||
--radius-card: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-body);
|
font-family: 'Inter', sans-serif;
|
||||||
background-color: var(--color-bg);
|
background-color: var(--nano-gray);
|
||||||
color: var(--color-text);
|
color: var(--nano-black);
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6, .navbar-brand {
|
.fw-black { font-weight: 900; }
|
||||||
font-family: var(--font-heading);
|
.bg-yellow-soft { background-color: var(--nano-yellow-soft); }
|
||||||
letter-spacing: -0.03em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Utilities */
|
|
||||||
.text-primary { color: var(--color-primary) !important; }
|
|
||||||
.bg-black { background-color: #000 !important; }
|
|
||||||
.text-white { color: #fff !important; }
|
|
||||||
.shadow-hard { box-shadow: var(--shadow-hard); }
|
|
||||||
.border-2-black { border: var(--border-width) solid #000; }
|
|
||||||
.py-section { padding-top: 5rem; padding-bottom: 5rem; }
|
|
||||||
|
|
||||||
/* Navbar */
|
/* Navbar */
|
||||||
.navbar {
|
.navbar {
|
||||||
background: rgba(255, 255, 255, 0.9);
|
border-bottom: 3px solid var(--nano-black);
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border-bottom: var(--border-width) solid transparent;
|
|
||||||
transition: all 0.3s;
|
|
||||||
padding-top: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar.scrolled {
|
/* Cards */
|
||||||
border-bottom-color: #000;
|
.card-nano {
|
||||||
padding-top: 0.5rem;
|
background: var(--nano-white);
|
||||||
padding-bottom: 0.5rem;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-text {
|
.history-card:hover {
|
||||||
font-size: 1.5rem;
|
transform: translate(-2px, -2px);
|
||||||
font-weight: 800;
|
box-shadow: 12px 12px 0px var(--nano-black);
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-text);
|
|
||||||
margin-left: 1rem;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover, .nav-link.active {
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.btn {
|
.btn-nano {
|
||||||
font-weight: 700;
|
background-color: var(--nano-yellow);
|
||||||
font-family: var(--font-heading);
|
color: var(--nano-black);
|
||||||
padding: 0.8rem 2rem;
|
border: 3px solid var(--nano-black);
|
||||||
border-radius: var(--radius-pill);
|
border-radius: var(--nano-radius);
|
||||||
border: var(--border-width) solid #000;
|
font-weight: 800;
|
||||||
transition: all 0.2s cubic-bezier(0.25, 1, 0.5, 1);
|
padding: 12px 24px;
|
||||||
box-shadow: var(--shadow-hard);
|
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:hover {
|
.btn-nano:hover {
|
||||||
transform: translate(-2px, -2px);
|
background-color: var(--nano-black);
|
||||||
box-shadow: var(--shadow-hover);
|
color: var(--nano-yellow);
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:active {
|
.btn-nano:active {
|
||||||
transform: translate(2px, 2px);
|
transform: scale(0.95);
|
||||||
box-shadow: 0 0 0 #000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
/* Form Controls */
|
||||||
background-color: var(--color-primary);
|
.form-control-nano {
|
||||||
border-color: #000;
|
border: 3px solid var(--nano-black);
|
||||||
color: #fff;
|
border-radius: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.form-control-nano:focus {
|
||||||
background-color: #1d4ed8;
|
box-shadow: none;
|
||||||
border-color: #000;
|
background-color: #FFFBE6;
|
||||||
color: #fff;
|
border-color: var(--nano-black);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline-dark {
|
/* Editor Specifics */
|
||||||
background-color: #fff;
|
.editor-preview-container {
|
||||||
color: #000;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-cta {
|
#editor-canvas {
|
||||||
background-color: var(--color-accent);
|
max-width: 100%;
|
||||||
color: #000;
|
max-height: 70vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-cta:hover {
|
.canvas-container {
|
||||||
background-color: #8cc629;
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||||
color: #000;
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hero Section */
|
/* Tabs */
|
||||||
.hero-section {
|
.nav-tabs {
|
||||||
min-height: 100vh;
|
border-bottom: 2px solid var(--nano-black) !important;
|
||||||
padding-top: 80px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-blob {
|
.nav-tabs .nav-link {
|
||||||
position: absolute;
|
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 {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #E0E0E0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid var(--nano-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-range::-webkit-slider-thumb {
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
filter: blur(80px);
|
background: var(--nano-yellow);
|
||||||
opacity: 0.6;
|
cursor: pointer;
|
||||||
z-index: 1;
|
-webkit-appearance: none;
|
||||||
|
margin-top: -10px;
|
||||||
|
border: 3px solid var(--nano-black);
|
||||||
|
box-shadow: 2px 2px 0px var(--nano-black);
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob-1 {
|
/* Custom Color Picker */
|
||||||
top: -10%;
|
.form-control-color {
|
||||||
right: -10%;
|
width: 50px;
|
||||||
width: 600px;
|
height: 45px;
|
||||||
height: 600px;
|
padding: 5px;
|
||||||
background: radial-gradient(circle, var(--color-accent), transparent);
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob-2 {
|
/* Cropper Overrides */
|
||||||
bottom: 10%;
|
.cropper-view-box,
|
||||||
left: -10%;
|
.cropper-face {
|
||||||
width: 500px;
|
border-radius: 0;
|
||||||
height: 500px;
|
|
||||||
background: radial-gradient(circle, var(--color-primary), transparent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight-text {
|
.cropper-line, .cropper-point {
|
||||||
background: linear-gradient(120deg, transparent 0%, transparent 40%, var(--color-accent) 40%, var(--color-accent) 100%);
|
background-color: var(--nano-yellow);
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 100% 40%;
|
|
||||||
background-position: 0 88%;
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot { color: var(--color-primary); }
|
/* Badges */
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
.badge-pill {
|
/* Tool Buttons */
|
||||||
display: inline-block;
|
.tool-btn {
|
||||||
padding: 0.5rem 1rem;
|
border: 2px solid var(--nano-black);
|
||||||
border: 2px solid #000;
|
border-radius: 12px;
|
||||||
border-radius: 50px;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
background: #fff;
|
transition: all 0.2s;
|
||||||
box-shadow: 4px 4px 0 #000;
|
|
||||||
font-family: var(--font-heading);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Marquee */
|
.tool-btn.active {
|
||||||
.marquee-container {
|
background-color: var(--nano-yellow);
|
||||||
overflow: hidden;
|
box-shadow: 3px 3px 0px var(--nano-black);
|
||||||
white-space: nowrap;
|
transform: translate(-1px, -1px);
|
||||||
border-top: 2px solid #000;
|
|
||||||
border-bottom: 2px solid #000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rotate-divider {
|
/* Sticker Items */
|
||||||
transform: rotate(-2deg) scale(1.05);
|
.sticker-item {
|
||||||
z-index: 10;
|
|
||||||
position: relative;
|
|
||||||
margin-top: -50px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee-content {
|
|
||||||
display: inline-block;
|
|
||||||
animation: marquee 20s linear infinite;
|
|
||||||
font-family: var(--font-heading);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
letter-spacing: 2px;
|
width: 45px;
|
||||||
}
|
height: 45px;
|
||||||
|
|
||||||
@keyframes marquee {
|
|
||||||
0% { transform: translateX(0); }
|
|
||||||
100% { transform: translateX(-50%); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Portfolio Cards */
|
|
||||||
.project-card {
|
|
||||||
border: 2px solid #000;
|
|
||||||
border-radius: var(--radius-card);
|
|
||||||
overflow: hidden;
|
|
||||||
background: #fff;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
box-shadow: var(--shadow-hard);
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-card:hover {
|
|
||||||
transform: translateY(-10px);
|
|
||||||
box-shadow: 8px 8px 0 #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-img-holder {
|
|
||||||
height: 250px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-bottom: 2px solid #000;
|
|
||||||
position: relative;
|
|
||||||
font-size: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-art {
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-card:hover .placeholder-art {
|
|
||||||
transform: scale(1.2) rotate(10deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-soft-blue { background-color: #e0f2fe; }
|
|
||||||
.bg-soft-green { background-color: #dcfce7; }
|
|
||||||
.bg-soft-purple { background-color: #f3e8ff; }
|
|
||||||
.bg-soft-yellow { background-color: #fef9c3; }
|
|
||||||
|
|
||||||
.category-tag {
|
|
||||||
position: absolute;
|
|
||||||
top: 15px;
|
|
||||||
right: 15px;
|
|
||||||
background: #000;
|
|
||||||
color: #fff;
|
|
||||||
padding: 5px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body { padding: 1.5rem; }
|
|
||||||
|
|
||||||
.link-arrow {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #000;
|
|
||||||
font-weight: 700;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-arrow i { transition: transform 0.2s; margin-left: 5px; }
|
|
||||||
.link-arrow:hover i { transform: translateX(5px); }
|
|
||||||
|
|
||||||
/* About */
|
|
||||||
.about-image-stack {
|
|
||||||
position: relative;
|
|
||||||
height: 400px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stack-card {
|
|
||||||
position: absolute;
|
|
||||||
width: 80%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: var(--radius-card);
|
|
||||||
border: 2px solid #000;
|
|
||||||
box-shadow: var(--shadow-hard);
|
|
||||||
left: 10%;
|
|
||||||
transform: rotate(-3deg);
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forms */
|
|
||||||
.form-control {
|
|
||||||
border: 2px solid #000;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.sticker-item:hover {
|
||||||
box-shadow: 4px 4px 0 var(--color-primary);
|
background: var(--nano-yellow-soft);
|
||||||
border-color: #000;
|
border-color: var(--nano-yellow);
|
||||||
background: #fff;
|
transform: scale(1.1) rotate(5deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
.animate-up {
|
@keyframes banana-float {
|
||||||
opacity: 0;
|
0% { transform: translateY(0) rotate(0deg); }
|
||||||
transform: translateY(30px);
|
50% { transform: translateY(-10px) rotate(10deg); }
|
||||||
animation: fadeUp 0.8s ease forwards;
|
100% { transform: translateY(0) rotate(0deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.delay-100 { animation-delay: 0.1s; }
|
.fas.fa-banana {
|
||||||
.delay-200 { animation-delay: 0.2s; }
|
animation: banana-float 2s ease-in-out infinite;
|
||||||
|
|
||||||
@keyframes fadeUp {
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Social */
|
|
||||||
.social-links a {
|
|
||||||
transition: transform 0.2s;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.social-links a:hover {
|
|
||||||
transform: scale(1.2) rotate(10deg);
|
|
||||||
color: var(--color-accent) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
/* Scrollbar */
|
||||||
@media (max-width: 991px) {
|
::-webkit-scrollbar {
|
||||||
.rotate-divider {
|
width: 8px;
|
||||||
transform: rotate(0);
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-section {
|
|
||||||
padding-top: 120px;
|
|
||||||
text-align: center;
|
|
||||||
min-height: auto;
|
|
||||||
padding-bottom: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-1 { font-size: 3.5rem; }
|
|
||||||
|
|
||||||
.blob-1 { width: 300px; height: 300px; right: -20%; }
|
|
||||||
.blob-2 { width: 300px; height: 300px; left: -20%; }
|
|
||||||
}
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--nano-black);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
BIN
assets/images/pexels/2416483.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
assets/images/pexels/gen_2766c2b5e438b7584be2a2b709ce59fc.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
assets/images/pexels/gen_2db11fa6a0c517d176327af35602fc3f.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
assets/images/pexels/gen_2e4b0855eec62a17f61bb46cdce9ffb1.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
assets/images/pexels/gen_68d5114c20a78dadeeed57878c123376.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
assets/images/pexels/gen_c61ca490ae21d80fd26565e96bff8734.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
assets/images/pexels/gen_db78761d52f98962dc88bfa8c6c3e519.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
assets/images/pexels/gen_e718695e9cf74bedd73931b397f91dfa.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
@ -1,73 +1,475 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
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');
|
||||||
|
|
||||||
// Smooth scrolling for navigation links
|
// Controls
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
const filterRanges = document.querySelectorAll('.filter-range');
|
||||||
anchor.addEventListener('click', function (e) {
|
const brushToggle = document.getElementById('brush-toggle');
|
||||||
e.preventDefault();
|
const brushColor = document.getElementById('brush-color');
|
||||||
const targetId = this.getAttribute('href');
|
const brushSize = document.getElementById('brush-size');
|
||||||
if (targetId === '#') return;
|
const textInput = document.getElementById('text-input');
|
||||||
|
const addTextBtn = document.getElementById('add-text-btn');
|
||||||
const targetElement = document.querySelector(targetId);
|
const textColor = document.getElementById('text-color');
|
||||||
if (targetElement) {
|
const fontFamily = document.getElementById('font-family');
|
||||||
// Close mobile menu if open
|
const stickerItems = document.querySelectorAll('.sticker-item');
|
||||||
const navbarToggler = document.querySelector('.navbar-toggler');
|
|
||||||
const navbarCollapse = document.querySelector('.navbar-collapse');
|
const startCropBtn = document.getElementById('start-crop-btn');
|
||||||
if (navbarCollapse.classList.contains('show')) {
|
const applyCropBtn = document.getElementById('apply-crop-btn');
|
||||||
navbarToggler.click();
|
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');
|
||||||
|
|
||||||
// Scroll with offset
|
// --- Editor State ---
|
||||||
const offset = 80;
|
let canvas = null;
|
||||||
const elementPosition = targetElement.getBoundingClientRect().top;
|
let cropper = null;
|
||||||
const offsetPosition = elementPosition + window.pageYOffset - offset;
|
let fabricImage = null; // background image
|
||||||
|
let currentResultUrl = '';
|
||||||
|
let originalPrompt = '';
|
||||||
|
let isDrawing = false;
|
||||||
|
|
||||||
window.scrollTo({
|
// --- Generation Logic ---
|
||||||
top: offsetPosition,
|
form.addEventListener('submit', async (e) => {
|
||||||
behavior: "smooth"
|
e.preventDefault();
|
||||||
});
|
const formData = new FormData(form);
|
||||||
}
|
originalPrompt = formData.get('prompt');
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navbar scroll effect
|
showLoading();
|
||||||
const navbar = document.querySelector('.navbar');
|
|
||||||
window.addEventListener('scroll', () => {
|
try {
|
||||||
if (window.scrollY > 50) {
|
const response = await fetch('api/generate.php', {
|
||||||
navbar.classList.add('scrolled', 'shadow-sm', 'bg-white');
|
method: 'POST',
|
||||||
navbar.classList.remove('bg-transparent');
|
body: formData
|
||||||
} else {
|
});
|
||||||
navbar.classList.remove('scrolled', 'shadow-sm', 'bg-white');
|
const result = await response.json();
|
||||||
navbar.classList.add('bg-transparent');
|
if (result.success) renderResult(result);
|
||||||
|
else alert('Ошибка: ' + (result.error || 'Сбой генерации'));
|
||||||
|
} catch (error) {
|
||||||
|
alert('Сетевая ошибка');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Intersection Observer for fade-up animations
|
function showLoading() {
|
||||||
const observerOptions = {
|
placeholderText.classList.add('d-none');
|
||||||
threshold: 0.1,
|
contentContainer.classList.add('d-none');
|
||||||
rootMargin: "0px 0px -50px 0px"
|
actionButtons.classList.add('d-none');
|
||||||
};
|
infoOverlay.classList.add('d-none');
|
||||||
|
statusMessage.classList.add('d-none');
|
||||||
|
loadingState.classList.remove('d-none');
|
||||||
|
generateBtn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
function hideLoading() {
|
||||||
entries.forEach(entry => {
|
loadingState.classList.add('d-none');
|
||||||
if (entry.isIntersecting) {
|
generateBtn.disabled = false;
|
||||||
entry.target.classList.add('animate-up');
|
}
|
||||||
entry.target.style.opacity = "1";
|
|
||||||
observer.unobserve(entry.target); // Only animate once
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, observerOptions);
|
|
||||||
|
|
||||||
// Select elements to animate (add a class 'reveal' to them in HTML if not already handled by CSS animation)
|
function renderResult(result) {
|
||||||
// For now, let's just make sure the hero animations run.
|
contentContainer.innerHTML = '';
|
||||||
// If we want scroll animations, we'd add opacity: 0 to elements in CSS and reveal them here.
|
contentContainer.classList.remove('d-none');
|
||||||
// Given the request, the CSS animation I added runs on load for Hero.
|
actionButtons.classList.remove('d-none');
|
||||||
// Let's make the project cards animate in.
|
infoOverlay.classList.remove('d-none');
|
||||||
|
currentResultUrl = result.url;
|
||||||
const projectCards = document.querySelectorAll('.project-card');
|
providerBadge.textContent = result.provider;
|
||||||
projectCards.forEach((card, index) => {
|
providerBadge.className = result.is_ai ? 'badge-nano bg-info' : 'badge-nano bg-warning';
|
||||||
card.style.opacity = "0";
|
|
||||||
card.style.animationDelay = `${index * 0.1}s`;
|
if (result.type === 'photo') {
|
||||||
observer.observe(card);
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
7
db/migrations/001_init_history.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS media_history (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
type VARCHAR(10) NOT NULL,
|
||||||
|
prompt TEXT NOT NULL,
|
||||||
|
result_url TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
11
healthz.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
db()->query('SELECT 1');
|
||||||
|
echo json_encode(['status' => 'ok', 'database' => 'connected', 'php_version' => PHP_VERSION, 'time' => date('c')]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
27
includes/pexels.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
function pexels_key() {
|
||||||
|
$k = getenv('PEXELS_KEY');
|
||||||
|
return $k && strlen($k) > 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pexels_get($url) {
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_URL => $url,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ],
|
||||||
|
CURLOPT_TIMEOUT => 15,
|
||||||
|
]);
|
||||||
|
$resp = curl_exec($ch);
|
||||||
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function download_to($srcUrl, $destPath) {
|
||||||
|
$data = file_get_contents($srcUrl);
|
||||||
|
if ($data === false) return false;
|
||||||
|
if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0775, true);
|
||||||
|
return file_put_contents($destPath, $data) !== false;
|
||||||
|
}
|
||||||
597
index.php
@ -1,150 +1,461 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
require_once 'db/config.php';
|
||||||
@ini_set('display_errors', '1');
|
$project_name = $_SERVER['PROJECT_NAME'] ?? 'Nano Media AI';
|
||||||
@error_reporting(E_ALL);
|
$project_description = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Generate unique AI photos and high-quality stock videos.';
|
||||||
@date_default_timezone_set('UTC');
|
|
||||||
|
|
||||||
$phpVersion = PHP_VERSION;
|
|
||||||
$now = date('Y-m-d H:i:s');
|
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>New Style</title>
|
<title><?php echo htmlspecialchars($project_name); ?></title>
|
||||||
<?php
|
<meta name="description" content="<?php echo htmlspecialchars($project_description); ?>">
|
||||||
// Read project preview data from environment
|
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
<!-- Bootstrap 5 CSS -->
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
?>
|
<!-- Google Fonts: Inter & Montserrat -->
|
||||||
<?php if ($projectDescription): ?>
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@800&display=swap" rel="stylesheet">
|
||||||
<!-- Meta description -->
|
<!-- Font Awesome -->
|
||||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<!-- Open Graph meta tags -->
|
|
||||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
<!-- Editor Libraries -->
|
||||||
<!-- Twitter meta tags -->
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
|
||||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
|
||||||
<?php endif; ?>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
|
||||||
<?php if ($projectImageUrl): ?>
|
|
||||||
<!-- Open Graph image -->
|
<!-- Custom CSS -->
|
||||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
<!-- Twitter image -->
|
<style>
|
||||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
:root {
|
||||||
<?php endif; ?>
|
--nano-yellow: #FFDE59;
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
--nano-black: #1A1A1A;
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
--nano-white: #FFFFFF;
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
--nano-gray: #F5F5F5;
|
||||||
<style>
|
--nano-radius: 24px;
|
||||||
:root {
|
}
|
||||||
--bg-color-start: #6a11cb;
|
body {
|
||||||
--bg-color-end: #2575fc;
|
background-color: var(--nano-gray);
|
||||||
--text-color: #ffffff;
|
font-family: 'Inter', sans-serif;
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
}
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
.navbar {
|
||||||
}
|
background-color: var(--nano-white) !important;
|
||||||
body {
|
border-bottom: 2px solid var(--nano-black);
|
||||||
margin: 0;
|
padding: 15px 0;
|
||||||
font-family: 'Inter', sans-serif;
|
}
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
.navbar-brand {
|
||||||
color: var(--text-color);
|
font-family: 'Montserrat', sans-serif;
|
||||||
display: flex;
|
color: var(--nano-black) !important;
|
||||||
justify-content: center;
|
font-size: 1.5rem;
|
||||||
align-items: center;
|
text-transform: uppercase;
|
||||||
min-height: 100vh;
|
}
|
||||||
text-align: center;
|
.btn-nano {
|
||||||
overflow: hidden;
|
background-color: var(--nano-yellow);
|
||||||
position: relative;
|
color: var(--nano-black);
|
||||||
}
|
border: 2px solid var(--nano-black);
|
||||||
body::before {
|
border-radius: var(--nano-radius);
|
||||||
content: '';
|
font-weight: 700;
|
||||||
position: absolute;
|
padding: 10px 25px;
|
||||||
top: 0;
|
transition: all 0.2s ease;
|
||||||
left: 0;
|
}
|
||||||
width: 100%;
|
.btn-nano:hover {
|
||||||
height: 100%;
|
background-color: var(--nano-black);
|
||||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
color: var(--nano-yellow);
|
||||||
animation: bg-pan 20s linear infinite;
|
transform: translateY(-2px);
|
||||||
z-index: -1;
|
}
|
||||||
}
|
.card-nano {
|
||||||
@keyframes bg-pan {
|
background: var(--nano-white);
|
||||||
0% { background-position: 0% 0%; }
|
border: 2px solid var(--nano-black);
|
||||||
100% { background-position: 100% 100%; }
|
border-radius: var(--nano-radius);
|
||||||
}
|
box-shadow: 8px 8px 0px var(--nano-black);
|
||||||
main {
|
overflow: hidden;
|
||||||
padding: 2rem;
|
}
|
||||||
}
|
.form-control-nano {
|
||||||
.card {
|
border: 2px solid var(--nano-black);
|
||||||
background: var(--card-bg-color);
|
border-radius: 15px;
|
||||||
border: 1px solid var(--card-border-color);
|
padding: 12px;
|
||||||
border-radius: 16px;
|
font-weight: 500;
|
||||||
padding: 2rem;
|
}
|
||||||
backdrop-filter: blur(20px);
|
.form-control-nano:focus {
|
||||||
-webkit-backdrop-filter: blur(20px);
|
box-shadow: none;
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
border-color: var(--nano-yellow);
|
||||||
}
|
background-color: #fffde7;
|
||||||
.loader {
|
}
|
||||||
margin: 1.25rem auto 1.25rem;
|
.badge-nano {
|
||||||
width: 48px;
|
background: var(--nano-yellow);
|
||||||
height: 48px;
|
color: var(--nano-black);
|
||||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
border: 1px solid var(--nano-black);
|
||||||
border-top-color: #fff;
|
border-radius: 10px;
|
||||||
border-radius: 50%;
|
padding: 5px 12px;
|
||||||
animation: spin 1s linear infinite;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@keyframes spin {
|
.editor-preview-container {
|
||||||
from { transform: rotate(0deg); }
|
background: #eee;
|
||||||
to { transform: rotate(360deg); }
|
border: 2px solid var(--nano-black);
|
||||||
}
|
border-radius: var(--nano-radius);
|
||||||
.hint {
|
overflow: hidden;
|
||||||
opacity: 0.9;
|
position: relative;
|
||||||
}
|
}
|
||||||
.sr-only {
|
.canvas-container {
|
||||||
position: absolute;
|
margin: 0 auto;
|
||||||
width: 1px; height: 1px;
|
}
|
||||||
padding: 0; margin: -1px;
|
.sticker-item {
|
||||||
overflow: hidden;
|
cursor: pointer;
|
||||||
clip: rect(0, 0, 0, 0);
|
transition: transform 0.2s;
|
||||||
white-space: nowrap; border: 0;
|
border: 2px solid transparent;
|
||||||
}
|
border-radius: 10px;
|
||||||
h1 {
|
padding: 5px;
|
||||||
font-size: 3rem;
|
font-size: 2rem;
|
||||||
font-weight: 700;
|
display: flex;
|
||||||
margin: 0 0 1rem;
|
align-items: center;
|
||||||
letter-spacing: -1px;
|
justify-content: center;
|
||||||
}
|
width: 50px;
|
||||||
p {
|
height: 50px;
|
||||||
margin: 0.5rem 0;
|
}
|
||||||
font-size: 1.1rem;
|
.sticker-item:hover {
|
||||||
}
|
transform: scale(1.1);
|
||||||
code {
|
border-color: var(--nano-yellow);
|
||||||
background: rgba(0,0,0,0.2);
|
}
|
||||||
padding: 2px 6px;
|
.tool-btn {
|
||||||
border-radius: 4px;
|
border: 2px solid var(--nano-black);
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
border-radius: 12px;
|
||||||
}
|
padding: 8px;
|
||||||
footer {
|
background: white;
|
||||||
position: absolute;
|
transition: all 0.2s;
|
||||||
bottom: 1rem;
|
}
|
||||||
font-size: 0.8rem;
|
.tool-btn:hover, .tool-btn.active {
|
||||||
opacity: 0.7;
|
background: var(--nano-yellow);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
|
||||||
<div class="card">
|
<nav class="navbar navbar-expand-lg sticky-top">
|
||||||
<h1>Analyzing your requirements and generating your website…</h1>
|
<div class="container">
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<a class="navbar-brand fw-bold" href="/">
|
||||||
<span class="sr-only">Loading…</span>
|
<i class="fas fa-banana text-warning me-2"></i><?php echo htmlspecialchars($project_name); ?>
|
||||||
</div>
|
</a>
|
||||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
<div class="ms-auto d-flex align-items-center">
|
||||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
<a href="#history" class="btn btn-nano btn-sm">История</a>
|
||||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</nav>
|
||||||
<footer>
|
|
||||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
<main class="container py-5">
|
||||||
</footer>
|
<!-- Hero -->
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<h1 class="display-4 fw-black mb-3" style="font-family: 'Montserrat', sans-serif;">NANO GENERATOR 🍌</h1>
|
||||||
|
<p class="lead text-dark fw-medium">Создавай крутые ИИ фото и качественные сток-видео.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generator Section -->
|
||||||
|
<section class="row justify-content-center mb-5">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="card-nano p-4">
|
||||||
|
<form id="generation-form">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small fw-bold">ТИП КОНТЕНТА</label>
|
||||||
|
<select class="form-select form-control-nano" id="media-type" name="type">
|
||||||
|
<option value="photo">ФОТО (ИИ)</option>
|
||||||
|
<option value="video">ВИДЕО (STOCK)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small fw-bold">СТИЛЬ</label>
|
||||||
|
<select class="form-select form-control-nano" id="style" name="style">
|
||||||
|
<option value="">ОРИГИНАЛ</option>
|
||||||
|
<option value="anime">АНИМЕ</option>
|
||||||
|
<option value="cyberpunk">КИБЕРПАНК</option>
|
||||||
|
<option value="3d-render">3D SOFT</option>
|
||||||
|
<option value="minimalism">МИНИМАЛИЗМ</option>
|
||||||
|
<option value="cinematic">КИНО</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">ТВОЙ ЗАПРОС</label>
|
||||||
|
<input type="text" class="form-control form-control-nano" id="prompt" name="prompt" placeholder="Что нарисуем или найдем?.." required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 d-flex align-items-end">
|
||||||
|
<button type="submit" class="btn btn-nano w-100" id="generate-btn">
|
||||||
|
СОЗДАТЬ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 d-flex align-items-end">
|
||||||
|
<input type="file" id="upload-image" accept="image/*" class="d-none">
|
||||||
|
<button type="button" class="btn btn-outline-dark w-100 rounded-pill py-2" onclick="document.getElementById('upload-image').click()">
|
||||||
|
<i class="fas fa-upload me-1"></i> СВОЁ ФОТО
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="result-preview" class="mt-4 position-relative" style="min-height: 400px; background: #fafafa; border: 2px dashed #ccc; border-radius: 20px; display: flex; align-items: center; justify-content: center;">
|
||||||
|
<div class="text-center text-muted" id="placeholder-text">
|
||||||
|
<i class="fas fa-image fa-3x mb-3"></i>
|
||||||
|
<p class="fw-bold">ТУТ БУДЕТ МАГИЯ</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="loading-spinner text-center d-none" id="loading-state">
|
||||||
|
<div class="spinner-border text-dark" role="status"></div>
|
||||||
|
<p class="mt-3 fw-bold">ГОТОВИМ БАНАНЫ...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content-container" class="d-none w-100 h-100 p-2 text-center"></div>
|
||||||
|
|
||||||
|
<div id="info-overlay" class="position-absolute top-0 start-0 p-3 d-none">
|
||||||
|
<span class="badge-nano" id="provider-badge"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="action-buttons" class="position-absolute bottom-0 end-0 p-3 d-none d-flex gap-2">
|
||||||
|
<button class="btn btn-nano btn-sm" id="edit-btn">
|
||||||
|
<i class="fas fa-wand-magic-sparkles"></i> EDIT
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-dark btn-sm rounded-pill" id="download-btn">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status-message" class="mt-3 alert alert-warning border-2 border-dark d-none" role="alert"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- History -->
|
||||||
|
<section id="history" class="py-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="fw-black mb-0" style="font-family: 'Montserrat', sans-serif;">ГАЛЕРЕЯ 📂</h2>
|
||||||
|
<button class="btn btn-outline-dark btn-sm rounded-pill" onclick="location.reload()"><i class="fas fa-sync"></i> Обновить</button>
|
||||||
|
</div>
|
||||||
|
<div class="row g-4" id="history-grid">
|
||||||
|
<?php
|
||||||
|
try {
|
||||||
|
$stmt = db()->query("SELECT * FROM media_history ORDER BY created_at DESC LIMIT 12");
|
||||||
|
$history = $stmt->fetchAll();
|
||||||
|
foreach ($history as $item):
|
||||||
|
?>
|
||||||
|
<div class="col-md-4 col-sm-6">
|
||||||
|
<div class="card-nano h-100 history-card">
|
||||||
|
<?php if ($item['type'] === 'photo'): ?>
|
||||||
|
<img src="<?php echo htmlspecialchars($item['result_url']); ?>" class="w-100" style="height: 250px; object-fit: cover; border-bottom: 2px solid #000;">
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="bg-dark" style="height: 250px; border-bottom: 2px solid #000; position: relative;">
|
||||||
|
<video class="w-100 h-100" style="object-fit: cover;" muted onmouseover="this.play()" onmouseout="this.pause()">
|
||||||
|
<source src="<?php echo htmlspecialchars($item['result_url']); ?>" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
<div class="position-absolute top-50 start-50 translate-middle pointer-events-none">
|
||||||
|
<i class="fas fa-play text-white fa-2x opacity-50"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="p-3">
|
||||||
|
<p class="small fw-bold text-truncate mb-2 history-prompt"><?php echo htmlspecialchars($item['prompt']); ?></p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span class="badge-nano py-1 px-2" style="font-size: 0.7rem;"><?php echo strtoupper($item['type']); ?></span>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<?php if ($item['type'] === 'photo'): ?>
|
||||||
|
<button class="btn btn-sm btn-outline-dark rounded-pill history-edit-btn" data-url="<?php echo htmlspecialchars($item['result_url']); ?>">
|
||||||
|
<i class="fas fa-magic"></i>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a href="<?php echo htmlspecialchars($item['result_url']); ?>" download class="btn btn-sm btn-dark rounded-pill">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; } catch (Exception $e) {} ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Nano Editor Modal -->
|
||||||
|
<div class="modal fade" id="editorModal" data-bs-backdrop="static" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||||
|
<div class="modal-content card-nano border-0">
|
||||||
|
<div class="modal-header border-bottom border-2 border-dark bg-yellow-soft">
|
||||||
|
<h5 class="modal-title fw-black"><i class="fas fa-banana text-warning"></i> NANO EDITOR AI PRO</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-0">
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-lg-8 bg-light p-3 border-end border-2 border-dark d-flex flex-column">
|
||||||
|
<div class="editor-preview-container d-flex align-items-center justify-content-center flex-grow-1" style="min-height: 500px;">
|
||||||
|
<div id="fabric-wrapper">
|
||||||
|
<canvas id="editor-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
<img id="cropper-image" src="" style="display: none; max-width: 100%;">
|
||||||
|
|
||||||
|
<div id="editor-loading" class="position-absolute top-0 start-0 w-100 h-100 d-none flex-column align-items-center justify-content-center" style="background: rgba(255,255,255,0.8); z-index: 10;">
|
||||||
|
<div class="spinner-border text-dark"></div>
|
||||||
|
<span class="fw-bold mt-2">МАГИЯ В ПРОЦЕССЕ...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transform Controls (Quick Access) -->
|
||||||
|
<div id="transform-bar" class="mt-3 p-2 bg-white rounded-3 border border-2 border-dark d-flex justify-content-center gap-3 d-none">
|
||||||
|
<button class="btn btn-sm tool-btn" onclick="rotateLeft()"><i class="fas fa-undo"></i> -90°</button>
|
||||||
|
<button class="btn btn-sm tool-btn" onclick="rotateRight()"><i class="fas fa-redo"></i> +90°</button>
|
||||||
|
<button class="btn btn-sm tool-btn" onclick="flipH()"><i class="fas fa-arrows-alt-h"></i> Flip H</button>
|
||||||
|
<button class="btn btn-sm tool-btn" onclick="flipV()"><i class="fas fa-arrows-alt-v"></i> Flip V</button>
|
||||||
|
<button class="btn btn-sm btn-dark rounded-pill px-3" id="apply-crop-btn">APPLY CROP</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 p-4 overflow-auto" style="max-height: 80vh;">
|
||||||
|
<ul class="nav nav-tabs border-0 mb-3 flex-nowrap overflow-auto" id="editor-tabs">
|
||||||
|
<li class="nav-item"><button class="nav-link active fw-bold text-dark border-0 small" data-bs-toggle="tab" data-bs-target="#filters-panel">ФИЛЬТРЫ</button></li>
|
||||||
|
<li class="nav-item"><button class="nav-link fw-bold text-dark border-0 small" data-bs-toggle="tab" data-bs-target="#transform-panel">ТРАНСФОРМ</button></li>
|
||||||
|
<li class="nav-item"><button class="nav-link fw-bold text-dark border-0 small" data-bs-toggle="tab" data-bs-target="#decor-panel">ДЕКОР</button></li>
|
||||||
|
<li class="nav-item"><button class="nav-link fw-bold text-dark border-0 small" data-bs-toggle="tab" data-bs-target="#ai-magic-panel">AI MAGIC</button></li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content pt-2">
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="tab-pane fade show active" id="filters-panel">
|
||||||
|
<?php
|
||||||
|
$filters = [
|
||||||
|
'brightness' => ['label' => 'Яркость', 'min' => 0, 'max' => 200, 'val' => 100],
|
||||||
|
'contrast' => ['label' => 'Контраст', 'min' => 0, 'max' => 200, 'val' => 100],
|
||||||
|
'saturate' => ['label' => 'Насыщенность', 'min' => 0, 'max' => 200, 'val' => 100],
|
||||||
|
'blur' => ['label' => 'Размытие', 'min' => 0, 'max' => 20, 'val' => 0],
|
||||||
|
'hue-rotate' => ['label' => 'Оттенок', 'min' => 0, 'max' => 360, 'val' => 0],
|
||||||
|
'sepia' => ['label' => 'Сепия', 'min' => 0, 'max' => 100, 'val' => 0],
|
||||||
|
'grayscale' => ['label' => 'Ч/Б', 'min' => 0, 'max' => 100, 'val' => 0],
|
||||||
|
'vignette' => ['label' => 'Виньетка', 'min' => 0, 'max' => 100, 'val' => 0],
|
||||||
|
'noise' => ['label' => 'Шум', 'min' => 0, 'max' => 100, 'val' => 0]
|
||||||
|
];
|
||||||
|
foreach ($filters as $id => $f): ?>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<label class="form-label small fw-bold"><?php echo $f['label']; ?></label>
|
||||||
|
<span class="small fw-bold text-muted" id="val-<?php echo $id; ?>"><?php echo $f['val']; ?></span>
|
||||||
|
</div>
|
||||||
|
<input type="range" class="form-range filter-range" data-filter="<?php echo $id; ?>" min="<?php echo $f['min']; ?>" max="<?php echo $f['max']; ?>" value="<?php echo $f['val']; ?>">
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transform -->
|
||||||
|
<div class="tab-pane fade" id="transform-panel">
|
||||||
|
<div class="d-grid gap-3">
|
||||||
|
<button class="btn btn-nano w-100" id="start-crop-btn">
|
||||||
|
<i class="fas fa-crop-alt"></i> ИНСТРУМЕНТ ОБРЕЗКИ
|
||||||
|
</button>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-6">
|
||||||
|
<button class="btn btn-outline-dark w-100 py-3" onclick="rotateLeft()">
|
||||||
|
<i class="fas fa-undo d-block mb-1"></i> -90°
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<button class="btn btn-outline-dark w-100 py-3" onclick="rotateRight()">
|
||||||
|
<i class="fas fa-redo d-block mb-1"></i> +90°
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<button class="btn btn-outline-dark w-100 py-3" onclick="flipH()">
|
||||||
|
<i class="fas fa-arrows-alt-h d-block mb-1"></i> Flip H
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<button class="btn btn-outline-dark w-100 py-3" onclick="flipV()">
|
||||||
|
<i class="fas fa-arrows-alt-v d-block mb-1"></i> Flip V
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decor -->
|
||||||
|
<div class="tab-pane fade" id="decor-panel">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label small fw-bold">РИСОВАНИЕ КИСТЬЮ</label>
|
||||||
|
<div class="d-flex gap-2 mb-2">
|
||||||
|
<button class="btn tool-btn flex-grow-1" id="brush-toggle"><i class="fas fa-paint-brush"></i> Кисть</button>
|
||||||
|
<input type="color" class="form-control form-control-color border-2 border-dark" id="brush-color" value="#FFDE59" title="Цвет кисти">
|
||||||
|
</div>
|
||||||
|
<input type="range" class="form-range" id="brush-size" min="1" max="100" value="10">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label small fw-bold">ТЕКСТ</label>
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<input type="text" id="text-input" class="form-control form-control-nano" placeholder="Ваш текст...">
|
||||||
|
<button class="btn btn-dark" id="add-text-btn"><i class="fas fa-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<select id="font-family" class="form-select form-control-nano py-1">
|
||||||
|
<option value="Montserrat">Montserrat</option>
|
||||||
|
<option value="Inter">Inter</option>
|
||||||
|
<option value="Arial">Arial</option>
|
||||||
|
<option value="Courier New">Monospace</option>
|
||||||
|
</select>
|
||||||
|
<input type="color" class="form-control form-control-color border-2 border-dark" id="text-color" value="#000000">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label small fw-bold">СЛОИ (OBJECTS)</label>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button class="btn btn-outline-dark btn-sm flex-grow-1" onclick="bringToFront()"><i class="fas fa-layer-group"></i> Вперёд</button>
|
||||||
|
<button class="btn btn-outline-dark btn-sm flex-grow-1" onclick="sendToBack()"><i class="fas fa-level-down-alt"></i> Назад</button>
|
||||||
|
<button class="btn btn-outline-danger btn-sm" onclick="deleteObject()"><i class="fas fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="form-label small fw-bold">СТИКЕРЫ 🍌✨</label>
|
||||||
|
<div class="d-flex flex-wrap gap-2 p-2 bg-white rounded-3 border border-2 border-dark">
|
||||||
|
<div class="sticker-item" data-sticker="🍌">🍌</div>
|
||||||
|
<div class="sticker-item" data-sticker="🐒">🐒</div>
|
||||||
|
<div class="sticker-item" data-sticker="🌴">🌴</div>
|
||||||
|
<div class="sticker-item" data-sticker="🕶️">🕶️</div>
|
||||||
|
<div class="sticker-item" data-sticker="🔥">🔥</div>
|
||||||
|
<div class="sticker-item" data-sticker="❤️">❤️</div>
|
||||||
|
<div class="sticker-item" data-sticker="✨">✨</div>
|
||||||
|
<div class="sticker-item" data-sticker="🚀">🚀</div>
|
||||||
|
<div class="sticker-item" data-sticker="🎨">🎨</div>
|
||||||
|
<div class="sticker-item" data-sticker="⭐">⭐</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI Magic -->
|
||||||
|
<div class="tab-pane fade" id="ai-magic-panel">
|
||||||
|
<div class="p-3 bg-yellow-soft rounded-4 border border-2 border-dark mb-3">
|
||||||
|
<label class="form-label small fw-bold">ЧТО ДОРИСОВАТЬ?</label>
|
||||||
|
<textarea class="form-control form-control-nano mb-2" id="ai-edit-prompt" rows="3" placeholder="Напр: Добавь солнечные очки..."></textarea>
|
||||||
|
<button class="btn btn-nano w-100" id="apply-ai-magic">ПРИМЕНИТЬ МАГИЮ</button>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button class="btn btn-outline-dark btn-sm rounded-pill" id="remove-bg-btn"><i class="fas fa-user-slash me-1"></i> Удалить фон</button>
|
||||||
|
<button class="btn btn-outline-dark btn-sm rounded-pill" id="upscale-btn"><i class="fas fa-expand-arrows-alt me-1"></i> Улучшить (HD)</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="border-2 border-dark">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button class="btn btn-outline-danger btn-sm rounded-pill" id="reset-editor"><i class="fas fa-undo"></i> СБРОСИТЬ ВСЁ</button>
|
||||||
|
<button class="btn btn-nano w-100 py-3 mt-2" id="save-edited-btn">
|
||||||
|
<i class="fas fa-download me-2"></i> СКАЧАТЬ PNG
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-dark w-100 py-2 rounded-pill" id="save-to-gallery-btn">
|
||||||
|
<i class="fas fa-cloud-upload-alt me-2"></i> СОХРАНИТЬ В ГАЛЕРЕЮ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="py-5 text-center">
|
||||||
|
<p class="small fw-bold">© <?php echo date('Y'); ?> <?php echo htmlspecialchars($project_name); ?> 🍌</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||