feat: right-click contrast, Ctrl+saturation, Shift+rotate filters
Some checks failed
Build Windows / build-windows (push) Has been cancelled
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
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
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;
|
||||
@@ -15,10 +17,27 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
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 (_isFilled) {
|
||||
// Fill mode: viewport tabanli clamp
|
||||
final double w = _vpSize.width * _sc;
|
||||
final double h = _vpSize.height * _sc;
|
||||
if (w >= _vpSize.width) {
|
||||
@@ -31,7 +50,6 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
} else { if (_ty != 0) _ty = 0; }
|
||||
return;
|
||||
}
|
||||
// Fit mode: resmin gercek boyutuna gore clamp (letterbox dahil)
|
||||
double m = 0, n = 0;
|
||||
double w = _vpSize.width * _sc;
|
||||
double h = _vpSize.height * _sc;
|
||||
@@ -77,17 +95,60 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
}
|
||||
}
|
||||
|
||||
@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; });
|
||||
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; });
|
||||
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) {
|
||||
@@ -113,6 +174,95 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
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';
|
||||
}
|
||||
|
||||
// ── Color filters ──
|
||||
|
||||
static const double _rLum = 0.2126;
|
||||
static const double _gLum = 0.7152;
|
||||
static const double _bLum = 0.0722;
|
||||
|
||||
ColorFilter _buildSaturationFilter() {
|
||||
final s = _saturation;
|
||||
final invS = 1.0 - s;
|
||||
return ColorFilter.matrix(<double>[
|
||||
invS * _rLum + s, invS * _gLum, invS * _bLum, 0, 0,
|
||||
invS * _rLum, invS * _gLum + s, invS * _bLum, 0, 0,
|
||||
invS * _rLum, invS * _gLum, invS * _bLum + s, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
}
|
||||
|
||||
ColorFilter _buildContrastFilter() {
|
||||
final c = _contrast;
|
||||
final t = (1.0 - c) / 2.0;
|
||||
return ColorFilter.matrix(<double>[
|
||||
c, 0, 0, 0, t,
|
||||
0, c, 0, 0, t,
|
||||
0, 0, c, 0, t,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildImage() {
|
||||
return Image.file(
|
||||
key: _imageKey,
|
||||
@@ -129,25 +279,42 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
builder: (context, constraints) {
|
||||
_vpSize = constraints.biggest;
|
||||
_clamp();
|
||||
|
||||
final vpCenterX = _vpSize.width / 2;
|
||||
final vpCenterY = _vpSize.height / 2;
|
||||
final angleRad = _angle * math.pi / 180;
|
||||
|
||||
return SizedBox.expand(
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRect(
|
||||
child: GestureDetector(
|
||||
onDoubleTap: _handleDoubleTap,
|
||||
onPanUpdate: _onPanUpdate,
|
||||
child: Listener(
|
||||
onPointerSignal: (event) {
|
||||
if (event is PointerScrollEvent) _handleScroll(event);
|
||||
},
|
||||
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(vpCenterX, vpCenterY)
|
||||
..rotateZ(angleRad)
|
||||
..translate(-vpCenterX, -vpCenterY)
|
||||
..translate(_tx, _ty)
|
||||
..scale(_sc),
|
||||
child: _isFilled
|
||||
? SizedBox.expand(child: _buildImage())
|
||||
: Center(child: _buildImage()),
|
||||
child: ColorFiltered(
|
||||
colorFilter: _buildSaturationFilter(),
|
||||
child: ColorFiltered(
|
||||
colorFilter: _buildContrastFilter(),
|
||||
child: _isFilled
|
||||
? SizedBox.expand(child: _buildImage())
|
||||
: Center(child: _buildImage()),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user