Last active
January 11, 2022 10:58
-
-
Save kouseralamin/333d08328d0d7349b4bd53d755bc9ae9 to your computer and use it in GitHub Desktop.
Animatics
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:convert'; | |
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({Key? key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Ken Burn Instruction Widget', | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
home: const MyHomePage(), | |
); | |
} | |
} | |
class MyHomePage extends StatelessWidget { | |
const MyHomePage({Key? key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(), | |
body: Column( | |
children: [ | |
TextButton( | |
onPressed: () { | |
double aspectRatio = 0.0; | |
showDialog( | |
context: context, | |
builder: (context) { | |
return Dialog( | |
child: Column( | |
children: [ | |
const Text('Aspect Ratio'), | |
TextField( | |
keyboardType: TextInputType.number, | |
onChanged: (value) { | |
aspectRatio = double.parse(value); | |
}, | |
), | |
TextButton( | |
onPressed: () { | |
Navigator.pop(context, aspectRatio.toString()); | |
}, | |
child: const Text('OK'),), | |
], | |
), | |
); | |
}, | |
).then((value) { | |
if (value != null) { | |
Navigator.push( | |
context, | |
MaterialPageRoute(builder: (context) => KenBurnIWC(aspectRatio)), | |
); | |
} | |
}); | |
}, | |
child: const Text('Create Screen'), | |
), | |
TextButton( | |
onPressed: () { | |
String json = ''; | |
showDialog( | |
context: context, | |
builder: (context) { | |
return Dialog( | |
child: Column( | |
children: [ | |
const Text('JSON'), | |
TextField( | |
onChanged: (value) { | |
json = value; | |
}, | |
), | |
TextButton( | |
onPressed: () { | |
Navigator.pop(context, json); | |
}, | |
child: const Text('OK'),), | |
], | |
), | |
); | |
}, | |
).then((value) { | |
if (value != null) { | |
Navigator.push( | |
context, | |
MaterialPageRoute(builder: (context) => CreateViewer(json)), | |
); | |
} | |
}); | |
}, | |
child: const Text('View Shared'), | |
), | |
], | |
), | |
); | |
} | |
} | |
class CreateViewer extends StatefulWidget { | |
const CreateViewer(this.instruction, {Key? key}) : super(key: key); | |
final String instruction; | |
@override | |
State<CreateViewer> createState() => _CreateViewerState(); | |
} | |
class _CreateViewerState extends State<CreateViewer> { | |
Map i = {}; | |
@override | |
void initState() { | |
super.initState(); | |
i = jsonDecode(widget.instruction); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
leading: IconButton( | |
icon: const Icon(Icons.copy), | |
onPressed: () { | |
print("--------------------------------------------"); | |
print(widget.instruction); | |
print("--------------------------------------------"); | |
}, | |
), | |
), | |
body: Row( | |
children: [ | |
Expanded( | |
child: Align( | |
alignment: Alignment.center, | |
child: i.isNotEmpty ? AnimaticsIW(i) : const Text('MAKING')), | |
), | |
], | |
), | |
); | |
} | |
} | |
class KenBurnIWC extends StatefulWidget { | |
const KenBurnIWC(this.aspectRatio, {Key? key}) : super(key: key); | |
final double aspectRatio; | |
@override | |
_KenBurnIWCState createState() => _KenBurnIWCState(); | |
} | |
class _KenBurnIWCState extends State<KenBurnIWC> { | |
void _go() { | |
instructionList['il'] = il; | |
Navigator.push( | |
context, | |
MaterialPageRoute(builder: (context) => CreateViewer(jsonEncode(instructionList))), | |
); | |
} | |
dynamic il = []; | |
Map instructionList = {'meta': {'aspectRatio': 0.0, 'audioUrl': ''}, 'il': []}; | |
Map currentInstruction = {}; | |
String _state = ''; | |
String imageURL = ''; | |
int duration = 0; | |
double tx = 0.25; | |
double y0 = 0.75; | |
double x1 = 0.25; | |
double x2 = 0.75; | |
String controller = ''; | |
@override | |
void initState() { | |
super.initState(); | |
instructionList['meta']['aspectRatio'] = widget.aspectRatio; | |
} | |
Future<void> _setImage() async { | |
setState(() { | |
_state = 'setImage'; | |
}); | |
await showDialog( | |
context: context, | |
builder: (context) { | |
return Dialog( | |
child: Column( | |
children: [ | |
const Text('Image Direct Url'), | |
TextField( | |
keyboardType: TextInputType.url, | |
onChanged: (value) { | |
setState(() { | |
imageURL = value; | |
}); | |
}, | |
), | |
TextButton( | |
onPressed: () { | |
_getCoordinates(); | |
Navigator.pop(context, imageURL.toString()); | |
}, | |
child: const Text('OK'), | |
), | |
], | |
), | |
); | |
}, | |
); | |
} | |
void _setImageOK() async { | |
await showDialog( | |
context: context, | |
builder: (context) { | |
return Dialog( | |
child: Column( | |
children: [ | |
const Text('Set Duration'), | |
TextField( | |
keyboardType: TextInputType.number, | |
onChanged: (value) { | |
setState(() { | |
duration = int.parse(value); | |
}); | |
}, | |
), | |
TextButton( | |
onPressed: () { | |
List _clipArea = _getCoordinates(); | |
currentInstruction = { | |
'instruction': 'setImage', | |
'imageUrl': imageURL, | |
'clipArea': _clipArea, | |
'duration': duration | |
}; | |
il.add(currentInstruction); | |
Navigator.pop(context, imageURL.toString()); | |
setState(() { | |
_state = ''; | |
}); | |
}, | |
child: const Text('OK'), | |
), | |
], | |
), | |
); | |
}, | |
); | |
} | |
void _clipArea() async { | |
await showDialog( | |
context: context, | |
builder: (context) { | |
return Dialog( | |
child: Column( | |
children: [ | |
const Text('Duration'), | |
TextField( | |
keyboardType: TextInputType.number, | |
onChanged: (value) { | |
setState(() { | |
duration = int.parse(value); | |
}); | |
}, | |
), | |
TextButton( | |
onPressed: () { | |
List _clipArea = _getCoordinates(); | |
currentInstruction = { | |
'instruction': 'clip', | |
'clipArea': _clipArea, | |
'duration': duration | |
}; | |
il.add(currentInstruction); | |
Navigator.pop(context, imageURL.toString()); | |
setState(() { | |
_state = ''; | |
}); | |
}, | |
child: const Text('OK'), | |
), | |
], | |
), | |
); | |
}, | |
); | |
} | |
final canvas = GlobalKey(); | |
final clipArea = GlobalKey(); | |
List _getCoordinates() { | |
RenderBox? _canvas = canvas.currentContext!.findRenderObject()! as RenderBox; | |
final _canvasBox = _canvas.localToGlobal(Offset.zero); | |
double _canvasBoxPY = _canvasBox.dy; | |
double _canvasBoxPX = _canvasBox.dx; | |
double _canvasBoxH = _canvas.size.height; | |
double _canvasBoxW = _canvas.size.width; | |
RenderBox? _clipArea = clipArea.currentContext!.findRenderObject()! as RenderBox; | |
final _clipAreaBox = _clipArea.localToGlobal(Offset.zero); | |
double _clipAreaPY = _clipAreaBox.dy; | |
double _clipAreaPX = _clipAreaBox.dx; | |
double _clipAreaH = _clipArea.size.height; | |
double _cliaAreaW = _clipArea.size.width; | |
double _y0 = (_clipAreaPY + _clipAreaH - _canvasBoxPY) / _canvasBoxH; | |
double _x1 = (_clipAreaPX - _canvasBoxPX) / _canvasBoxW; | |
double _x2 = (_clipAreaPX + _cliaAreaW - _canvasBoxPX) / _canvasBoxW; | |
return [_y0, _x1, _x2]; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(), | |
body: Column( | |
children: [ | |
const Text('SPACE'), | |
Row( | |
children: [ | |
const Text('SPACE'), | |
Container( | |
child: _state == '' | |
? Column( | |
children: [ | |
TextButton( | |
onPressed: () { | |
_setImage(); | |
}, | |
child: const Text('setImage'), | |
), | |
TextButton( | |
onPressed: () { | |
_clipArea(); | |
}, | |
child: const Text('clip'), | |
), | |
TextButton( | |
onPressed: () { | |
_go(); | |
}, | |
child: const Text('go'), | |
) | |
], | |
) | |
: _state == 'setImage' | |
? TextButton( | |
onPressed: () { | |
_setImageOK(); | |
}, | |
child: const Text('OK'), | |
) | |
: null), | |
], | |
), | |
Expanded( | |
child: Align( | |
alignment: Alignment.center, | |
child: AspectRatio( | |
key: canvas, | |
aspectRatio: widget.aspectRatio, | |
child: LayoutBuilder( | |
builder: (BuildContext context, BoxConstraints constraints) { | |
return Container( | |
color: Colors.black, | |
child: Stack( | |
alignment: Alignment.center, | |
children: [ | |
Container( | |
color: Colors.black, | |
child: imageURL != '' | |
? SizedBox( | |
width: constraints.maxWidth, | |
height: constraints.maxHeight, | |
child: Image.network(imageURL, fit: BoxFit.contain,), | |
) | |
: Container() | |
), | |
Positioned( | |
top: tx * constraints.maxHeight, | |
left: x1 * constraints.maxWidth, | |
child: Container( | |
color: Colors.white.withOpacity(0.5), | |
width: (x2 - x1) * constraints.maxWidth, | |
height: (y0 - tx) * constraints.maxHeight, | |
child: Align( | |
alignment: Alignment.center, | |
child: AspectRatio( | |
aspectRatio: widget.aspectRatio, | |
child: Container( | |
key: clipArea, | |
color: Colors.green.withOpacity(0.5), | |
), | |
), | |
), | |
), | |
), | |
Positioned( | |
top: tx * constraints.maxHeight - 10, | |
left: x1 * constraints.maxWidth - 10, | |
child: Container( | |
color: Colors.red, | |
width: 20, | |
height: 20, | |
), | |
), | |
Positioned( | |
top: y0 * constraints.maxHeight - 10, | |
left: x2 * constraints.maxWidth - 10, | |
child: Container( | |
color: Colors.blue, | |
width: 20, | |
height: 20, | |
), | |
), | |
Listener( | |
onPointerDown: (PointerEvent details) { | |
if((tx * constraints.maxHeight + 10) > details.localPosition.dy | |
&& (tx * constraints.maxHeight - 10) < details.localPosition.dy | |
&& (x1 * constraints.maxWidth + 10) > details.localPosition.dx | |
&& (x1 * constraints.maxWidth - 10) < details.localPosition.dx | |
) { | |
controller = 'red'; | |
} | |
if((y0 * constraints.maxHeight + 10) > details.localPosition.dy | |
&& (y0 * constraints.maxHeight - 10) < details.localPosition.dy | |
&& (x2 * constraints.maxWidth + 10) > details.localPosition.dx | |
&& (x2 * constraints.maxWidth - 10) < details.localPosition.dx | |
) { | |
controller = 'blue'; | |
} | |
}, | |
onPointerMove: (PointerEvent details) { | |
final double dy; | |
final double dx; | |
if(details.localPosition.dy > constraints.maxHeight) { | |
dy = constraints.maxHeight; | |
} else if(details.localPosition.dy < 0) { | |
dy = 0; | |
} else { | |
dy = details.localPosition.dy; | |
} | |
if(details.localPosition.dx > constraints.maxWidth) { | |
dx = constraints.maxWidth; | |
} else if(details.localPosition.dx < 0) { | |
dx = 0; | |
} else { | |
dx = details.localPosition.dx; | |
} | |
if(controller == 'red' | |
&& (x1 * constraints.maxWidth + 10) <= x2 * constraints.maxWidth) { | |
setState(() { | |
tx = dy / constraints.maxHeight; | |
x1 = dx / constraints.maxWidth; | |
}); | |
} | |
if(controller == 'blue') { | |
setState(() { | |
y0 = dy / constraints.maxHeight; | |
x2 = dx / constraints.maxWidth; | |
}); | |
} | |
}, | |
onPointerUp: (PointerEvent details) { | |
controller = ''; | |
}, | |
child: Container( | |
color: Colors.transparent, | |
), | |
), | |
], | |
), | |
); | |
} | |
), | |
), | |
), | |
), | |
Text('tx: $tx'), | |
Text('y0: $y0'), | |
Text('x1: $x1'), | |
Text('x2: $x2'), | |
], | |
), | |
); | |
} | |
} | |
class AnimaticsIW extends StatefulWidget { | |
const AnimaticsIW(this.instruction, {Key? key}) : super(key: key); | |
final Map instruction; | |
@override | |
_AnimaticsIWState createState() => _AnimaticsIWState(); | |
} | |
class _AnimaticsIWState extends State<AnimaticsIW> with TickerProviderStateMixin { | |
late Map i; | |
late AnimationController controller; | |
String imageURL = ''; | |
//old clip position | |
double y0old = 1.0; | |
double x1old = 0.0; | |
double x2old = 1.0; | |
double y0 = 1.0; | |
double x1 = 0.0; | |
double x2 = 1.0; | |
double opacity = 0.0; | |
//background image | |
String bimageURL = ''; | |
double by0 = 1.0; | |
double bx1 = 1.0; | |
double bx2 = 1.0; | |
@override | |
void initState() { | |
super.initState(); | |
i = widget.instruction; | |
debugPrint(i.toString()); | |
_kenburnDO(); | |
} | |
void _kenburnDO() async { | |
debugPrint('doKenBurn'); | |
await Future.forEach(i['il'], (dynamic instruction) async { | |
switch (instruction['instruction']) { | |
case 'setImage': | |
debugPrint('setImage'); | |
setState(() { | |
bimageURL = instruction['imageUrl'].toString(); | |
by0 = instruction['clipArea'][0]; | |
bx1 = instruction['clipArea'][1]; | |
bx2 = instruction['clipArea'][2]; | |
}); | |
controller = AnimationController( | |
vsync: this, | |
duration: Duration(milliseconds: instruction['duration']), | |
); | |
controller.addListener(() { | |
setState(() { | |
opacity = 1 - controller.value; | |
}); | |
}); | |
await controller.forward(); | |
setState(() { | |
imageURL = bimageURL; | |
bimageURL = ''; | |
opacity = 1.0; | |
y0old = by0; | |
x1old = bx1; | |
x2old = bx2; | |
y0 = by0; | |
x1 = bx1; | |
x2 = bx2; | |
}); | |
break; | |
case 'clip': | |
debugPrint('clip'); | |
controller = AnimationController( | |
vsync: this, | |
duration: Duration(milliseconds: instruction['duration']), | |
); | |
controller.addListener(() { | |
setState(() { | |
y0 = (1 - controller.value) * y0old + controller.value * instruction['clipArea'][0]; | |
x1 = (1 - controller.value) * x1old + controller.value * instruction['clipArea'][1]; | |
x2 = (1 - controller.value) * x2old + controller.value * instruction['clipArea'][2]; | |
}); | |
}); | |
await controller.forward(); | |
y0old = y0; | |
x1old = x1; | |
x2old = x2; | |
break; | |
} | |
}); | |
} | |
@override | |
void dispose() { | |
controller.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return AspectRatio( | |
aspectRatio: i['meta']['aspectRatio'].toDouble(), | |
child: LayoutBuilder( | |
builder: (BuildContext context, BoxConstraints constraints) { | |
return Container( | |
color: Colors.black, | |
child: ClipRect( | |
child: Stack( | |
alignment: Alignment.center, | |
children: [ | |
//background | |
Opacity( | |
opacity: 1 - opacity, | |
child: Transform.translate( | |
// width height | |
offset: Offset( | |
constraints.maxWidth/(bx2 - bx1)/2 - constraints.maxWidth/2 - constraints.maxWidth/(bx2 - bx1) * bx1, | |
constraints.maxHeight/(bx2 - bx1)/2 - constraints.maxHeight/2 + constraints.maxHeight - constraints.maxHeight/(bx2 - bx1) * by0, | |
), | |
child: Transform.scale( | |
scale: 1 / (bx2 - bx1), | |
child: SizedBox( | |
width: constraints.maxWidth, | |
height: constraints.maxHeight, | |
child: bimageURL != '' | |
? Image.network(bimageURL, fit: BoxFit.contain,) | |
: Container() | |
), | |
), | |
), | |
), | |
//foreground | |
Opacity( | |
opacity: opacity, | |
child: Transform.translate( | |
// width height | |
offset: Offset( | |
constraints.maxWidth/(x2 - x1)/2 - constraints.maxWidth/2 - constraints.maxWidth/(x2 -x1) * x1, | |
constraints.maxHeight/(x2 - x1)/2 - constraints.maxHeight/2 + constraints.maxHeight - constraints.maxHeight/(x2 -x1) * y0, | |
), | |
child: Transform.scale( | |
scale: 1 / (x2 - x1), | |
child: SizedBox( | |
width: constraints.maxWidth, | |
height: constraints.maxHeight, | |
child: imageURL != '' | |
? Image.network(imageURL, fit: BoxFit.contain,) | |
: Container(color: Colors.black,), | |
), | |
), | |
), | |
), | |
], | |
), | |
), | |
); | |
}, | |
) | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment