xvrh / lottie-flutter

Render After Effects animations natively on Flutter. This package is a pure Dart implementation of a Lottie player.
https://pub.dev/packages/lottie
MIT License
1.17k stars 199 forks source link

Draw frame by frame and export frame to image #42

Closed hungtk closed 4 years ago

hungtk commented 4 years ago

How can we draw frame by frame and export these frames to image?

xvrh commented 4 years ago

To draw each frame in a separate file, you can use something like that:

save() async {
  var data = await rootBundle.load('assets/HamburgerArrow.json');
  var composition = await LottieComposition.fromByteData(data);
  var drawable = LottieDrawable(composition);

  var size = Size(composition.bounds.width.toDouble(),
      composition.bounds.height.toDouble());
  for (var i = composition.startFrame; i < composition.endFrame; i += 1) {
    drawable.setProgress(i / composition.durationFrames);

    var pictureRecorder = PictureRecorder();
    var canvas = Canvas(pictureRecorder);

    drawable.draw(canvas, Offset.zero & size);

    var picture = pictureRecorder.endRecording();
    var image = await picture.toImage(size.width.toInt(), size.height.toInt());
    var bytes = await image.toByteData(format: ImageByteFormat.png);
    await File('output_$i.png')
        .writeAsBytes(bytes.buffer.asUint8List());
  }
}

To draw all frames in a single file:

save() async {
  var data = await rootBundle.load('assets/HamburgerArrow.json');
  var composition = await LottieComposition.fromByteData(data);
  var drawable = LottieDrawable(composition);

  var pictureRecorder = PictureRecorder();
  var canvas = Canvas(pictureRecorder);

  var size = Size(500, 500);
  var columns = 10;
  for (var i = composition.startFrame; i < composition.endFrame; i += 1) {
    drawable.setProgress(i / composition.durationFrames);

    var destRect = Offset(i % columns * 50.0, i ~/ 10 * 80.0) & (size / 5);
    drawable.draw(canvas, destRect);
  }

  var picture = pictureRecorder.endRecording();
  var image = await picture.toImage(size.width.toInt(), size.height.toInt());
  var bytes = await image.toByteData(format: ImageByteFormat.png);
  await File('output.png')
      .writeAsBytes(bytes.buffer.asUint8List());
}

You can run the code either as a normal Flutter app or directly on your dev machine using the Flutter test runner

xvrh commented 4 years ago

I added this 2 examples here: https://github.com/xvrh/lottie-flutter/blob/master/example/lib/examples/save_frames.dart

hungtk commented 4 years ago

Thank you so much @xvrh .

hungtk commented 4 years ago

I have a issue. Can you explain for me?

I saved frames to temp directory. When I show 1 frame to UI. I got empty block. And I converted image to base64 string, and I got: iVBORw0KGgoAAAANSUhEUgAAEYAAAAnYCAYAAADZA7WJAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAACAASURBVHic7MEBAQAAAICQ/q/uCAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA I am using Image.file(). My frames located like: /data/user/0/com.example.test_lottie/cache/images/0070.png

xvrh commented 4 years ago

Can you show your full code to help to debug?

hungtk commented 4 years ago

Here's my code

Future<String> tempDir(String folderName) async {
    final tempDirectory = await getTemporaryDirectory();
    Directory dir = Directory(tempDirectory.path + '/' + folderName);
    if (await dir.exists()) {
      return dir.path;
    }
    final Directory newDir = await dir.create(recursive: true);
    return newDir.path;
  }
Future<String> saveAllFrames(ByteData data, String destination) async {
    print('saveAllFrames...');
    var composition = await LottieComposition.fromByteData(data);
    var drawable = LottieDrawable(composition);

    var size = Size(composition.bounds.width.toDouble(),
        composition.bounds.height.toDouble());
    for (var i = composition.startFrame; i < composition.endFrame; i += 1) {
      var progress = i / composition.durationFrames;
      drawable.setProgress(progress);
      var pictureRecorder = PictureRecorder();
      var canvas = Canvas(pictureRecorder);

      drawable.draw(canvas, Offset.zero & size);

      var picture = pictureRecorder.endRecording();
      var image =
          await picture.toImage(size.width.toInt(), size.height.toInt());
      var bytes = await image.toByteData(format: ImageByteFormat.png);
      var fileName = (i + 1).toInt().toString().padLeft(4, '0');
      var filePath = join(destination, '$fileName.png');
      frames.add(filePath.toString());
      await File(filePath).writeAsBytes(bytes.buffer.asUint8List());
      final bs64 = base64Decode(bytes.buffer.asUint8List().toString());
      print((progress * 100).round().toString() + "%");
      print(filePath);
    }

  }
xvrh commented 4 years ago

I tried to improve my original example. Now it saves the files in the temp folder and displays the images immediately. To improve the performance, I only export 10 frames from the animation and in a lower resolution.

import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lottie/lottie.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  List<File> _frames;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: SafeArea(
          child: Column(
            children: [
              RaisedButton(
                child: Text('Export all frames'),
                onPressed: _export,
              ),
              if (_frames != null)
                Expanded(
                    child: Wrap(
                  children: [..._frames.map((f) => Image.file(f, width: 50))],
                ))
            ],
          ),
        ),
      ),
    );
  }

  Future<void> _export() async {
    var data = await rootBundle.load('assets/HamburgerArrow.json');

    var frames = await exportFrames(
        data, await _createTempDirectory('hamburger'),
        progresses: [for (var i = 0.0; i <= 1; i += 0.1) i],
        size: Size(50, 50));

    setState(() {
      _frames = frames;
    });
  }
}

Future<List<File>> exportFrames(ByteData data, String directory,
    {@required Size size, @required List<double> progresses}) async {
  var composition = await LottieComposition.fromByteData(data);
  var drawable = LottieDrawable(composition);

  var frames = <File>[];
  for (var progress in progresses) {
    drawable.setProgress(progress);

    var pictureRecorder = PictureRecorder();
    var canvas = Canvas(pictureRecorder);

    drawable.draw(canvas, Offset.zero & size);

    var picture = pictureRecorder.endRecording();
    var image = await picture.toImage(size.width.toInt(), size.height.toInt());
    var bytes = await image.toByteData(format: ImageByteFormat.png);
    var fileName = (progress * 100).round().toString().padLeft(3, '0');

    var file = File(p.join(directory, '$fileName.png'));
    await file.writeAsBytes(bytes.buffer.asUint8List());

    frames.add(file);
  }

  return frames;
}

Future<String> _createTempDirectory(String folderName) async {
  final tempDirectory = await getTemporaryDirectory();
  var dir = Directory(p.join(tempDirectory.path, folderName));
  if (!dir.existsSync()) {
    await dir.create(recursive: true);
  }
  return dir.path;
}
hungtk commented 4 years ago

@xvrh Thank you so much. When I run with assets/HamburgerArrow.json with your code everything is perfect. But with https://github.com/xvrh/lottie-flutter/blob/master/example/assets/lottiefiles/airbnb.json it save a empty image.

xvrh commented 4 years ago

The airbnb uses a png image to display the logo. So to draw the animation it needs to also load the image after the json.

To do that, you can use the existing AssetLottie class that has the code to load the images from the assets. So instead of loading the file directly from the rootBundle Something like:

  var composition = await AssetLottie('assets/lottiefiles/airbnb.json').load();

I updated the example here: https://github.com/xvrh/lottie-flutter/blob/master/example/lib/examples/save_frames.dart

hungtk commented 4 years ago

@xvrh wow, awsome. Thank you so much for helping.