Skip to content
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
10 changes: 10 additions & 0 deletions lib/core/commands/command_factory/command_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/clipboard_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/delete_region_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/fill_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/text_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/line_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/path_command.dart';
Expand Down Expand Up @@ -136,4 +137,13 @@ class CommandFactory {
Paint(),
region,
);

FillCommand createFillCommand(
Paint paint,
Uint8List imageData,
) =>
FillCommand(
paint,
imageData,
);
}
3 changes: 3 additions & 0 deletions lib/core/commands/command_implementation/command.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:equatable/equatable.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/clipboard_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/delete_region_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/fill_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/ellipse_shape_command.dart';
Expand Down Expand Up @@ -34,6 +35,8 @@ abstract class Command with EquatableMixin {
return DeleteRegionCommand.fromJson(json);
case SerializerType.TEXT_COMMAND:
return TextCommand.fromJson(json);
case SerializerType.FILL_COMMAND:
return FillCommand.fromJson(json);
case SerializerType.HEART_SHAPE_COMMAND:
return HeartShapeCommand.fromJson(json);
case SerializerType.STAR_SHAPE_COMMAND:
Expand Down
68 changes: 68 additions & 0 deletions lib/core/commands/command_implementation/graphic/fill_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// ignore_for_file: must_be_immutable

import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:paintroid/core/commands/command_implementation/graphic/graphic_command.dart';
import 'package:paintroid/core/json_serialization/converter/paint_converter.dart';
import 'package:paintroid/core/json_serialization/converter/uint8list_base64_converter.dart';
import 'package:paintroid/core/json_serialization/versioning/serializer_version.dart';
import 'package:paintroid/core/json_serialization/versioning/version_strategy.dart';

class FillCommand extends GraphicCommand {
final Uint8List imageData;
final int version;
final String type;

ui.Image? _runtimeImage;

FillCommand(
super.paint,
this.imageData, {
int? version,
this.type = SerializerType.FILL_COMMAND,
}) : version =
version ?? VersionStrategyManager.strategy.getFillCommandVersion();

@override
Future<void> prepareForRuntime() async {
if (_runtimeImage != null || imageData.isEmpty) {
return;
}
final buffer = await ui.ImmutableBuffer.fromUint8List(imageData);
final descriptor = await ui.ImageDescriptor.encoded(buffer);
final codec = await descriptor.instantiateCodec();
final frameInfo = await codec.getNextFrame();
_runtimeImage = frameInfo.image;
}

@override
void call(ui.Canvas canvas) {
if (_runtimeImage == null) {
return;
}
canvas.drawImage(_runtimeImage!, ui.Offset.zero, ui.Paint());
}

@override
Map<String, dynamic> toJson() {
return {
'paint': const PaintConverter().toJson(paint),
'imageData': const Uint8ListBase64Converter().toJson(imageData),
'version': version,
'type': type,
};
}

factory FillCommand.fromJson(Map<String, dynamic> json) {
return FillCommand(
const PaintConverter().fromJson(json['paint'] as Map<String, dynamic>),
const Uint8ListBase64Converter().fromJson(json['imageData'] as String),
version: json['version'] as int? ?? Version.v1,
type: json['type'] as String? ?? SerializerType.FILL_COMMAND,
);
}

@override
List<Object?> get props => [paint, imageData, version, type];
}
3 changes: 3 additions & 0 deletions lib/core/commands/command_manager/command_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:ui';

import 'package:paintroid/core/commands/command_implementation/command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/clipboard_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/fill_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/text_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/graphic_command.dart';
import 'package:paintroid/core/commands/command_implementation/graphic/line_command.dart';
Expand Down Expand Up @@ -123,6 +124,8 @@ class CommandManager {
return ToolData.TEXT;
} else if (command.runtimeType == SprayCommand) {
return ToolData.SPRAY;
} else if (command.runtimeType == FillCommand) {
return ToolData.FILL;
} else if (command.runtimeType == StarShapeCommand) {
return ToolData.SHAPES;
} else if (command.runtimeType == HeartShapeCommand) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class SerializerVersion {
static const int SPRAY_COMMAND_VERSION = Version.v1;
static const int CLIPBOARD_COMMAND_VERSION = Version.v1;
static const int DELETE_REGION_COMMAND_VERSION = Version.v1;
static const int FILL_COMMAND_VERSION = Version.v1;
}

class Version {
Expand All @@ -33,4 +34,5 @@ class SerializerType {
static const String SPRAY_COMMAND = 'SprayCommand';
static const String CLIPBOARD_COMMAND = 'ClipboardCommand';
static const String DELETE_REGION_COMMAND = 'DeleteRegionCommand';
static const String FILL_COMMAND = 'FillCommand';
}
5 changes: 5 additions & 0 deletions lib/core/json_serialization/versioning/version_strategy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ abstract class IVersionStrategy {
int getClipboardCommandVersion();

int getDeleteRegionCommandVersion();

int getFillCommandVersion();
}

class ProductionVersionStrategy implements IVersionStrategy {
Expand Down Expand Up @@ -63,6 +65,9 @@ class ProductionVersionStrategy implements IVersionStrategy {
@override
int getDeleteRegionCommandVersion() =>
SerializerVersion.DELETE_REGION_COMMAND_VERSION;

@override
int getFillCommandVersion() => SerializerVersion.FILL_COMMAND_VERSION;
}

class VersionStrategyManager {
Expand Down
52 changes: 52 additions & 0 deletions lib/core/providers/object/tools/fill_tool_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'dart:ui' as ui;

import 'package:flutter_riverpod/flutter_riverpod.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/providers/object/canvas_painter_provider.dart';
import 'package:paintroid/core/providers/state/canvas_state_provider.dart';
import 'package:paintroid/core/tools/implementation/fill_tool.dart';

final fillToolProvider = Provider<FillTool>((ref) {
Future<ui.Image?> sourceImageProvider() async {
final canvasState = ref.read(canvasStateProvider);
final size = canvasState.size;
if (size.width <= 0 || size.height <= 0) {
return null;
}

final recorder = ref.read(graphicFactoryProvider).createPictureRecorder();
final canvas =
ref.read(graphicFactoryProvider).createCanvasWithRecorder(recorder);
final bounds = ui.Rect.fromLTWH(0, 0, size.width, size.height);
canvas.clipRect(bounds);

if (canvasState.backgroundImage != null) {
canvas.drawImage(
canvasState.backgroundImage!, ui.Offset.zero, ui.Paint());
}
if (canvasState.cachedImage != null) {
canvas.drawImage(canvasState.cachedImage!, ui.Offset.zero, ui.Paint());
}

final picture = recorder.endRecording();
return picture.toImage(size.width.toInt(), size.height.toInt());
}

Future<void> onFillApplied() async {
await ref.read(canvasStateProvider.notifier).updateCachedImage();
ref.read(canvasPainterProvider.notifier).repaint();
}

return FillTool(
commandManager: ref.watch(commandManagerProvider),
commandFactory: ref.watch(commandFactoryProvider),
graphicFactory: ref.watch(graphicFactoryProvider),
type: ToolType.FILL,
getSourceImage: sourceImageProvider,
onFillApplied: onFillApplied,
);
});
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 @@ -6,6 +6,7 @@ import 'package:paintroid/core/providers/object/canvas_painter_provider.dart';
import 'package:paintroid/core/providers/object/tools/brush_tool_provider.dart';
import 'package:paintroid/core/providers/object/tools/clipboard_tool_provider.dart';
import 'package:paintroid/core/providers/object/tools/eraser_tool_provider.dart';
import 'package:paintroid/core/providers/object/tools/fill_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';
Expand Down Expand Up @@ -77,6 +78,9 @@ class ToolBoxStateProvider extends _$ToolBoxStateProvider {
state = state.copyWith(currentTool: ref.read(shapesToolProvider));
ref.read(canvasPainterProvider.notifier).repaint();
break;
case ToolType.FILL:
state = state.copyWith(currentTool: ref.read(fillToolProvider));
break;
case ToolType.TEXT:
state = state.copyWith(currentTool: ref.read(textToolProvider));
break;
Expand Down
167 changes: 167 additions & 0 deletions lib/core/tools/implementation/fill_tool.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import 'dart:async';
import 'dart:collection';
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:paintroid/core/commands/graphic_factory/graphic_factory.dart';
import 'package:paintroid/core/tools/tool.dart';

class FillTool extends Tool {
final GraphicFactory graphicFactory;
final Future<ui.Image?> Function() getSourceImage;
final Future<void> Function() onFillApplied;
bool _isFilling = false;

FillTool({
required super.commandFactory,
required super.commandManager,
required super.type,
required this.graphicFactory,
required this.getSourceImage,
required this.onFillApplied,
super.hasAddFunctionality = false,
super.hasFinalizeFunctionality = false,
});

@override
void onDown(ui.Offset point, ui.Paint paint) {
if (_isFilling) {
return;
}
_isFilling = true;
unawaited(_fill(point, paint));
}

Future<void> _fill(ui.Offset point, ui.Paint paint) async {
try {
final sourceImage = await getSourceImage();
if (sourceImage == null) {
return;
}

final width = sourceImage.width;
final height = sourceImage.height;
if (width <= 0 || height <= 0) {
return;
}

final startX = point.dx.floor();
final startY = point.dy.floor();
if (startX < 0 || startX >= width || startY < 0 || startY >= height) {
return;
}

final sourceData =
await sourceImage.toByteData(format: ui.ImageByteFormat.rawRgba);
if (sourceData == null) {
return;
}
final pixels = sourceData.buffer.asUint8List();

final startIndex = (startY * width + startX) * 4;
final sourceR = pixels[startIndex];
final sourceG = pixels[startIndex + 1];
final sourceB = pixels[startIndex + 2];
final sourceA = pixels[startIndex + 3];

final targetColor = paint.color;
final targetR = (targetColor.r * 255).round();
final targetG = (targetColor.g * 255).round();
final targetB = (targetColor.b * 255).round();
final targetA = (targetColor.a * 255).round();

if (sourceR == targetR &&
sourceG == targetG &&
sourceB == targetB &&
sourceA == targetA) {
return;
}

final queue = Queue<int>()..add(startY * width + startX);
while (queue.isNotEmpty) {
final pixelPos = queue.removeFirst();
final x = pixelPos % width;
final y = pixelPos ~/ width;
final index = pixelPos * 4;

if (pixels[index] != sourceR ||
pixels[index + 1] != sourceG ||
pixels[index + 2] != sourceB ||
pixels[index + 3] != sourceA) {
continue;
}

pixels[index] = targetR;
pixels[index + 1] = targetG;
pixels[index + 2] = targetB;
pixels[index + 3] = targetA;

if (x > 0) queue.add(pixelPos - 1);
if (x < width - 1) queue.add(pixelPos + 1);
if (y > 0) queue.add(pixelPos - width);
if (y < height - 1) queue.add(pixelPos + width);
}

final filledImage = await _decodeFromRgba(
Uint8List.fromList(pixels),
width,
height,
);
final pngData =
await filledImage.toByteData(format: ui.ImageByteFormat.png);
if (pngData == null) {
return;
}

final savedPaint = graphicFactory.copyPaint(paint);
final command = commandFactory.createFillCommand(
savedPaint,
pngData.buffer.asUint8List(),
);
await command.prepareForRuntime();
commandManager.addGraphicCommand(command);
await onFillApplied();
} finally {
_isFilling = false;
}
}

Future<ui.Image> _decodeFromRgba(Uint8List rgba, int width, int height) {
final completer = Completer<ui.Image>();
ui.decodeImageFromPixels(
rgba,
width,
height,
ui.PixelFormat.rgba8888,
completer.complete,
);
return completer.future;
}

@override
void onDrag(ui.Offset point, ui.Paint paint) {}

@override
void onUp(ui.Offset point, ui.Paint paint) {}

@override
void onCancel() {
_isFilling = false;
}

@override
void onCheckmark(ui.Paint paint) {}

@override
void onPlus() {}

@override
void onUndo() {
commandManager.undo();
}

@override
void onRedo() {
commandManager.redo();
}
}
Loading
Loading