# 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:** `Matrix4` ile sadece `translate(_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) = sol - `kSecondaryMouseButton` (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): ```dart 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: ```dart 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) ```dart 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ş - `GestureDetector` yerine `Listener` kullan - `onPointerDown`, `onPointerMove`, `onPointerUp` handler'larını yaz - Sol tık pan'i koru, sağ tık modlarını ekle ### Adım 2: Rotate - `_angle` state değişkeni ekle - `Matrix4`'e rotate transform'u ekle - `_resetView()` ve `_toggleFillFit()` içinde `_angle = 0` ### Adım 3: Contrast ve Brightness - `_contrast`, `_brightness` state değişkenleri ekle - `ColorFiltered` widget'ı 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 |