Skip to content

PAINTROID-793 implement watercolor tool #124

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions lib/core/commands/command_manager/command_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:paintroid/core/commands/command_implementation/command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/graphic_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/line_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/path_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/shape/circle_shape_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/shape/square_shape_command.dart';
import 'package:paintroid/core/tools/line_tool/vertex.dart';
Expand Down Expand Up @@ -96,23 +97,30 @@ class CommandManager {
Command? command;
switch (actionType) {
case ActionType.UNDO:
if (_undoStack.isEmpty) return ToolData.BRUSH;
command = _undoStack.last;
break;
case ActionType.REDO:
if (_redoStack.isEmpty) return ToolData.BRUSH;
command = _redoStack.last;
break;
}

///TODO implement for all tools after implementing unique commands
if (command.runtimeType == LineCommand) {
if (command is LineCommand) {
return ToolData.LINE;
} else if (command.runtimeType == SquareShapeCommand) {
} else if (command is SquareShapeCommand) {
return ToolData.SHAPES;
} else if (command.runtimeType == CircleShapeCommand) {
} else if (command is CircleShapeCommand) {
return ToolData.SHAPES;
}
else if (command.runtimeType == SprayCommand) {
} else if (command is SprayCommand) {
return ToolData.SPRAY;
} else if (command is PathCommand) {
if (command.paint.maskFilter != null) {
return ToolData.WATERCOLOR;
} else {
return ToolData.BRUSH;
}
} else {
return ToolData.BRUSH;
}
Expand Down
16 changes: 14 additions & 2 deletions lib/core/commands/command_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:paintroid/core/commands/command_manager/command_manager.dart';
import 'package:paintroid/core/commands/command_manager/command_manager_provider.dart';
import 'package:paintroid/core/enums/tool_types.dart';
import 'package:paintroid/core/providers/state/canvas_state_provider.dart';
import 'package:paintroid/core/providers/state/paint_provider.dart';
import 'package:paintroid/core/providers/state/toolbox_state_provider.dart';
import 'package:paintroid/core/tools/implementation/brush_tool.dart';
import 'package:paintroid/core/tools/implementation/shapes_tool/shapes_tool.dart';
import 'package:paintroid/core/tools/line_tool/line_tool.dart';
import 'package:paintroid/core/tools/tool.dart';

class CommandPainter extends CustomPainter {
Tool currentTool;
CommandManager commandManager;
bool isCachingCommand;

CommandPainter(this.ref)
: currentTool = ref.read(toolBoxStateProvider).currentTool,
commandManager = ref.read(commandManagerProvider);
commandManager = ref.read(commandManagerProvider),
isCachingCommand = ref.read(
canvasStateProvider.select((state) => state.isCachingCommand));

final WidgetRef ref;

Expand All @@ -33,7 +39,13 @@ class CommandPainter extends CustomPainter {
..drawGuides(canvas);
break;
default:
commandManager.executeLastCommand(canvas);
if (currentTool is BrushTool) {
if ((currentTool as BrushTool).isDrawing || isCachingCommand) {
commandManager.executeLastCommand(canvas);
}
} else {
commandManager.executeLastCommand(canvas);
}
break;
}
}
Expand Down
11 changes: 11 additions & 0 deletions lib/core/commands/graphic_factory/graphic_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ class GraphicFactory {

Canvas createCanvasWithRecorder(PictureRecorder recorder) => Canvas(recorder);

Paint createWatercolorPaint(Paint originalPaint, double blurSigma) {
return Paint()
..color = originalPaint.color
..strokeCap = originalPaint.strokeCap
..strokeWidth = originalPaint.strokeWidth
..style = originalPaint.style
..blendMode = originalPaint.blendMode
..isAntiAlias = true
..maskFilter = MaskFilter.blur(BlurStyle.inner, blurSigma);
}

Paint copyPaint(Paint original) {
return Paint()
..blendMode = original.blendMode
Expand Down
10 changes: 10 additions & 0 deletions lib/core/json_serialization/converter/paint_converter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ class PaintConverter implements JsonConverter<Paint, Map<String, dynamic>> {
paint.style = PaintingStyle.values[json['style']];
paint.strokeJoin = StrokeJoin.values[json['strokeJoin']];
paint.blendMode = BlendMode.values[json['blendMode']];
final num? blurSigmaNum = json['maskFilterSigma'] as num?;
if (blurSigmaNum != null) {
paint.maskFilter =
MaskFilter.blur(BlurStyle.inner, blurSigmaNum.toDouble());
}
}
if (version >= Version.v2) {
// paint.newAttribute = json['newAttribute'];
Expand All @@ -42,6 +47,11 @@ class PaintConverter implements JsonConverter<Paint, Map<String, dynamic>> {
json['strokeJoin'] = paint.strokeJoin.index;
json['blendMode'] = paint.blendMode.index;
}
if (paint.maskFilter != null) {
json['maskFilterSigma'] = 20.0;
} else {
json['maskFilterSigma'] = null;
}
if (SerializerVersion.PAINT_VERSION >= Version.v2) {
// json['newAttribute'] = paint.newAttribute;
}
Expand Down
22 changes: 22 additions & 0 deletions lib/core/providers/object/tools/watercolor_tool_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'package:paintroid/core/commands/command_factory/command_factory_provider.dart';
import 'package:paintroid/core/commands/command_manager/command_manager_provider.dart';
import 'package:paintroid/core/commands/graphic_factory/graphic_factory_provider.dart';
import 'package:paintroid/core/enums/tool_types.dart';
import 'package:paintroid/core/tools/implementation/watercolor_tool.dart';

part 'watercolor_tool_provider.g.dart';

@riverpod
class WatercolorToolProvider extends _$WatercolorToolProvider {
@override
WatercolorTool build() {
return WatercolorTool(
commandManager: ref.watch(commandManagerProvider),
commandFactory: ref.watch(commandFactoryProvider),
graphicFactory: ref.watch(graphicFactoryProvider),
type: ToolType.WATERCOLOR,
);
}
}
27 changes: 27 additions & 0 deletions lib/core/providers/object/tools/watercolor_tool_provider.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/core/providers/state/canvas_state_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ class CanvasStateData with _$CanvasStateData {
required Size size,
required CommandManager commandManager,
required GraphicFactory graphicFactory,
@Default(false) bool isCachingCommand,
}) = _CanvasStateData;
}
36 changes: 29 additions & 7 deletions lib/core/providers/state/canvas_state_data.freezed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mixin _$CanvasStateData {
ui.Size get size => throw _privateConstructorUsedError;
CommandManager get commandManager => throw _privateConstructorUsedError;
GraphicFactory get graphicFactory => throw _privateConstructorUsedError;
bool get isCachingCommand => throw _privateConstructorUsedError;

/// Create a copy of CanvasStateData
/// with the given fields replaced by the non-null parameter values.
Expand All @@ -40,7 +41,8 @@ abstract class $CanvasStateDataCopyWith<$Res> {
ui.Image? cachedImage,
ui.Size size,
CommandManager commandManager,
GraphicFactory graphicFactory});
GraphicFactory graphicFactory,
bool isCachingCommand});
}

