| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:math' as math; |
| import 'dart:ui' as ui; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_sprites/flutter_sprites.dart'; |
| import 'package:vector_math/vector_math_64.dart' as vec; |
| |
| ImageMap _images; |
| SpriteSheet _sprites; |
| |
| class FitnessDemo extends StatelessWidget { |
| FitnessDemo({ Key key }) : super(key: key); |
| |
| Widget build(BuildContext context) { |
| return new Scaffold( |
| appBar: new AppBar( |
| title: new Text("Fitness") |
| ), |
| body: new _FitnessDemoContents() |
| ); |
| } |
| } |
| |
| class _FitnessDemoContents extends StatefulWidget { |
| _FitnessDemoContents({ Key key }) : super(key: key); |
| _FitnessDemoContentsState createState() => new _FitnessDemoContentsState(); |
| } |
| |
| class _FitnessDemoContentsState extends State<_FitnessDemoContents> { |
| |
| Future<Null> _loadAssets(AssetBundle bundle) async { |
| _images = new ImageMap(bundle); |
| await _images.load(<String>[ |
| 'packages/flutter_gallery_assets/jumpingjack.png', |
| ]); |
| |
| String json = await DefaultAssetBundle.of(context).loadString('packages/flutter_gallery_assets/jumpingjack.json'); |
| _sprites = new SpriteSheet(_images['packages/flutter_gallery_assets/jumpingjack.png'], json); |
| } |
| |
| void initState() { |
| super.initState(); |
| |
| AssetBundle bundle = DefaultAssetBundle.of(context); |
| _loadAssets(bundle).then((_) { |
| setState(() { |
| assetsLoaded = true; |
| workoutAnimation = new _WorkoutAnimationNode( |
| onPerformedJumpingJack: () { |
| setState(() { |
| count += 1; |
| }); |
| }, |
| onSecondPassed: (int seconds) { |
| setState(() { |
| time = seconds; |
| }); |
| } |
| ); |
| }); |
| }); |
| } |
| |
| bool assetsLoaded = false; |
| int count = 0; |
| int time = 0; |
| int get kcal => (count * 0.2).toInt(); |
| |
| _WorkoutAnimationNode workoutAnimation; |
| |
| Widget build(BuildContext context) { |
| if (!assetsLoaded) |
| return new Container(); |
| |
| Color buttonColor; |
| String buttonText; |
| VoidCallback onButtonPressed; |
| |
| if (workoutAnimation.workingOut) { |
| buttonColor = Colors.red[500]; |
| buttonText = "STOP WORKOUT"; |
| onButtonPressed = endWorkout; |
| } else { |
| buttonColor = Theme.of(context).primaryColor; |
| buttonText = "START WORKOUT"; |
| onButtonPressed = startWorkout; |
| } |
| |
| return new Material( |
| child: new Column( |
| justifyContent: FlexJustifyContent.center, |
| children: <Widget>[ |
| new Flexible( |
| child: new Container( |
| decoration: new BoxDecoration(backgroundColor: Colors.grey[800]), |
| child: new SpriteWidget(workoutAnimation, SpriteBoxTransformMode.scaleToFit) |
| ) |
| ), |
| new Padding( |
| padding: new EdgeInsets.only(top: 20.0), |
| child: new Text("JUMPING JACKS", style: Theme.of(context).textTheme.title) |
| ), |
| new Padding( |
| padding: new EdgeInsets.only(top: 20.0, bottom: 20.0), |
| child: new Row( |
| justifyContent: FlexJustifyContent.center, |
| children: <Widget>[ |
| _createInfoPanelCell(Icons.accessibility, '$count', 'COUNT'), |
| _createInfoPanelCell(Icons.timer, _formatSeconds(time), 'TIME'), |
| _createInfoPanelCell(Icons.flash_on, '$kcal', 'KCAL') |
| ] |
| ) |
| ), |
| new Padding( |
| padding: new EdgeInsets.only(bottom: 16.0), |
| child: new SizedBox( |
| width: 300.0, |
| height: 72.0, |
| child: new RaisedButton ( |
| onPressed: onButtonPressed, |
| color: buttonColor, |
| child: new Text( |
| buttonText, |
| style: new TextStyle(color: Colors.white, fontSize: 20.0) |
| ) |
| ) |
| ) |
| ) |
| ] |
| ) |
| ); |
| } |
| |
| Widget _createInfoPanelCell(IconData icon, String value, String description) { |
| Color color; |
| if (workoutAnimation.workingOut) |
| color = Colors.black87; |
| else |
| color = Theme.of(context).disabledColor; |
| |
| return new Container( |
| width: 100.0, |
| child: new Center( |
| child: new Column( |
| children: <Widget>[ |
| new Icon(icon: icon, size: 48.0, color: color), |
| new Text(value, style: new TextStyle(fontSize: 24.0, color: color)), |
| new Text(description, style: new TextStyle(color: color)) |
| ] |
| ) |
| ) |
| ); |
| } |
| |
| String _formatSeconds(int seconds) { |
| int minutes = seconds ~/ 60; |
| String secondsStr = "${seconds % 60}".padLeft(2, "0"); |
| return "$minutes:$secondsStr"; |
| } |
| |
| void startWorkout() { |
| setState(() { |
| count = 0; |
| time = 0; |
| workoutAnimation.start(); |
| }); |
| } |
| |
| void endWorkout() { |
| setState(() { |
| workoutAnimation.stop(); |
| |
| if (count >= 3) { |
| showDialog( |
| context: context, |
| child: new Stack(children: <Widget>[ |
| new _Fireworks(), |
| new Dialog( |
| title: new Text("Awesome workout"), |
| content: new Text("You have completed $count jumping jacks. Good going!"), |
| actions: <Widget>[ |
| new FlatButton( |
| child: new Text("SWEET"), |
| onPressed: () { Navigator.pop(context); } |
| ) |
| ] |
| ) |
| ]) |
| ); |
| } |
| }); |
| } |
| } |
| |
| typedef void _SecondPassedCallback(int seconds); |
| |
| class _WorkoutAnimationNode extends NodeWithSize { |
| _WorkoutAnimationNode({ |
| this.onPerformedJumpingJack, |
| this.onSecondPassed |
| }) : super(const Size(1024.0, 1024.0)) { |
| reset(); |
| |
| _progress = new _ProgressCircle(const Size(800.0, 800.0)); |
| _progress.pivot = const Point(0.5, 0.5); |
| _progress.position = const Point(512.0, 512.0); |
| addChild(_progress); |
| |
| _jumpingJack = new _JumpingJack((){ |
| onPerformedJumpingJack(); |
| }); |
| _jumpingJack.scale = 0.5; |
| _jumpingJack.position = const Point(512.0, 550.0); |
| addChild(_jumpingJack); |
| } |
| |
| final VoidCallback onPerformedJumpingJack; |
| final _SecondPassedCallback onSecondPassed; |
| |
| int seconds; |
| |
| bool workingOut; |
| |
| static const int _kTargetMillis = 1000 * 30; |
| int _startTimeMillis; |
| _ProgressCircle _progress; |
| _JumpingJack _jumpingJack; |
| |
| void reset() { |
| seconds = 0; |
| workingOut = false; |
| } |
| |
| void start() { |
| reset(); |
| _startTimeMillis = new DateTime.now().millisecondsSinceEpoch; |
| workingOut = true; |
| _jumpingJack.animateJumping(); |
| } |
| |
| void stop() { |
| workingOut = false; |
| _jumpingJack.neutralPose(); |
| } |
| |
| void update(double dt) { |
| if (workingOut) { |
| int millis = new DateTime.now().millisecondsSinceEpoch - _startTimeMillis; |
| int newSeconds = (millis) ~/ 1000; |
| if (newSeconds != seconds) { |
| seconds = newSeconds; |
| onSecondPassed(seconds); |
| } |
| |
| _progress.value = millis / _kTargetMillis; |
| } else { |
| _progress.value = 0.0; |
| } |
| } |
| } |
| |
| class _ProgressCircle extends NodeWithSize { |
| _ProgressCircle(Size size, [this.value = 0.0]) : super(size); |
| |
| static const double _kTwoPI = math.PI * 2.0; |
| static const double _kEpsilon = .0000001; |
| static const double _kSweep = _kTwoPI - _kEpsilon; |
| |
| double value; |
| |
| void paint(Canvas canvas) { |
| applyTransformForPivot(canvas); |
| |
| Paint circlePaint = new Paint() |
| ..color = Colors.white30 |
| ..strokeWidth = 24.0 |
| ..style = ui.PaintingStyle.stroke; |
| |
| canvas.drawCircle( |
| new Point(size.width / 2.0, size.height / 2.0), |
| size.width / 2.0, |
| circlePaint |
| ); |
| |
| Paint pathPaint = new Paint() |
| ..color = Colors.purple[500] |
| ..strokeWidth = 25.0 |
| ..style = ui.PaintingStyle.stroke; |
| |
| double angle = value.clamp(0.0, 1.0) * _kSweep; |
| Path path = new Path() |
| ..arcTo(Point.origin & size, -math.PI / 2.0, angle, false); |
| canvas.drawPath(path, pathPaint); |
| } |
| } |
| |
| class _JumpingJack extends Node { |
| _JumpingJack(VoidCallback onPerformedJumpingJack) { |
| left = new _JumpingJackSide(false, onPerformedJumpingJack); |
| right = new _JumpingJackSide(true, null); |
| addChild(left); |
| addChild(right); |
| } |
| |
| void animateJumping() { |
| left.animateJumping(); |
| right.animateJumping(); |
| } |
| |
| void neutralPose() { |
| left.neutralPosition(true); |
| right.neutralPosition(true); |
| } |
| |
| _JumpingJackSide left; |
| _JumpingJackSide right; |
| } |
| |
| class _JumpingJackSide extends Node { |
| _JumpingJackSide(bool right, this.onPerformedJumpingJack) { |
| // Torso and head |
| torso = _createPart('torso.png', const Point(512.0, 512.0)); |
| addChild(torso); |
| |
| head = _createPart('head.png', const Point(512.0, 160.0)); |
| torso.addChild(head); |
| |
| if (right) { |
| torso.opacity = 0.0; |
| head.opacity = 0.0; |
| torso.scaleX = -1.0; |
| } |
| |
| // Left side movable parts |
| upperArm = _createPart('upper-arm.png', const Point(445.0, 220.0)); |
| torso.addChild(upperArm); |
| lowerArm = _createPart('lower-arm.png', const Point(306.0, 200.0)); |
| upperArm.addChild(lowerArm); |
| hand = _createPart('hand.png', const Point(215.0, 127.0)); |
| lowerArm.addChild(hand); |
| upperLeg = _createPart('upper-leg.png', const Point(467.0, 492.0)); |
| torso.addChild(upperLeg); |
| lowerLeg = _createPart('lower-leg.png', const Point(404.0, 660.0)); |
| upperLeg.addChild(lowerLeg); |
| foot = _createPart('foot.png', const Point(380.0, 835.0)); |
| lowerLeg.addChild(foot); |
| |
| torso.setPivotAndPosition(Point.origin); |
| |
| neutralPosition(false); |
| } |
| |
| _JumpingJackPart torso; |
| _JumpingJackPart head; |
| _JumpingJackPart upperArm; |
| _JumpingJackPart lowerArm; |
| _JumpingJackPart hand; |
| _JumpingJackPart lowerLeg; |
| _JumpingJackPart upperLeg; |
| _JumpingJackPart foot; |
| |
| final VoidCallback onPerformedJumpingJack; |
| |
| _JumpingJackPart _createPart(String textureName, Point pivotPosition) { |
| return new _JumpingJackPart(_sprites[textureName], pivotPosition); |
| } |
| |
| void animateJumping() { |
| actions.stopAll(); |
| actions.run(new ActionSequence([ |
| _createPoseAction(null, 0, 0.5), |
| new ActionCallFunction(_animateJumpingLoop) |
| ])); |
| } |
| |
| void _animateJumpingLoop() { |
| actions.run(new ActionRepeatForever( |
| new ActionSequence(<Action>[ |
| _createPoseAction(0, 1, 0.30), |
| _createPoseAction(1, 2, 0.30), |
| _createPoseAction(2, 1, 0.30), |
| _createPoseAction(1, 0, 0.30), |
| new ActionCallFunction(() { |
| if (onPerformedJumpingJack != null) |
| onPerformedJumpingJack(); |
| }) |
| ]) |
| )); |
| } |
| |
| void neutralPosition(bool animate) { |
| actions.stopAll(); |
| if (animate) { |
| actions.run(_createPoseAction(null, 1, 0.5)); |
| } else { |
| List<double> d = _dataForPose(1); |
| upperArm.rotation = d[0]; |
| lowerArm.rotation = d[1]; |
| hand.rotation = d[2]; |
| upperLeg.rotation = d[3]; |
| lowerLeg.rotation = d[4]; |
| foot.rotation = d[5]; |
| torso.position = new Point(0.0, d[6]); |
| } |
| } |
| |
| ActionInterval _createPoseAction(int startPose, int endPose, double duration) { |
| List<double> d0 = _dataForPose(startPose); |
| List<double> d1 = _dataForPose(endPose); |
| |
| List<ActionTween> tweens = <ActionTween>[ |
| _tweenRotation(upperArm, d0[0], d1[0], duration), |
| _tweenRotation(lowerArm, d0[1], d1[1], duration), |
| _tweenRotation(hand, d0[2], d1[2], duration), |
| _tweenRotation(upperLeg, d0[3], d1[3], duration), |
| _tweenRotation(lowerLeg, d0[4], d1[4], duration), |
| _tweenRotation(foot, d0[5], d1[5], duration), |
| new ActionTween( |
| (Point a) => torso.position = a, |
| new Point(0.0, d0[6]), |
| new Point(0.0, d1[6]), |
| duration |
| ) |
| ]; |
| |
| return new ActionGroup(tweens); |
| } |
| |
| ActionTween _tweenRotation(_JumpingJackPart part, double r0, double r1, double duration) { |
| return new ActionTween( |
| (double a) => part.rotation = a, |
| r0, |
| r1, |
| duration |
| ); |
| } |
| |
| List<double> _dataForPose(int pose) { |
| if (pose == null) |
| return _dataForCurrentPose(); |
| |
| if (pose == 0) { |
| return <double>[ |
| -80.0, // Upper arm rotation |
| -30.0, // Lower arm rotation |
| -10.0, // Hand rotation |
| -15.0, // Upper leg rotation |
| 5.0, // Lower leg rotation |
| 15.0, // Foot rotation |
| 0.0 // Torso y offset |
| ]; |
| } else if (pose == 1) { |
| return <double>[ |
| 0.0, |
| 0.0, |
| 0.0, |
| 0.0, |
| 0.0, |
| 0.0, |
| -70.0 |
| ]; |
| } else { |
| return <double>[ |
| 40.0, |
| 30.0, |
| 10.0, |
| 20.0, |
| -20.0, |
| 15.0, |
| 40.0 |
| ]; |
| } |
| } |
| |
| List<double> _dataForCurrentPose() { |
| return <double>[ |
| upperArm.rotation, |
| lowerArm.rotation, |
| hand.rotation, |
| upperLeg.rotation, |
| lowerLeg.rotation, |
| foot.rotation, |
| torso.position.y |
| ]; |
| } |
| } |
| |
| class _JumpingJackPart extends Sprite { |
| _JumpingJackPart(Texture texture, this.pivotPosition) : super(texture); |
| final Point pivotPosition; |
| |
| void setPivotAndPosition(Point newPosition) { |
| pivot = new Point(pivotPosition.x / 1024.0, pivotPosition.y / 1024.0); |
| position = newPosition; |
| |
| for (Node child in children) { |
| _JumpingJackPart subPart = child; |
| subPart.setPivotAndPosition( |
| new Point( |
| subPart.pivotPosition.x - pivotPosition.x, |
| subPart.pivotPosition.y - pivotPosition.y |
| ) |
| ); |
| } |
| } |
| } |
| |
| class _Fireworks extends StatefulWidget { |
| _Fireworks({ Key key }) : super(key: key); |
| |
| _FireworksState createState() => new _FireworksState(); |
| } |
| |
| class _FireworksState extends State<_Fireworks> { |
| void initState() { |
| super.initState(); |
| fireworks = new _FireworksNode(); |
| } |
| |
| _FireworksNode fireworks; |
| |
| Widget build(BuildContext context) { |
| return new SpriteWidget(fireworks); |
| } |
| } |
| |
| class _FireworksNode extends NodeWithSize { |
| _FireworksNode() : super(const Size(1024.0, 1024.0)); |
| double _countDown = 0.0; |
| |
| void update(double dt) { |
| if (_countDown <= 0.0) { |
| _addExplosion(); |
| _countDown = randomDouble(); |
| } |
| |
| _countDown -= dt; |
| } |
| |
| Color _randomExplosionColor() { |
| double rand = randomDouble(); |
| if (rand < 0.25) |
| return Colors.pink[200]; |
| else if (rand < 0.5) |
| return Colors.lightBlue[200]; |
| else if (rand < 0.75) |
| return Colors.purple[200]; |
| else |
| return Colors.cyan[200]; |
| } |
| |
| void _addExplosion() { |
| Color startColor = _randomExplosionColor(); |
| Color endColor = startColor.withAlpha(0); |
| |
| ParticleSystem system = new ParticleSystem( |
| _sprites['particle-0.png'], |
| numParticlesToEmit: 100, |
| emissionRate: 1000.0, |
| rotateToMovement: true, |
| startRotation: 90.0, |
| endRotation: 90.0, |
| speed: 100.0, |
| speedVar: 50.0, |
| startSize: 1.0, |
| startSizeVar: 0.5, |
| gravity: new vec.Vector2(0.0, 30.0), |
| colorSequence: new ColorSequence.fromStartAndEndColor(startColor, endColor) |
| ); |
| system.position = new Point(randomDouble() * 1024.0, randomDouble() * 1024.0); |
| addChild(system); |
| } |
| } |