diff --git a/engine/core/painting/Offset.dart b/engine/core/painting/Offset.dart index 6bbbbc8eb99..cd959753c14 100644 --- a/engine/core/painting/Offset.dart +++ b/engine/core/painting/Offset.dart @@ -21,6 +21,10 @@ class Offset extends OffsetBase { Offset operator -() => new Offset(-dx, -dy); Offset operator -(Offset other) => new Offset(dx - other.dx, dy - other.dy); Offset operator +(Offset other) => new Offset(dx + other.dx, dy + other.dy); + Offset operator *(double operand) => new Offset(dx * operand, dy * operand); + Offset operator /(double operand) => new Offset(dx / operand, dy / operand); + Offset operator ~/(double operand) => new Offset((dx ~/ operand).toDouble(), (dy ~/ operand).toDouble()); + Offset operator %(double operand) => new Offset(dx % operand, dy % operand); Rect operator &(Size other) => new Rect.fromLTWH(dx, dy, other.width, other.height); // does the equivalent of "return new Point(0,0) + this" diff --git a/sdk/BUILD.gn b/sdk/BUILD.gn index bb2663b23d5..742be4b053b 100644 --- a/sdk/BUILD.gn +++ b/sdk/BUILD.gn @@ -9,6 +9,7 @@ dart_pkg("sky") { "CHANGELOG.md", "bin/init.dart", "lib/animation/animated_value.dart", + "lib/animation/animation_performance.dart", "lib/animation/curves.dart", "lib/animation/fling_curve.dart", "lib/animation/generators.dart", @@ -18,6 +19,7 @@ dart_pkg("sky") { "lib/assets/material-design-icons.sha1", "lib/base/debug.dart", "lib/base/hit_test.dart", + "lib/base/lerp.dart", "lib/base/node.dart", "lib/base/scheduler.dart", "lib/download_material_design_icons", diff --git a/sdk/lib/animation/animation_performance.dart b/sdk/lib/animation/animation_performance.dart new file mode 100644 index 00000000000..c730d8011cb --- /dev/null +++ b/sdk/lib/animation/animation_performance.dart @@ -0,0 +1,89 @@ +// 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 'animated_value.dart'; +import 'curves.dart'; + +// TODO(mpcomplete): merge this stuff with AnimatedValue somehow. We shouldn't +// have 2 different ways to animate values. +abstract class AnimatedVariable { + void setFraction(double t); +} + +class AnimatedType extends AnimatedVariable { + T value; + final T begin, end; + final Curve curve; + + AnimatedType(this.begin, this.end, {this.curve: linear}) { + value = begin; + } + + void setFraction(double t) { + // TODO(mpcomplete): Reverse the timeline and curve. + value = begin + (end - begin) * curve.transform(t); + } +} + +// This class manages a "performance" - a collection of values that change +// based on a timeline. For example, a performance may handle an animation +// of a menu opening by sliding and fading in (changing Y value and opacity) +// over .5 seconds. The performance can move forwards (present) or backwards +// (dismiss). A consumer may also take direct control of the timeline by +// manipulating |progress|, or |fling| the timeline causing a physics-based +// simulation to take over the progression. +class AnimationPerformance { + // TODO(mpcomplete): make this a list, or composable somehow. + AnimatedVariable variable; + // Advances from 0 to 1. On each tick, we'll update our variable's values. + AnimatedValue timeline = new AnimatedValue(0.0); + // TODO(mpcomplete): duration should be on a director. + Duration duration; + + AnimationPerformance() { + timeline.onValueChanged.listen((double t) { + variable.setFraction(t); + }); + } + + double get progress => timeline.value; + void set progress(double t) { + stop(); + timeline.value = t.clamp(0.0, 1.0); + } + + bool get isDismissed => progress == 0.0; + bool get isCompleted => progress == 1.0; + bool get isAnimating => timeline.isAnimating; + + void play() { + _animateTo(1.0); + } + void reverse() { + _animateTo(0.0); + } + + void _animateTo(double target) { + double remainingDistance = (target - timeline.value).abs(); + timeline.stop(); + if (remainingDistance != 0.0) + timeline.animateTo(target, remainingDistance * duration.inMilliseconds); + } + + void stop() { + timeline.stop(); + } + + // Resume animating in a direction, with the given velocity. + // TODO(mpcomplete): this should be a force with friction so it slows over + // time. + void fling({double velocity: 1.0}) { + double target = velocity.sign < 0.0 ? 0.0 : 1.0; + double distance = (target - timeline.value).abs(); + double duration = distance / velocity.abs(); + + if (distance > 0.0) + timeline.animateTo(target, duration, curve: linear); + } +} diff --git a/sdk/lib/animation/generators.dart b/sdk/lib/animation/generators.dart index 60e3f90c632..48690407e74 100644 --- a/sdk/lib/animation/generators.dart +++ b/sdk/lib/animation/generators.dart @@ -89,7 +89,7 @@ class AnimationGenerator extends Generator { startTime = timeStamp; double t = (timeStamp - (startTime + initialDelay)) / duration; - _lastTime = math.max(0.0, math.min(t, 1.0)); + _lastTime = t.clamp(0.0, 1.0); return _lastTime; }) .takeWhile(_checkForCompletion) diff --git a/sdk/lib/base/lerp.dart b/sdk/lib/base/lerp.dart new file mode 100644 index 00000000000..c40934c024b --- /dev/null +++ b/sdk/lib/base/lerp.dart @@ -0,0 +1,19 @@ +// 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:sky'; + +num lerpNum(num a, num b, double t) => a + (b - a) * t; + +Color lerpColor(Color a, Color b, double t) { + return new Color.fromARGB( + lerpNum(a.alpha, b.alpha, t).toInt(), + lerpNum(a.red, b.red, t).toInt(), + lerpNum(a.green, b.green, t).toInt(), + lerpNum(a.blue, b.blue, t).toInt()); +} + +Offset lerpOffset(Offset a, Offset b, double t) { + return new Offset(lerpNum(a.dx, b.dx, t), lerpNum(a.dy, b.dy, t)); +} diff --git a/sdk/lib/painting/box_painter.dart b/sdk/lib/painting/box_painter.dart index 6044b644844..4ce567f51fb 100644 --- a/sdk/lib/painting/box_painter.dart +++ b/sdk/lib/painting/box_painter.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'dart:sky' as sky; import 'dart:sky' show Point, Offset, Size, Rect, Color, Paint, Path; +import '../base/lerp.dart'; import 'shadows.dart'; import 'package:sky/mojo/net/image_cache.dart' as image_cache; @@ -72,6 +73,13 @@ class BoxShadow { String toString() => 'BoxShadow($color, $offset, $blur)'; } +BoxShadow lerpBoxShadow(BoxShadow a, BoxShadow b, double t) { + return new BoxShadow( + color: lerpColor(a.color, b.color, t), + offset: lerpOffset(a.offset, b.offset, t), + blur: lerpNum(a.blur, b.blur, t)); +} + abstract class Gradient { sky.Shader createShader(); } diff --git a/sdk/lib/widgets/animated_component.dart b/sdk/lib/widgets/animated_component.dart index 025e3a4fb3f..5b1451a8873 100644 --- a/sdk/lib/widgets/animated_component.dart +++ b/sdk/lib/widgets/animated_component.dart @@ -4,7 +4,11 @@ import 'dart:async'; +import 'package:vector_math/vector_math.dart'; + import '../animation/animated_value.dart'; +import '../animation/animation_performance.dart'; +import '../animation/curves.dart'; import 'basic.dart'; class _AnimationEntry { @@ -50,3 +54,22 @@ abstract class AnimatedComponent extends Component { } } + +// Types of things that can be animated in a component. Use build() to +// construct the final Widget based on the animation state. +// TODO(mpcomplete): the idea here is to eventually have an AnimatedCollection +// which assembles a container based on a list of animated things. e.g. if you +// want to animate position, opacity, and shadow, you add those animators to an +// AnimatedCollection and just call collection.build() to construct your +// widget. + +class AnimatedPosition extends AnimatedType { + AnimatedPosition(Point begin, Point end, {Curve curve: linear}) + : super(begin, end, curve: curve); + + Widget build(Widget child) { + Matrix4 transform = new Matrix4.identity(); + transform.translate(value.x, value.y); + return new Transform(transform: transform, child: child); + } +} diff --git a/sdk/lib/widgets/drawer.dart b/sdk/lib/widgets/drawer.dart index c2c0fb7bfcb..5a2501a7850 100644 --- a/sdk/lib/widgets/drawer.dart +++ b/sdk/lib/widgets/drawer.dart @@ -8,6 +8,7 @@ import 'dart:sky' as sky; import 'package:vector_math/vector_math.dart'; import '../animation/animated_value.dart'; +import '../animation/animation_performance.dart'; import '../animation/curves.dart'; import '../theme/colors.dart'; import '../theme/shadows.dart'; @@ -29,22 +30,26 @@ import 'basic.dart'; const double _kWidth = 304.0; const double _kMinFlingVelocity = 0.4; -const double _kBaseSettleDurationMS = 246.0; -const double _kMaxSettleDurationMS = 600.0; +const int _kBaseSettleDurationMS = 246; const Curve _kAnimationCurve = parabolicRise; typedef void DrawerStatusChangeHandler (bool showing); class DrawerController { - DrawerController(this.onStatusChange) { - position = new AnimatedValue(-_kWidth, onChange: _checkValue); + performance = new AnimationPerformance() + ..duration = new Duration(milliseconds: _kBaseSettleDurationMS) + ..variable = position; + performance.timeline.onValueChanged.listen(_checkValue); } final DrawerStatusChangeHandler onStatusChange; - AnimatedValue position; + + AnimationPerformance performance; + final AnimatedPosition position = new AnimatedPosition( + new Point(-_kWidth, 0.0), Point.origin, curve: _kAnimationCurve); bool _oldClosedState = true; - void _checkValue() { + void _checkValue(_) { var newClosedState = isClosed; if (onStatusChange != null && _oldClosedState != newClosedState) { onStatusChange(!newClosedState); @@ -52,69 +57,52 @@ class DrawerController { } } - bool get isClosed => position.value == -_kWidth; - bool get _isMostlyClosed => position.value <= -_kWidth / 2; - void toggle() => _isMostlyClosed ? open() : close(); + bool get isClosed => performance.isDismissed; + bool get _isMostlyClosed => position.value.x <= -_kWidth/2; + + void open() => performance.play(); + + void close() => performance.reverse(); + + void _settle() => _isMostlyClosed ? close() : open(); void handleMaskTap(_) => close(); - void handlePointerDown(_) => position.stop(); + + // TODO(mpcomplete): Figure out how to generalize these handlers on a + // "PannableThingy" interface. + void handlePointerDown(_) => performance.stop(); void handlePointerMove(sky.PointerEvent event) { - if (position.isAnimating) + if (performance.isAnimating) return; - position.value = math.min(0.0, math.max(position.value + event.dx, -_kWidth)); + performance.progress += event.dx / _kWidth; } void handlePointerUp(_) { - if (!position.isAnimating) + if (!performance.isAnimating) _settle(); } void handlePointerCancel(_) { - if (!position.isAnimating) + if (!performance.isAnimating) _settle(); } - void open() => _animateToPosition(0.0); - - void close() => _animateToPosition(-_kWidth); - - void _settle() => _isMostlyClosed ? close() : open(); - - void _animateToPosition(double targetPosition) { - double distance = (targetPosition - position.value).abs(); - if (distance != 0) { - double targetDuration = distance / _kWidth * _kBaseSettleDurationMS; - double duration = math.min(targetDuration, _kMaxSettleDurationMS); - position.animateTo(targetPosition, duration, curve: _kAnimationCurve); - } - } - void handleFlingStart(event) { - double direction = event.velocityX.sign; - double velocityX = event.velocityX.abs() / 1000; - if (velocityX < _kMinFlingVelocity) - return; - - double targetPosition = direction < 0.0 ? -_kWidth : 0.0; - double distance = (targetPosition - position.value).abs(); - double duration = distance / velocityX; - - if (distance > 0) - position.animateTo(targetPosition, duration, curve: linear); + double velocityX = event.velocityX / 1000; + if (velocityX.abs() >= _kMinFlingVelocity) + performance.fling(velocity: velocityX / _kWidth); } - } class Drawer extends AnimatedComponent { - Drawer({ String key, this.controller, this.children, this.level: 0 }) : super(key: key) { - watch(controller.position); + watch(controller.performance.timeline); } List children; @@ -128,34 +116,35 @@ class Drawer extends AnimatedComponent { super.syncFields(source); } + // TODO(mpcomplete): the animation system should handle building, maybe? Or + // at least setting the transform. Figure out how this could work for things + // like fades, slides, rotates, pinch, etc. Widget build() { - Matrix4 transform = new Matrix4.identity(); - transform.translate(controller.position.value); - - double scaler = controller.position.value / _kWidth + 1; + // TODO(mpcomplete): animate as a fade-in. + double scaler = controller.performance.progress + 1.0; Color maskColor = new Color.fromARGB((0x7F * scaler).floor(), 0, 0, 0); var mask = new Listener( child: new Container(decoration: new BoxDecoration(backgroundColor: maskColor)), - onGestureTap: controller.handleMaskTap, - onGestureFlingStart: controller.handleFlingStart + onGestureTap: controller.handleMaskTap ); - Container content = new Container( - decoration: new BoxDecoration( - backgroundColor: Grey[50], - boxShadow: shadows[level]), - width: _kWidth, - transform: transform, - child: new Block(children) - ); + Widget content = controller.position.build( + new Container( + decoration: new BoxDecoration( + backgroundColor: Grey[50], + boxShadow: shadows[level]), + width: _kWidth, + child: new Block(children) + )); return new Listener( child: new Stack([ mask, content ]), onPointerDown: controller.handlePointerDown, onPointerMove: controller.handlePointerMove, onPointerUp: controller.handlePointerUp, - onPointerCancel: controller.handlePointerCancel + onPointerCancel: controller.handlePointerCancel, + onGestureFlingStart: controller.handleFlingStart ); } diff --git a/sdk/lib/widgets/material.dart b/sdk/lib/widgets/material.dart index 793fe960663..4120aa8665c 100644 --- a/sdk/lib/widgets/material.dart +++ b/sdk/lib/widgets/material.dart @@ -2,36 +2,69 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import '../base/lerp.dart'; +import '../animation/animated_value.dart'; import '../painting/box_painter.dart'; import '../theme/edges.dart'; import '../theme/shadows.dart'; +import 'animated_component.dart'; import 'basic.dart'; import 'default_text_style.dart'; import 'theme.dart'; export '../theme/edges.dart' show MaterialEdge; -class Material extends Component { +const double _kAnimateShadowDurationMS = 100.0; + +List _computeShadow(double level) { + if (level < 1.0) // shadows[1] is the first shadow + return null; + + int level1 = level.floor(); + int level2 = level.ceil(); + double t = level - level1.toDouble(); + + List shadow = new List(); + for (int i = 0; i < shadows[level1].length; ++i) + shadow.add(lerpBoxShadow(shadows[level1][i], shadows[level2][i], t)); + return shadow; +} + +class Material extends AnimatedComponent { Material({ String key, this.child, this.edge: MaterialEdge.card, - this.level: 0, + int level: 0, this.color - }) : super(key: key); + }) : super(key: key) { + this.level = new AnimatedValue(level.toDouble()); + watch(this.level); + } - final Widget child; - final int level; - final MaterialEdge edge; - final Color color; + Widget child; + MaterialEdge edge; + AnimatedValue level; + Color color; - // TODO(ianh): we should make this animate level changes and color changes + void syncFields(Material source) { + child = source.child; + edge = source.edge; + // TODO(mpcomplete): duration is wrong, because the current level may be + // anything. We really want |rate|. + if (level.value != source.level.value) + level.animateTo(source.level.value.toDouble(), _kAnimateShadowDurationMS); + color = source.color; + super.syncFields(source); + } + + // TODO(mpcomplete): make this animate color changes. Widget build() { return new Container( decoration: new BoxDecoration( - boxShadow: shadows[level], + boxShadow: _computeShadow(level.value), borderRadius: edges[edge], backgroundColor: color, shape: edge == MaterialEdge.circle ? Shape.circle : Shape.rectangle