import 'dart:io'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; class ImageCanvas extends StatefulWidget { final String filePath; const ImageCanvas({super.key, required this.filePath}); @override State createState() => _ImageCanvasState(); } class _ImageCanvasState extends State { bool _isFilled = true; final GlobalKey _imageKey = GlobalKey(); double _tx = 0, _ty = 0, _sc = 1.0; Size _vpSize = Size.zero; // ── Görüntü ayarları ── double _angle = 0; double _contrast = 1.0; double _saturation = 1.0; // ── Gesture tracking ── String _interactionMode = 'none'; // 'contrast' | 'rotate' | 'saturation' Offset _pointerDownPos = Offset.zero; double _startAngle = 0; double _startContrast = 1.0; double _startSaturation = 1.0; DateTime? _lastRightTapTime; Offset _lastRightTapPos = Offset.zero; // ── Klavye modifier tracking ── bool _ctrlPressed = false; bool _shiftPressed = false; void _clamp() { if (_vpSize.width <= 0 || _vpSize.height <= 0) return; if (_angle != 0) return; // rotate varsa serbest pan if (_isFilled) { final double w = _vpSize.width * _sc; final double h = _vpSize.height * _sc; if (w >= _vpSize.width) { if (_tx > 0) _tx = 0; else if (_tx + w < _vpSize.width) _tx = _vpSize.width - w; } else { if (_tx != 0) _tx = 0; } if (h >= _vpSize.height) { if (_ty > 0) _ty = 0; else if (_ty + h < _vpSize.height) _ty = _vpSize.height - h; } else { if (_ty != 0) _ty = 0; } return; } double m = 0, n = 0; double w = _vpSize.width * _sc; double h = _vpSize.height * _sc; final box = _imageKey.currentContext?.findRenderObject() as RenderBox?; if (box != null && box.hasSize && box.size.width > 0) { final Size img = box.size; m = (_vpSize.width - img.width) / 2; n = (_vpSize.height - img.height) / 2; w = img.width * _sc; h = img.height * _sc; } if (w >= _vpSize.width) { final double lo = -m * _sc; final double hi = _vpSize.width - m * _sc - w; if (lo <= hi) { if (_tx < lo) _tx = lo; if (_tx > hi) _tx = hi; } else { if (_tx < hi) _tx = hi; if (_tx > lo) _tx = lo; } } else { final double lo = -m * _sc; final double hi = _vpSize.width - m * _sc - w; if (_tx < lo) _tx = lo; if (_tx > hi) _tx = hi; } if (h >= _vpSize.height) { final double lo = -n * _sc; final double hi = _vpSize.height - n * _sc - h; if (lo <= hi) { if (_ty < lo) _ty = lo; if (_ty > hi) _ty = hi; } else { if (_ty < hi) _ty = hi; if (_ty > lo) _ty = lo; } } else { final double lo = -n * _sc; final double hi = _vpSize.height - n * _sc - h; if (_ty < lo) _ty = lo; if (_ty > hi) _ty = hi; } } @override void initState() { super.initState(); HardwareKeyboard.instance.addHandler(_onHardwareKey); } @override void dispose() { HardwareKeyboard.instance.removeHandler(_onHardwareKey); super.dispose(); } bool _onHardwareKey(KeyEvent event) { final key = event.logicalKey; if (event is KeyDownEvent) { if (key == LogicalKeyboardKey.controlLeft || key == LogicalKeyboardKey.controlRight) { _ctrlPressed = true; } else if (key == LogicalKeyboardKey.shiftLeft || key == LogicalKeyboardKey.shiftRight) { _shiftPressed = true; } } else if (event is KeyUpEvent) { if (key == LogicalKeyboardKey.controlLeft || key == LogicalKeyboardKey.controlRight) { _ctrlPressed = false; } else if (key == LogicalKeyboardKey.shiftLeft || key == LogicalKeyboardKey.shiftRight) { _shiftPressed = false; } } return false; } @override void didUpdateWidget(ImageCanvas old) { super.didUpdateWidget(old); if (old.filePath != widget.filePath) _resetView(); } void _resetView() { setState(() { _tx = 0; _ty = 0; _sc = 1.0; _isFilled = true; _vpSize = Size.zero; _angle = 0; _contrast = 1.0; _saturation = 1.0; }); } void _toggleFillFit() { setState(() { _isFilled = !_isFilled; _tx = 0; _ty = 0; _sc = 1.0; _vpSize = Size.zero; _angle = 0; _contrast = 1.0; _saturation = 1.0; }); } void _handleDoubleTap() => _toggleFillFit(); void _handleScroll(PointerScrollEvent e) { if (_vpSize.width <= 0 || _vpSize.height <= 0) return; final double vx = _angle == 0 ? e.localPosition.dx : _vpSize.width / 2; final double vy = _angle == 0 ? e.localPosition.dy : _vpSize.height / 2; final double cx = (vx - _tx) / _sc; final double cy = (vy - _ty) / _sc; const double factor = 1.1; final double rawSc = _sc * (e.scrollDelta.dy < 0 ? factor : 1.0 / factor); final double newSc = rawSc.clamp(1.0, 10.0); if (_angle == 0) { _tx = _tx + cx * _sc - cx * newSc; _ty = _ty + cy * _sc - cy * newSc; } else { // Rotate'li zoom: viewport merkezini koru final double dw = _vpSize.width * (newSc - _sc); final double dh = _vpSize.height * (newSc - _sc); _tx -= dw / 2; _ty -= dh / 2; } _sc = newSc; _clamp(); setState(() {}); } void _onPanUpdate(DragUpdateDetails d) { _tx += d.delta.dx; _ty += d.delta.dy; _clamp(); setState(() {}); } void _onPointerDown(PointerDownEvent e) { if (e.buttons != kSecondaryMouseButton) return; _pointerDownPos = e.localPosition; final now = DateTime.now(); if (_lastRightTapTime != null && now.difference(_lastRightTapTime!) < const Duration(milliseconds: 400) && (e.localPosition - _lastRightTapPos).distance < 20) { setState(() { _angle = 0; _contrast = 1.0; _saturation = 1.0; _interactionMode = 'none'; }); _lastRightTapTime = null; return; } _lastRightTapTime = now; _lastRightTapPos = e.localPosition; if (_ctrlPressed) { _interactionMode = 'saturation'; _startSaturation = _saturation; } else if (_shiftPressed) { _interactionMode = 'rotate'; _startAngle = _angle; } else { _interactionMode = 'contrast'; _startContrast = _contrast; } } void _onPointerMove(PointerMoveEvent e) { if (e.buttons != kSecondaryMouseButton) return; final dy = e.localPosition.dy - _pointerDownPos.dy; switch (_interactionMode) { case 'contrast': setState(() { _contrast = (_startContrast + dy * 0.005).clamp(0.0, 2.0); }); break; case 'rotate': setState(() { _angle = _startAngle + dy * 0.9; }); break; case 'saturation': setState(() { _saturation = (_startSaturation + dy * 0.005).clamp(0.0, 2.0); }); break; } } void _onPointerUp(PointerUpEvent e) { _interactionMode = 'none'; } // ── Combined color filter (saturation × contrast) ── static const double _rLum = 0.2126; static const double _gLum = 0.7152; static const double _bLum = 0.0722; ColorFilter? _cachedFilter; double _cachedSat = -1; double _cachedCon = -1; ColorFilter _buildCombinedFilter() { if (_cachedFilter != null && _cachedSat == _saturation && _cachedCon == _contrast) { return _cachedFilter!; } _cachedSat = _saturation; _cachedCon = _contrast; final s = _saturation; final c = _contrast; final t = (1.0 - c) / 2.0; final invS = 1.0 - s; 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 sB_R = invS * _rLum, sB_G = invS * _gLum, sB_B = invS * _bLum + s; _cachedFilter = ColorFilter.matrix([ c * sR_R, c * sR_G, c * sR_B, 0, t, c * sG_R, c * sG_G, c * sG_B, 0, t, c * sB_R, c * sB_G, c * sB_B, 0, t, 0, 0, 0, 1, 0, ]); return _cachedFilter!; } Widget _buildImage() { return Image.file( key: _imageKey, File(widget.filePath), fit: _isFilled ? BoxFit.cover : BoxFit.contain, filterQuality: FilterQuality.high, errorBuilder: (_, _, _) => _errorWidget(), ); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { _vpSize = constraints.biggest; _clamp(); final vpCenterX = _vpSize.width / 2; final vpCenterY = _vpSize.height / 2; final angleRad = _angle * math.pi / 180; Widget imageContent = Builder( builder: (context) { final hasFilter = _saturation != 1.0 || _contrast != 1.0; final image = _isFilled ? SizedBox.expand(child: _buildImage()) : Center(child: _buildImage()); return hasFilter ? ColorFiltered(colorFilter: _buildCombinedFilter(), child: image) : image; }, ); if (_angle != 0) { imageContent = Transform( alignment: Alignment.topLeft, transform: Matrix4.identity() ..translate(vpCenterX, vpCenterY) ..rotateZ(angleRad) ..translate(-vpCenterX, -vpCenterY), child: imageContent, ); } return SizedBox.expand( child: Stack( children: [ ClipRect( child: Listener( onPointerDown: _onPointerDown, onPointerMove: _onPointerMove, onPointerUp: _onPointerUp, onPointerSignal: (event) { if (event is PointerScrollEvent) _handleScroll(event); }, child: GestureDetector( onDoubleTap: _handleDoubleTap, onPanUpdate: _onPanUpdate, child: Transform( alignment: Alignment.topLeft, transform: Matrix4.identity() ..translate(_tx, _ty) ..scale(_sc), child: imageContent, ), ), ), ), ], ), ); }, ); } Widget _errorWidget() { return const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.broken_image, size: 48, color: Colors.white38), SizedBox(height: 8), Text('Resim yüklenemedi', style: TextStyle(color: Colors.white38)), ], ), ); } }