Some checks failed
Build Windows / build-windows (push) Has been cancelled
- 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
188 lines
6.6 KiB
Markdown
188 lines
6.6 KiB
Markdown
# 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 |
|