diff --git a/api/lib/src/models/tool.dart b/api/lib/src/models/tool.dart index 3f9073011007..e22d29405a5e 100644 --- a/api/lib/src/models/tool.dart +++ b/api/lib/src/models/tool.dart @@ -39,6 +39,8 @@ enum ImportType { image, camera, svg, pdf, document, markdown, xopp } enum SelectMode { rectangle, lasso } +enum LaserAnimation { fade, path } + @Freezed(equal: false) sealed class Tool with _$Tool { Tool._(); @@ -123,9 +125,11 @@ sealed class Tool with _$Tool { @Default('') String name, @Default('') String displayIcon, @Default(5) double duration, + @Default(0.5) double hideDuration, @Default(5) double strokeWidth, @Default(0.4) double thinning, @Default(BasicColors.red) int color, + @Default(LaserAnimation.fade) LaserAnimation animation, }) = LaserTool; factory Tool.shape({ diff --git a/api/lib/src/models/tool.freezed.dart b/api/lib/src/models/tool.freezed.dart index 42b200e70385..31ac41864475 100644 --- a/api/lib/src/models/tool.freezed.dart +++ b/api/lib/src/models/tool.freezed.dart @@ -1560,9 +1560,11 @@ abstract class _$$LaserToolImplCopyWith<$Res> implements $ToolCopyWith<$Res> { {String name, String displayIcon, double duration, + double hideDuration, double strokeWidth, double thinning, - int color}); + int color, + LaserAnimation animation}); } /// @nodoc @@ -1581,9 +1583,11 @@ class __$$LaserToolImplCopyWithImpl<$Res> Object? name = null, Object? displayIcon = null, Object? duration = null, + Object? hideDuration = null, Object? strokeWidth = null, Object? thinning = null, Object? color = null, + Object? animation = null, }) { return _then(_$LaserToolImpl( name: null == name @@ -1598,6 +1602,10 @@ class __$$LaserToolImplCopyWithImpl<$Res> ? _value.duration : duration // ignore: cast_nullable_to_non_nullable as double, + hideDuration: null == hideDuration + ? _value.hideDuration + : hideDuration // ignore: cast_nullable_to_non_nullable + as double, strokeWidth: null == strokeWidth ? _value.strokeWidth : strokeWidth // ignore: cast_nullable_to_non_nullable @@ -1610,6 +1618,10 @@ class __$$LaserToolImplCopyWithImpl<$Res> ? _value.color : color // ignore: cast_nullable_to_non_nullable as int, + animation: null == animation + ? _value.animation + : animation // ignore: cast_nullable_to_non_nullable + as LaserAnimation, )); } } @@ -1621,9 +1633,11 @@ class _$LaserToolImpl extends LaserTool { {this.name = '', this.displayIcon = '', this.duration = 5, + this.hideDuration = 0.5, this.strokeWidth = 5, this.thinning = 0.4, this.color = BasicColors.red, + this.animation = LaserAnimation.fade, final String? $type}) : $type = $type ?? 'laser', super._(); @@ -1642,6 +1656,9 @@ class _$LaserToolImpl extends LaserTool { final double duration; @override @JsonKey() + final double hideDuration; + @override + @JsonKey() final double strokeWidth; @override @JsonKey() @@ -1649,13 +1666,16 @@ class _$LaserToolImpl extends LaserTool { @override @JsonKey() final int color; + @override + @JsonKey() + final LaserAnimation animation; @JsonKey(name: 'type') final String $type; @override String toString() { - return 'Tool.laser(name: $name, displayIcon: $displayIcon, duration: $duration, strokeWidth: $strokeWidth, thinning: $thinning, color: $color)'; + return 'Tool.laser(name: $name, displayIcon: $displayIcon, duration: $duration, hideDuration: $hideDuration, strokeWidth: $strokeWidth, thinning: $thinning, color: $color, animation: $animation)'; } /// Create a copy of Tool @@ -1679,9 +1699,11 @@ abstract class LaserTool extends Tool { {final String name, final String displayIcon, final double duration, + final double hideDuration, final double strokeWidth, final double thinning, - final int color}) = _$LaserToolImpl; + final int color, + final LaserAnimation animation}) = _$LaserToolImpl; LaserTool._() : super._(); factory LaserTool.fromJson(Map json) = @@ -1692,9 +1714,11 @@ abstract class LaserTool extends Tool { @override String get displayIcon; double get duration; + double get hideDuration; double get strokeWidth; double get thinning; int get color; + LaserAnimation get animation; /// Create a copy of Tool /// with the given fields replaced by the non-null parameter values. diff --git a/api/lib/src/models/tool.g.dart b/api/lib/src/models/tool.g.dart index 8b9ac0de2bda..2732bb24af43 100644 --- a/api/lib/src/models/tool.g.dart +++ b/api/lib/src/models/tool.g.dart @@ -223,9 +223,13 @@ _$LaserToolImpl _$$LaserToolImplFromJson(Map json) => _$LaserToolImpl( name: json['name'] as String? ?? '', displayIcon: json['displayIcon'] as String? ?? '', duration: (json['duration'] as num?)?.toDouble() ?? 5, + hideDuration: (json['hideDuration'] as num?)?.toDouble() ?? 0.5, strokeWidth: (json['strokeWidth'] as num?)?.toDouble() ?? 5, thinning: (json['thinning'] as num?)?.toDouble() ?? 0.4, color: (json['color'] as num?)?.toInt() ?? BasicColors.red, + animation: + $enumDecodeNullable(_$LaserAnimationEnumMap, json['animation']) ?? + LaserAnimation.fade, $type: json['type'] as String?, ); @@ -234,12 +238,19 @@ Map _$$LaserToolImplToJson(_$LaserToolImpl instance) => 'name': instance.name, 'displayIcon': instance.displayIcon, 'duration': instance.duration, + 'hideDuration': instance.hideDuration, 'strokeWidth': instance.strokeWidth, 'thinning': instance.thinning, 'color': instance.color, + 'animation': _$LaserAnimationEnumMap[instance.animation]!, 'type': instance.$type, }; +const _$LaserAnimationEnumMap = { + LaserAnimation.fade: 'fade', + LaserAnimation.path: 'path', +}; + _$ShapeToolImpl _$$ShapeToolImplFromJson(Map json) => _$ShapeToolImpl( name: json['name'] as String? ?? '', displayIcon: json['displayIcon'] as String? ?? '', diff --git a/app/lib/handlers/laser.dart b/app/lib/handlers/laser.dart index bd2714f188b0..32e1c8d24d8c 100644 --- a/app/lib/handlers/laser.dart +++ b/app/lib/handlers/laser.dart @@ -2,14 +2,17 @@ part of 'handler.dart'; class LaserHandler extends Handler with ColoredHandler { bool _hideCursorWhileDrawing = false; - final Map elements = {}; - final List submittedElements = []; + final Map _elements = {}; + final List _submittedElements = []; DateTime? _lastChanged; Timer? _timer; LaserHandler(super.data); Duration _getDuration() => Duration(milliseconds: (data.duration * 1000).round()); + Duration _getHideDuration() => + Duration(milliseconds: (data.hideDuration * 1000).round()); + Duration _getFullDuration() => _getDuration() + _getHideDuration(); void _startTimer(DocumentBloc bloc) { _lastChanged = DateTime.now(); @@ -18,80 +21,77 @@ class LaserHandler extends Handler with ColoredHandler { final DateTime now = DateTime.now(); // Test if the last change was more than [duration] seconds ago final difference = now.difference(_lastChanged!); - if (difference > _getDuration()) { + if (difference > _getFullDuration()) { _lastChanged = null; - submittedElements.clear(); - elements.clear(); + _submittedElements.clear(); + _elements.clear(); _stopTimer(); } // Fade out the elements - _updateColors(); bloc.refresh(); }); } - void _updateColors() { - final difference = _lastChanged == null - ? Duration.zero - : DateTime.now().difference(_lastChanged!); + PenElement _updateElement(PenElement element, Duration difference) { final duration = _getDuration(); + final hideDuration = _getHideDuration(); + final delta = + ((difference - duration).inMilliseconds / hideDuration.inMilliseconds) + .clamp(0, 1); + if (data.animation == LaserAnimation.path) { + final points = element.points; + final subPoints = + points.sublist((points.length * delta).round(), points.length); + return element.copyWith(points: subPoints); + } var color = Color(data.color); final toolOpacity = color.opacity; - submittedElements.forEachIndexed((index, element) { - var color = Color(element.property.color); - final opacity = - (1 - (difference.inMilliseconds / duration.inMilliseconds)) * - toolOpacity; - color = color.withOpacity(opacity.clamp(0, 1)); - submittedElements[index] = element.copyWith( - property: element.property.copyWith(color: color.value), - ); - }); - // Fade out opacity - final opacity = - (1 - (difference.inMilliseconds / duration.inMilliseconds)) * - toolOpacity; + final opacity = (1 - delta) * toolOpacity; color = color.withOpacity(opacity.clamp(0, 1)); - final colorValue = color.value; - elements.forEach((key, element) { - elements[key] = element.copyWith( - property: element.property.copyWith(color: colorValue), - ); - }); + return element.copyWith( + property: element.property.copyWith(color: color.value), + ); + } + + List _getSubmitted() { + final difference = _lastChanged == null + ? Duration.zero + : DateTime.now().difference(_lastChanged!); + return _submittedElements + .map((e) => _updateElement(e, difference)) + .toList(); } void _stopTimer() { _timer?.cancel(); _timer = null; - _updateColors(); } @override List createForegrounds(CurrentIndexCubit currentIndexCubit, NoteData document, DocumentPage page, DocumentInfo info, [Area? currentArea]) => - elements.values - .map((e) { - if (e.points.length > 1) return PenRenderer(e); - return null; - }) - .whereType() - .toList() - ..addAll(submittedElements.map((e) => PenRenderer(e))); + [ + ..._elements.values.map((e) { + if (e.points.length > 1) return PenRenderer(e); + return null; + }).nonNulls, + ..._getSubmitted().map((e) => PenRenderer(e)) + ]; @override void resetInput(DocumentBloc bloc) { - _submit(bloc, elements.keys.toList()); - elements.clear(); - submittedElements.clear(); + _submit(bloc, _elements.keys.toList()); + _elements.clear(); + _submittedElements.clear(); _stopTimer(); } void _submit(DocumentBloc bloc, List indexes) { final elements = - indexes.map((e) => this.elements.remove(e)).whereNotNull().toList(); + indexes.map((e) => _elements.remove(e)).whereNotNull().toList(); if (elements.isEmpty) return; - submittedElements.addAll(elements); + _submittedElements.addAll(elements); bloc.refresh(); } @@ -117,10 +117,10 @@ class LaserHandler extends Handler with ColoredHandler { if (penOnlyInput && kind != PointerDeviceKind.stylus) { return; } - if (!elements.containsKey(pointer) && !forceCreate) { + if (!_elements.containsKey(pointer) && !forceCreate) { return; } - final element = elements[pointer] ?? + final element = _elements[pointer] ?? PenElement( collection: state.currentCollection, property: PenProperty( @@ -129,7 +129,7 @@ class LaserHandler extends Handler with ColoredHandler { color: data.color), ); - elements[pointer] = element.copyWith( + _elements[pointer] = element.copyWith( points: List.from(element.points) ..add(PathPoint.fromPoint( transform.localToGlobal(localPosition).toPoint(), pressure))); @@ -143,7 +143,7 @@ class LaserHandler extends Handler with ColoredHandler { context.refresh(); final currentIndex = context.getCurrentIndex(); if (currentIndex.moveEnabled && event.kind != PointerDeviceKind.stylus) { - elements.clear(); + _elements.clear(); return; } addPoint(context.buildContext, event.pointer, event.localPosition, @@ -165,7 +165,7 @@ class LaserHandler extends Handler with ColoredHandler { LaserTool setColor(int color) => data.copyWith(color: color); @override - MouseCursor get cursor => (_hideCursorWhileDrawing && elements.isNotEmpty) + MouseCursor get cursor => (_hideCursorWhileDrawing && _elements.isNotEmpty) ? SystemMouseCursors.none : SystemMouseCursors.precise; } diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index db6f8abe4426..c9b095d69f3b 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -636,5 +636,6 @@ }, "colorToolbar": "Color toolbar", "yesButShowButtons": "Yes, but show buttons", - "optionsPanelPosition": "Options panel position" + "optionsPanelPosition": "Options panel position", + "hideDuration": "Hide duration" } diff --git a/app/lib/renderers/elements/path.dart b/app/lib/renderers/elements/path.dart index 0d83f7e8f740..ddefb85cf32d 100644 --- a/app/lib/renderers/elements/path.dart +++ b/app/lib/renderers/elements/path.dart @@ -34,6 +34,7 @@ abstract class PathRenderer extends Renderer { NoteData document, AssetService assetService, DocumentPage page) { final current = element as PathElement; final points = current.points; + if (points.isEmpty) return null; final property = current.property; var topLeftCorner = points.first.toOffset(); var bottomRightCorner = points.first.toOffset(); @@ -68,8 +69,8 @@ abstract class PathRenderer extends Renderer { [ColorScheme? colorScheme, bool foreground = false]) { final current = element as PathElement; final points = current.points; - final paint = buildPaint(page, foreground); if (points.isEmpty) return; + final paint = buildPaint(page, foreground); if (paint.style == PaintingStyle.fill) { final path = Path(); final first = points.first; diff --git a/app/lib/selections/tools/laser.dart b/app/lib/selections/tools/laser.dart index 231a8c449fbb..3cd26f9b6c6b 100644 --- a/app/lib/selections/tools/laser.dart +++ b/app/lib/selections/tools/laser.dart @@ -49,6 +49,42 @@ class LaserToolSelection extends ToolSelection { selected.map((e) => e.copyWith(duration: value)).toList(), ), ), + ExactSlider( + value: selected.first.hideDuration, + min: 1, + max: 20, + defaultValue: 5, + header: Text(AppLocalizations.of(context).hideDuration), + onChangeEnd: (value) => update( + context, + selected.map((e) => e.copyWith(hideDuration: value)).toList(), + ), + ), + ListTile( + title: Text(AppLocalizations.of(context).shape), + trailing: DropdownButton( + items: LaserAnimation.values + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(switch (e) { + LaserAnimation.fade => AppLocalizations.of(context).color, + LaserAnimation.path => AppLocalizations.of(context).path, + }), + ), + ) + .toList(), + value: selected.first.animation, + onChanged: (LaserAnimation? value) { + if (value != null) { + update( + context, + selected.map((e) => e.copyWith(animation: value)).toList(), + ); + } + }, + ), + ), const SizedBox(height: 15), ]; } diff --git a/metadata/en-US/changelogs/123.txt b/metadata/en-US/changelogs/123.txt index 0437a01c68b9..8ad4f1f1fc1f 100644 --- a/metadata/en-US/changelogs/123.txt +++ b/metadata/en-US/changelogs/123.txt @@ -2,6 +2,8 @@ * Add save button indicator for autosave ([#757](https://github.com/LinwoodDev/Butterfly/issues/757)) * Add duplicate layer button * Add tool options panel position +* Separate laser duration in normal duration and hide duration +* Add path laser animation * Use long press to move tools on all platforms to improve desktop touch behavior * Use sha checksum for assets * Separate personalization settings in new view settings