Skip to content

Commit

Permalink
feat!: Add OpacityProvider (#2062)
Browse files Browse the repository at this point in the history
This PR adds a new OpacityProvider interface which should be implemented by components that want to use OpacityEffect. This makes OpacityEffect decoupled from HasPaint.
  • Loading branch information
ufrshubham authored Oct 21, 2022
1 parent 3649b9b commit 0255cc3
Show file tree
Hide file tree
Showing 10 changed files with 387 additions and 62 deletions.
29 changes: 26 additions & 3 deletions doc/flame/effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,9 +360,8 @@ final effect = AnchorToEffect(

### `OpacityToEffect`

This effect will change over time the opacity of the target to the specified alpha-value. Currently
this effect can only be applied to components that have a `HasPaint` mixin. If the target component
uses multiple paints, the effect can target any individual color using the `paintId` parameter.
This effect will change the opacity of the target over time to the specified alpha-value.
It can only be applied to components that implement the `OpacityProvider`.

```{flutter-app}
:sources: ../flame/examples
Expand All @@ -379,6 +378,30 @@ final effect = OpacityEffect.to(
);
```

If the component uses multiple paints, the effect can target one more more of those paints
using the `target` parameter. The `HasPaint` mixin implements `OpacityProvider` and exposes APIs
to easily create providers for desired paintIds. For single paintId `opacityProviderOf` can be used
and for multiple paintIds and `opacityProviderOfList` can be used.


```{flutter-app}
:sources: ../flame/examples
:page: opacity_effect_with_target
:show: widget code infobox
:width: 180
:height: 160
```

```dart
final effect = OpacityEffect.to(
0.2,
EffectController(duration: 0.75),
target: component.opacityProviderOfList(
paintIds: const [paintId1, paintId2],
),
);
```

The opacity value of 0 corresponds to a fully transparent component, and the opacity value of 1 is
fully opaque. Convenience constructors `OpacityEffect.fadeOut()` and `OpacityEffect.fadeIn()` will
animate the target into full transparency / full visibility respectively.
Expand Down
22 changes: 14 additions & 8 deletions doc/flame/examples/lib/flower.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import 'package:flame/rendering.dart';

const tau = 2 * pi;

class Flower extends PositionComponent with TapCallbacks {
enum FlowerPaint { paintId1, paintId2, paintId3, paintId4, paintId5 }

class Flower extends PositionComponent
with TapCallbacks, HasPaint<FlowerPaint> {
Flower({
required double size,
void Function(Flower)? onTap,
Expand All @@ -22,15 +25,15 @@ class Flower extends PositionComponent with TapCallbacks {
_paths.add(_makePath(radius * 0.8, 6, 0.3, 1.4));
_paths.add(_makePath(radius * 0.55, 6, 0.2, 1.5));
_paths.add(_makePath(radius * 0.1, 12, 0.1, 6));
_paints.add(Paint()..color = const Color(0xff255910));
_paints.add(Paint()..color = const Color(0xffee3f3f));
_paints.add(Paint()..color = const Color(0xffffbd66));
_paints.add(Paint()..color = const Color(0xfff6f370));
_paints.add(Paint()..color = const Color(0xfffffff0));

setPaint(FlowerPaint.paintId1, Paint()..color = const Color(0xff255910));
setPaint(FlowerPaint.paintId2, Paint()..color = const Color(0xffee3f3f));
setPaint(FlowerPaint.paintId3, Paint()..color = const Color(0xffffbd66));
setPaint(FlowerPaint.paintId4, Paint()..color = const Color(0xfff6f370));
setPaint(FlowerPaint.paintId5, Paint()..color = const Color(0xfffffff0));
}

final List<Path> _paths = [];
final List<Paint> _paints = [];
final void Function(Flower)? _onTap;

Path _makePath(double radius, int n, double sharpness, double f) {
Expand All @@ -50,7 +53,10 @@ class Flower extends PositionComponent with TapCallbacks {
@override
void render(Canvas canvas) {
for (var i = 0; i < _paths.length; i++) {
canvas.drawPath(_paths[i], _paints[i]);
canvas.drawPath(
_paths[i],
getPaint(FlowerPaint.values.elementAt(i)),
);
}
}

Expand Down
2 changes: 2 additions & 0 deletions doc/flame/examples/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:doc_flame_examples/move_along_path_effect.dart';
import 'package:doc_flame_examples/move_by_effect.dart';
import 'package:doc_flame_examples/move_to_effect.dart';
import 'package:doc_flame_examples/opacity_by_effect.dart';
import 'package:doc_flame_examples/opacity_effect_with_target.dart';
import 'package:doc_flame_examples/opacity_to_effect.dart';
import 'package:doc_flame_examples/rotate_by_effect.dart';
import 'package:doc_flame_examples/rotate_to_effect.dart';
Expand Down Expand Up @@ -43,6 +44,7 @@ void main() {
'decorator_tint': DecoratorTintGame.new,
'drag_events': DragEventsGame.new,
'opacity_to_effect': OpacityToEffectGame.new,
'opacity_effect_with_target': OpacityEffectWithTargetGame.new,
'opacity_by_effect': OpacityByEffectGame.new,
'move_along_path_effect': MoveAlongPathEffectGame.new,
'move_by_effect': MoveByEffectGame.new,
Expand Down
47 changes: 47 additions & 0 deletions doc/flame/examples/lib/opacity_effect_with_target.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:doc_flame_examples/flower.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/experimental.dart';
import 'package:flame/game.dart';

class OpacityEffectWithTargetGame extends FlameGame with HasTappableComponents {
bool reset = false;

// This reference needs to be stored because every new instance of
// OpacityProvider caches the opacity ratios at the time on creation.
late OpacityProvider _borderOpacityProvider;

@override
Future<void> onLoad() async {
final flower = Flower(
position: size / 2,
size: 60,
onTap: _onTap,
)..anchor = Anchor.center;

_borderOpacityProvider = flower.opacityProviderOfList(
paintIds: const [FlowerPaint.paintId1, FlowerPaint.paintId2],
);

add(flower);
}

void _onTap(Flower flower) {
if (reset = !reset) {
flower.add(
OpacityEffect.to(
0.2,
EffectController(duration: 0.75),
target: _borderOpacityProvider,
),
);
} else {
flower.add(
OpacityEffect.fadeIn(
EffectController(duration: 0.75),
target: _borderOpacityProvider,
),
);
}
}
}
44 changes: 23 additions & 21 deletions doc/flame/examples/lib/opacity_to_effect.dart
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
import 'package:doc_flame_examples/ember.dart';
import 'package:doc_flame_examples/flower.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/experimental.dart';
import 'package:flame/game.dart';

class OpacityToEffectGame extends FlameGame with HasTappableComponents {
bool reset = false;

@override
Future<void> onLoad() async {
final ember = EmberPlayer(
final flower = Flower(
position: size / 2,
size: size / 4,
onTap: (ember) {
if (reset = !reset) {
ember.add(
OpacityEffect.to(
0.2,
EffectController(duration: 0.75),
),
);
} else {
ember.add(
OpacityEffect.to(
1.0,
EffectController(duration: 0.75),
),
);
}
},
size: 60,
onTap: _onTap,
)..anchor = Anchor.center;

add(ember);
add(flower);
}

void _onTap(Flower flower) {
if (reset = !reset) {
flower.add(
OpacityEffect.to(
0.2,
EffectController(duration: 0.75),
),
);
} else {
flower.add(
OpacityEffect.fadeIn(
EffectController(duration: 0.75),
),
);
}
}
}
3 changes: 2 additions & 1 deletion packages/flame/lib/effects.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export 'src/effects/provider_interfaces.dart'
AngleProvider,
PositionProvider,
ScaleProvider,
SizeProvider;
SizeProvider,
OpacityProvider;
export 'src/effects/remove_effect.dart';
export 'src/effects/rotate_effect.dart';
export 'src/effects/scale_effect.dart';
Expand Down
91 changes: 90 additions & 1 deletion packages/flame/lib/src/components/mixins/has_paint.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:math';
import 'dart:ui';

import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/src/palette.dart';

/// Adds a collection of paints to a component
Expand All @@ -9,7 +11,7 @@ import 'package:flame/src/palette.dart';
/// by the [paint] attribute and other paints can be manipulated/accessed
/// using [getPaint], [setPaint] and [deletePaint] by a paintId of generic type
/// [T], that can be omitted if the component only have one paint.
mixin HasPaint<T extends Object> on Component {
mixin HasPaint<T extends Object> on Component implements OpacityProvider {
final Map<T, Paint> _paints = {};

Paint paint = BasicPalette.white.paint();
Expand Down Expand Up @@ -97,4 +99,91 @@ mixin HasPaint<T extends Object> on Component {
void tint(Color color, {T? paintId}) {
getPaint(paintId).colorFilter = ColorFilter.mode(color, BlendMode.srcATop);
}

@override
double get opacity => paint.color.opacity;

@override
set opacity(double value) {
paint.color = paint.color.withOpacity(value);
for (final paint in _paints.values) {
paint.color = paint.color.withOpacity(value);
}
}

/// Creates an [OpacityProvider] for given [paintId] and can be used as
/// `target` for [OpacityEffect].
OpacityProvider opacityProviderOf(T paintId) {
return _ProxyOpacityProvider(paintId, this);
}

/// Creates an [OpacityProvider] for given list of [paintIds] and can be
/// used as `target` for [OpacityEffect].
///
/// When opacities of all the given [paintIds] are not same, this provider
/// directly effects opacity of the most opaque paint. Additionally, it
/// modifies other paints such that their respective opacity ratio with most
/// opaque paint is maintained.
///
/// If [paintIds] is null or empty, all the paints are used for creating the
/// [OpacityProvider].
///
/// Note: Each call results in a new [OpacityProvider] and hence the cached
/// opacity ratios are calculated using opacities when this method was called.
OpacityProvider opacityProviderOfList({List<T?>? paintIds}) {
return _MultiPaintOpacityProvider(
paintIds ?? (List<T?>.from(_paints.keys)..add(null)),
this,
);
}
}

class _ProxyOpacityProvider<T extends Object> implements OpacityProvider {
_ProxyOpacityProvider(this.paintId, this.target);

final T paintId;
final HasPaint<T> target;

@override
double get opacity => target.getOpacity(paintId: paintId);

@override
set opacity(double value) => target.setOpacity(value, paintId: paintId);
}

class _MultiPaintOpacityProvider<T extends Object> implements OpacityProvider {
_MultiPaintOpacityProvider(this.paintIds, this.target) {
final maxOpacity = opacity;

_opacityRatios = List<double>.generate(
paintIds.length,
(index) =>
target.getOpacity(paintId: paintIds.elementAt(index)) / maxOpacity,
);
}

final List<T?> paintIds;
final HasPaint<T> target;
late final List<double> _opacityRatios;

@override
double get opacity {
var maxOpacity = 0.0;

for (final paintId in paintIds) {
maxOpacity = max(target.getOpacity(paintId: paintId), maxOpacity);
}

return maxOpacity;
}

@override
set opacity(double value) {
for (var i = 0; i < paintIds.length; ++i) {
target.setOpacity(
value * _opacityRatios.elementAt(i),
paintId: paintIds.elementAt(i),
);
}
}
}
Loading

0 comments on commit 0255cc3

Please sign in to comment.