Files
imajviewer/docs/image-canvas-interactions.md
Alhan 35d3bc3749
Some checks failed
Build Windows / build-windows (push) Has been cancelled
feat: right-click contrast, Ctrl+saturation, Shift+rotate filters
- 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
2026-07-27 19:39:54 +03:00

188 lines
6.6 KiB
Markdown
Raw Permalink 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.
# 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 |