diff --git a/doc/effects.md b/doc/effects.md index ef21b4b6b50..7ef51508cb8 100644 --- a/doc/effects.md +++ b/doc/effects.md @@ -63,6 +63,7 @@ There are multiple effect controllers provided by the Flame framework as well: - [`SpeedEffectController`](#speedeffectcontroller) - [`DelayedEffectController`](#delayedeffectcontroller) - [`RandomEffectController`](#randomeffectcontroller) +- [`SineEffectController`](#sineeffectcontroller) - [`ZigzagEffectController`](#zigzageffectcontroller) @@ -524,6 +525,17 @@ of the produced random durations. Two distributions -- `.uniform` and `.exponent any other can be implemented by the user. +### `SineEffectController` + +An effect controller that represents a single period of the sine function. Use this to create +natural-looking harmonic oscillations. Two perpendicular move effects governed by +`SineEffectControllers` with different periods, will create a [Lissajous curve]. + +```dart +final ec = SineEffectController(period: 1); +``` + + ### `ZigzagEffectController` Simple alternating effect controller. Over the course of one `period`, this controller will proceed @@ -540,4 +552,6 @@ final ec = ZigzagEffectController(period: 2); * [Examples of various effects](https://examples.flame-engine.org/#/). + [tau]: https://en.wikipedia.org/wiki/Tau_(mathematical_constant) +[Lissajous curve]: https://en.wikipedia.org/wiki/Lissajous_curve diff --git a/examples/lib/stories/effects/effect_controllers_example.dart b/examples/lib/stories/effects/effect_controllers_example.dart index fec64f6862f..36620261d9e 100644 --- a/examples/lib/stories/effects/effect_controllers_example.dart +++ b/examples/lib/stories/effects/effect_controllers_example.dart @@ -8,10 +8,15 @@ class EffectControllersExample extends FlameGame { static const description = ''' This page demonstrates application of various non-standard effect controllers. - + The first white square has a ZigzagEffectController with period 1. The - orange square next to it has two move effects, each with a + orange square next to it has two move effects, each with a ZigzagEffectController. + + The lime square has a SineEffectController with the same period of 1s. The + violet square next to it has two move effects, each with a + SineEffectController with periods, but one of the effects is slightly + delayed. '''; @override @@ -44,5 +49,35 @@ class EffectControllersExample extends FlameGame { ), ]), ); + + add( + RectangleComponent.square( + position: Vector2(140, 50), + size: 20, + paint: Paint()..color = const Color(0xffbeff63), + )..add( + MoveEffect.by( + Vector2(0, 20), + InfiniteEffectController(SineEffectController(period: 1)), + ), + ), + ); + add( + RectangleComponent.square( + position: Vector2(190, 50), + size: 10, + paint: Paint()..color = const Color(0xffb663ff), + )..addAll([ + MoveEffect.by( + Vector2(0, 20), + InfiniteEffectController(SineEffectController(period: 1)) + ..advance(0.25), + ), + MoveEffect.by( + Vector2(10, 0), + InfiniteEffectController(SineEffectController(period: 1)), + ), + ]), + ); } } diff --git a/packages/flame/lib/effects.dart b/packages/flame/lib/effects.dart index f00c3ba20fc..6c95ff56fac 100644 --- a/packages/flame/lib/effects.dart +++ b/packages/flame/lib/effects.dart @@ -12,6 +12,7 @@ export 'src/effects/controllers/repeated_effect_controller.dart'; export 'src/effects/controllers/reverse_curved_effect_controller.dart'; export 'src/effects/controllers/reverse_linear_effect_controller.dart'; export 'src/effects/controllers/sequence_effect_controller.dart'; +export 'src/effects/controllers/sine_effect_controller.dart'; export 'src/effects/controllers/speed_effect_controller.dart'; export 'src/effects/controllers/zigzag_effect_controller.dart'; export 'src/effects/effect.dart'; diff --git a/packages/flame/lib/src/effects/controllers/sine_effect_controller.dart b/packages/flame/lib/src/effects/controllers/sine_effect_controller.dart new file mode 100644 index 00000000000..542cea224aa --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/sine_effect_controller.dart @@ -0,0 +1,23 @@ +import 'dart:math' as math; +import 'duration_effect_controller.dart'; +import 'infinite_effect_controller.dart'; +import 'repeated_effect_controller.dart'; + +/// This effect controller follows a sine wave. +/// +/// Use this controller to create effects that exhibit natural-looking harmonic +/// motion. +/// +/// Combine with [RepeatedEffectController] or [InfiniteEffectController] in +/// order to create longer waves. +class SineEffectController extends DurationEffectController { + SineEffectController({required double period}) + : assert(period > 0, 'Period must be positive: $period'), + super(period); + + @override + double get progress { + const tau = math.pi * 2; + return math.sin(tau * timer / duration); + } +} diff --git a/packages/flame/test/effects/controllers/sine_effect_controller_test.dart b/packages/flame/test/effects/controllers/sine_effect_controller_test.dart new file mode 100644 index 00000000000..3c9c2cac3f0 --- /dev/null +++ b/packages/flame/test/effects/controllers/sine_effect_controller_test.dart @@ -0,0 +1,40 @@ +import 'dart:math'; + +import 'package:flame/effects.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('SineEffectController', () { + test('general properties', () { + final ec = SineEffectController(period: 1); + expect(ec.duration, 1); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.progress, 0); + expect(ec.isRandom, false); + }); + + test('progression', () { + final ec = SineEffectController(period: 3); + final expectedProgress = + List.generate(101, (i) => sin(i * 0.01 * 2 * pi)); + for (final p in expectedProgress) { + expect(ec.progress, closeTo(p, 2e-14)); + ec.advance(0.01 * 3); + } + expect(ec.completed, true); + }); + + test('errors', () { + expect( + () => SineEffectController(period: 0), + failsAssert('Period must be positive: 0.0'), + ); + expect( + () => SineEffectController(period: -1.1), + failsAssert('Period must be positive: -1.1'), + ); + }); + }); +}