Skip to content

Commit

Permalink
Merge pull request #113 from seel-channel/fixInitRatio
Browse files Browse the repository at this point in the history
Init aspect ratio, fix crop ratio and scale
  • Loading branch information
LeGoffMael authored Nov 3, 2022
2 parents 357e69b + d237101 commit 8597ac7
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 186 deletions.
4 changes: 2 additions & 2 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class _VideoEditorState extends State<VideoEditor> {
void initState() {
_controller = VideoEditorController.file(widget.file,
maxDuration: const Duration(seconds: 30))
..initialize().then((_) => setState(() {}));
..initialize(aspectRatio: 9 / 16).then((_) => setState(() {}));
super.initState();
}

Expand Down Expand Up @@ -486,7 +486,7 @@ class CropScreen extends StatelessWidget {
buildSplashTap("16:9", 16 / 9,
padding: const EdgeInsets.symmetric(horizontal: 10)),
buildSplashTap("1:1", 1 / 1),
buildSplashTap("4:5", 4 / 5,
buildSplashTap("9:16", 9 / 16,
padding: const EdgeInsets.symmetric(horizontal: 10)),
buildSplashTap("NO", null,
padding: const EdgeInsets.only(right: 10)),
Expand Down
28 changes: 27 additions & 1 deletion lib/domain/bloc/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:ffmpeg_kit_flutter_min_gpl/return_code.dart';
import 'package:ffmpeg_kit_flutter_min_gpl/statistics.dart';
import 'package:path/path.dart' as path;
import 'package:flutter/material.dart';
import 'package:video_editor/domain/helpers.dart';
import 'package:video_player/video_player.dart';
import 'package:path_provider/path_provider.dart';

Expand Down Expand Up @@ -195,14 +196,38 @@ class VideoEditorController extends ChangeNotifier {
notifyListeners();
}

/// Update the [preferredCropAspectRatio] param and init/reset crop parameters [minCrop] & [maxCrop] to match the desired ratio
/// The crop area will be at the center of the layout
void cropAspectRatio(double? value) {
preferredCropAspectRatio = value;

if (value != null) {
final newSize =
computeSizeWithRatio(Size(_videoWidth, _videoHeight), value);

Rect centerCrop = Rect.fromCenter(
center: Offset(_videoWidth / 2, _videoHeight / 2),
width: newSize.width,
height: newSize.height,
);

minCrop =
Offset(centerCrop.left / _videoWidth, centerCrop.top / _videoHeight);
maxCrop = Offset(
centerCrop.right / _videoWidth, centerCrop.bottom / _videoHeight);
notifyListeners();
}
}

//----------------//
//VIDEO CONTROLLER//
//----------------//

/// Attempts to open the given video [File] and load metadata about the video.
/// Update the trim position depending on the [maxDuration] param
/// Generate the default cover [_selectedCover]
Future<void> initialize() async {
/// Initialize [minCrop] & [maxCrop] values based on [aspectRatio]
Future<void> initialize({double? aspectRatio}) async {
await _video.initialize().then((_) {
_videoWidth = _video.value.size.width;
_videoHeight = _video.value.size.height;
Expand All @@ -221,6 +246,7 @@ class VideoEditorController extends ChangeNotifier {
_updateTrimRange();
}

cropAspectRatio(aspectRatio);
generateDefaultCoverThumbnail();

notifyListeners();
Expand Down
26 changes: 10 additions & 16 deletions lib/domain/entities/transform_data.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:video_editor/domain/bloc/controller.dart';
import 'package:video_editor/domain/helpers.dart';

class TransformData {
TransformData({
Expand All @@ -11,25 +14,16 @@ class TransformData {
final Offset translate;

factory TransformData.fromRect(
// the selected crop rect area
Rect rect,
// the maximum size of the crop area
Size layout,
// the maximum size to display
Size maxSize,
VideoEditorController controller,
) {
final double videoAspect = controller.video.value.aspectRatio;
final double relativeAspect = rect.width / rect.height;

final double xScale = layout.width / rect.width;
final double yScale = layout.height / rect.height;

final double scale = videoAspect < 0.8
? relativeAspect <= 1
? yScale
: xScale + videoAspect
: relativeAspect < 0.8
? yScale + videoAspect
: xScale;

final double rotation = -controller.rotation * (3.1416 / 180.0);
final double scale = scaleToSize(maxSize, rect);
final double rotation = -controller.rotation * (pi / 180.0);
final Offset translate = Offset(
((layout.width - rect.width) / 2) - rect.left,
((layout.height - rect.height) / 2) - rect.top,
Expand All @@ -46,7 +40,7 @@ class TransformData {
VideoEditorController controller,
) {
return TransformData(
rotation: -controller.rotation * (3.1416 / 180.0),
rotation: -controller.rotation * (pi / 180.0),
scale: 1.0,
translate: Offset.zero,
);
Expand Down
73 changes: 73 additions & 0 deletions lib/domain/helpers.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'dart:math';

import 'package:flutter/material.dart';

/// Returns a desired dimension of [layout] that respect [r] aspect ratio
Size computeSizeWithRatio(Size layout, double r) {
if (layout.aspectRatio == r) {
return layout;
}

if (layout.aspectRatio > r) {
return Size(layout.height * r, layout.height);
}

if (layout.aspectRatio < r) {
return Size(layout.width, layout.width / r);
}

assert(false, 'An error occured while computing the aspectRatio');
return Size.zero;
}

/// Returns a new crop [Rect] that respect [r] aspect ratio
/// inside a [layout] and based on an existing [crop] area
///
/// This rect must not become smaller and smaller, or be out of bounds from [layout]
Rect resizeCropToRatio(Size layout, Rect crop, double r) {
// if target ratio is smaller than current crop ratio
if (r < crop.size.aspectRatio) {
// use longest crop side if smaller than layout longest side
final maxSide = min(crop.longestSide, layout.shortestSide);
// to calculate the ratio of the new crop area
final size = Size(maxSide, maxSide / r);

final rect = Rect.fromCenter(
center: crop.center,
width: size.width,
height: size.height,
);

// if res is smaller than layout we can return it
if (rect.size <= layout) return translateRectIntoBounds(layout, rect);
}

// if there is not enough space crop to the middle of the current [crop]
final newCenteredCrop = computeSizeWithRatio(crop.size, r);
final rect = Rect.fromCenter(
center: crop.center,
width: newCenteredCrop.width,
height: newCenteredCrop.height,
);

// return rect into bounds
return translateRectIntoBounds(layout, rect);
}

/// Returns a translated [Rect] that fit [layout] size
Rect translateRectIntoBounds(Size layout, Rect rect) {
final double translateX = (rect.left < 0 ? rect.left.abs() : 0) +
(rect.right > layout.width ? layout.width - rect.right : 0);
final double translateY = (rect.top < 0 ? rect.top.abs() : 0) +
(rect.bottom > layout.height ? layout.height - rect.bottom : 0);

if (translateX != 0 || translateY != 0) {
return rect.translate(translateX, translateY);
}

return rect;
}

/// Return the scale for [rect] to fit [layout]
double scaleToSize(Size layout, Rect rect) =>
min(layout.width / rect.width, layout.height / rect.height);
4 changes: 4 additions & 0 deletions lib/ui/cover/cover_selection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class _CoverSelectionState extends State<CoverSelection>
with AutomaticKeepAliveClientMixin {
double _aspect = 1.0, _width = 1.0;
Duration? _startTrim, _endTrim;

Size _viewerSize = Size.zero;
Size _layout = Size.zero;
final ValueNotifier<Rect> _rect = ValueNotifier<Rect>(Rect.zero);
final ValueNotifier<TransformData> _transform =
Expand Down Expand Up @@ -76,6 +78,7 @@ class _CoverSelectionState extends State<CoverSelection>
_transform.value = TransformData.fromRect(
_rect.value,
_layout,
_viewerSize,
widget.controller,
);

Expand Down Expand Up @@ -150,6 +153,7 @@ class _CoverSelectionState extends State<CoverSelection>
Widget build(BuildContext context) {
super.build(context);
return LayoutBuilder(builder: (_, box) {
_viewerSize = box.biggest;
final double width = box.maxWidth;
if (_width != width) {
_width = width;
Expand Down
113 changes: 59 additions & 54 deletions lib/ui/cover/cover_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class _CoverViewerState extends State<CoverViewer> {
final ValueNotifier<TransformData> _transform =
ValueNotifier<TransformData>(TransformData());

Size _viewerSize = Size.zero;
Size _layout = Size.zero;

late VideoEditorController _controller;
Expand Down Expand Up @@ -55,6 +56,7 @@ class _CoverViewerState extends State<CoverViewer> {
_transform.value = TransformData.fromRect(
_rect.value,
_layout,
_viewerSize,
_controller,
);

Expand Down Expand Up @@ -82,59 +84,62 @@ class _CoverViewerState extends State<CoverViewer> {

@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: _transform,
builder: (_, TransformData transform, __) => ValueListenableBuilder(
valueListenable: widget.controller.selectedCoverNotifier,
builder: (context, CoverData? selectedCover, __) => selectedCover
?.thumbData ==
null
? Center(child: Text(widget.noCoverText))
: CropTransform(
transform: transform,
child: Center(
child: Stack(children: [
AspectRatio(
aspectRatio: widget.controller.video.value.aspectRatio,
child: Image(
image: MemoryImage(selectedCover!.thumbData!),
alignment: Alignment.center,
),
),
AspectRatio(
aspectRatio:
widget.controller.video.value.aspectRatio,
child: LayoutBuilder(
builder: (_, constraints) {
Size size = Size(
constraints.maxWidth, constraints.maxHeight);
if (_layout != size) {
_layout = size;
// init the widget with controller values
WidgetsBinding.instance
.addPostFrameCallback((_) {
_scaleRect();
});
}

return ValueListenableBuilder(
valueListenable: _rect,
builder: (_, Rect value, __) {
return CustomPaint(
size: Size.infinite,
painter: CropGridPainter(
value,
style: _controller.cropStyle,
showGrid: false,
showCenterRects: _controller
.preferredCropAspectRatio ==
null,
),
);
},
);
},
))
])))));
return LayoutBuilder(builder: (_, constraints) {
_viewerSize = constraints.biggest;

return ValueListenableBuilder(
valueListenable: _transform,
builder: (_, TransformData transform, __) => ValueListenableBuilder(
valueListenable: widget.controller.selectedCoverNotifier,
builder: (context, CoverData? selectedCover, __) =>
selectedCover?.thumbData == null
? Center(child: Text(widget.noCoverText))
: CropTransform(
transform: transform,
child: Center(
child: Stack(children: [
AspectRatio(
aspectRatio:
widget.controller.video.value.aspectRatio,
child: Image(
image: MemoryImage(selectedCover!.thumbData!),
alignment: Alignment.center,
),
),
AspectRatio(
aspectRatio:
widget.controller.video.value.aspectRatio,
child: LayoutBuilder(
builder: (_, constraints) {
Size size = constraints.biggest;
if (_layout != size) {
_layout = size;
// init the widget with controller values
WidgetsBinding.instance
.addPostFrameCallback((_) {
_scaleRect();
});
}

return ValueListenableBuilder(
valueListenable: _rect,
builder: (_, Rect value, __) {
return CustomPaint(
size: Size.infinite,
painter: CropGridPainter(
value,
style: _controller.cropStyle,
showGrid: false,
showCenterRects: _controller
.preferredCropAspectRatio ==
null,
),
);
},
);
},
))
])))));
});
}
}
Loading

0 comments on commit 8597ac7

Please sign in to comment.