402 lines
12 KiB
PHP
402 lines
12 KiB
PHP
<!DOCTYPE html>
|
||
<html lang="pl">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Edytor Zdjęć Produktowych</title>
|
||
|
||
<style>
|
||
/* --- GŁÓWNE STYLE --- */
|
||
body { font-family: sans-serif; padding: 20px; color: #333; }
|
||
h2 { margin-bottom: 20px; }
|
||
|
||
/* --- WRAPPER CROPPERA --- */
|
||
.cropper-wrapper {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 500px;
|
||
background: #222;
|
||
margin-bottom: 15px;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
display: none; /* Ukryty domyślnie */
|
||
user-select: none;
|
||
touch-action: none;
|
||
}
|
||
|
||
cropper-canvas { width: 100%; height: 100%; }
|
||
|
||
/* Stała ramka wyboru */
|
||
cropper-selection {
|
||
outline: 2px solid #e74c3c !important; /* Czerwona ramka */
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* --- OVERLAY I UCHWYTY (HANDLES) --- */
|
||
.photo-handles {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none; /* Kluczowe: przepuszcza kliki do obrazka */
|
||
z-index: 20;
|
||
}
|
||
|
||
.photo-outline {
|
||
position: absolute;
|
||
inset: 6px;
|
||
border: 2px dashed rgba(255, 255, 255, 0.6);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.handle {
|
||
position: absolute;
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background: #fff;
|
||
border: 2px solid #007bff;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.4);
|
||
pointer-events: auto; /* Tylko kropki są klikalne */
|
||
transition: transform 0.1s;
|
||
}
|
||
.handle:active { transform: scale(1.2); }
|
||
|
||
/* Pozycje uchwytów */
|
||
.handle.tl { left: 10px; top: 10px; cursor: nwse-resize; }
|
||
.handle.tr { right: 10px; top: 10px; cursor: nesw-resize; }
|
||
.handle.bl { left: 10px; bottom: 10px; cursor: nesw-resize; }
|
||
.handle.br { right: 10px; bottom: 10px; cursor: nwse-resize; }
|
||
|
||
/* --- PASEK NARZĘDZI --- */
|
||
#toolbar {
|
||
display: none;
|
||
gap: 10px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 20px;
|
||
}
|
||
#toolbar.show { display: flex; }
|
||
|
||
.btn {
|
||
padding: 8px 16px;
|
||
background-color: #007bff;
|
||
color: white;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 15px;
|
||
border-radius: 6px;
|
||
transition: background 0.2s;
|
||
}
|
||
.btn:hover { background-color: #0056b3; }
|
||
.btn:disabled { background-color: #ccc; cursor: not-allowed; }
|
||
|
||
.chip {
|
||
padding: 6px 12px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 20px;
|
||
font-size: 13px;
|
||
background: #f8f9fa;
|
||
font-weight: 600;
|
||
}
|
||
|
||
#hint { font-size: 13px; color: #666; margin-left: auto; }
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
|
||
<h2>Przesyłanie zdjęcia produktu</h2>
|
||
|
||
<div style="margin-bottom: 20px;">
|
||
<label class="btn" for="inputImage">📂 Wybierz zdjęcie</label>
|
||
<input type="file" id="inputImage" accept="image/*" style="display:none" />
|
||
<span id="fileName" style="margin-left: 10px; color:#555;"></span>
|
||
</div>
|
||
|
||
<div class="cropper-wrapper" id="cropperWrapper">
|
||
<cropper-canvas id="my-cropper" background>
|
||
<cropper-image
|
||
id="cropperImage"
|
||
initial-center-size="cover"
|
||
translatable
|
||
scalable
|
||
></cropper-image>
|
||
|
||
<cropper-handle action="move" plain></cropper-handle>
|
||
|
||
<cropper-selection id="cropperSelection" initial-coverage="0.6"></cropper-selection>
|
||
</cropper-canvas>
|
||
|
||
<div class="photo-handles" id="photoHandles">
|
||
<div class="photo-outline"></div>
|
||
<div class="handle tl" data-pos="top" title="Zmień rozmiar"></div>
|
||
<div class="handle tr" data-pos="top" title="Zmień rozmiar"></div>
|
||
<div class="handle bl" data-pos="bottom" title="Zmień rozmiar"></div>
|
||
<div class="handle br" data-pos="bottom" title="Zmień rozmiar"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="toolbar">
|
||
<button type="button" class="btn" id="btnZoomIn">+</button>
|
||
<button type="button" class="btn" id="btnZoomOut">-</button>
|
||
<button type="button" class="btn" id="btnReset">Reset</button>
|
||
<span class="chip" id="formatInfo"></span>
|
||
<span id="hint">Mysz/Dotyk: Przesuń | Kółko/Rogi: Zoom</span>
|
||
</div>
|
||
|
||
<button id="btnUpload" class="btn" style="display:none; width: 100%; padding: 12px;">Przytnij i Zapisz</button>
|
||
<div id="result" style="margin-top: 20px;"></div>
|
||
|
||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||
<script src="https://unpkg.com/cropperjs@2.1.0/dist/cropper.js"></script>
|
||
|
||
<script>
|
||
/**
|
||
* KONFIGURACJA
|
||
* Wszystkie stałe w jednym miejscu dla łatwej edycji.
|
||
*/
|
||
const CONFIG = {
|
||
dims: {
|
||
adminW: 8, // cm
|
||
adminH: 12, // cm
|
||
outputW: 800, // px
|
||
outputH: 1200 // px
|
||
},
|
||
zoomSensitivity: 0.004,
|
||
selectors: {
|
||
input: '#inputImage',
|
||
fileName: '#fileName',
|
||
wrapper: '#cropperWrapper',
|
||
uploadBtn: '#btnUpload',
|
||
toolbar: '#toolbar',
|
||
result: '#result',
|
||
cropperCanvas: 'my-cropper',
|
||
cropperImage: 'cropperImage',
|
||
cropperSelection: 'cropperSelection',
|
||
handles: '.handle'
|
||
}
|
||
};
|
||
|
||
/**
|
||
* STATE
|
||
* Przechowywanie stanu aplikacji
|
||
*/
|
||
const State = {
|
||
isResizing: false,
|
||
startY: 0,
|
||
isInverted: false // Czy używamy dolnych uchwytów (odwrócona logika)
|
||
};
|
||
|
||
/**
|
||
* DOM MANAGER
|
||
* Cache'owanie elementów jQuery i JS
|
||
*/
|
||
const DOM = {
|
||
$input: $(CONFIG.selectors.input),
|
||
$name: $(CONFIG.selectors.fileName),
|
||
$wrapper: $(CONFIG.selectors.wrapper),
|
||
$uploadBtn: $(CONFIG.selectors.uploadBtn),
|
||
$toolbar: $(CONFIG.selectors.toolbar),
|
||
$result: $(CONFIG.selectors.result),
|
||
elCanvas: document.getElementById(CONFIG.selectors.cropperCanvas),
|
||
elImage: document.getElementById(CONFIG.selectors.cropperImage),
|
||
elSelection: document.getElementById(CONFIG.selectors.cropperSelection),
|
||
elsHandles: document.querySelectorAll(CONFIG.selectors.handles)
|
||
};
|
||
|
||
/**
|
||
* GŁÓWNY KONTROLER APLIKACJI
|
||
*/
|
||
const App = {
|
||
init() {
|
||
this.renderInfo();
|
||
this.bindGlobalEvents();
|
||
this.bindZoomHandlers();
|
||
},
|
||
|
||
// Wyświetlenie formatu na toolbarze
|
||
renderInfo() {
|
||
const { adminW, adminH } = CONFIG.dims;
|
||
$('#formatInfo').text(`Format: ${adminW}×${adminH} cm`);
|
||
},
|
||
|
||
// Obliczanie i centrowanie ramki wyboru
|
||
centerSelection() {
|
||
const { adminW, adminH } = CONFIG.dims;
|
||
const ratio = adminW / adminH;
|
||
|
||
DOM.elSelection.aspectRatio = ratio;
|
||
|
||
const rect = DOM.elCanvas.getBoundingClientRect();
|
||
if (!rect.width || !rect.height) return;
|
||
|
||
// Maksymalnie 70% obszaru
|
||
const maxW = rect.width * 0.7;
|
||
const maxH = rect.height * 0.7;
|
||
|
||
let selW, selH;
|
||
|
||
// Dopasowanie do węższego wymiaru
|
||
if (maxW / maxH > ratio) {
|
||
selH = maxH;
|
||
selW = selH * ratio;
|
||
} else {
|
||
selW = maxW;
|
||
selH = selW / ratio;
|
||
}
|
||
|
||
const x = (rect.width - selW) / 2;
|
||
const y = (rect.height - selH) / 2;
|
||
|
||
DOM.elSelection.$change(x, y, selW, selH, ratio);
|
||
},
|
||
|
||
// Obsługa zdarzeń globalnych (pliki, przyciski)
|
||
bindGlobalEvents() {
|
||
// 1. Wczytanie pliku
|
||
DOM.$input.on('change', function() {
|
||
const [file] = this.files;
|
||
if (!file) return;
|
||
|
||
if (!/^image\/\w+$/.test(file.type)) {
|
||
alert("To nie jest plik graficzny!");
|
||
return;
|
||
}
|
||
|
||
DOM.$name.text(file.name);
|
||
DOM.elImage.src = URL.createObjectURL(file);
|
||
|
||
// Pokaż UI
|
||
DOM.$wrapper.fadeIn();
|
||
DOM.$uploadBtn.fadeIn();
|
||
DOM.$toolbar.addClass('show');
|
||
|
||
// Inicjalizacja croppera
|
||
DOM.elImage.$ready(() => {
|
||
DOM.elImage.$center('cover');
|
||
App.centerSelection();
|
||
});
|
||
});
|
||
|
||
// 2. Toolbar - Przyciski
|
||
$('#btnZoomIn').on('click', () => DOM.elImage.$zoom(0.1));
|
||
$('#btnZoomOut').on('click', () => DOM.elImage.$zoom(-0.1));
|
||
$('#btnReset').on('click', () => DOM.elImage.$center('cover'));
|
||
|
||
// 3. Zoom kółkiem myszy
|
||
DOM.$wrapper.on('wheel', (e) => {
|
||
e.preventDefault();
|
||
const delta = e.originalEvent.deltaY;
|
||
DOM.elImage.$zoom(delta < 0 ? 0.08 : -0.08);
|
||
});
|
||
|
||
// 4. Resize okna przeglądarki
|
||
window.addEventListener('resize', () => {
|
||
if (DOM.$wrapper.is(':visible')) App.centerSelection();
|
||
});
|
||
|
||
// 5. Upload (Zapisz)
|
||
DOM.$uploadBtn.on('click', this.handleUpload);
|
||
},
|
||
|
||
// Logika "Ciągnij za rogi by zmienić zoom"
|
||
bindZoomHandlers() {
|
||
const onPointerDown = (e) => {
|
||
State.isResizing = true;
|
||
State.startY = e.clientY;
|
||
// Sprawdzamy czy to dolny uchwyt (bl, br) używając dataset lub klasy
|
||
State.isInverted = e.target.dataset.pos === 'bottom';
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
document.addEventListener('pointermove', onPointerMove, { passive: false });
|
||
document.addEventListener('pointerup', onPointerUp, { passive: false, once: true });
|
||
};
|
||
|
||
const onPointerMove = (e) => {
|
||
if (!State.isResizing) return;
|
||
|
||
let dy = e.clientY - State.startY;
|
||
State.startY = e.clientY;
|
||
|
||
// Jeśli dolny uchwyt -> odwracamy znak
|
||
if (State.isInverted) dy = -dy;
|
||
|
||
// Oblicz zoom
|
||
const step = (-dy) * CONFIG.zoomSensitivity;
|
||
if (step !== 0) DOM.elImage.$zoom(step);
|
||
|
||
e.preventDefault();
|
||
};
|
||
|
||
const onPointerUp = () => {
|
||
State.isResizing = false;
|
||
document.removeEventListener('pointermove', onPointerMove);
|
||
};
|
||
|
||
// Podpięcie pod wszystkie uchwyty
|
||
DOM.elsHandles.forEach(el => {
|
||
el.addEventListener('pointerdown', onPointerDown, { passive: false });
|
||
});
|
||
},
|
||
|
||
// Logika wysyłki na serwer
|
||
handleUpload() {
|
||
const btn = $(this);
|
||
btn.prop('disabled', true).text('Przetwarzanie...');
|
||
|
||
const { outputW, outputH, adminW, adminH } = CONFIG.dims;
|
||
|
||
// Generowanie canvasu
|
||
DOM.elSelection.$toCanvas({ width: outputW, height: outputH })
|
||
.then(canvas => {
|
||
canvas.toBlob(blob => {
|
||
const formData = new FormData();
|
||
|
||
// Destrukturyzacja do dodania danych
|
||
formData.append("original_image", DOM.$input[0].files[0]);
|
||
formData.append("cropped_image", blob, "cropped.jpg");
|
||
|
||
// Metadane
|
||
const meta = { product_id: 123, admin_w_cm: adminW, admin_h_cm: adminH, out_w_px: outputW, out_h_px: outputH };
|
||
Object.entries(meta).forEach(([key, val]) => formData.append(key, val));
|
||
|
||
// AJAX
|
||
$.ajax({
|
||
url: "product-img-save.php",
|
||
method: "POST",
|
||
data: formData,
|
||
processData: false,
|
||
contentType: false,
|
||
success: (resp) => {
|
||
DOM.$result.html(resp);
|
||
App.resetUI();
|
||
},
|
||
error: () => {
|
||
alert("Błąd zapisu!");
|
||
btn.prop('disabled', false).text('Przytnij i Zapisz');
|
||
}
|
||
});
|
||
}, "image/jpeg", 0.92);
|
||
})
|
||
.catch(() => {
|
||
alert("Błąd generowania obrazu.");
|
||
btn.prop('disabled', false).text('Przytnij i Zapisz');
|
||
});
|
||
},
|
||
|
||
// Reset po udanym wgraniu
|
||
resetUI() {
|
||
DOM.$wrapper.hide();
|
||
DOM.$toolbar.removeClass('show');
|
||
DOM.$uploadBtn.hide().prop('disabled', false).text('Przytnij i Zapisz');
|
||
DOM.$input.val('');
|
||
DOM.$name.text('');
|
||
}
|
||
};
|
||
|
||
// START
|
||
$(document).ready(() => App.init());
|
||
</script>
|
||
</body>
|
||
</html> |