diff --git a/examples/lib/stories/collision_detection/bouncing_ball_example.dart b/examples/lib/stories/collision_detection/bouncing_ball_example.dart index e79f93cf7..4e94845ac 100644 --- a/examples/lib/stories/collision_detection/bouncing_ball_example.dart +++ b/examples/lib/stories/collision_detection/bouncing_ball_example.dart @@ -34,7 +34,8 @@ class Ball extends CircleComponent static const degree = math.pi / 180; @override - Future? onLoad() { + Future onLoad() async { + super.onLoad(); _resetBall; final hitBox = CircleHitbox( radius: radius, @@ -43,8 +44,6 @@ class Ball extends CircleComponent addAll([ hitBox, ]); - - return super.onLoad(); } @override diff --git a/packages/flame/lib/src/components/mixins/has_paint.dart b/packages/flame/lib/src/components/mixins/has_paint.dart index d25d5e0f8..295e9e9a8 100644 --- a/packages/flame/lib/src/components/mixins/has_paint.dart +++ b/packages/flame/lib/src/components/mixins/has_paint.dart @@ -4,21 +4,22 @@ import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flame/src/palette.dart'; +import 'package:meta/meta.dart'; -/// Adds a collection of paints to a component +/// Adds a collection of paints and paint layers to a component /// /// Component will always have a main Paint that can be accessed /// 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. +/// [T], that can be omitted if the component only has one paint. +/// [paintLayers] paints should be drawn in list order during the render. The +/// main Paint is the first element. mixin HasPaint on Component implements OpacityProvider { - final Map _paints = {}; - + late final Map _paints = {}; Paint paint = BasicPalette.white.paint(); - void _assertGenerics() { - assert(T != Object, 'A generics type is missing on the HasPaint mixin'); - } + @internal + List? paintLayersInternal; /// Gets a paint from the collection. /// @@ -28,7 +29,6 @@ mixin HasPaint on Component implements OpacityProvider { return paint; } - _assertGenerics(); final _paint = _paints[paintId]; if (_paint == null) { @@ -40,16 +40,29 @@ mixin HasPaint on Component implements OpacityProvider { /// Sets a paint on the collection. void setPaint(T paintId, Paint paint) { - _assertGenerics(); _paints[paintId] = paint; } /// Removes a paint from the collection. void deletePaint(T paintId) { - _assertGenerics(); _paints.remove(paintId); } + /// List of paints to use (in order) during render. + List get paintLayers { + if (!hasPaintLayers) { + return paintLayersInternal = []; + } + return paintLayersInternal!; + } + + set paintLayers(List paintLayers) { + paintLayersInternal = paintLayers; + } + + /// Whether there are any paint layers defined for the component. + bool get hasPaintLayers => paintLayersInternal?.isNotEmpty ?? false; + /// Manipulate the paint to make it fully transparent. void makeTransparent({T? paintId}) { setOpacity(0, paintId: paintId); @@ -130,9 +143,13 @@ mixin HasPaint on Component implements 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? paintIds}) { + OpacityProvider opacityProviderOfList({ + List? paintIds, + bool includeLayers = true, + }) { return _MultiPaintOpacityProvider( paintIds ?? (List.from(_paints.keys)..add(null)), + includeLayers, this, ); } @@ -152,19 +169,25 @@ class _ProxyOpacityProvider implements OpacityProvider { } class _MultiPaintOpacityProvider implements OpacityProvider { - _MultiPaintOpacityProvider(this.paintIds, this.target) { + _MultiPaintOpacityProvider(this.paintIds, this.includeLayers, this.target) { final maxOpacity = opacity; - _opacityRatios = List.generate( - paintIds.length, - (index) => - target.getOpacity(paintId: paintIds.elementAt(index)) / maxOpacity, - ); + _opacityRatios = [ + for (final paintId in paintIds) + target.getOpacity(paintId: paintId) / maxOpacity, + ]; + _layerOpacityRatios = target.paintLayersInternal + ?.map( + (paint) => paint.color.opacity / maxOpacity, + ) + .toList(growable: false); } final List paintIds; final HasPaint target; + final bool includeLayers; late final List _opacityRatios; + late final List? _layerOpacityRatios; @override double get opacity { @@ -173,6 +196,11 @@ class _MultiPaintOpacityProvider implements OpacityProvider { for (final paintId in paintIds) { maxOpacity = max(target.getOpacity(paintId: paintId), maxOpacity); } + if (includeLayers) { + target.paintLayersInternal?.forEach( + (paint) => maxOpacity = max(paint.color.opacity, maxOpacity), + ); + } return maxOpacity; } @@ -185,5 +213,13 @@ class _MultiPaintOpacityProvider implements OpacityProvider { paintId: paintIds.elementAt(i), ); } + if (includeLayers) { + final paintLayersInternal = target.paintLayersInternal; + for (var i = 0; i < (paintLayersInternal?.length ?? 0); ++i) { + paintLayersInternal![i].color = paintLayersInternal[i] + .color + .withOpacity(value * _layerOpacityRatios![i]); + } + } } } diff --git a/packages/flame/lib/src/geometry/circle_component.dart b/packages/flame/lib/src/geometry/circle_component.dart index 8a601e025..34d90b517 100644 --- a/packages/flame/lib/src/geometry/circle_component.dart +++ b/packages/flame/lib/src/geometry/circle_component.dart @@ -5,6 +5,7 @@ import 'package:flame/extensions.dart'; import 'package:flame/geometry.dart'; import 'package:flame/src/effects/provider_interfaces.dart'; import 'package:flame/src/utils/solve_quadratic.dart'; +import 'package:meta/meta.dart'; class CircleComponent extends ShapeComponent implements SizeProvider { /// With this constructor you can create your [CircleComponent] from a radius @@ -18,6 +19,7 @@ class CircleComponent extends ShapeComponent implements SizeProvider { super.children, super.priority, super.paint, + super.paintLayers, }) : super(size: Vector2.all((radius ?? 0) * 2)); /// With this constructor you define the [CircleComponent] in relation to the @@ -30,8 +32,19 @@ class CircleComponent extends ShapeComponent implements SizeProvider { super.angle, super.anchor, super.paint, + super.paintLayers, }) : super(size: Vector2.all(relation * min(parentSize.x, parentSize.y))); + @override + @mustCallSuper + Future onLoad() async { + void updateCenterOffset() => _centerOffset = Offset(size.x / 2, size.y / 2); + size.addListener(updateCenterOffset); + updateCenterOffset(); + } + + late Offset _centerOffset; + /// Get the radius of the circle before scaling. double get radius { return min(size.x, size.y) / 2; @@ -56,14 +69,20 @@ class CircleComponent extends ShapeComponent implements SizeProvider { @override void render(Canvas canvas) { if (renderShape) { - canvas.drawCircle((size / 2).toOffset(), radius, paint); + if (hasPaintLayers) { + for (final paint in paintLayers) { + canvas.drawCircle(_centerOffset, radius, paint); + } + } else { + canvas.drawCircle(_centerOffset, radius, paint); + } } } @override void renderDebugMode(Canvas canvas) { super.renderDebugMode(canvas); - canvas.drawCircle((size / 2).toOffset(), radius, debugPaint); + canvas.drawCircle(_centerOffset, radius, debugPaint); } /// Checks whether the represented circle contains the [point]. diff --git a/packages/flame/lib/src/geometry/polygon_component.dart b/packages/flame/lib/src/geometry/polygon_component.dart index 7b2b72f33..262ef28ea 100644 --- a/packages/flame/lib/src/geometry/polygon_component.dart +++ b/packages/flame/lib/src/geometry/polygon_component.dart @@ -38,6 +38,7 @@ class PolygonComponent extends ShapeComponent { super.children, super.priority, super.paint, + super.paintLayers, bool? shrinkToBounds, }) : assert( _vertices.length > 2, @@ -76,6 +77,7 @@ class PolygonComponent extends ShapeComponent { Anchor? anchor, int? priority, Paint? paint, + List? paintLayers, bool? shrinkToBounds, }) : this( normalsToVertices(relation, parentSize), @@ -86,6 +88,7 @@ class PolygonComponent extends ShapeComponent { scale: scale, priority: priority, paint: paint, + paintLayers: paintLayers, shrinkToBounds: shrinkToBounds, ); @@ -171,7 +174,13 @@ class PolygonComponent extends ShapeComponent { @override void render(Canvas canvas) { if (renderShape) { - canvas.drawPath(_path, paint); + if (hasPaintLayers) { + for (final paint in paintLayers) { + canvas.drawPath(_path, paint); + } + } else { + canvas.drawPath(_path, paint); + } } } diff --git a/packages/flame/lib/src/geometry/rectangle_component.dart b/packages/flame/lib/src/geometry/rectangle_component.dart index e4e64565c..030aa6c14 100644 --- a/packages/flame/lib/src/geometry/rectangle_component.dart +++ b/packages/flame/lib/src/geometry/rectangle_component.dart @@ -13,6 +13,7 @@ class RectangleComponent extends PolygonComponent { super.children, super.priority, super.paint, + super.paintLayers, }) : super(sizeToVertices(size ?? Vector2.zero(), anchor)); RectangleComponent.square({ @@ -22,6 +23,7 @@ class RectangleComponent extends PolygonComponent { super.anchor, super.priority, super.paint, + super.paintLayers, super.children, }) : super(sizeToVertices(Vector2.all(size), anchor)); @@ -38,6 +40,7 @@ class RectangleComponent extends PolygonComponent { super.anchor, super.priority, super.paint, + super.paintLayers, super.shrinkToBounds, }) : super.relative([ relation.clone(), @@ -53,6 +56,7 @@ class RectangleComponent extends PolygonComponent { Anchor anchor = Anchor.topLeft, int? priority, Paint? paint, + List? paintLayers, }) { return RectangleComponent( position: anchor == Anchor.topLeft @@ -67,6 +71,7 @@ class RectangleComponent extends PolygonComponent { anchor: anchor, priority: priority, paint: paint, + paintLayers: paintLayers, ); } diff --git a/packages/flame/lib/src/geometry/shape_component.dart b/packages/flame/lib/src/geometry/shape_component.dart index d54b57f5c..28ac86408 100644 --- a/packages/flame/lib/src/geometry/shape_component.dart +++ b/packages/flame/lib/src/geometry/shape_component.dart @@ -16,8 +16,14 @@ abstract class ShapeComponent extends PositionComponent with HasPaint { super.children, super.priority, Paint? paint, + List? paintLayers, }) { this.paint = paint ?? this.paint; + // Only read from this.paintLayers if paintLayers not null to prevent + // unnecessary creation of the paintLayers list. + if (paintLayers != null) { + this.paintLayers = paintLayers; + } } bool renderShape = true; diff --git a/packages/flame/test/components/mixins/has_paint_test.dart b/packages/flame/test/components/mixins/has_paint_test.dart index e50d67d24..3c917f010 100644 --- a/packages/flame/test/components/mixins/has_paint_test.dart +++ b/packages/flame/test/components/mixins/has_paint_test.dart @@ -1,77 +1,113 @@ import 'dart:ui'; import 'package:flame/components.dart'; -import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('HasPaint', () { test('paint returns the default paint', () { - final comp = _MyComponent(); + final component = _MyComponent(); - expect(comp.paint, comp.getPaint()); + expect(component.paint, component.getPaint()); }); test( 'paint setter sets the main paint', () { - final comp = _MyComponent(); + final component = _MyComponent(); const color = Color(0xFFE5E5E5); - comp.paint = Paint()..color = color; + component.paint = Paint()..color = color; - expect(comp.getPaint().color, color); + expect(component.getPaint().color, color); }, ); + test('paintLayers defaults to empty list', () { + final component = _MyComponent(); + + const color = Color(0xFFE5E5E5); + component.paint = Paint()..color = color; + + expect( + component.paintLayers, + equals([]), + ); + }); + + test('paintLayers returns correct colors', () { + const firstColor = Color(0xFFE5E5E5); + const secondColor = Color(0xFF123456); + const thirdColor = Color(0xFFABABAB); + final firstPaint = Paint()..color = firstColor; + final secondPaint = Paint()..color = secondColor; + final thirdPaint = Paint()..color = thirdColor; + + final circle = CircleComponent( + radius: 10, + paint: firstPaint, + paintLayers: [secondPaint, thirdPaint], + ); + + expect( + circle.paintLayers, + equals([secondPaint, thirdPaint]), + ); + expect( + circle.paint, + equals(firstPaint), + ); + }); + + test('can clear paintLayers', () { + const firstColor = Color(0xFFE5E5E5); + const secondColor = Color(0xFF123456); + const thirdColor = Color(0xFFABABAB); + final firstPaint = Paint()..color = firstColor; + final secondPaint = Paint()..color = secondColor; + final thirdPaint = Paint()..color = thirdColor; + + final circle = CircleComponent( + radius: 10, + paint: firstPaint, + paintLayers: [secondPaint, thirdPaint], + ); + + circle.paintLayers.clear(); + + expect( + circle.paintLayers, + equals([]), + ); + expect( + circle.paint, + equals(firstPaint), + ); + }); + test( - 'getPaint throws exception when retrieving a paint that does not exists', + 'getPaint throws exception when retrieving a paint that does not exist', () { - final comp = _MyComponentWithType(); + final component = _MyComponentWithType(); expect( - () => comp.getPaint(_MyComponentKeys.background), + () => component.getPaint(_MyComponentKeys.background), throwsArgumentError, ); }, ); - test( - 'getPaint throws exception when used on genericless component', - () { - final comp = _MyComponent(); - - expect( - () => comp.getPaint(_MyComponentKeys.background), - failsAssert('A generics type is missing on the HasPaint mixin'), - ); - }, - ); - test( 'setPaint sets a paint', () { - final comp = _MyComponentWithType(); + final component = _MyComponentWithType(); const color = Color(0xFFA9A9A9); - comp.setPaint(_MyComponentKeys.background, Paint()..color = color); + component.setPaint(_MyComponentKeys.background, Paint()..color = color); - expect(comp.getPaint(_MyComponentKeys.background).color, color); - }, - ); - - test( - 'setPaint throws exception when used on genericless component', - () { - final comp = _MyComponent(); - - const color = Color(0xFFA9A9A9); expect( - () => comp.setPaint( - _MyComponentKeys.background, - Paint()..color = color, - ), - failsAssert('A generics type is missing on the HasPaint mixin'), + component.getPaint(_MyComponentKeys.background).color, + equals(color), ); }, ); @@ -79,26 +115,51 @@ void main() { test( 'deletePaint removes a paint from the map', () { - final comp = _MyComponentWithType(); + final component = _MyComponentWithType(); - comp.setPaint(_MyComponentKeys.foreground, Paint()); - comp.deletePaint(_MyComponentKeys.foreground); + component.setPaint(_MyComponentKeys.foreground, Paint()); + component.deletePaint(_MyComponentKeys.foreground); expect( - () => comp.getPaint(_MyComponentKeys.foreground), + () => component.getPaint(_MyComponentKeys.foreground), throwsArgumentError, ); }, ); test( - 'deletePaint throws exception when used on genericless component', + 'append paint to paintLayers', () { - final comp = _MyComponent(); + final component = _MyComponent(); + + const color = Color(0xFFE5E5E5); + component.paintLayers.add(Paint()..color = color); + + expect(component.paintLayers[0].color, equals(color)); + }, + ); + + test( + 'use setPaintLayers to set multiple paintIds in paintLayers', + () { + final component = _MyComponent(); + + const color = Color(0xFFE5E5E5); + const anotherColor = Color(0xFFABABAB); + const thirdColor = Color(0xFF123456); + component.setPaint('test', Paint()..color = color); + component.setPaint('anotherTest', Paint()..color = anotherColor); + component.setPaint('thirdTest', Paint()..color = thirdColor); + + component.paintLayers = [ + component.getPaint('thirdTest'), + component.getPaint('test'), + ]; expect( - () => comp.deletePaint(_MyComponentKeys.background), - failsAssert('A generics type is missing on the HasPaint mixin'), + (component.paintLayers[0].color == thirdColor) && + (component.paintLayers[1].color == color), + isTrue, ); }, ); @@ -106,77 +167,86 @@ void main() { test( 'makeTransparent sets opacity to 0 on the main when paintId is omitted', () { - final comp = _MyComponent(); - comp.makeTransparent(); + final component = _MyComponent(); + component.makeTransparent(); - expect(comp.paint.color.opacity, 0); + expect(component.paint.color.opacity, 0); }, ); test( 'makeTransparent sets opacity to 0 on informed paintId', () { - final comp = _MyComponentWithType(); - comp.setPaint(_MyComponentKeys.background, Paint()); - comp.makeTransparent(paintId: _MyComponentKeys.background); + final component = _MyComponentWithType(); + component.setPaint(_MyComponentKeys.background, Paint()); + component.makeTransparent(paintId: _MyComponentKeys.background); - expect(comp.getPaint(_MyComponentKeys.background).color.opacity, 0); + expect( + component.getPaint(_MyComponentKeys.background).color.opacity, + 0, + ); }, ); test( 'makeOpaque sets opacity to 1 on the main when paintId is omitted', () { - final comp = _MyComponent(); - comp.makeTransparent(); - comp.makeOpaque(); + final component = _MyComponent(); + component.makeTransparent(); + component.makeOpaque(); - expect(comp.paint.color.opacity, 1); + expect(component.paint.color.opacity, 1); }, ); test( 'makeOpaque sets opacity to 1 on informed paintId', () { - final comp = _MyComponentWithType(); - comp.setPaint( + final component = _MyComponentWithType(); + component.setPaint( _MyComponentKeys.background, Paint()..color = const Color(0x00E5E5E5), ); - comp.makeOpaque(paintId: _MyComponentKeys.background); + component.makeOpaque(paintId: _MyComponentKeys.background); - expect(comp.getPaint(_MyComponentKeys.background).color.opacity, 1); + expect( + component.getPaint(_MyComponentKeys.background).color.opacity, + 1, + ); }, ); test( 'setOpacity sets opacity of the main when paintId is omitted', () { - final comp = _MyComponent(); - comp.setOpacity(0.2); + final component = _MyComponent(); + component.setOpacity(0.2); - expect(comp.paint.color.opacity, 0.2); + expect(component.paint.color.opacity, 0.2); }, ); test( 'setOpacity sets opacity of the informed paintId', () { - final comp = _MyComponentWithType(); - comp.setPaint(_MyComponentKeys.background, Paint()); - comp.setOpacity(0.2, paintId: _MyComponentKeys.background); + final component = _MyComponentWithType(); + component.setPaint(_MyComponentKeys.background, Paint()); + component.setOpacity(0.2, paintId: _MyComponentKeys.background); - expect(comp.getPaint(_MyComponentKeys.background).color.opacity, 0.2); + expect( + component.getPaint(_MyComponentKeys.background).color.opacity, + 0.2, + ); }, ); test( 'throws error if opacity is less than 0', () { - final comp = _MyComponent(); + final component = _MyComponent(); expect( - () => comp.setOpacity(-0.5), + () => component.setOpacity(-0.5), throwsArgumentError, ); }, @@ -185,10 +255,10 @@ void main() { test( 'throws error if opacity is greater than 1', () { - final comp = _MyComponent(); + final component = _MyComponent(); expect( - () => comp.setOpacity(1.1), + () => component.setOpacity(1.1), throwsArgumentError, ); }, @@ -197,35 +267,35 @@ void main() { test( 'setColor sets color of the main when paintId is omitted', () { - final comp = _MyComponent(); + final component = _MyComponent(); const color = Color(0xFFE5E5E5); - comp.setColor(color); + component.setColor(color); - expect(comp.paint.color, color); + expect(component.paint.color, color); }, ); test( 'setOpacity sets opacity of the informed paintId', () { - final comp = _MyComponentWithType(); + final component = _MyComponentWithType(); const color = Color(0xFFE5E5E5); - comp.setPaint(_MyComponentKeys.background, Paint()); - comp.setColor(color, paintId: _MyComponentKeys.background); + component.setPaint(_MyComponentKeys.background, Paint()); + component.setColor(color, paintId: _MyComponentKeys.background); - expect(comp.getPaint(_MyComponentKeys.background).color, color); + expect(component.getPaint(_MyComponentKeys.background).color, color); }, ); test( 'tint sets the correct color filter of the main when paintId is omitted', () { - final comp = _MyComponent(); + final component = _MyComponent(); const color = Color(0xFFE5E5E5); - comp.tint(color); + component.tint(color); expect( - comp.paint.colorFilter, + component.paint.colorFilter, const ColorFilter.mode(color, BlendMode.srcATop), ); }, @@ -234,13 +304,13 @@ void main() { test( 'setOpacity sets opacity of the informed paintId', () { - final comp = _MyComponentWithType(); + final component = _MyComponentWithType(); const color = Color(0xFFE5E5E5); - comp.setPaint(_MyComponentKeys.background, Paint()); - comp.tint(color, paintId: _MyComponentKeys.background); + component.setPaint(_MyComponentKeys.background, Paint()); + component.tint(color, paintId: _MyComponentKeys.background); expect( - comp.getPaint(_MyComponentKeys.background).colorFilter, + component.getPaint(_MyComponentKeys.background).colorFilter, const ColorFilter.mode(color, BlendMode.srcATop), ); },