/// @nodoc
Expand All @@ -63,6 +65,7 @@ class _$CanvasStateDataCopyWithImpl<$Res, $Val extends CanvasStateData>
Object? size = null,
Object? commandManager = null,
Object? graphicFactory = null,
Object? isCachingCommand = null,
}) {
return _then(_value.copyWith(
backgroundImage: freezed == backgroundImage
Expand All @@ -85,6 +88,10 @@ class _$CanvasStateDataCopyWithImpl<$Res, $Val extends CanvasStateData>
? _value.graphicFactory
: graphicFactory // ignore: cast_nullable_to_non_nullable
as GraphicFactory,
isCachingCommand: null == isCachingCommand
? _value.isCachingCommand
: isCachingCommand // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
Expand All @@ -102,7 +109,8 @@ abstract class _$$CanvasStateDataImplCopyWith<$Res>
ui.Image? cachedImage,
ui.Size size,
CommandManager commandManager,
GraphicFactory graphicFactory});
GraphicFactory graphicFactory,
bool isCachingCommand});
}

/// @nodoc
Expand All @@ -123,6 +131,7 @@ class __$$CanvasStateDataImplCopyWithImpl<$Res>
Object? size = null,
Object? commandManager = null,
Object? graphicFactory = null,
Object? isCachingCommand = null,
}) {
return _then(_$CanvasStateDataImpl(
backgroundImage: freezed == backgroundImage
Expand All @@ -145,6 +154,10 @@ class __$$CanvasStateDataImplCopyWithImpl<$Res>
? _value.graphicFactory
: graphicFactory // ignore: cast_nullable_to_non_nullable
as GraphicFactory,
isCachingCommand: null == isCachingCommand
? _value.isCachingCommand
: isCachingCommand // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
Expand All @@ -157,7 +170,8 @@ class _$CanvasStateDataImpl implements _CanvasStateData {
this.cachedImage,
required this.size,
required this.commandManager,
required this.graphicFactory});
required this.graphicFactory,
this.isCachingCommand = false});

@override
final ui.Image? backgroundImage;
Expand All @@ -169,10 +183,13 @@ class _$CanvasStateDataImpl implements _CanvasStateData {
final CommandManager commandManager;
@override
final GraphicFactory graphicFactory;
@override
@JsonKey()
final bool isCachingCommand;

@override
String toString() {
return 'CanvasStateData(backgroundImage: $backgroundImage, cachedImage: $cachedImage, size: $size, commandManager: $commandManager, graphicFactory: $graphicFactory)';
return 'CanvasStateData(backgroundImage: $backgroundImage, cachedImage: $cachedImage, size: $size, commandManager: $commandManager, graphicFactory: $graphicFactory, isCachingCommand: $isCachingCommand)';
}

@override
Expand All @@ -188,12 +205,14 @@ class _$CanvasStateDataImpl implements _CanvasStateData {
(identical(other.commandManager, commandManager) ||
other.commandManager == commandManager) &&
(identical(other.graphicFactory, graphicFactory) ||
other.graphicFactory == graphicFactory));
other.graphicFactory == graphicFactory) &&
(identical(other.isCachingCommand, isCachingCommand) ||
other.isCachingCommand == isCachingCommand));
}

