timcreatedit / scribble

Scribble is a lightweight library for freehand drawing in Flutter supporting pressure, variable line width and more!
https://pub.dev/packages/scribble
MIT License
115 stars 39 forks source link

Playback Scribble #26

Open jtkeyva opened 2 years ago

jtkeyva commented 2 years ago

Is it possible to "play" the scribble from JSON? Like as if it is being drawn from scratch? Thanks

timcreatedit commented 2 years ago

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!

jtkeyva commented 2 years ago

Ok thanks. Do you have any quick bits of advice on where to start?

jtkeyva commented 1 year ago

@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 createState() => _HomePageState(); }

class _HomePageState extends State { 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 _saveImage(BuildContext context) async { final image = await notifier.renderImage(); showDialog( context: context, builder: (context) => AlertDialog( title: const Text("Your Image"), content: Image.memory(image.buffer.asUint8List()), ), ); }

// The replay function. Future _replayDrawing(BuildContext context) async { // Create a new ScribbleNotifier for the replay. final replayNotifier = ScribbleNotifier();

// 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( 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( 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), ); } } `

danschewy commented 1 year ago

@jtkeyva

Did you get it working? If not what is issue?

jtkeyva commented 1 year ago

@danschewy i ended up making one from scratch using chat gpt. it works ok but not line width and it's a bit hacky