scribble
scribble copied to clipboard
Playback Scribble
Is it possible to "play" the scribble from JSON? Like as if it is being drawn from scratch? Thanks
This should be possible easily by iterating through the Lines and points. Currently it's outside of the scope of our team, but feel free to open a PR!
Ok thanks. Do you have any quick bits of advice on where to start?
@timcreatedit i've been trying this for hours. can you give me a couple clues on how to do this? i'd like to store the json for replay at a later time but i'm stumped. any help appreciated.
here's some code i modified but it's broken: `import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_state_notifier/flutter_state_notifier.dart'; import 'package:scribble/scribble.dart';
void main() { runApp(const MyApp()); }
class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Scribble', theme: ThemeData( primarySwatch: Colors.blue, ), home: const HomePage(title: 'Scribble'), ); } }
class HomePage extends StatefulWidget { const HomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override State<HomePage> createState() => _HomePageState(); }
class _HomePageState extends State<HomePage> { late ScribbleNotifier notifier;
// Initialize the list of strokes. List<Map<String, dynamic>> savedStrokes = [];
@override void initState() { notifier = ScribbleNotifier(); notifier.addListener((ScribbleState state) { // Update the saved strokes list whenever the notifier state changes. savedStrokes = notifier._strokes.map<Map<String, dynamic>>( (stroke) { return { 'path': stroke.path .computeMetrics() .map((metric) => metric.extractPath(0, metric.length)) .map((path) => path.toSvgString()) .toList(), 'color': stroke.color.value, 'strokeWidth': stroke.strokeWidth, }; }, ).toList(); }); super.initState(); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), leading: IconButton( icon: const Icon(Icons.save), tooltip: "Save to Image", onPressed: () => _saveImage(context), ), ), body: SingleChildScrollView( child: SizedBox( height: MediaQuery.of(context).size.height * 2, child: Stack( children: [ Scribble( notifier: notifier, drawPen: true, ), Positioned( top: 16, right: 16, child: Column( children: [ _buildReplayButton(context), const Divider( height: 32, ), _buildColorToolbar(context), const Divider( height: 32, ), _buildStrokeToolbar(context), ], ), ) ], ), ), ), ); }
// Add a replay button to the toolbar. Widget _buildReplayButton(BuildContext context) { return FloatingActionButton.small( tooltip: "Replay", onPressed: () => _replayDrawing(context), backgroundColor: Colors.blueGrey, child: const Icon(Icons.play_arrow), ); }
Future
// The replay function.
Future
// Iterate through the saved strokes and redraw them on a canvas.
for (var stroke in savedStrokes) {
final path = Path();
for (var offset in stroke['path']) {
path.lineTo(offset['dx'], offset['dy']);
}
final color = Color(stroke['color']);
final strokeWidth = stroke['strokeWidth'];
replayNotifier.draw(
path: path,
color: color,
strokeWidth: strokeWidth,
);
}
// Display the replayed drawing in a dialog.
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Replayed Drawing"),
content: StateNotifierBuilder<ScribbleState>(
stateNotifier: replayNotifier,
builder: (context, state, _) => SizedBox(
width: MediaQuery.of(context).size.width * 0.8,
height: MediaQuery.of(context).size.height * 0.8,
child: Scribble(
notifier: replayNotifier,
drawPen: false,
),
),
),
),
);
}
Widget _buildColorToolbar(BuildContext context) { return StateNotifierBuilder<ScribbleState>( stateNotifier: notifier, builder: (context, state, _) => Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start, children: [ _buildUndoButton(context), const Divider( height: 4.0, ), _buildRedoButton(context), const Divider( height: 4.0, ), _buildClearButton(context), const Divider( height: 20.0, ), _buildPointerModeSwitcher(context, penMode: state.allowedPointersMode == ScribblePointerMode.penOnly), const Divider( height: 20.0, ), _buildEraserButton(context, isSelected: state is Erasing), _buildColorButton(context, color: Colors.black, state: state), _buildColorButton(context, color: Colors.red, state: state), _buildColorButton(context, color: Colors.green, state: state), _buildColorButton(context, color: Colors.blue, state: state), _buildColorButton(context, color: Colors.yellow, state: state), ], ), ); }
Widget _buildPointerModeSwitcher(BuildContext context, {required bool penMode}) { return FloatingActionButton.small( onPressed: () => notifier.setAllowedPointersMode( penMode ? ScribblePointerMode.all : ScribblePointerMode.penOnly, ), tooltip: "Switch drawing mode to " + (penMode ? "all pointers" : "pen only"), child: AnimatedSwitcher( duration: kThemeAnimationDuration, child: !penMode ? const Icon( Icons.touch_app, key: ValueKey(true), ) : const Icon( Icons.do_not_touch, key: ValueKey(false), ), ), ); } Widget _buildStrokeToolbar(BuildContext context) { return StateNotifierBuilder<ScribbleState>( stateNotifier: notifier, builder: (context, state, _) => Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start, children: [ for (final w in notifier.widths) _buildStrokeButton( context, strokeWidth: w, state: state, ), ], ), ); }
Widget buildStrokeButton( BuildContext context, { required double strokeWidth, required ScribbleState state, }) { final selected = state.selectedWidth == strokeWidth; return Padding( padding: const EdgeInsets.all(4), child: Material( elevation: selected ? 4 : 0, shape: const CircleBorder(), child: InkWell( onTap: () => notifier.setStrokeWidth(strokeWidth), customBorder: const CircleBorder(), child: AnimatedContainer( duration: kThemeAnimationDuration, width: strokeWidth * 2, height: strokeWidth * 2, decoration: BoxDecoration( color: state.map( drawing: (s) => Color(s.selectedColor), erasing: () => Colors.transparent, ), border: state.map( drawing: () => null, erasing: () => Border.all(width: 1), ), borderRadius: BorderRadius.circular(50.0)), ), ), ), ); }
Widget _buildEraserButton(BuildContext context, {required bool isSelected}) { return Padding( padding: const EdgeInsets.all(4), child: FloatingActionButton.small( tooltip: "Erase", backgroundColor: const Color(0xFFF7FBFF), elevation: isSelected ? 10 : 2, shape: !isSelected ? const CircleBorder() : RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.remove, color: Colors.blueGrey), onPressed: notifier.setEraser, ), ); }
Widget _buildColorButton( BuildContext context, { required Color color, required ScribbleState state, }) { final isSelected = state is Drawing && state.selectedColor == color.value; return Padding( padding: const EdgeInsets.all(4), child: FloatingActionButton.small( backgroundColor: color, elevation: isSelected ? 10 : 2, shape: !isSelected ? const CircleBorder() : RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), child: Container(), onPressed: () => notifier.setColor(color)), ); }
Widget _buildUndoButton( BuildContext context, ) { return FloatingActionButton.small( tooltip: "Undo", onPressed: notifier.canUndo ? notifier.undo : null, disabledElevation: 0, backgroundColor: notifier.canUndo ? Colors.blueGrey : Colors.grey, child: const Icon( Icons.undo_rounded, color: Colors.white, ), ); }
Widget _buildRedoButton( BuildContext context, ) { return FloatingActionButton.small( tooltip: "Redo", onPressed: notifier.canRedo ? notifier.redo : null, disabledElevation: 0, backgroundColor: notifier.canRedo ? Colors.blueGrey : Colors.grey, child: const Icon( Icons.redo_rounded, color: Colors.white, ), ); }
Widget _buildClearButton(BuildContext context) { return FloatingActionButton.small( tooltip: "Clear", onPressed: notifier.clear, disabledElevation: 0, backgroundColor: Colors.blueGrey, child: const Icon(Icons.clear), ); } } `
@jtkeyva
Did you get it working? If not what is issue?
@danschewy i ended up making one from scratch using chat gpt. it works ok but not line width and it's a bit hacky