From 06bfddff8139dfcea708a6e457d7d98062ff560e Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 13 Mar 2015 09:31:10 -0700 Subject: [PATCH] Drive overscroll animations with a physics simulation This CL uses a simple physics simulation to drive overscroll animations. We model settling the overscroll as a particle climbing a hill, which gives us a pleasing parabolic trajectory. This CL also includes machinery for spring-based simulations. We'll use these to drive the drawer animation. R=eseidel@chromium.org Review URL: https://codereview.chromium.org/999423004 --- framework/animation/generator.dart | 16 +++- framework/animation/mechanics.dart | 126 ++++++++++++++++++++++++++ framework/animation/scroll_curve.dart | 18 ++++ framework/animation/simulation.dart | 45 +++++++++ framework/components/scrollable.dart | 48 ++++------ 5 files changed, 220 insertions(+), 33 deletions(-) create mode 100644 framework/animation/mechanics.dart create mode 100644 framework/animation/simulation.dart diff --git a/framework/animation/generator.dart b/framework/animation/generator.dart index 065d1e5f656..7530f04a4ad 100644 --- a/framework/animation/generator.dart +++ b/framework/animation/generator.dart @@ -51,13 +51,15 @@ class FrameGenerator { } } -class AnimationGenerator extends FrameGenerator { +class AnimationGenerator { Stream get onTick => _stream; final double initialDelay; final double duration; final double begin; final double end; final Curve curve; + + FrameGenerator _generator; Stream _stream; bool _done = false; @@ -68,21 +70,27 @@ class AnimationGenerator extends FrameGenerator { this.end: 1.0, this.curve: linear, Function onDone - }):super(onDone: onDone) { + }) { assert(duration != null && duration > 0.0); + _generator = new FrameGenerator(onDone: onDone); + double startTime = 0.0; - _stream = super.onTick.map((timeStamp) { + _stream = _generator.onTick.map((timeStamp) { if (startTime == 0.0) startTime = timeStamp; double t = (timeStamp - (startTime + initialDelay)) / duration; return math.max(0.0, math.min(t, 1.0)); }) - .takeWhile(_checkForCompletion) // + .takeWhile(_checkForCompletion) .where((t) => t >= 0.0) .map(_transform); } + void cancel() { + _generator.cancel(); + } + double _transform(double t) { if (_done) return end; diff --git a/framework/animation/mechanics.dart b/framework/animation/mechanics.dart new file mode 100644 index 00000000000..7ed6da19f7e --- /dev/null +++ b/framework/animation/mechanics.dart @@ -0,0 +1,126 @@ +// 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:math' as math; + +const double kGravity = -0.980; +const double _kMinVelocity = 0.1; + +abstract class System { + void update(double deltaT); +} + +class Particle extends System { + final double mass; + double velocity; + double position; + + Particle({this.mass: 1.0, this.velocity: 0.0, this.position: 0.0}); + + void applyImpluse(double impluse) { + velocity += impluse / mass; + } + + void update(double deltaT) { + position += velocity * deltaT; + } + + double get energy => 0.5 * mass * velocity * velocity; + set energy(double e) { + assert(e >= 0.0); + velocity = math.sqrt(2 * e / mass); + } +} + +class Box { + final double min; + final double max; + + Box({this.min, this.max}) { + assert(min == null || max == null || min <= max); + } + + void confine(Particle p) { + if (min != null) { + p.position = math.max(min, p.position); + if (p.position == min) + p.velocity = math.max(0.0, p.velocity); + } + if (max != null) { + p.position = math.min(max, p.position); + if (p.position == max) + p.velocity = math.min(0.0, p.velocity); + } + } +} + +class ParticleInBox extends System { + final Particle particle; + final Box box; + + ParticleInBox({this.particle, this.box}) { + box.confine(particle); + } + + void update(double deltaT) { + particle.update(deltaT); + box.confine(particle); + } +} + +class Spring { + final double k; + double displacement; + + Spring(this.k, {this.displacement: 0.0}); + + double get force => -k * displacement; +} + +class ParticleAndSpringInBox extends System { + final Particle particle; + final Spring spring; + final Box box; + + ParticleAndSpringInBox({this.particle, this.spring, this.box}) { + _applyInvariants(); + } + + void update(double deltaT) { + particle.applyImpluse(spring.force * deltaT); + particle.update(deltaT); + _applyInvariants(); + } + + void _applyInvariants() { + box.confine(particle); + spring.displacement = particle.position; + } +} + +class ParticleClimbingRamp extends System { + final Particle particle; + final Box box; + final double slope; + + ParticleClimbingRamp({ + this.particle, + this.box, + this.slope, + double targetPosition}) { + double deltaPosition = targetPosition - particle.position; + particle.energy = -kGravity * slope * deltaPosition * particle.mass; + box.confine(particle); + } + + void update(double deltaT) { + particle.applyImpluse(kGravity * slope * deltaT); + // If we don't apply a min velocity, error terms in the simulation can + // prevent us from reaching the targetPosition before gravity overtakes our + // initial velocity and we start rolling down the hill. + particle.velocity = math.max(_kMinVelocity, particle.velocity); + particle.update(deltaT); + box.confine(particle); + } +} diff --git a/framework/animation/scroll_curve.dart b/framework/animation/scroll_curve.dart index 54b6385bd1e..bee8ee9fa7a 100644 --- a/framework/animation/scroll_curve.dart +++ b/framework/animation/scroll_curve.dart @@ -3,8 +3,14 @@ // found in the LICENSE file. import 'dart:math' as math; +import 'mechanics.dart'; +import 'simulation.dart'; + +const double _kSlope = 0.01; abstract class ScrollCurve { + Simulation release(Particle particle) => null; + // Returns the new scroll offset. double apply(double scrollOffset, double scrollDelta); } @@ -26,6 +32,18 @@ class BoundedScrollCurve extends ScrollCurve { } class OverscrollCurve extends ScrollCurve { + Simulation release(Particle particle) { + if (particle.position >= 0.0) + return null; + System system = new ParticleClimbingRamp( + particle: particle, + box: new Box(max: 0.0), + slope: _kSlope, + targetPosition: 0.0); + return new Simulation(system, + terminationCondition: () => particle.position == 0.0); + } + double apply(double scrollOffset, double scrollDelta) { double newScrollOffset = scrollOffset + scrollDelta; if (newScrollOffset < 0.0) { diff --git a/framework/animation/simulation.dart b/framework/animation/simulation.dart new file mode 100644 index 00000000000..a1d79c394d0 --- /dev/null +++ b/framework/animation/simulation.dart @@ -0,0 +1,45 @@ +// 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 'generator.dart'; +import 'mechanics.dart'; + +class Simulation { + Stream get onTick => _stream; + final System system; + + FrameGenerator _generator; + Stream _stream; + double _previousTime = 0.0; + + Simulation(this.system, {Function terminationCondition, Function onDone}) { + _generator = new FrameGenerator(onDone: onDone); + _stream = _generator.onTick.map(_update); + + if (terminationCondition != null) { + bool done = false; + _stream = _stream.takeWhile((_) { + if (done) + return false; + done = terminationCondition(); + return true; + }); + } + } + + void cancel() { + _generator.cancel(); + } + + double _update(double timeStamp) { + double previousTime = _previousTime; + _previousTime = timeStamp; + if (previousTime == 0.0) + return timeStamp; + double deltaT = timeStamp - previousTime; + system.update(deltaT); + return timeStamp; + } +} diff --git a/framework/components/scrollable.dart b/framework/components/scrollable.dart index 838dbe9c818..e8169e5632c 100644 --- a/framework/components/scrollable.dart +++ b/framework/components/scrollable.dart @@ -6,6 +6,8 @@ import '../animation/curves.dart'; import '../animation/fling_curve.dart'; import '../animation/generator.dart'; import '../animation/scroll_curve.dart'; +import '../animation/mechanics.dart'; +import '../animation/simulation.dart'; import '../fn.dart'; import 'dart:sky' as sky; @@ -16,7 +18,7 @@ abstract class Scrollable extends Component { double _scrollOffset = 0.0; FlingCurve _flingCurve; int _flingAnimationId; - AnimationGenerator _scrollAnimation; + Simulation _simulation; Scrollable({Object key, this.scrollCurve}) : super(key: key) { events.listen('pointerdown', _handlePointerDown); @@ -31,7 +33,7 @@ abstract class Scrollable extends Component { void didUnmount() { super.didUnmount(); _stopFling(); - _stopScrollAnimation(); + _stopSimulation(); } bool scrollBy(double scrollDelta) { @@ -44,25 +46,6 @@ abstract class Scrollable extends Component { return true; } - void animateScrollTo(double targetScrollOffset, { - double initialDelay: 0.0, - double duration: 0.0, - Curve curve: linear}) { - _stopScrollAnimation(); - _scrollAnimation = new AnimationGenerator( - duration: duration, - begin: _scrollOffset, - end: targetScrollOffset, - initialDelay: initialDelay, - curve: curve); - _scrollAnimation.onTick.listen((newScrollOffset) { - if (!scrollBy(newScrollOffset - _scrollOffset)) - _stopScrollAnimation(); - }, onDone: () { - _scrollAnimation = null; - }); - } - void _scheduleFlingUpdate() { _flingAnimationId = sky.window.requestAnimationFrame(_updateFling); } @@ -75,11 +58,11 @@ abstract class Scrollable extends Component { _flingAnimationId = null; } - void _stopScrollAnimation() { - if (_scrollAnimation == null) + void _stopSimulation() { + if (_simulation == null) return; - _scrollAnimation.cancel(); - _scrollAnimation = null; + _simulation.cancel(); + _simulation = null; } void _updateFling(double timeStamp) { @@ -91,13 +74,20 @@ abstract class Scrollable extends Component { void _settle() { _stopFling(); - if (_scrollOffset < 0.0) - animateScrollTo(0.0, duration: 200.0, curve: easeOut); + Particle particle = new Particle(position: scrollOffset); + _simulation = scrollCurve.release(particle); + if (_simulation == null) + return; + _simulation.onTick.listen((_) { + setState(() { + _scrollOffset = particle.position; + }); + }); } void _handlePointerDown(_) { _stopFling(); - _stopScrollAnimation(); + _stopSimulation(); } void _handlePointerUpOrCancel(_) { @@ -110,7 +100,7 @@ abstract class Scrollable extends Component { } void _handleFlingStart(sky.GestureEvent event) { - _stopScrollAnimation(); + _stopSimulation(); _flingCurve = new FlingCurve(-event.velocityY, event.timeStamp); _scheduleFlingUpdate(); }