Skip to content

Instantly share code, notes, and snippets.

@av
Created October 25, 2019 14:00
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save av/899ec44e816abe025eb1fc313d7febdd to your computer and use it in GitHub Desktop.
Save av/899ec44e816abe025eb1fc313d7febdd to your computer and use it in GitHub Desktop.
Flutte: magic
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
void main() async {
runApp(MyApp());
}
/// Represents some additional [Color]s to be used
/// across the application
class Palette {
static final background = Color(0xff202030);
static final background700 = Color(0xff303040);
static final accent = Colors.deepPurpleAccent.shade200;
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
var textTheme = Typography.whiteMountainView.copyWith(
subtitle: Typography.whiteMountainView.subtitle.copyWith(
fontWeight: FontWeight.w900,
),
title: Typography.whiteMountainView.title.copyWith(
fontWeight: FontWeight.w900,
letterSpacing: -1,
fontSize: 28,
color: Colors.white,
),
display1: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w600,
letterSpacing: -2,
),
caption: Typography.whiteMountainView.caption.copyWith(
fontStyle: FontStyle.italic,
),
);
return MaterialApp(
theme: ThemeData.dark().copyWith(
textTheme: textTheme,
accentColor: Palette.accent,
scaffoldBackgroundColor: Palette.background,
backgroundColor: Palette.background,
),
home: Root(),
);
}
}
/// Application root container
class Root extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
// Black backdrop when outscrolling
// the view to the top
Container(
color: Colors.black,
height: 350,
),
SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black,
Palette.background,
],
stops: [
0,
.2
]),
),
child: Column(
children: <Widget>[
Toolbar(),
Categories(),
Padding(
padding: const EdgeInsets.all(32.0),
child: Quote(
body:
'It\'s still magic even if you know how it\'s done. 🔮',
author: 'Terry Pratchett, A Hat Full of Sky',
),
),
ProductSetout(products: firstRowProducts.values.toList()),
ProductSetout(products: secondRowProducts.values.toList()),
Container(
height: 80,
)
],
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
height: 80,
child: Cart(),
)
],
),
);
}
}
class Quote extends StatelessWidget {
final String body;
final String author;
const Quote({this.body, this.author});
@override
Widget build(BuildContext context) {
var textTheme = Theme.of(context).textTheme;
return Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Icon(
Icons.format_quote,
size: 40.0,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'$body',
style: textTheme.display1,
),
Container(
height: 8.0,
),
Text(
author,
style: textTheme.caption,
)
],
),
),
],
),
);
}
}
class Categories extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 20,
child: ListView(
scrollDirection: Axis.horizontal,
physics: BouncingScrollPhysics(),
children: <Widget>[
Category(name: 'All', selected: true),
Category(name: 'Cards'),
Category(name: 'Coins'),
Category(name: 'Mentalism'),
Category(name: 'Comedy'),
],
),
);
}
}
class Category extends StatelessWidget {
final String name;
final bool selected;
Category({
this.name,
this.selected = false,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 32.0),
padding: selected ? const EdgeInsets.symmetric(horizontal: 8.0) : null,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
color: selected ? Colors.white : Colors.transparent,
),
child: Text(
name.toUpperCase(),
style: Theme.of(context).textTheme.subtitle.copyWith(
height: 1.3, color: selected ? Palette.background : Colors.white),
),
);
}
}
class Toolbar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 48.0, horizontal: 32.0),
child: Row(children: <Widget>[
Icon(Icons.menu),
flexy(),
Text('Pure Magic Inc.'.toUpperCase(),
style: Theme.of(context).textTheme.title)
]),
);
}
}
class ProductSetout extends StatefulWidget {
final List<Product> products;
const ProductSetout({this.products});
@override
_ProductSetoutState createState() => _ProductSetoutState();
}
class _ProductSetoutState extends State<ProductSetout> {
final Random rnd = Random();
ScrollController controller;
@override
void initState() {
controller = ScrollController(
initialScrollOffset: rnd.nextInt(500).toDouble(),
);
super.initState();
}
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: BoxConstraints.tightFor(height: 380),
child: ListView(
padding: const EdgeInsets.all(16.0),
controller: controller,
scrollDirection: Axis.horizontal,
physics: BouncingScrollPhysics(),
children: widget.products
.map<Widget>((product) => ProductCard(product: product))
.toList(),
),
);
}
}
class ProductCard extends StatelessWidget {
final Product product;
ProductCard({this.product});
@override
Widget build(BuildContext context) {
return Container(
width: 240,
margin: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Palette.background700,
borderRadius: BorderRadius.circular(32.0),
boxShadow: [
BoxShadow(
blurRadius: 16,
offset: Offset(0, 16),
color: Colors.black26,
)
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(32.0),
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Container(
foregroundDecoration: BoxDecoration(
color: Colors.grey.withOpacity(.5),
backgroundBlendMode: BlendMode.saturation,
),
child: Image.network(
product.photo,
fit: BoxFit.cover,
),
),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: [0.2, 0.8],
colors: [Colors.transparent, Palette.background700],
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
flexy(),
Text(
product.name,
style: Theme.of(context).textTheme.title,
),
Container(
height: 8.0,
),
Text(
product.description,
style: Theme.of(context).textTheme.caption,
),
Container(
height: 16.0,
),
OutlineButton(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('\$${product.price}',
style: Theme.of(context).textTheme.title),
],
),
),
onPressed: () {
print('Adding $product to cart!');
},
)
],
),
)
],
),
),
);
}
}
class Cart extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(32.0),
topRight: Radius.circular(32.0),
),
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
),
child: Container(
decoration: BoxDecoration(
color: Colors.white24,
),
padding: const EdgeInsets.all(16.0),
child: Row(
children: <Widget>[
flexy(),
OutlineButton(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0, horizontal: 16.0),
child: Text(
'Checkout',
style: Theme.of(context).textTheme.title,
),
),
onPressed: () {
print('Checking out all items in the cart!');
},
)
],
),
),
),
),
);
}
}
Widget flexy() {
return Expanded(
child: Container(),
);
}
class Product {
final String name;
final String description;
final String photo;
final double price;
Product({this.name, this.description, this.photo, this.price});
@override
String toString() => 'Product($name)';
}
final Map<String, Product> firstRowProducts = {
'vanishingCoins': Product(
name: 'Bitcoin',
description: 'Make money instantly vanish in your hand!',
photo:
'https://cdn.pixabay.com/photo/2017/02/11/10/24/bitcoin-2057405__340.jpg',
price: 59.99,
),
'diamondsCard': Product(
name: 'Hand of midas',
description: 'Do not pet your dog with it!',
photo:
'https://www.smashinglists.com/wp-content/uploads/2010/07/Hand-of-Midas1-600x399.jpg',
price: 99.99,
),
'flyingCarpter': Product(
name: 'Flying Carpet',
description: 'You best last mile transport. Works for any other mile too!',
price: 499.99,
photo:
'https://www.smashinglists.com/wp-content/uploads/2010/07/flying-carpet-600x330.jpg',
),
};
final Map<String, Product> secondRowProducts = {
'magicLamp': Product(
name: 'Magic Lamp',
description: 'Rub with caution, do not shake.',
price: 69.99,
photo:
'https://www.smashinglists.com/wp-content/uploads/2010/07/magic-lamp-600x416.jpg',
),
'theRing': Product(
name: 'The Ring',
description:
'Just a ring. Nothing special about it. Just move on. Do not look. Skip it.',
price: 89.99,
photo:
'https://www.smashinglists.com/wp-content/uploads/2010/07/onering-600x431.jpg',
),
'philosophersStone': Product(
name: 'Philosopher\'s stone',
description:
'Philosopher didn\'t tell what the stone is for. Should be useful.',
price: 129.99,
photo:
'https://static1.therichestimages.com/wordpress/wp-content/uploads/2016/11/pottermore.jpg?q=50&fit=crop&w=963&h=617'),
};
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
mixin Drawable {
void draw(Canvas canvas, Size size);
}
mixin Updatable {
void update(Animation animation);
}
enum FadingDirection { fadeIn, fadeOut }
mixin Fading on Updatable {
FadingDirection direction = FadingDirection.fadeOut;
double opacity;
@override
void update(Animation controller) {
if (direction == FadingDirection.fadeOut) {
opacity = (1 - controller.value);
} else {
opacity = controller.value;
}
}
}
mixin Scaling on Updatable {
double from = 0;
double to = 1;
double current;
@override
void update(Animation animation) {
current = lerpDouble(from, to, animation.value);
}
}
mixin Tweened on Updatable {
Tween tween;
double current;
@override
void update(Animation animation) {
super.update(tween.animate(animation));
}
}
mixin Curved on Updatable {
Curve curve;
@override
void update(Animation animation) {
super.update(
CurveTween(curve: curve).animate(animation),
);
}
}
mixin Moving on Updatable {
Offset from;
Offset to;
Offset current;
@override
void update(Animation animation) {
current = Offset.lerp(from, to, animation.value);
}
}
typedef ParticlesWidgetBuilder = Widget Function(
BuildContext context,
AnimationController controller,
);
class Particles extends StatefulWidget {
final Duration duration;
final Particle particle;
final ParticlesWidgetBuilder builder;
final Curve curve;
const Particles({
@required this.particle,
@required this.builder,
this.duration = const Duration(milliseconds: 400),
this.curve = Curves.linear,
});
@override
_ParticlesState createState() => _ParticlesState();
}
class _ParticlesState extends State<Particles>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation animation;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: widget.duration,
);
animation = CurvedAnimation(curve: widget.curve, parent: controller);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
return CustomPaint(
child: child,
painter: ParticlePainter(
animation: animation,
particle: widget.particle,
),
);
},
child: widget.builder(context, controller),
);
}
}
abstract class Particle implements Drawable, Updatable {
@override
void draw(Canvas canvas, Size size) {
// Does nothing by default
}
@override
void update(Animation controller) {
// Does nothing by default
}
}
mixin NestedParticle on Particle {
Particle child;
@override
void draw(Canvas canvas, Size size) {
super.draw(canvas, size);
child.draw(canvas, size);
}
@override
void update(Animation controller) {
super.update(controller);
child.update(controller);
}
}
mixin CompositeParticle on Particle {
List<Particle> children;
@override
void draw(Canvas canvas, Size size) {
super.draw(canvas, size);
for (var child in children) {
child.draw(canvas, size);
}
}
@override
void update(Animation animation) {
super.update(animation);
for (var child in children) {
child.update(animation);
}
}
}
class Aligned extends Particle with NestedParticle {
final Alignment alignment;
Aligned({
this.alignment = Alignment.center,
@required Particle child,
}) {
this.child = child;
}
@override
void draw(Canvas canvas, Size size) {
var offset = alignment.alongSize(size);
canvas.save();
canvas.translate(offset.dx, offset.dy);
super.draw(canvas, size);
canvas.restore();
}
}
class FadingRect extends Particle with Fading {
Size size;
FadingRect({
this.size = const Size(50, 50),
});
@override
void draw(Canvas canvas, Size size) {
canvas.drawRect(
Rect.fromCenter(
center: Offset.zero,
width: this.size.width,
height: this.size.height),
Paint()..color = Colors.white.withOpacity(opacity),
);
}
}
/// Helpers for randomized dimensions
class Randoms {
static final rnd = Random();
/// Returns a random [Offset] from the "center" point of given size
static Offset offsetFromSize(Size size) {
return Offset(
(rnd.nextDouble() * size.width) - (size.width / 2),
(rnd.nextDouble() * size.height) - (size.height / 2),
);
}
static Alignment alignment() {
return Alignment(
rnd.nextDouble() * 2 - 1,
rnd.nextDouble() * 2 - 1,
);
}
}
/// [Burst] takes a list of children [Particle],
/// and wraps each one of them with [MovingParticle] in a random direction
/// from the center of the canvas, within specified [Size]
class Burst extends Particle with CompositeParticle {
List<Particle> children;
Burst({
@required List<Particle> children,
Size size = const Size(100, 100),
}) {
this.children = children
.map<Particle>(
(particle) => CurvedParticle(
curve: Interval(
Randoms.rnd.nextDouble() * .3,
Randoms.rnd.nextDouble() * .4 + .6,
),
child: MovingParticle(
from: Offset.zero,
to: Randoms.offsetFromSize(size),
child: particle,
),
),
)
.toList();
}
}
/// A function which returns [Particle] when called
typedef ParticleProvider = Particle Function(int i);
/// A [CompositeParticle] which allows to use [ParticleProvider]
/// generator functions as source for children particles
///
/// ```dart
/// // 10 plain fading circles
/// ParticleGenerator(10, (i) => FadingCircle());
///
/// // 10 randomly sized fading circles
/// ParticleGenerator(10, (i) => FadingCircle(radius: Randoms.rnd.nextDouble() * 10));
///
/// // 5 small and 5 large fading circles
/// ParticleGenerator(10, (i) => FadingCircle(radius: i < 5 ? 10 : 20));
/// ```
class ParticleGenerator extends Particle with CompositeParticle {
List<Particle> children;
ParticleGenerator(
int count,
ParticleProvider generator,
) {
this.children = List<Particle>.generate(count, generator);
}
}
class FadingCircle extends Particle with Fading {
final double radius;
final Color color;
FadingCircle({
this.radius = 10,
this.color = Colors.white,
});
@override
void draw(Canvas canvas, Size size) {
canvas.drawCircle(
Offset.zero,
radius,
Paint()..color = color.withOpacity(opacity),
);
}
}
/// Just a container for parameters
/// allowing to draw a circle on a [Canvas] later
class CircleParameters {
double radius;
Offset offset;
CircleParameters({@required this.radius, @required this.offset});
}
/// Renders certain amount of circles on a [Canvas],
/// as per given [CircleParameters] list.
class Circles extends Particle with Fading {
final List<CircleParameters> circles;
/// Generates randomly positioned circles in
/// given count with radius between given bounds.
factory Circles.random({
int count = 10,
double maxRadius = 10,
double minRadius,
}) {
if (minRadius == null) {
minRadius = maxRadius * .1;
}
return Circles(
circles: List<CircleParameters>.generate(
count,
(i) => CircleParameters(
radius: Randoms.rnd.nextDouble() * maxRadius + minRadius,
offset: Randoms.offsetFromSize(
Size(maxRadius, maxRadius),
),
),
),
);
}
Circles({this.circles});
@override
void draw(Canvas canvas, Size size) {
for (var circle in circles) {
canvas.drawCircle(
circle.offset,
circle.radius,
Paint()..color = Colors.white.withOpacity(opacity),
);
}
}
}
class MovingParticle extends Particle with Moving, NestedParticle {
MovingParticle({
@required Offset from,
@required Offset to,
@required Particle child,
}) {
this.from = from;
this.to = to;
this.child = child;
}
@override
void draw(Canvas canvas, Size size) {
canvas.save();
canvas.translate(current.dx, current.dy);
super.draw(canvas, size);
canvas.restore();
}
}
class ScalingParticle extends Particle with Scaling, NestedParticle {
double from;
double to;
Particle child;
ScalingParticle({
this.from = 0.0,
this.to = 1.0,
@required this.child,
});
@override
void draw(Canvas canvas, Size size) {
canvas.save();
canvas.scale(current);
super.draw(canvas, size);
canvas.restore();
}
}
class TweenedParticle extends Particle with NestedParticle, Tweened {
Tween tween;
Particle child;
TweenedParticle({
@required this.tween,
@required this.child,
});
}
class CurvedParticle extends Particle with NestedParticle, Curved {
Curve curve;
Particle child;
CurvedParticle({
@required this.curve,
@required this.child,
});
}
class DrawablePainter extends CustomPainter {
final Drawable child;
DrawablePainter({this.child});
@override
void paint(Canvas canvas, Size size) {
child.draw(canvas, size);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
class ParticlePainter extends CustomPainter {
final Animation animation;
final Particle particle;
ParticlePainter({@required this.animation, @required this.particle}) {
particle.update(animation);
}
@override
void paint(Canvas canvas, Size size) {
particle.draw(canvas, size);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) =>
!(animation.isCompleted && animation.isDismissed);
}
class Puff extends Particle with NestedParticle {
Particle child;
Puff() {
// Will generate 50 nested particles
this.child = ParticleGenerator(
50,
// Each of which would be positioned randomly
// within enclosing canvas
(i) => Aligned(
alignment: Randoms.alignment(),
// And with equal possibility would be either
child: Randoms.rnd.nextDouble() > .5
// Burst of large white circles flowing
// slightly upwards
? CurvedParticle(
curve: Interval(
.3,
1,
),
child: Burst(
children: List<Particle>.generate(
2,
(_) => MovingParticle(
child: ScalingParticle(
child: Circles.random(count: 2, maxRadius: 50),
),
from: Offset.zero,
to: Offset(0, -30),
),
),
),
)
// Or a circle appearing slightly lately
// and not moving anywhere
: CurvedParticle(
curve: Interval(
Randoms.rnd.nextDouble() * .5 + .5,
1.0,
curve: Curves.fastOutSlowIn,
),
child: ScalingParticle(
child: FadingCircle(
// Of either of two given colors
color: Randoms.rnd.nextDouble() > .5
? Palette.accent
: Colors.purpleAccent,
),
),
),
),
);
}
}
class OutlineButton extends StatelessWidget {
final Widget child;
final void Function() onPressed;
const OutlineButton({
@required this.child,
@required this.onPressed,
});
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.circular(32.0);
return Particles(
curve: Curves.easeOutQuint,
duration: const Duration(milliseconds: 1400),
builder: (context, controller) {
return ClipRRect(
borderRadius: borderRadius,
child: RawMaterialButton(
onPressed: () {
controller.reset();
controller.forward();
onPressed();
},
child: Container(
child: child,
decoration: BoxDecoration(
borderRadius: borderRadius,
border: Border.all(
color: Colors.white,
width: 3.0,
),
),
),
),
);
},
// particle: Aligned(
// child: Burst(
// children: List<Particle>.generate(
// 5,
// (i) => Burst(
// children: List<Particle>.generate(
// 5,
// (i) => Randoms.rnd.nextDouble() > .5
// ? FadingCircle(radius: Randoms.rnd.nextDouble() * 10)
// : FadingRect(
// size: Size(
// Randoms.rnd.nextDouble() * 10,
// Randoms.rnd.nextDouble() * 10,
// )),
// ),
// ),
// ),
// ),
// ),
particle: ParticleGenerator(
50,
(i) => Aligned(
alignment: Randoms.alignment(),
child: Randoms.rnd.nextDouble() > .5
? CurvedParticle(
curve: Interval(
.3,
1,
),
child: Burst(
children: List<Particle>.generate(
2,
(_) => MovingParticle(
child: ScalingParticle(
child: Circles.random(count: 2, maxRadius: 50),
),
from: Offset.zero,
to: Offset(0, -30),
),
),
),
)
: CurvedParticle(
curve: Interval(
Randoms.rnd.nextDouble() * .5 + .5,
1.0,
curve: Curves.fastOutSlowIn,
),
child: ScalingParticle(
child: FadingCircle(
color: Randoms.rnd.nextDouble() > .5
? Palette.accent
: Colors.purpleAccent,
),
),
),
),
),
);
}
}
/// mixin Particle implements Drawable, Animatable {}
///
/// class CompositeParticle extends Particle {
/// Particle child;
///
/// @override
/// void draw(Canvas canvas, Size size) {
/// child.draw(canvas, size);
/// }
///
/// @override
/// void update(AnimationController controller) {
/// child.update(controller);
/// }
/// }
/// class SimpleExample extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return Particles(
/// particle: Aligned(
/// child: FadingCircle(),
/// ),
/// child: (context, particles) {
/// return Button(
/// onPressed: () {
/// particles.forward();
/// }
/// );
/// }
/// );
/// }
/// }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment