This commit is contained in:
Naser Elziadna
2024-12-22 19:53:22 +02:00
parent ba671710b8
commit 655ccdfd63
13 changed files with 335 additions and 133 deletions

View File

@@ -138,7 +138,7 @@ class CanvasNotifier extends _$CanvasNotifier {
Future<ui.Image> canvasToImage(GlobalKey globalKey) async {
final boundary =
globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
return await boundary.toImage();
return await boundary.toImage(pixelRatio:5);
}
Future<void> saveToGallery(GlobalKey globalKey) async {

View File

@@ -1,33 +1,65 @@
import 'dart:ui';
import 'package:doddle/domain/models/effects/pen_effect.dart';
import 'package:doddle/domain/models/point.dart';
import 'package:flutter/material.dart';
class RainbowStroke {
final Offset position;
final Color color;
final double size;
RainbowStroke({
required this.position,
required this.color,
required this.size,
});
}
class NormalWithShaderEffect extends PenEffect {
final List<RainbowStroke> strokes = [];
int colorIndex = 0;
static const List<Color> rainbowColors = [
Color.fromARGB(255, 255, 0, 0), // Red
Color.fromARGB(255, 255, 127, 0), // Orange
Color.fromARGB(255, 255, 255, 0), // Yellow
Color.fromARGB(255, 0, 255, 0), // Green
Color.fromARGB(255, 0, 0, 255), // Blue
Color.fromARGB(255, 75, 0, 130), // Indigo
Color.fromARGB(255, 148, 0, 211), // Violet
];
@override
void paint(Canvas canvas, Path path, Paint paint) {
canvas.drawPath(
path,
Paint()
..shader = sweepShader
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke
..strokeWidth = drawController.penSize!,
);
for (var stroke in strokes) {
final positions = getSymmetricalPositions(stroke.position);
canvas.drawPoints(PointMode.lines, positions,
Paint()
..color = stroke.color
..style = PaintingStyle.stroke
..strokeWidth = stroke.size
..strokeCap = StrokeCap.round
);
}
}
static final SweepGradient colorWheelGradient = SweepGradient(
center: Alignment.bottomRight,
colors: [
Color.fromARGB(255, 255, 0, 0),
Color.fromARGB(255, 255, 255, 0),
Color.fromARGB(255, 0, 255, 0),
Color.fromARGB(255, 0, 255, 255),
Color.fromARGB(255, 0, 0, 255),
Color.fromARGB(255, 255, 0, 255),
Color.fromARGB(255, 255, 0, 0),
]);
// If we create a shader from the above SweepGraident, we get
// a crash on web, but only on web.
static final Shader sweepShader =
colorWheelGradient.createShader(const Rect.fromLTWH(0, 0, 100, 10));
@override
void onPointAdd(Point point) {
if (point.offset == null) return;
strokes.add(RainbowStroke(
position: point.offset!,
color: rainbowColors[colorIndex],
size: drawController.penSize ?? 2.0,
));
colorIndex = (colorIndex + 1) % rainbowColors.length;
}
@override
void onPointEnd() {
colorIndex = 0;
strokes.clear();
}
}

View File

@@ -171,14 +171,22 @@ class BrushConfigs {
maxValue: 72.0,
icon: Icons.format_size,
),
'spacing': BrushSettingConfig(
label: 'Spacing',
'wordSpacing': BrushSettingConfig(
label: 'Word Spacing',
type: SettingType.slider,
defaultValue: 50.0,
minValue: 10.0,
maxValue: 200.0,
icon: Icons.space_bar,
),
'letterSpacing': BrushSettingConfig(
label: 'Letter Spacing',
type: SettingType.slider,
defaultValue: 0.0,
minValue: -50.0,
maxValue: 50.0,
icon: Icons.space_bar,
),
'randomRotation': BrushSettingConfig(
label: 'Random Rotation',
type: SettingType.toggle,

View File

@@ -13,7 +13,8 @@ class TextEffect extends PenEffect {
String get text => settings.getValue('text') ?? 'Hello';
double get fontSize => settings.getValue('fontSize') ?? 20.0;
bool get randomRotation => settings.getValue('randomRotation') ?? false;
double get spacing => settings.getValue('spacing') ?? 50.0;
double get wordSpacing => settings.getValue('wordSpacing') ?? 0.0;
double get letterSpacing => settings.getValue('letterSpacing') ?? 0.0;
@override
void paint(Canvas canvas, Path path, Paint paint) {
@@ -25,6 +26,8 @@ class TextEffect extends PenEffect {
style: TextStyle(
color: drawController.currentColor,
fontSize: fontSize,
wordSpacing: wordSpacing,
letterSpacing: letterSpacing,
),
),
textDirection: TextDirection.ltr,

View File

@@ -1,5 +1,6 @@
import 'package:doddle/core/theme/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sizer/sizer.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -23,7 +24,8 @@ Future<void> initializeApp() async {
void main() async {
await initializeApp();
//dont allow rotation
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
runApp(
const ProviderScope(
child: MyApp(),

View File

@@ -117,6 +117,13 @@ class BrushSettingsPanel extends ConsumerWidget {
),
);
case SettingType.text:
// Create a persistent TextEditingController to maintain state
final controller = TextEditingController(text: currentValue);
// Maintain cursor position
controller.selection = TextSelection.fromPosition(
TextPosition(offset: controller.text.length)
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
@@ -133,7 +140,7 @@ class BrushSettingsPanel extends ConsumerWidget {
),
const SizedBox(height: 8),
TextField(
controller: TextEditingController(text: currentValue),
controller: controller,
decoration: InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
@@ -155,7 +162,7 @@ class BrushSettingsPanel extends ConsumerWidget {
return Center(
child: TextButton.icon(
icon: const Icon(Icons.restart_alt),
label: const Text('Reset to Default'),
label: const Text('Reset Brush Settings to Default'),
onPressed: () {
ref.read(brushSettingsProvider(penTool).notifier).resetToDefault();
},

View File

@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
class CanvasSize {
final String name;
final Size size;
const CanvasSize({
required this.name,
required this.size,
});
}
class CanvasSizeDialog extends StatelessWidget {
CanvasSizeDialog({Key? key}) : super(key: key);
final List<CanvasSize> canvasSizes = [
CanvasSize(name: 'Small (1080x1080)', size: const Size(1080, 1080)),
CanvasSize(name: 'Medium (1920x1080)', size: const Size(1920, 1080)),
CanvasSize(name: 'Large (2560x1440)', size: const Size(2560, 1440)),
CanvasSize(name: 'Custom', size: Size.zero),
];
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Select Canvas Size'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: canvasSizes.map((size) {
return ListTile(
title: Text(size.name),
onTap: () {
if (size.name == 'Custom') {
_showCustomSizeDialog(context);
} else {
Navigator.of(context).pop(size.size);
}
},
);
}).toList(),
),
),
);
}
void _showCustomSizeDialog(BuildContext context) {
final widthController = TextEditingController();
final heightController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Custom Size'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: widthController,
decoration: const InputDecoration(labelText: 'Width'),
keyboardType: TextInputType.number,
),
TextField(
controller: heightController,
decoration: const InputDecoration(labelText: 'Height'),
keyboardType: TextInputType.number,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
final width = double.tryParse(widthController.text);
final height = double.tryParse(heightController.text);
if (width != null && height != null) {
Navigator.of(context).pop();
Navigator.of(context).pop(Size(width, height));
}
},
child: const Text('OK'),
),
],
),
);
}
}

View File

@@ -1,13 +1,47 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
class Popover extends StatelessWidget {
class Popover extends StatefulWidget {
const Popover({
Key? key,
this.child,
}) : super(key: key);
}): super(key: key);
final Widget? child;
@override
State<Popover> createState() => _PopoverState();
}
class _PopoverState extends State<Popover> with SingleTickerProviderStateMixin {
Widget _buildHandle(BuildContext context) {
final theme = Theme.of(context);
return SizedBox(
width: double.infinity,
child: Stack(
alignment: Alignment.topCenter,
children: [
FractionallySizedBox(
widthFactor: 0.25,
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 12.0,
),
child: Container(
height: 5.0,
decoration: BoxDecoration(
color: theme.dividerColor,
borderRadius: const BorderRadius.all(Radius.circular(2.5)),
),
),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@@ -15,33 +49,23 @@ class Popover extends StatelessWidget {
return Container(
margin: const EdgeInsets.all(16.0),
clipBehavior: Clip.antiAlias,
constraints: BoxConstraints(
maxWidth: 90.w,
),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [_buildHandle(context), if (child != null) child!],
),
);
}
Widget _buildHandle(BuildContext context) {
final theme = Theme.of(context);
return FractionallySizedBox(
widthFactor: 0.25,
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 12.0,
),
child: Container(
height: 5.0,
decoration: BoxDecoration(
color: theme.dividerColor,
borderRadius: const BorderRadius.all(Radius.circular(2.5)),
children: [
_buildHandle(context),
Flexible(
child: SingleChildScrollView(
child: widget.child,
),
),
),
],
),
);
}

View File

@@ -19,9 +19,9 @@ class ToolsWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return Container(
height: MediaQuery.of(context).size.height * .1,
color: Colors.purple[800],
color: Colors.purple[600],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildToolButton(
context,
@@ -41,16 +41,20 @@ class ToolsWidget extends ConsumerWidget {
orElse: () => symmetryLines.first,
);
return _buildToolButton(
context,
icon: Stack(
alignment: AlignmentDirectional.center,
children: [
Assets.svg.symmetricalLineBg.svg(width: 80),
selectedLine.picture,
],
return SizedBox(
width: 80,
height: 80,
child: _buildToolButton(
context,
icon: Stack(
alignment: AlignmentDirectional.center,
children: [
Assets.svg.symmetricalLineBg.svg(width: 80),
selectedLine.picture,
],
),
toolType: ToolType.symmyrticllLine,
),
toolType: ToolType.symmyrticllLine,
);
}
),
@@ -69,7 +73,7 @@ class ToolsWidget extends ConsumerWidget {
context,
icon: const Icon(
Icons.settings,
size: 30,
size: 80,
color: Colors.white,
),
toolType: ToolType.canvasSettings,
@@ -86,11 +90,7 @@ class ToolsWidget extends ConsumerWidget {
}) {
return GestureDetector(
onTap: () {
if (toolType == ToolType.brushs) {
_showBrushSettings(context);
} else {
_showToolSettings(context, toolType);
}
_showToolSettings(context, toolType);
},
child: icon,
);
@@ -107,8 +107,9 @@ class ToolsWidget extends ConsumerWidget {
void _showToolSettings(BuildContext context, ToolType toolType) {
showModalBottomSheet<void>(
backgroundColor: Colors.transparent,
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return Popover(
child: _buildToolContent(toolType),
@@ -117,24 +118,6 @@ class ToolsWidget extends ConsumerWidget {
);
}
void _showBrushSettings(BuildContext context) {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return const SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
BrushToolGrid(),
Divider(),
BrushSettingsPanel(),
],
),
);
},
);
}
Widget _buildToolContent(ToolType toolType) {
switch (toolType) {
case ToolType.brushs:
@@ -144,7 +127,15 @@ class ToolsWidget extends ConsumerWidget {
case ToolType.symmyrticllLine:
return const SymmetryToolGrid();
case ToolType.canvasSettings:
return const CanvasSettingsToolGrid();
return SingleChildScrollView(
child: Column(
children: [
CanvasSettingsToolGrid(),
Divider(),
BrushSettingsPanel(),
],
),
);
}
}
}

View File

@@ -1,3 +1,4 @@
import 'package:doddle/application/providers/brush_settings_provider.dart';
import 'package:doddle/application/providers/canvas/canvas_provider.dart';
import 'package:doddle/domain/models/effects/pen_effect.dart';
import 'package:flutter/material.dart';
@@ -17,32 +18,76 @@ class BrushPreviewPainter extends CustomPainter {
final center = Offset(size.width / 2, size.height / 2);
canvas.translate(center.dx, center.dy);
// Create a wavy line for preview
final points = _generatePreviewPoints(size);
final controller = DrawController(
points: points,
penTool: ref.read(canvasNotifierProvider).penTool,
penSize: ref.read(canvasNotifierProvider).penSize,
currentColor: ref.read(canvasNotifierProvider).currentColor,
effects: ref.read(canvasNotifierProvider).effects,
);
final effect = controller.effects[controller.penTool];
final currentPenTool = ref.read(canvasNotifierProvider).penTool;
if (effect != null) {
Path path = Path();
Paint paint = Paint()
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
// Special handling for text brush preview
if (currentPenTool == PenTool.textPen) {
_drawTextPreview(canvas, size);
} else {
// Original wave preview for other brushes
final points = _generatePreviewPoints(size);
final controller = DrawController(
points: points,
penTool: currentPenTool,
penSize: ref.read(canvasNotifierProvider).penSize,
currentColor: ref.read(canvasNotifierProvider).currentColor,
effects: ref.read(canvasNotifierProvider).effects,
);
_drawPreviewPoints(canvas, path, paint, effect, points);
effect.paint(canvas, path, paint);
final effect = controller.effects[controller.penTool];
if (effect != null) {
Path path = Path();
Paint paint = Paint()
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = controller.penSize ?? 2.0;
_drawPreviewPoints(canvas, path, paint, effect, points);
effect.paint(canvas, path, paint);
}
}
canvas.restore();
}
void _drawTextPreview(Canvas canvas, Size size) {
final settings = ref.read(brushSettingsProvider(PenTool.textPen));
final text = settings.getValue('text') ?? 'Hello';
final fontSize = settings.getValue('fontSize') ?? 20.0;
final color = ref.read(canvasNotifierProvider).currentColor;
final textPainter = TextPainter(
text: TextSpan(
text: text,
style: TextStyle(
color: color,
fontSize: fontSize,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
// Draw multiple instances of text along a curved path
for (double x = -size.width/3; x <= size.width/3; x += 50) {
final y = math.sin(x * 0.1) * 20;
canvas.save();
canvas.translate(x, y);
// Add slight rotation for visual interest
canvas.rotate(math.sin(x * 0.05) * 0.3);
textPainter.paint(
canvas,
Offset(-textPainter.width / 2, -textPainter.height / 2),
);
canvas.restore();
}
}
List<Point?> _generatePreviewPoints(Size size) {
final points = <Point?>[];
final width = size.width * 0.4;

View File

@@ -9,11 +9,14 @@ class LastImageAsBackground extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
if (image != null) {
canvas.drawImage(image!, Offset.zero, ui.Paint()
..blendMode = BlendMode.srcIn
..filterQuality = ui.FilterQuality.high
// ..isAntiAlias = true,
);
paintImage(
canvas: canvas,
rect: Offset.zero & size,
image: image!,
fit: BoxFit.cover,
filterQuality: FilterQuality.high,
isAntiAlias: true,
);
}
}

View File

@@ -7,23 +7,23 @@ class AboutMeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 100),
body:Padding(
padding: const EdgeInsets.all(8.0),
child: ContactUs(
logo: const AssetImage("assets/NaserElziadna.jpg"),
email: 'elzianda10@gmail.com',
companyName: 'Naser Elzianda',
phoneNumber: '+972584029927',
dividerThickness: 2,
website: 'https://www.nmmsoft.com',
githubUserName: 'NaserElziadna',
linkedinURL: 'https://www.linkedin.com/in/naser-hassan-b452411a1/',
tagLine: 'Full Stack Developer',
cardColor: Colors.white,
companyColor: Colors.red,
taglineColor: Colors.green,
textColor: Colors.black,
),
logo: const AssetImage("assets/NaserElziadna.jpg"),
email: 'elzianda10@gmail.com',
companyName: 'Naser Elzianda',
phoneNumber: '+972584029927',
dividerThickness: 2,
website: 'https://www.nmmsoft.com',
githubUserName: 'NaserElziadna',
linkedinURL: 'https://www.linkedin.com/in/naser-hassan-b452411a1/',
tagLine: 'Full Stack Developer',
cardColor: Colors.white,
companyColor: Colors.red,
taglineColor: Colors.green,
textColor: Colors.black,
),
),
);
}

View File

@@ -300,10 +300,7 @@ class _CanvasScreenState extends ConsumerState<CanvasScreen> {
? null
: drawController.stamp?.last?.image,
),
size: Size(
kCanvasSize.width / 2,
kCanvasSize.height / 2,
),
size: kCanvasSize,
willChange: true,
isComplex: true,
child: const SizedBox.expand(),