- Add ColorFilter.matrix (row-major) for saturation and contrast - Saturation: 0=grayscale, 1=normal, 2=oversaturated (luminance-weighted) - Contrast: standard contrast matrix (pivot at 0.5) - Keyboard modifier tracking via HardwareKeyboard.addHandler - Right-click + vertical drag: contrast (0-2) - Ctrl + right-click + vertical drag: saturation (0-2) - Shift + right-click + vertical drag: rotate - Right double-click: reset all values - Update install.sh to check debug/ bundle before release/ - Update .deb package with new release build
6.6 KiB
Image Canvas Interactions — Analiz ve İmplementasyon Planı
Tarih: 2026-07-27
Kapsam: lib/widgets/image_canvas.dart
1. Mevcut Durum
ImageCanvas widget'ı şu anda:
- Sol tık + sürükle: Pan (imleç hareketiyle resmi kaydırma) —
GestureDetector.onPanUpdate - Tekerlek: Zoom (imleç pozisyonu merkezli) —
Listener.onPointerSignal → PointerScrollEvent - Çift tık: Fill/Fit mod geçişi —
GestureDetector.onDoubleTap - Transform:
Matrix4ile sadecetranslate(_tx, _ty)+scale(_sc), rotate yok - Renk/efekt filtresi: Yok
Sorun: GestureDetector.onPanUpdate hangi mouse tuşuna basıldığını ayırt etmez. Tüm tuşlarla yapılan sürüklemeyi onPanUpdate olarak yakalar. Sağ tık için ayrı bir işlem yapılamaz.
2. Gereksinimler
| # | Etkileşim | Davranış |
|---|---|---|
| R1 | Sağ tık + Dikey sürükle | Rotate. Yukarı sürükle → saat yönü, aşağı sürükle → saat tersi. Viewport merkezinden dönmeli. Hassasiyet: ~100px = 90° |
| R2 | Ctrl + Sağ tık + Dikey sürükle | Contrast ayarı. Yukarı sürükle → artır, aşağı sürükle → azalt. Aralık: 0.0 – 2.0 (default: 1.0) |
| R3 | Shift + Sağ tık + Dikey sürükle | Brightness ayarı. Yukarı sürükle → artır, aşağı sürükle → azalt. Aralık: -1.0 – 1.0 (default: 0.0) |
| R4 | Sağ çift tık | Reset: rotate, contrast, brightness sıfırlanır (0°, 1.0, 0.0) |
| R5 | Sol tık + sürükle | Mevcut pan davranışı korunacak |
3. Teknik Tasarım
3.1 Gesture Handling: GestureDetector → Listener
Mevcut GestureDetector tuş ayrımı yapamadığı için, Listener widget'ı ile pointer event'leri manuel yakalayacağız:
Listener(
onPointerDown: _onPointerDown, // hangi tuş + modifier → mod belirle
onPointerMove: _onPointerMove, // moda göre pan / contrast / brightness
onPointerUp: _onPointerUp, // tıklama ise rotate uygula
onPointerSignal: _handleScroll, // zoom (mevcut)
)
Mod State Machine:
PointerDown:
if (sol tuş) → mod = pan
if (sağ tuş, no modifier) → mod = rotate, başlangıç açısını kaydet
if (sağ tuş + Ctrl) → mod = contrast, başlangıç değerini kaydet
if (sağ tuş + Shift) → mod = brightness, başlangıç değerini kaydet
PointerMove:
pan: _tx += dx, _ty += dy
rotate: _angle += dy * sensitivity (clamp yok, serbest dönüş)
contrast: _contrast += dy * sensitivity, clamp(0.0, 2.0)
brightness: _brightness += dy * sensitivity, clamp(-1.0, 1.0)
PointerUp:
modu temizle
Çift tık (sağ tuş):
_angle = 0; _contrast = 1.0; _brightness = 0.0
Tuş tespiti: PointerEvent.buttons bitmask'i:
kPrimaryMouseButton(1) = solkSecondaryMouseButton(2) = sağkMiddleMouseButton(4) = orta
Modifier tespiti: HardwareKeyboard.instance.isControlPressed / isShiftPressed
3.2 Rotate — Matrix4 Güncellemesi
Rotate, viewport'un tam merkezi etrafında dönmeli. Matrix4 işlem sırası (son yazılan ilk uygulanır):
Matrix4.identity()
..translate(vpCenterX, vpCenterY) // 4. pivot noktasına geri taşı
..rotateZ(_angleRad) // 3. döndür
..translate(-vpCenterX, -vpCenterY) // 2. pivot'u orijine taşı
..translate(_tx, _ty) // 1. pan offset'i
..scale(_sc); // 0. zoom
_angleRad = _angle * pi / 180
3.3 Contrast ve Brightness — ColorFilter
Image widget'ını ColorFiltered ile saracağız. Hem contrast hem brightness tek bir matrix'te birleştirilebilir:
ColorFiltered(
colorFilter: ColorFilter.matrix(_buildColorMatrix()),
child: imageWidget,
)
Birleşik matrix (contrast × brightness):
Önce brightness, sonra contrast uygulanır:
b = brightness (-1.0 .. 1.0)
c = contrast (0.0 .. 2.0)
t = (1.0 - c) / 2.0
Matrix:
c, 0, 0, 0, t + b,
0, c, 0, 0, t + b,
0, 0, c, 0, t + b,
0, 0, 0, 1, 0,
Default değerler: c = 1.0, b = 0.0 → birim matrix (görüntü değişmez).
3.4 Sensitivity (Hassasiyet)
- Rotate:
_angle += dy * 0.9(100px sürükleme ≈ 90°) - Contrast:
_contrast += dy * 0.005(200px sürükleme = 1.0 birim) - Brightness:
_brightness += dy * 0.005(200px sürükleme = 1.0 birim)
Değerler setState içinde clamp'lenir (rotate hariç, serbest dönüş).
3.5 State Değişkenleri (Eklenecekler)
double _angle = 0; // derece cinsinden, serbest
double _contrast = 1.0; // 0.0 – 2.0
double _brightness = 0.0; // -1.0 – 1.0
// Gesture tracking
String _interactionMode = 'none'; // 'pan' | 'rotate' | 'contrast' | 'brightness'
Offset _pointerDownPos = Offset.zero;
double _startAngle = 0;
double _startContrast = 1.0;
double _startBrightness = 0.0;
4. Riskler ve Dikkat Edilecek Noktalar
| Risk | Etki | Çözüm |
|---|---|---|
| Sağ tık + sürükleme, pan ile çakışabilir | Yanlış moda girme | rotate_pending modunda eşik kontrolü: 5px'den az hareket → rotate, fazla → pan |
| Rotate sonrası pan yönü bozulabilir | Kullanıcı deneyimi | Rotate, pan offset'lerinden (_tx, _ty) önce değil sonra uygulandığı için etkilenmez. Ama fit mode'da letterbox offset'i (m, n) rotate'den etkilenir — test edilecek |
| ColorFilter performansı | Büyük resimlerde takılma | Flutter'da ColorFiltered GPU'da çalışır, performans sorunu beklenmiyor |
| Ctrl/Shift + scroll zoom ile çakışma | Zoom yerine contrast değişebilir | Scroll event'leri onPointerSignal ile ayrı yakalanıyor, karışmaz |
| Fill/Fit mod geçişinde rotate sıfırlanmalı mı? | UX kararı | _toggleFillFit() içinde _angle = 0 yapalım — fill ↔ fit geçişinde rotate sıfırlansın |
5. İmplementasyon Planı
Adım 1: Listener tabanlı gesture sistemine geçiş
GestureDetectoryerineListenerkullanonPointerDown,onPointerMove,onPointerUphandler'larını yaz- Sol tık pan'i koru, sağ tık modlarını ekle
Adım 2: Rotate
_anglestate değişkeni ekleMatrix4'e rotate transform'u ekle_resetView()ve_toggleFillFit()içinde_angle = 0
Adım 3: Contrast ve Brightness
_contrast,_brightnessstate değişkenleri ekleColorFilteredwidget'ı ile image'ı sar_buildColorMatrix()metodunu yaz
Adım 4: Test ve clamp
- Tüm modlarda clamp/değer aralığı kontrolü
- Rotate + zoom + pan birlikte çalışıyor mu test et
6. Etkilenen Dosyalar
| Dosya | Değişiklik |
|---|---|
lib/widgets/image_canvas.dart |
Ana değişikliklerin tümü burada |
| (başka dosya yok) | Diğer widget'lar etkilenmez |