feat: klasor tabanli galeri, brightness efekti (Ctrl+Shift+sag tik), .deb guncellendi
This commit is contained in:
@@ -10,6 +10,7 @@ Ultra hafif görüntü izleyici. Flutter ile yazıldı — Linux & Windows deste
|
|||||||
- **Fill/Fit geçişi:** Sol çift tık
|
- **Fill/Fit geçişi:** Sol çift tık
|
||||||
- **Contrast ayarı:** Sağ tık + dikey sürükle
|
- **Contrast ayarı:** Sağ tık + dikey sürükle
|
||||||
- **Saturation ayarı:** Ctrl + Sağ tık + dikey sürükle
|
- **Saturation ayarı:** Ctrl + Sağ tık + dikey sürükle
|
||||||
|
- **Brightness ayarı:** Ctrl+Shift + Sağ tık + dikey sürükle
|
||||||
- **Rotate:** Shift + Sağ tık + dikey sürükle (~100px = 90°)
|
- **Rotate:** Shift + Sağ tık + dikey sürükle (~100px = 90°)
|
||||||
- **Reset (tüm efektler):** Sağ çift tık
|
- **Reset (tüm efektler):** Sağ çift tık
|
||||||
- Sürükle-bırak ile resim açma
|
- Sürükle-bırak ile resim açma
|
||||||
@@ -110,6 +111,7 @@ flutter build windows --release
|
|||||||
| Sol çift tık | Fill / Fit geçişi |
|
| Sol çift tık | Fill / Fit geçişi |
|
||||||
| Sağ tık + dikey sürükle | Contrast (0.0 – 2.0) |
|
| Sağ tık + dikey sürükle | Contrast (0.0 – 2.0) |
|
||||||
| Ctrl + Sağ tık + dikey sürükle | Saturation (0.0 – 2.0) |
|
| Ctrl + Sağ tık + dikey sürükle | Saturation (0.0 – 2.0) |
|
||||||
|
| Ctrl+Shift + Sağ tık + dikey sürükle | Brightness (-100 – 100) |
|
||||||
| Shift + Sağ tık + dikey sürükle | Rotate (serbest) |
|
| Shift + Sağ tık + dikey sürükle | Rotate (serbest) |
|
||||||
| Sağ çift tık | Reset (tüm efektler) |
|
| Sağ çift tık | Reset (tüm efektler) |
|
||||||
| Ctrl + O | Dosya aç |
|
| Ctrl + O | Dosya aç |
|
||||||
|
|||||||
BIN
dist/imajviewer_1.0.0_amd64.deb
vendored
BIN
dist/imajviewer_1.0.0_amd64.deb
vendored
Binary file not shown.
65
docs/galeri-klasor-tabanli.md
Normal file
65
docs/galeri-klasor-tabanli.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Galeri: Klasör Tabanlı Gezinme — Rapor ve Plan
|
||||||
|
|
||||||
|
Tarih: 2026-08-03
|
||||||
|
Durum: Uygulandı (2026-08-03)
|
||||||
|
|
||||||
|
## 1. Mevcut Sorunun Analizi
|
||||||
|
|
||||||
|
### RAM şişmesi — `lib/services/image_manager.dart`
|
||||||
|
- `addImages()` her eklenen yol için `precacheImage(FileImage(...))` çağırıyor (satır 33-35).
|
||||||
|
- Sonuç: toplu sürüklemede tüm resimler tam çözünürlükte decode edilip ImageCache'e yazılıyor.
|
||||||
|
- Örnek: 12MP JPEG dekode ≈ 48MB RGBA; 30 resim ≈ 1.4GB.
|
||||||
|
- Decode UI thread'inde yapılıyor → pencere donması, "çok pencere açmış gibi" hissi.
|
||||||
|
- `configureCache()` tavanı: 1000 görsel / 1GB (satır 62-65).
|
||||||
|
|
||||||
|
### Sağ tık efektlerinde çökme — `lib/widgets/image_canvas.dart`
|
||||||
|
- Her pointer move'da `setState` → `ColorFiltered` tüm görseli yeniden boyuyor (satır 224-240).
|
||||||
|
- Bellek zaten yüksekken + VM'de yazılımsal render → çökme en çok burada görülüyor.
|
||||||
|
- Kök neden RAM şişmesi; o çözülünce bu senaryo da hafifler.
|
||||||
|
|
||||||
|
## 2. Alınan Kararlar (kullanıcı onayı)
|
||||||
|
|
||||||
|
1. **Precache tamamen kaldırılacak** — hiçbir resim önceden decode edilmeyecek.
|
||||||
|
2. Görüntüleme anında decode: sıradaki resme geçişte yüklenene kadar beklenir.
|
||||||
|
3. **RAM kısıtlaması yok** — cache limitlerine dokunulmayacak, daraltılmayacak.
|
||||||
|
4. Önceki resim cache'ten düşebilir (zorunlu tutma yok). Precache olmadığı için
|
||||||
|
bellekte aynı anda yalnızca açık olan resim(ler) durur; kabarık galeri listesi
|
||||||
|
sorun yaratmaz.
|
||||||
|
|
||||||
|
## 3. Yeni Özellik — Klasör Tabanlı Galeri
|
||||||
|
|
||||||
|
Amaç: resimleri topluca seçip sürükleyerek galeri kurmak yerine, açılan herhangi
|
||||||
|
bir resmin bulunduğu dizin otomatik galeri olsun.
|
||||||
|
|
||||||
|
Davranış:
|
||||||
|
- Bir resim açıldığında (çift tık, CLI argümanı, sürükle-bırak) bulunduğu dizin taranır.
|
||||||
|
- Dizindeki desteklenen imaj dosyaları (png/jpg/jpeg/jfif/jpe/webp/bmp/gif/wbmp)
|
||||||
|
galeri listesi olarak set edilir.
|
||||||
|
- Açılan resim listedeki konumunu bulur ve o an gösterilir.
|
||||||
|
- Ctrl+Tab / Ctrl+Shift+Tab ile klasör içinde ileri-geri gezilir.
|
||||||
|
- Mevcut pencere başına singleton yapısı korunur — her pencere ayrı process,
|
||||||
|
her biri kendi klasör galerisini kurar.
|
||||||
|
|
||||||
|
### Sıralama
|
||||||
|
- Dosya yöneticisinin (Thunar/PCManFM/Nautilus) klasör sort'u OS seviyesinde
|
||||||
|
okunamaz — uygulama içi ayardır, dosya sisteminde saklanmaz. Bu seçenek pratikte yok.
|
||||||
|
- Bu yüzden **tarihe göre** sıralanacak (dosyanın değiştirilme zamanı).
|
||||||
|
- Yön: **en yeni önce, en eski sonra** (azalan).
|
||||||
|
|
||||||
|
## 4. Uygulama (uygulandı)
|
||||||
|
|
||||||
|
- `lib/services/image_manager.dart` — precache döngüsü ve `buildContext` kaldırıldı.
|
||||||
|
`addImages` yerine `setGalleryFromPaths(paths)` geldi: ilk geçerli resmin klasörünü
|
||||||
|
tarar, desteklenenleri filtreler, değiştirilme zamanına göre en yeni önce sıralar,
|
||||||
|
açılan resmi o anki konum yapar. (Aynı zaman damgasında dosya adına göre sabit sıra)
|
||||||
|
- `lib/services/file_handler.dart` — `addImages` çağrıları `setGalleryFromPaths` oldu
|
||||||
|
(Ctrl+O ve sürükle-bırak aynı akıştan geçiyor).
|
||||||
|
- `lib/screens/viewer_screen.dart` — `buildContext` ataması kaldırıldı (precache yok).
|
||||||
|
- RAM kısıtlaması eklenmedi — `configureCache` aynen duruyor.
|
||||||
|
|
||||||
|
## 5. Açık Sorular
|
||||||
|
|
||||||
|
Çözüldü:
|
||||||
|
1. ~~Tarih sıralama yönü~~ → **en yeni önce, en eski sonra**
|
||||||
|
2. ~~Pencereye birden fazla resim sürüklenirse~~ → **sürüklenen resmin klasörüne geçilir**
|
||||||
|
(galeri açıkken yeni resim gelirse de aynı: o resmin klasörü yeni galeri olur)
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
| Tarih | Dosya | Konu |
|
| Tarih | Dosya | Konu |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-08-03 | [rapor-brightness.md](rapor-brightness.md) | **Rapor** — brightness efekti (Ctrl+Shift+sağ tık) + 4x5 matris birleştirme |
|
||||||
|
| 2026-08-03 | [galeri-klasor-tabanli.md](galeri-klasor-tabanli.md) | **Rapor** — klasör tabanlı galeri + precache/RAM kararı |
|
||||||
| 2026-08-03 | [session-2026-08-03.md](session-2026-08-03.md) | **Session özeti** — jfif/jpe/wbmp, pencere cascade, konum kaydı düzeltmesi |
|
| 2026-08-03 | [session-2026-08-03.md](session-2026-08-03.md) | **Session özeti** — jfif/jpe/wbmp, pencere cascade, konum kaydı düzeltmesi |
|
||||||
| 2026-08-03 | [format-destegi-ek-formatlar.md](format-destegi-ek-formatlar.md) | **Plan** — jfif/jpe/wbmp desteği (TIFF vazgeçildi) |
|
| 2026-08-03 | [format-destegi-ek-formatlar.md](format-destegi-ek-formatlar.md) | **Plan** — jfif/jpe/wbmp desteği (TIFF vazgeçildi) |
|
||||||
| 2026-08-03 | [pencere-cascade-yerlesim.md](pencere-cascade-yerlesim.md) | **Plan** — yeni pencereyi yan yana / alt satıra açma (screen_retriever) |
|
| 2026-08-03 | [pencere-cascade-yerlesim.md](pencere-cascade-yerlesim.md) | **Plan** — yeni pencereyi yan yana / alt satıra açma (screen_retriever) |
|
||||||
@@ -9,3 +11,9 @@
|
|||||||
| 2026-07-27 | [session-2026-07-27.md](session-2026-07-27.md) | **Session özeti** — rotate aktif, StartupWMClass ile taskbar gruplama |
|
| 2026-07-27 | [session-2026-07-27.md](session-2026-07-27.md) | **Session özeti** — rotate aktif, StartupWMClass ile taskbar gruplama |
|
||||||
| 2026-07-27 | [image-canvas-interactions.md](image-canvas-interactions.md) | Detaylı analiz: contrast, saturation, rotate implementasyon planı |
|
| 2026-07-27 | [image-canvas-interactions.md](image-canvas-interactions.md) | Detaylı analiz: contrast, saturation, rotate implementasyon planı |
|
||||||
| önceki | [zoom-clamp-mekanizmasi.md](zoom-clamp-mekanizmasi.md) | Zoom clamp mekanizması (letterbox-aware) |
|
| önceki | [zoom-clamp-mekanizmasi.md](zoom-clamp-mekanizmasi.md) | Zoom clamp mekanizması (letterbox-aware) |
|
||||||
|
|
||||||
|
## Geliştirme Akışı — Otomatik Build ve Kurulum
|
||||||
|
|
||||||
|
Projede değişiklik yapıldıktan sonra otomatik yapılır (ayrıca istenmez):
|
||||||
|
1. Debug build: `flutter build linux --debug`
|
||||||
|
2. Sisteme kurulum: `./install.sh` (debug bundle tercih edilir → `~/.local/lib/imajviewer/`)
|
||||||
|
|||||||
62
docs/rapor-brightness.md
Normal file
62
docs/rapor-brightness.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Rapor: Brightness Efekti + Sağ Tık Etkileşim Düzeni
|
||||||
|
|
||||||
|
Tarih: 2026-08-03
|
||||||
|
Durum: Rapor — uygulama için onay bekleniyor
|
||||||
|
|
||||||
|
## 1. Gönderilen Araştırmanın Özeti
|
||||||
|
|
||||||
|
- `ColorMatrixHelper`: Rec.709 luma katsayıları (0.2126 / 0.7152 / 0.0722) — projedekiyle aynı.
|
||||||
|
- Üç ayrı 4x5 renk matrisi:
|
||||||
|
- **Brightness:** RGB'ye offset ekler (`b` → -255..255)
|
||||||
|
- **Contrast:** `c` ile çarpar + `128*(1-c)` offset (0..2 arası, 1.0 orijinal)
|
||||||
|
- **Saturation:** Rec.709 ağırlıklı karışım (0..2 arası, 1.0 orijinal)
|
||||||
|
- Birleştirme: `M_final = M_contrast * M_brightness * M_saturation` — genel 4x5 matris
|
||||||
|
çarpımı; uygulama sırası saturation → brightness → contrast.
|
||||||
|
- Uygulama: `ColorFiltered` + `ColorFilter.matrix` ile GPU shader'ı — projedekiyle aynı mekanizma.
|
||||||
|
- Örnek widget (`ImageAdjusterWidget`) **slider tabanlı**; bizim uygulama jest tabanlı
|
||||||
|
olduğu için bu kısım direkt uygulanmayacak. Asıl değer matris hesabında.
|
||||||
|
|
||||||
|
## 2. Projedeki Mevcut Durum — `lib/widgets/image_canvas.dart`
|
||||||
|
|
||||||
|
- State: `_contrast` (1.0), `_saturation` (1.0) — brightness yok.
|
||||||
|
- `_buildCombinedFilter()`: saturation × contrast tek matriste elle birleştiriliyor
|
||||||
|
(contrast offset: `t = (1-c)/2` → 255 ölçeğinde 127.5*(1-c)).
|
||||||
|
- Cache: `_cachedFilter` + `_cachedSat` / `_cachedCon`.
|
||||||
|
- Etkileşim: sağ tık = contrast, Ctrl+sağ tık = saturation, Shift+sağ tık = rotate,
|
||||||
|
sağ çift tık = reset (tüm efektler).
|
||||||
|
- Modifier takibi: `_onHardwareKey` yalnızca ctrl ve shift'i izliyor.
|
||||||
|
|
||||||
|
## 3. Karşılaştırma — Araştırma vs Mevcut Kod
|
||||||
|
|
||||||
|
| Konu | Mevcut | Araştırma | Fark |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Saturation matrisi | Rec.709, aynı formül | Rec.709, aynı formül | Matematik birebir aynı |
|
||||||
|
| Contrast offset | 127.5*(1-c) | 128*(1-c) | ~%0.4, görsel fark yok |
|
||||||
|
| Brightness | yok | `b` offset matrisi | **Eklenecek** |
|
||||||
|
| Birleştirme | elle 2 matris | genel 4x5 çarpım | 3 efekt için genel çarpım daha temiz |
|
||||||
|
| Arayüz | jest (sağ tık + sürükle) | slider | Widget değil, matris katmanı alınacak |
|
||||||
|
|
||||||
|
## 4. Uygulama Planı
|
||||||
|
|
||||||
|
1. `_brightness` state ekle (varsayılan 0.0, aralık -100..100).
|
||||||
|
2. `_onPointerDown`: öncelik sırası — **Ctrl+Shift → brightness**, Ctrl → saturation,
|
||||||
|
Shift → rotate, yoksa contrast. (Başlangıçta Alt denenmişti; XFCE Alt+klik'i
|
||||||
|
pencere hareketine kaptığı için **Ctrl+Shift**'e geçildi.)
|
||||||
|
3. `_onPointerMove`: brightness modunda `_brightness = _startBrightness + dy * 0.5`
|
||||||
|
(clamp -100..100; hassasiyet açık soru).
|
||||||
|
4. `_buildCombinedFilter`: araştırmadaki 4x5 çarpım mantığıyla 3 efekt birleştir
|
||||||
|
(saturation → brightness → contrast); cache anahtarına `_brightness` ekle.
|
||||||
|
5. Reset (sağ çift tık), `_resetView`, `_toggleFillFit`: brightness 0'a döner.
|
||||||
|
6. `hasFilter` koşuluna `_brightness != 0` eklenir (ColorFiltered yalnızca gerektiğinde).
|
||||||
|
|
||||||
|
## 5. Dikkat Edilecekler
|
||||||
|
|
||||||
|
- **Linux/XFCE:** Alt+klik WM tarafından pencere hareketine kaptırıldığı için
|
||||||
|
brightness **Ctrl+Shift+sağ tık**'a alındı (Alt kaldırıldı).
|
||||||
|
- **Uygulama sırası:** saturation → brightness → contrast sabit kalacak; farklı sıra farklı sonuç verir.
|
||||||
|
- Reset davranışı brightness'i de kapsar.
|
||||||
|
|
||||||
|
## 6. Açık Sorular
|
||||||
|
|
||||||
|
1. Brightness hassasiyeti: `dy * 0.5`, aralık -100..100 uygun mu?
|
||||||
|
2. Rotate Shift+sağ tık'ta kalıyor (değişiklik yok) — onay?
|
||||||
@@ -86,7 +86,6 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
ImageManager.instance.buildContext = context;
|
|
||||||
return AppShortcuts(
|
return AppShortcuts(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ class FileHandler {
|
|||||||
.where((f) => f.path != null)
|
.where((f) => f.path != null)
|
||||||
.map((f) => f.path!)
|
.map((f) => f.path!)
|
||||||
.toList();
|
.toList();
|
||||||
ImageManager.instance.addImages(paths);
|
ImageManager.instance.setGalleryFromPaths(paths);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load images from given file paths
|
/// Load images from given file paths
|
||||||
static void loadImagesFromPaths(List<String> paths) {
|
static void loadImagesFromPaths(List<String> paths) {
|
||||||
if (paths.isNotEmpty) {
|
if (paths.isNotEmpty) {
|
||||||
ImageManager.instance.addImages(paths);
|
ImageManager.instance.setGalleryFromPaths(paths);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ class ImageManager extends ChangeNotifier {
|
|||||||
static ImageManager get instance => _instance;
|
static ImageManager get instance => _instance;
|
||||||
ImageManager._();
|
ImageManager._();
|
||||||
|
|
||||||
|
static const _supportedExtensions = [
|
||||||
|
'png', 'jpg', 'jpeg', 'jfif', 'jpe', 'webp', 'bmp', 'gif', 'wbmp',
|
||||||
|
];
|
||||||
|
|
||||||
final List<String> _images = [];
|
final List<String> _images = [];
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
|
|
||||||
@@ -18,22 +22,47 @@ class ImageManager extends ChangeNotifier {
|
|||||||
return _images[_currentIndex];
|
return _images[_currentIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
void addImages(List<String> paths) {
|
/// Açılan resmin bulunduğu klasörü galeri olarak kurar (en yeni önce).
|
||||||
final validPaths = paths.where((p) {
|
void setGalleryFromPaths(List<String> paths) {
|
||||||
|
String? anchor;
|
||||||
|
for (final p in paths) {
|
||||||
final ext = p.toLowerCase().split('.').last;
|
final ext = p.toLowerCase().split('.').last;
|
||||||
const supported = ['png', 'jpg', 'jpeg', 'jfif', 'jpe', 'webp', 'bmp', 'gif', 'wbmp'];
|
if (_supportedExtensions.contains(ext) && File(p).existsSync()) {
|
||||||
return supported.contains(ext) && File(p).existsSync();
|
anchor = p;
|
||||||
}).toList();
|
break;
|
||||||
|
}
|
||||||
if (validPaths.isEmpty) return;
|
|
||||||
|
|
||||||
_images.addAll(validPaths);
|
|
||||||
|
|
||||||
// Precache images
|
|
||||||
for (final path in validPaths) {
|
|
||||||
precacheImage(FileImage(File(path)), context!);
|
|
||||||
}
|
}
|
||||||
|
if (anchor == null) return;
|
||||||
|
|
||||||
|
final dir = File(anchor).parent;
|
||||||
|
if (!dir.existsSync()) return;
|
||||||
|
|
||||||
|
final List<(String, DateTime)> entries = [];
|
||||||
|
for (final entity in dir.listSync()) {
|
||||||
|
if (entity is! File) continue;
|
||||||
|
final p = entity.path;
|
||||||
|
final ext = p.toLowerCase().split('.').last;
|
||||||
|
if (!_supportedExtensions.contains(ext)) continue;
|
||||||
|
try {
|
||||||
|
entries.add((p, entity.lastModifiedSync()));
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
entries.sort((a, b) {
|
||||||
|
final byDate = b.$2.compareTo(a.$2);
|
||||||
|
return byDate != 0 ? byDate : a.$1.compareTo(b.$1);
|
||||||
|
});
|
||||||
|
|
||||||
|
_images
|
||||||
|
..clear()
|
||||||
|
..addAll(entries.map((e) => e.$1));
|
||||||
|
|
||||||
|
_currentIndex = _images.indexOf(anchor);
|
||||||
|
if (_currentIndex < 0) {
|
||||||
|
final base = anchor.split(Platform.pathSeparator).last;
|
||||||
|
_currentIndex = _images
|
||||||
|
.indexWhere((p) => p.split(Platform.pathSeparator).last == base);
|
||||||
|
}
|
||||||
|
if (_currentIndex < 0) _currentIndex = 0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,19 +84,12 @@ class ImageManager extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Precache needs a BuildContext
|
|
||||||
BuildContext? context;
|
|
||||||
|
|
||||||
/// Configure image cache for high-res images
|
/// Configure image cache for high-res images
|
||||||
static void configureCache() {
|
static void configureCache() {
|
||||||
PaintingBinding.instance.imageCache.maximumSize = 1000;
|
PaintingBinding.instance.imageCache.maximumSize = 1000;
|
||||||
PaintingBinding.instance.imageCache.maximumSizeBytes = 1024 * 1024 * 1024; // 1 GB
|
PaintingBinding.instance.imageCache.maximumSizeBytes = 1024 * 1024 * 1024; // 1 GB
|
||||||
}
|
}
|
||||||
|
|
||||||
set buildContext(BuildContext ctx) {
|
|
||||||
context = ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
_images.clear();
|
_images.clear();
|
||||||
_currentIndex = 0;
|
_currentIndex = 0;
|
||||||
|
|||||||
@@ -21,13 +21,15 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
double _angle = 0;
|
double _angle = 0;
|
||||||
double _contrast = 1.0;
|
double _contrast = 1.0;
|
||||||
double _saturation = 1.0;
|
double _saturation = 1.0;
|
||||||
|
double _brightness = 0;
|
||||||
|
|
||||||
// ── Gesture tracking ──
|
// ── Gesture tracking ──
|
||||||
String _interactionMode = 'none'; // 'contrast' | 'rotate' | 'saturation'
|
String _interactionMode = 'none'; // 'contrast' | 'rotate' | 'saturation' | 'brightness'
|
||||||
Offset _pointerDownPos = Offset.zero;
|
Offset _pointerDownPos = Offset.zero;
|
||||||
double _startAngle = 0;
|
double _startAngle = 0;
|
||||||
double _startContrast = 1.0;
|
double _startContrast = 1.0;
|
||||||
double _startSaturation = 1.0;
|
double _startSaturation = 1.0;
|
||||||
|
double _startBrightness = 0;
|
||||||
DateTime? _lastRightTapTime;
|
DateTime? _lastRightTapTime;
|
||||||
Offset _lastRightTapPos = Offset.zero;
|
Offset _lastRightTapPos = Offset.zero;
|
||||||
|
|
||||||
@@ -139,14 +141,14 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
void _resetView() {
|
void _resetView() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_tx = 0; _ty = 0; _sc = 1.0; _isFilled = true; _vpSize = Size.zero;
|
_tx = 0; _ty = 0; _sc = 1.0; _isFilled = true; _vpSize = Size.zero;
|
||||||
_angle = 0; _contrast = 1.0; _saturation = 1.0;
|
_angle = 0; _contrast = 1.0; _saturation = 1.0; _brightness = 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleFillFit() {
|
void _toggleFillFit() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isFilled = !_isFilled; _tx = 0; _ty = 0; _sc = 1.0; _vpSize = Size.zero;
|
_isFilled = !_isFilled; _tx = 0; _ty = 0; _sc = 1.0; _vpSize = Size.zero;
|
||||||
_angle = 0; _contrast = 1.0; _saturation = 1.0;
|
_angle = 0; _contrast = 1.0; _saturation = 1.0; _brightness = 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +198,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
_angle = 0;
|
_angle = 0;
|
||||||
_contrast = 1.0;
|
_contrast = 1.0;
|
||||||
_saturation = 1.0;
|
_saturation = 1.0;
|
||||||
|
_brightness = 0;
|
||||||
_interactionMode = 'none';
|
_interactionMode = 'none';
|
||||||
});
|
});
|
||||||
_lastRightTapTime = null;
|
_lastRightTapTime = null;
|
||||||
@@ -204,7 +207,10 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
_lastRightTapTime = now;
|
_lastRightTapTime = now;
|
||||||
_lastRightTapPos = e.localPosition;
|
_lastRightTapPos = e.localPosition;
|
||||||
|
|
||||||
if (_ctrlPressed) {
|
if (_ctrlPressed && _shiftPressed) {
|
||||||
|
_interactionMode = 'brightness';
|
||||||
|
_startBrightness = _brightness;
|
||||||
|
} else if (_ctrlPressed) {
|
||||||
_interactionMode = 'saturation';
|
_interactionMode = 'saturation';
|
||||||
_startSaturation = _saturation;
|
_startSaturation = _saturation;
|
||||||
} else if (_shiftPressed) {
|
} else if (_shiftPressed) {
|
||||||
@@ -237,6 +243,11 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
_saturation = (_startSaturation + dy * 0.005).clamp(0.0, 2.0);
|
_saturation = (_startSaturation + dy * 0.005).clamp(0.0, 2.0);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'brightness':
|
||||||
|
setState(() {
|
||||||
|
_brightness = (_startBrightness + dy * 0.5).clamp(-100.0, 100.0);
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +255,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
_interactionMode = 'none';
|
_interactionMode = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Combined color filter (saturation × contrast) ──
|
// ── Combined color filter (brightness × saturation × contrast) ──
|
||||||
|
|
||||||
static const double _rLum = 0.2126;
|
static const double _rLum = 0.2126;
|
||||||
static const double _gLum = 0.7152;
|
static const double _gLum = 0.7152;
|
||||||
@@ -253,29 +264,72 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
ColorFilter? _cachedFilter;
|
ColorFilter? _cachedFilter;
|
||||||
double _cachedSat = -1;
|
double _cachedSat = -1;
|
||||||
double _cachedCon = -1;
|
double _cachedCon = -1;
|
||||||
|
double _cachedBri = double.nan;
|
||||||
|
|
||||||
|
List<double> _multiply4x5(List<double> a, List<double> b) {
|
||||||
|
final result = List<double>.filled(20, 0.0);
|
||||||
|
for (int row = 0; row < 4; row++) {
|
||||||
|
for (int col = 0; col < 4; col++) {
|
||||||
|
double sum = 0.0;
|
||||||
|
for (int k = 0; k < 4; k++) {
|
||||||
|
sum += a[row * 5 + k] * b[k * 5 + col];
|
||||||
|
}
|
||||||
|
result[row * 5 + col] = sum;
|
||||||
|
}
|
||||||
|
double offsetSum = a[row * 5 + 4];
|
||||||
|
for (int k = 0; k < 4; k++) {
|
||||||
|
offsetSum += a[row * 5 + k] * b[k * 5 + 4];
|
||||||
|
}
|
||||||
|
result[row * 5 + 4] = offsetSum;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
ColorFilter _buildCombinedFilter() {
|
ColorFilter _buildCombinedFilter() {
|
||||||
if (_cachedFilter != null && _cachedSat == _saturation && _cachedCon == _contrast) {
|
if (_cachedFilter != null &&
|
||||||
|
_cachedSat == _saturation &&
|
||||||
|
_cachedCon == _contrast &&
|
||||||
|
_cachedBri == _brightness) {
|
||||||
return _cachedFilter!;
|
return _cachedFilter!;
|
||||||
}
|
}
|
||||||
_cachedSat = _saturation;
|
_cachedSat = _saturation;
|
||||||
_cachedCon = _contrast;
|
_cachedCon = _contrast;
|
||||||
|
_cachedBri = _brightness;
|
||||||
|
|
||||||
final s = _saturation;
|
final s = _saturation;
|
||||||
final c = _contrast;
|
final c = _contrast;
|
||||||
final t = (1.0 - c) / 2.0;
|
final b = _brightness;
|
||||||
final invS = 1.0 - s;
|
final invS = 1.0 - s;
|
||||||
|
|
||||||
final sR_R = invS * _rLum + s, sR_G = invS * _gLum, sR_B = invS * _bLum;
|
final sR_R = invS * _rLum + s, sR_G = invS * _gLum, sR_B = invS * _bLum;
|
||||||
final sG_R = invS * _rLum, sG_G = invS * _gLum + s, sG_B = invS * _bLum;
|
final sG_R = invS * _rLum, sG_G = invS * _gLum + s, sG_B = invS * _bLum;
|
||||||
final sB_R = invS * _rLum, sB_G = invS * _gLum, sB_B = invS * _bLum + s;
|
final sB_R = invS * _rLum, sB_G = invS * _gLum, sB_B = invS * _bLum + s;
|
||||||
|
|
||||||
_cachedFilter = ColorFilter.matrix(<double>[
|
final saturationMatrix = <double>[
|
||||||
c * sR_R, c * sR_G, c * sR_B, 0, t,
|
sR_R, sR_G, sR_B, 0, 0,
|
||||||
c * sG_R, c * sG_G, c * sG_B, 0, t,
|
sG_R, sG_G, sG_B, 0, 0,
|
||||||
c * sB_R, c * sB_G, c * sB_B, 0, t,
|
sB_R, sB_G, sB_B, 0, 0,
|
||||||
0, 0, 0, 1, 0,
|
0, 0, 0, 1, 0,
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
final brightnessMatrix = <double>[
|
||||||
|
1, 0, 0, 0, b,
|
||||||
|
0, 1, 0, 0, b,
|
||||||
|
0, 0, 1, 0, b,
|
||||||
|
0, 0, 0, 1, 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
final offset = 128.0 * (1.0 - c);
|
||||||
|
final contrastMatrix = <double>[
|
||||||
|
c, 0, 0, 0, offset,
|
||||||
|
0, c, 0, 0, offset,
|
||||||
|
0, 0, c, 0, offset,
|
||||||
|
0, 0, 0, 1, 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
// M_final = M_contrast * M_brightness * M_saturation
|
||||||
|
_cachedFilter = ColorFilter.matrix(
|
||||||
|
_multiply4x5(_multiply4x5(contrastMatrix, brightnessMatrix), saturationMatrix));
|
||||||
return _cachedFilter!;
|
return _cachedFilter!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +356,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
|
|
||||||
Widget imageContent = Builder(
|
Widget imageContent = Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final hasFilter = _saturation != 1.0 || _contrast != 1.0;
|
final hasFilter = _saturation != 1.0 || _contrast != 1.0 || _brightness != 0.0;
|
||||||
final image = _isFilled
|
final image = _isFilled
|
||||||
? SizedBox.expand(child: _buildImage())
|
? SizedBox.expand(child: _buildImage())
|
||||||
: Center(child: _buildImage());
|
: Center(child: _buildImage());
|
||||||
|
|||||||
Reference in New Issue
Block a user