draw_your_image 0.8.0
draw_your_image: ^0.8.0 copied to clipboard
A Flutter package which enables users to draw with fingers in your designed UI.
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'package:draw_your_image/draw_your_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(const ShaderDemoApp());
class ShaderDemoApp extends StatelessWidget {
const ShaderDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Shader Effects Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const ShaderDemoPage(),
);
}
}
/// Shader effect types
enum ShaderEffect {
defaultEffect('Default', 'Standard solid color'),
linearGradient('Linear', 'Start to end gradient'),
radialGradient('Radial', 'Center to edge gradient'),
sweepGradient('Sweep', 'Rotating gradient'),
rainbow('Rainbow', '7-color gradient'),
glow('Glow', 'Glowing effect'),
metallic('Metallic', 'Metallic shine'),
fragmentShader('Shader', 'Fragment shader tint'),
multiLayer('Multi-Layer', 'Multiple paint layers');
const ShaderEffect(this.label, this.description);
final String label;
final String description;
}
class ShaderDemoPage extends StatefulWidget {
const ShaderDemoPage({super.key});
@override
State<ShaderDemoPage> createState() => _ShaderDemoPageState();
}
class _ShaderDemoPageState extends State<ShaderDemoPage>
with SingleTickerProviderStateMixin {
List<Stroke> _strokes = [];
ShaderEffect _currentEffect = ShaderEffect.defaultEffect;
ui.FragmentProgram? _tintProgram;
late final Ticker _ticker;
double _time = 0.0;
void _clear() {
setState(() => _strokes = []);
}
@override
void initState() {
super.initState();
// Load fragment program for tint shader. If loading fails, we'll keep null and
// fall back to default painter.
ui.FragmentProgram.fromAsset('shaders/tint.frag').then((p) {
setState(() => _tintProgram = p);
});
_ticker = createTicker((elapsed) {
setState(() {
_time = elapsed.inMilliseconds / 1000.0;
});
});
_ticker.start();
}
@override
void dispose() {
_ticker.stop();
_ticker.dispose();
super.dispose();
}
/// Calculate bounding box for a stroke
Rect _getStrokeBounds(Stroke stroke) {
if (stroke.points.isEmpty) {
return Rect.zero;
}
double minX = stroke.points.first.dx;
double maxX = stroke.points.first.dx;
double minY = stroke.points.first.dy;
double maxY = stroke.points.first.dy;
for (final point in stroke.points) {
minX = math.min(minX, point.dx);
maxX = math.max(maxX, point.dx);
minY = math.min(minY, point.dy);
maxY = math.max(maxY, point.dy);
}
// Add padding for stroke width
final padding = stroke.width * 2;
return Rect.fromLTRB(
minX - padding,
minY - padding,
maxX + padding,
maxY + padding,
);
}
/// Get stroke painter based on current effect
List<Paint> _getStrokePainter(Stroke stroke, Size canvasSize) {
switch (_currentEffect) {
case ShaderEffect.defaultEffect:
return _defaultPainter(stroke, canvasSize);
case ShaderEffect.linearGradient:
return _linearGradientPainter(stroke, canvasSize);
case ShaderEffect.radialGradient:
return _radialGradientPainter(stroke, canvasSize);
case ShaderEffect.sweepGradient:
return _sweepGradientPainter(stroke, canvasSize);
case ShaderEffect.rainbow:
return _rainbowPainter(stroke, canvasSize);
case ShaderEffect.glow:
return _glowPainter(stroke, canvasSize);
case ShaderEffect.metallic:
return _metallicPainter(stroke, canvasSize);
case ShaderEffect.fragmentShader:
return _fragmentShaderPainter(stroke, canvasSize);
case ShaderEffect.multiLayer:
return _multiLayerPainter(stroke, canvasSize);
}
}
/// Default painter - solid color
List<Paint> _defaultPainter(Stroke stroke, Size canvasSize) {
return [paintWithDefault(stroke)];
}
/// Linear gradient from start to end
List<Paint> _linearGradientPainter(Stroke stroke, Size canvasSize) {
final canvasRect = Rect.fromLTWH(0, 0, canvasSize.width, canvasSize.height);
final _colors = [Colors.blue, Colors.purple, Colors.pink];
final _stops = List.generate(
_colors.length,
(i) => i / (_colors.length - 1),
);
final gradient = ui.Gradient.linear(
canvasRect.topLeft,
canvasRect.bottomRight,
_colors,
_stops,
);
return [
Paint()
..shader = gradient
..strokeWidth = stroke.width
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke,
];
}
/// Radial gradient from center
List<Paint> _radialGradientPainter(Stroke stroke, Size canvasSize) {
final canvasRect = Rect.fromLTWH(0, 0, canvasSize.width, canvasSize.height);
final center = canvasRect.center;
final radius = math.max(canvasRect.width, canvasRect.height) / 2;
final _colors = [Colors.yellow, Colors.orange, Colors.red];
final _stops = List.generate(
_colors.length,
(i) => i / (_colors.length - 1),
);
final gradient = ui.Gradient.radial(center, radius, _colors, _stops);
return [
Paint()
..shader = gradient
..strokeWidth = stroke.width
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke,
];
}
/// Sweep gradient (rotating effect)
List<Paint> _sweepGradientPainter(Stroke stroke, Size canvasSize) {
final canvasRect = Rect.fromLTWH(0, 0, canvasSize.width, canvasSize.height);
final center = canvasRect.center;
final _colors = [
Colors.red,
Colors.yellow,
Colors.green,
Colors.cyan,
Colors.blue,
const Color(0xFFFF00FF), // Magenta
Colors.red,
];
final _stops = List.generate(
_colors.length,
(i) => i / (_colors.length - 1),
);
final gradient = ui.Gradient.sweep(center, _colors, _stops);
return [
Paint()
..shader = gradient
..strokeWidth = stroke.width
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke,
];
}
/// Rainbow effect - 7 colors
List<Paint> _rainbowPainter(Stroke stroke, Size canvasSize) {
final canvasRect = Rect.fromLTWH(0, 0, canvasSize.width, canvasSize.height);
final _colors = [
const Color(0xFFFF0000), // Red
const Color(0xFFFF7F00), // Orange
const Color(0xFFFFFF00), // Yellow
const Color(0xFF00FF00), // Green
const Color(0xFF0000FF), // Blue
const Color(0xFF4B0082), // Indigo
const Color(0xFF9400D3), // Violet
];
final _stops = List.generate(
_colors.length,
(i) => i / (_colors.length - 1),
);
final gradient = ui.Gradient.linear(
canvasRect.topLeft,
canvasRect.bottomRight,
_colors,
_stops,
);
return [
Paint()
..shader = gradient
..strokeWidth = stroke.width
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke,
];
}
/// Glow effect with shadow layers
List<Paint> _glowPainter(Stroke stroke, Size canvasSize) {
final canvasRect = Rect.fromLTWH(0, 0, canvasSize.width, canvasSize.height);
final _colors = [Colors.cyan, Colors.blue, Colors.purple];
final _stops = List.generate(
_colors.length,
(i) => i / (_colors.length - 1),
);
final gradient = ui.Gradient.linear(
canvasRect.topLeft,
canvasRect.bottomRight,
_colors,
_stops,
);
return [
// Outer glow
Paint()
..shader = gradient
..strokeWidth = stroke.width + 8
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8),
// Middle glow
Paint()
..shader = gradient
..strokeWidth = stroke.width + 4
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4),
// Core
Paint()
..color = Colors.white
..strokeWidth = stroke.width
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke,
];
}
/// Metallic effect with highlights
List<Paint> _metallicPainter(Stroke stroke, Size canvasSize) {
final canvasRect = Rect.fromLTWH(0, 0, canvasSize.width, canvasSize.height);
// Gold metallic gradient
final gradient = ui.Gradient.linear(
Offset(canvasRect.left, canvasRect.center.dy),
Offset(canvasRect.right, canvasRect.center.dy),
[
const Color(0xFFFFD700), // Gold
const Color(0xFFFFE55C), // Light gold
const Color(0xFFFFD700), // Gold
const Color(0xFFB8860B), // Dark gold
const Color(0xFFFFD700), // Gold
],
[0.0, 0.25, 0.5, 0.75, 1.0],
);
return [
// Shadow
Paint()
..color = Colors.black45
..strokeWidth = stroke.width + 2
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2),
// Main metallic stroke
Paint()
..shader = gradient
..strokeWidth = stroke.width
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke,
];
}
/// Fragment shader painter (tint). Uses `example/shaders/tint.frag`.
List<Paint> _fragmentShaderPainter(Stroke stroke, Size canvasSize) {
if (_tintProgram == null) {
return _defaultPainter(stroke, canvasSize);
}
final fs = _tintProgram!.fragmentShader();
// Uniform layout in shader:
// 0: u_time
// 1..2: u_resolution.x, u_resolution.y
fs.setFloat(0, _time);
fs.setFloat(1, canvasSize.width);
fs.setFloat(2, canvasSize.height);
return [
Paint()
..shader = fs
..strokeWidth = stroke.width
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke,
];
}
/// Multi-layer with border and shadow
List<Paint> _multiLayerPainter(Stroke stroke, Size canvasSize) {
final canvasRect = Rect.fromLTWH(0, 0, canvasSize.width, canvasSize.height);
return [
// Shadow
paintWithOverride(
stroke,
strokeWidth: stroke.width + 6,
strokeColor: Colors.black26,
)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3),
// Outer border
paintWithOverride(
stroke,
strokeWidth: stroke.width + 4,
strokeColor: Colors.black,
),
// White highlight border
paintWithOverride(
stroke,
strokeWidth: stroke.width + 2,
strokeColor: Colors.white70,
),
// Main stroke: white base color
paintWithOverride(
stroke,
strokeWidth: stroke.width,
strokeColor: Colors.white,
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Shader Effects Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Column(
children: [
// Effect selector
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[100],
border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Shader Effect:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: ShaderEffect.values.map((effect) {
final isSelected = _currentEffect == effect;
return ChoiceChip(
label: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
effect.label,
style: TextStyle(
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
),
),
if (isSelected) ...[
const SizedBox(height: 2),
Text(
effect.description,
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
),
),
],
],
),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setState(() => _currentEffect = effect);
}
},
selectedColor: Colors.deepPurple[100],
);
}).toList(),
),
],
),
),
// Drawing canvas
Expanded(
child: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey[300]!, width: 2),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LayoutBuilder(
builder: (context, constraints) {
final canvasSize = Size(
constraints.maxWidth,
constraints.maxHeight,
);
return Draw(
strokes: _strokes,
strokeColor: Colors.black,
strokeWidth: 8.0,
backgroundColor: Colors.white,
onStrokeDrawn: (stroke) {
setState(() => _strokes = [..._strokes, stroke]);
},
strokePainter: (stroke) =>
_getStrokePainter(stroke, canvasSize),
);
},
),
),
),
),
// Control buttons
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey[300]!)),
),
child: Row(
children: [
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.clear),
label: const Text('Clear Canvas'),
onPressed: _strokes.isEmpty ? null : _clear,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[50],
foregroundColor: Colors.red[700],
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.info_outline),
label: const Text('About'),
onPressed: () => _showAboutDialog(context),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
),
],
),
);
}
void _showAboutDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Shader Effects Demo'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'This demo showcases various shader effects using the strokePainter feature of draw_your_image package.',
style: TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
const Text(
'Available Effects:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
...ShaderEffect.values.map(
(effect) => Padding(
padding: const EdgeInsets.only(left: 8, bottom: 4),
child: Row(
children: [
const Icon(Icons.brush, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
'${effect.label}: ${effect.description}',
style: const TextStyle(fontSize: 12),
),
),
],
),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
),
);
}
}