resim-duzenleyici/index.html

599 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Numex Codex Resim Düzenleyici</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
background-color: #f0f2f5;
margin: 0;
padding: 20px;
color: #333;
}
.container {
background-color: #fff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
padding: 30px;
width: 100%;
max-width: min(900px, 95vw); /* Geniş ekranda 900px, dar ekranda %95 — taşmaz */
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 25px;
}
h1 {
color: #1a202c;
margin-bottom: 20px;
text-align: center;
font-size: 1.6em; /* mobil: küçük */
line-height: 1.2;
}
@media (min-width: 640px) { h1 { font-size: 1.9em; } } /* sm */
@media (min-width: 768px) { h1 { font-size: 2.2em; } } /* md */
.controls, .filters, .actions {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
padding: 15px 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.controls label, .filters label {
display: flex;
flex-direction: column;
align-items: center;
font-size: 0.9em;
color: #555;
min-width: 80px;
min-height: 44px;
justify-content: center;
}
.controls input[type="range"] {
width: 100px;
margin-top: 8px;
-webkit-appearance: none;
appearance: none;
height: 44px; /* Dokunma/işaret hedefi büyük */
background: transparent;
outline: none;
opacity: 0.8;
transition: opacity .2s;
background-image: linear-gradient(#ddd, #ddd);
background-repeat: no-repeat;
background-size: 100% 6px;
background-position: center;
}
.controls input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #007bff;
cursor: pointer;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
}
.controls input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: #007bff;
cursor: pointer;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
}
button {
background-color: #0b5ed7; /* Daha koyu → kontrast güçlü */
color: #fff;
border: none;
padding: 10px 20px;
min-height: 44px; /* Dokunma hedefi ≥ 44px */
border-radius: 8px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s ease, transform 0.2s ease;
box-shadow: 0 2px 10px rgba(11, 94, 215, 0.25);
}
button:hover {
background-color: #084298;
transform: translateY(-2px);
}
button:active {
transform: translateY(0);
}
button#downloadBtn {
background-color: #198754;
}
button#downloadBtn:hover {
background-color: #157347;
}
button#resetBtn {
background-color: #bb2d3b;
}
button#resetBtn:hover {
background-color: #b02a37;
}
/* Aktif filtre durumu işareti (gri tonlama / sepia) */
button.active-filter {
outline: 3px solid #ffc107;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(255, 193, 7, 0.4);
}
.canvas-area {
display: flex;
justify-content: center;
align-items: center;
background-color: #e9ecef;
border-radius: 8px;
padding: 20px;
min-height: 400px;
overflow: hidden; /* Canvas'ı taşma durumunda gizle */
}
canvas {
max-width: 100%;
max-height: 60vh; /* Ekran yüksekliğinin %60'ı */
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
display: block; /* Alt boşluğu engellemek için */
}
input[type="file"] {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
}
.file-upload-label {
background-color: #565e64;
color: #fff;
padding: 10px 20px;
min-height: 44px;
display: inline-flex;
align-items: center;
border-radius: 8px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s ease;
box-shadow: 0 2px 10px rgba(86, 94, 100, 0.25);
}
.file-upload-label:hover {
background-color: #4b5257;
}
@media (max-width: 768px) {
.container {
padding: 20px;
gap: 20px;
}
h1 {
font-size: 1.8em;
}
.controls, .filters, .actions {
flex-direction: column;
align-items: center;
}
.controls label, .filters label {
width: 100%;
text-align: center;
}
.controls input[type="range"] {
width: 80%;
}
}
</style>
</head>
<body>
<div class="container">
<h1>Numex Codex Resim Düzenleyici</h1>
<div class="actions">
<input type="file" id="imageLoader" accept="image/*">
<label for="imageLoader" class="file-upload-label">Resim Yükle</label>
<button id="rotateLeftBtn">Sola Döndür</button>
<button id="rotateRightBtn">Sağa Döndür</button>
<button id="cropBtn">Kırp</button>
</div>
<div class="controls">
<label>Parlaklık: <input type="range" id="brightness" min="0" max="200" value="100"></label>
<label>Kontrast: <input type="range" id="contrast" min="0" max="200" value="100"></label>
<label>Doygunluk: <input type="range" id="saturation" min="0" max="200" value="100"></label>
</div>
<div class="filters">
<button id="grayscaleBtn">Gri Tonlama</button>
<button id="sepiaBtn">Sepia</button>
<button id="resetFiltersBtn">Filtreleri Sıfırla</button>
</div>
<div class="canvas-area">
<canvas id="imageCanvas"></canvas>
</div>
<div class="actions">
<button id="downloadBtn">Resmi İndir</button>
<button id="resetBtn">Sıfırla</button>
</div>
</div>
<script>
/**
* Numex Codex Resim Düzenleyici
* Saf Canvas API — harici kütüphane yok.
* Modüler yaklaşım: her davranış (durum, canvas, görüntü, filtre,
* kırpma, döndürme, işlemler) kendi küçük nesnesinde toplanır.
*/
(function () {
'use strict';
/* ==========================================================
* 1. DURUM (State) — Uygulamanın tek gerçek kaynağı
* ========================================================== */
const State = {
originalImage: null, // Orijinal Image nesnesi (yüklenen)
displayImage: null, // Mevcut düzenlenmiş Image (kırp/döndür sonrası)
brightness: 100, // % (100 = nötr)
contrast: 100, // % (100 = nötr)
saturation: 100, // % (100 = nötr)
grayscale: false, // Gri tonlama aktif mi
sepia: false, // Sepia aktif mi
cropMode: false, // Kırpma modu açık mı
// Orijinal değerlere dönmek için saklanan yedek
backup() {
return {
displayImage: this.displayImage,
brightness: this.brightness,
contrast: this.contrast,
saturation: this.saturation,
grayscale: this.grayscale,
sepia: this.sepia
};
},
restore(snapshot) {
this.displayImage = snapshot.displayImage;
this.brightness = snapshot.brightness;
this.contrast = snapshot.contrast;
this.saturation = snapshot.saturation;
this.grayscale = snapshot.grayscale;
this.sepia = snapshot.sepia;
}
};
/* ==========================================================
* 2. DOM YARDIMCILARI — Element erişimini tek yerden yönet
* ========================================================== */
const Dom = {
fileInput: document.getElementById('imageLoader'),
canvas: document.getElementById('imageCanvas'),
ctx: null,
init() {
this.ctx = this.canvas.getContext('2d');
}
};
/* ==========================================================
* 3. GÖRÜNTÜ (Image) — Yükleme, ayarlama, sıfırlama
* ========================================================== */
const ImageManager = {
load(file) {
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
State.originalImage = img;
State.displayImage = img;
resetAdjustments();
CanvasD.setup(img.width, img.height);
Renderer.render();
};
img.onerror = () => alert('Resim yüklenemedi! Lütfen geçerli bir resim dosyası seçin.');
img.src = event.target.result;
};
reader.readAsDataURL(file);
}
};
/* ==========================================================
* 4. CANVAS — Boyut ayarlama ve tuvale çizim
* ========================================================== */
const CanvasD = {
setup(width, height) {
const maxW = 800, maxH = 600;
let w = width, h = height;
if (w > maxW || h > maxH) {
const ratio = Math.min(maxW / w, maxH / h);
w = Math.round(w * ratio);
h = Math.round(h * ratio);
}
Dom.canvas.width = w;
Dom.canvas.height = h;
},
get ctx() { return Dom.ctx; }
};
/* ==========================================================
* 5. RENDER; — Tüm görsel ayarları uygulayıp tuvale çizer
* ========================================================== */
const Renderer = {
render() {
const img = State.displayImage;
if (!img) return;
const ctx = CanvasD.ctx;
// Mevcut kanvas boyutuna resmi sığdır (boyut korunsun)
CanvasD.setup(img.width, img.height);
ctx.clearRect(0, 0, Dom.canvas.width, Dom.canvas.height);
ctx.drawImage(img, 0, 0, Dom.canvas.width, Dom.canvas.height);
// CSS filtreleri tek seferde birleştirip uygula
let filterString = 'none';
if (State.brightness !== 100 || State.contrast !== 100 || State.saturation !== 100
|| State.grayscale || State.sepia) {
const parts = [];
parts.push('brightness(' + State.brightness + '%)');
parts.push('contrast(' + State.contrast + '%)');
parts.push('saturate(' + State.saturation + '%)');
if (State.grayscale) parts.push('grayscale(1)');
if (State.sepia) parts.push('sepia(1)');
filterString = parts.join(' ');
}
ctx.filter = filterString;
ctx.drawImage(img, 0, 0, Dom.canvas.width, Dom.canvas.height);
ctx.filter = 'none'; // Sonraki çizimleri etkilemesin
}
};
/* ==========================================================
* 6. AYARLAR (Filters) — Parlaklık/kontrast/doygunluk + renk filtreleri
* ========================================================== */
const Adjustments = {
brightness() { State.brightness = +DomBrightness.value; Renderer.render(); },
contrast() { State.contrast = +DomContrast.value; Renderer.render(); },
saturation() { State.saturation = +DomSaturation.value; Renderer.render(); },
toggleGrayscale() {
State.grayscale = !State.grayscale;
if (State.grayscale) State.sepia = false; // ikisi birden olmaz
updateFilterButtons();
Renderer.render();
},
toggleSepia() {
State.sepia = !State.sepia;
if (State.sepia) State.grayscale = false;
updateFilterButtons();
Renderer.render();
},
reset() {
State.brightness = 100;
State.contrast = 100;
State.saturation = 100;
State.grayscale = false;
State.sepia = false;
updateFilterButtons();
syncSliders();
Renderer.render();
}
};
/* ==========================================================
* 7. KIRPMA (Crop) — Sürükleme ile bölge seç, naklet
* ========================================================== */
const Crop = {
startX: 0, startY: 0, endX: 0, endY: 0,
active: false,
enterMode() {
if (!State.displayImage) { alert('Önce bir resim yükleyin!'); return; }
State.cropMode = true;
this.capture();
},
capture() {
State.cropMode = true;
const rect = Dom.canvas.getBoundingClientRect();
const scaleX = Dom.canvas.width / rect.width;
const scaleY = Dom.canvas.height / rect.height;
Dom.canvas.style.cursor = 'crosshair';
const onMouseDown = (e) => {
this.active = true;
this.startX = (e.clientX - rect.left) * scaleX;
this.startY = (e.clientY - rect.top) * scaleY;
this.endX = this.startX;
this.endY = this.startY;
};
const onMouseMove = (e) => {
if (!this.active) return;
this.endX = (e.clientX - rect.left) * scaleX;
this.endY = (e.clientY - rect.top) * scaleY;
this.drawOverlay();
};
const onMouseUp = () => {
if (!this.active) return;
this.active = false;
Dom.canvas.style.cursor = 'default';
this.commit();
Dom.canvas.removeEventListener('mousedown', onMouseDown);
Dom.canvas.removeEventListener('mousemove', onMouseMove);
Dom.canvas.removeEventListener('mouseup', onMouseUp);
};
Dom.canvas.addEventListener('mousedown', onMouseDown);
Dom.canvas.addEventListener('mousemove', onMouseMove);
Dom.canvas.addEventListener('mouseup', onMouseUp);
},
drawOverlay() {
const ctx = CanvasD.ctx;
ctx.clearRect(0, 0, Dom.canvas.width, Dom.canvas.height);
// Orijinal görüntüyü çiz, sonra seçimi vurgula
ctx.drawImage(State.displayImage, 0, 0, Dom.canvas.width, Dom.canvas.height);
const x = Math.min(this.startX, this.endX);
const y = Math.min(this.startY, this.endY);
const w = Math.abs(this.endX - this.startX);
const h = Math.abs(this.endY - this.startY);
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
// Dört kenar boyunca karart
ctx.fillRect(0, 0, Dom.canvas.width, y);
ctx.fillRect(0, y + h, Dom.canvas.width, Dom.canvas.height - (y + h));
ctx.fillRect(0, y, x, h);
ctx.fillRect(x + w, y, Dom.canvas.width - (x + w), h);
ctx.strokeStyle = 'rgba(0, 123, 255, 0.9)';
ctx.lineWidth = 2;
ctx.strokeRect(x, y, w, h);
},
commit() {
const x = Math.min(this.startX, this.endX);
const y = Math.min(this.startY, this.endY);
const w = Math.abs(this.endX - this.startX);
const h = Math.abs(this.endY - this.startY);
if (w < 5 || h < 5) {
Renderer.render(); // Çok küçük seçim → iptal
return;
}
// Yeni bir offscreen canvas'ta seçilen bölgeyi kırp
const cropCanvas = document.createElement('canvas');
cropCanvas.width = Math.round(w);
cropCanvas.height = Math.round(h);
const cropCtx = cropCanvas.getContext('2d');
cropCtx.drawImage(State.displayImage, Math.round(x), Math.round(y), Math.round(w), Math.round(h), 0, 0, Math.round(w), Math.round(h));
// Kırpılmış görüntüyü yeni bir Image olarak ayarla
const img = new Image();
img.onload = () => {
State.displayImage = img;
Renderer.render();
};
img.src = cropCanvas.toDataURL();
State.cropMode = false;
}
};
/* ==========================================================
* 8. DÖNDÜRME (Rotate) — 90° adımlarla, resim yeniden örneklenir
* ========================================================== */
const Rotator = {
rotate(angle) {
if (!State.displayImage) { alert('Önce bir resim yükleyin!'); return; }
const img = State.displayImage;
const rotated = document.createElement('canvas');
if (angle === 90 || angle === -90) {
rotated.width = img.height;
rotated.height = img.width;
} else {
rotated.width = img.width;
rotated.height = img.height;
}
const rctx = rotated.getContext('2d');
rctx.translate(rotated.width / 2, rotated.height / 2);
rctx.rotate(angle * Math.PI / 180);
rctx.drawImage(img, -img.width / 2, -img.height / 2);
const result = new Image();
result.onload = () => {
State.displayImage = result;
Renderer.render();
};
result.src = rotated.toDataURL();
},
left() { this.rotate(90); },
right() { this.rotate(-90); }
};
/* ==========================================================
* 9. İNDİRME (Export) — Canvas'ı PNG olarak kaydet
* ========================================================== */
const Export = {
download() {
if (!State.displayImage) { alert('Önce bir resim yükleyin!'); return; }
const link = document.createElement('a');
link.download = 'duzenlenmis-resim.png';
link.href = Dom.canvas.toDataURL('image/png');
link.click();
},
reset() {
if (!State.originalImage) { alert('Henüz resim yüklenmedi, sıfırlanacak bir şey yok.'); return; }
State.displayImage = State.originalImage;
resetAdjustments();
Renderer.render();
}
};
/* ==========================================================
* 10. SLAYT SENKRON VE SIFIRLAMA YARDIMCILARI
* ========================================================== */
const DomBrightness = document.getElementById('brightness');
const DomContrast = document.getElementById('contrast');
const DomSaturation = document.getElementById('saturation');
function resetAdjustments() {
State.brightness = 100;
State.contrast = 100;
State.saturation = 100;
State.grayscale = false;
State.sepia = false;
updateFilterButtons();
syncSliders();
}
function updateFilterButtons() {
const gBtn = document.getElementById('grayscaleBtn');
const sBtn = document.getElementById('sepiaBtn');
gBtn.classList.toggle('active-filter', State.grayscale);
sBtn.classList.toggle('active-filter', State.sepia);
}
function syncSliders() {
DomBrightness.value = State.brightness;
DomContrast.value = State.contrast;
DomSaturation.value = State.saturation;
}
/* ==========================================================
* 11. OLAY BAĞLANTILARI (Event Wiring)
* ========================================================== */
function wireEvents() {
Dom.fileInput.addEventListener('change', (e) => ImageManager.load(e.target.files[0]));
document.getElementById('brightness').addEventListener('input', Adjustments.brightness);
document.getElementById('contrast').addEventListener('input', Adjustments.contrast);
document.getElementById('saturation').addEventListener('input', Adjustments.saturation);
document.getElementById('grayscaleBtn').addEventListener('click', Adjustments.toggleGrayscale);
document.getElementById('sepiaBtn').addEventListener('click', Adjustments.toggleSepia);
document.getElementById('resetFiltersBtn').addEventListener('click', Adjustments.reset);
document.getElementById('rotateLeftBtn').addEventListener('click', () => Rotator.left());
document.getElementById('rotateRightBtn').addEventListener('click', () => Rotator.right());
document.getElementById('cropBtn').addEventListener('click', () => Crop.enterMode());
document.getElementById('downloadBtn').addEventListener('click', Export.download);
document.getElementById('resetBtn').addEventListener('click', Export.reset);
}
/* ==========================================================
* 12. UYGULAMA BAŞLATMA
* ========================================================== */
function init() {
Dom.init();
wireEvents();
}
document.addEventListener('DOMContentLoaded', init);
})();
</script>
</body>
</html>