@override
int get hashCode => Object.hash(runtimeType, backgroundImage, cachedImage,
size, commandManager, graphicFactory);
size, commandManager, graphicFactory, isCachingCommand);

/// Create a copy of CanvasStateData
/// with the given fields replaced by the non-null parameter values.
Expand All @@ -211,7 +230,8 @@ abstract class _CanvasStateData implements CanvasStateData {
final ui.Image? cachedImage,
required final ui.Size size,
required final CommandManager commandManager,
required final GraphicFactory graphicFactory}) = _$CanvasStateDataImpl;
required final GraphicFactory graphicFactory,
final bool isCachingCommand}) = _$CanvasStateDataImpl;

@override
ui.Image? get backgroundImage;
Expand All @@ -223,6 +243,8 @@ abstract class _CanvasStateData implements CanvasStateData {
CommandManager get commandManager;
@override
GraphicFactory get graphicFactory;
@override
bool get isCachingCommand;

/// Create a copy of CanvasStateData
/// with the given fields replaced by the non-null parameter values.
Expand Down
9 changes: 6 additions & 3 deletions lib/core/providers/state/canvas_state_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class CanvasStateProvider extends _$CanvasStateProvider {
size: initialCanvasSize,
commandManager: ref.watch(commandManagerProvider),
graphicFactory: ref.watch(graphicFactoryProvider),
isCachingCommand: false,
);
}

Expand All @@ -41,6 +42,7 @@ class CanvasStateProvider extends _$CanvasStateProvider {
);

Future<void> updateCachedImage() async {
state = state.copyWith(isCachingCommand: true);
final recorder = state.graphicFactory.createPictureRecorder();
final canvas = state.graphicFactory.createCanvasWithRecorder(recorder);
final size = state.size;
Expand All @@ -58,14 +60,14 @@ class CanvasStateProvider extends _$CanvasStateProvider {
state.commandManager.executeLastCommand(canvas);
final picture = recorder.endRecording();
final img = await picture.toImage(size.width.toInt(), size.height.toInt());
state = state.copyWith(cachedImage: img);
state = state.copyWith(cachedImage: img, isCachingCommand: false);
}

Future<void> resetCanvasWithNewCommands(Iterable<Command> commands) async {
state.commandManager.clearRedoStack();
state.commandManager.clearUndoStack(newCommands: commands);
if (commands.isEmpty) {
state = state.copyWith(cachedImage: null);
state = state.copyWith(cachedImage: null, isCachingCommand: false);
} else {
await _executeAllCommandsOnCanvas();
}
Expand All @@ -76,13 +78,14 @@ class CanvasStateProvider extends _$CanvasStateProvider {
}

Future<void> _executeAllCommandsOnCanvas() async {
state = state.copyWith(isCachingCommand: true);
final recorder = state.graphicFactory.createPictureRecorder();
final canvas = state.graphicFactory.createCanvasWithRecorder(recorder);
final size = state.size;
canvas.clipRect(Rect.fromLTWH(0, 0, size.width, size.height));
state.commandManager.executeAllCommands(canvas);
final picture = recorder.endRecording();
final img = await picture.toImage(size.width.toInt(), size.height.toInt());
state = state.copyWith(cachedImage: img);
state = state.copyWith(cachedImage: img, isCachingCommand: false);
}
}
2 changes: 1 addition & 1 deletion lib/core/providers/state/canvas_state_provider.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions lib/core/providers/state/toolbox_state_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:paintroid/core/providers/object/tools/eraser_tool_provider.dart'
import 'package:paintroid/core/providers/object/tools/hand_tool_provider.dart';
import 'package:paintroid/core/providers/object/tools/line_tool_provider.dart';
import 'package:paintroid/core/providers/object/tools/shapes_tool_provider.dart';
import 'package:paintroid/core/providers/object/tools/watercolor_tool_provider.dart';
import 'package:paintroid/core/providers/state/paint_provider.dart';
import 'package:paintroid/core/providers/state/spray_tool_provider.dart';
import 'package:paintroid/core/providers/state/toolbox_state_data.dart';
Expand Down Expand Up @@ -58,6 +59,9 @@ class ToolBoxStateProvider extends _$ToolBoxStateProvider {
case ToolType.BRUSH:
state = state.copyWith(currentTool: ref.read(brushToolProvider));
break;
case ToolType.WATERCOLOR:
state = state.copyWith(currentTool: ref.read(watercolorToolProvider));
break;
case ToolType.HAND:
state = state.copyWith(currentTool: ref.read(handToolProvider));
break;
Expand Down
2 changes: 1 addition & 1 deletion lib/core/providers/state/toolbox_state_provider.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading