feat: Add paint layers to HasPaint and associated component renders (#2073)

This commit is contained in:
Matt Bennett
2022-10-24 23:37:24 +01:00
committed by GitHub
parent 03e1f33d3d
commit 9e6bf4fbcc
7 changed files with 253 additions and 109 deletions

View File

@ -34,7 +34,8 @@ class Ball extends CircleComponent
static const degree = math.pi / 180;
@override
Future<void>? onLoad() {
Future<void> onLoad() async {
super.onLoad();
_resetBall;
final hitBox = CircleHitbox(
radius: radius,
@ -43,8 +44,6 @@ class Ball extends CircleComponent
addAll([
hitBox,
]);
return super.onLoad();
}
@override

View File

@ -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<T extends Object> on Component implements OpacityProvider {
final Map<T, Paint> _paints = {};
late final Map<T, Paint> _paints = {};
Paint paint = BasicPalette.white.paint();
void _assertGenerics() {
assert(T != Object, 'A generics type is missing on the HasPaint mixin');
}
@internal
List<Paint>? paintLayersInternal;
/// Gets a paint from the collection.
///
@ -28,7 +29,6 @@ mixin HasPaint<T extends Object> on Component implements OpacityProvider {
return paint;
}
_assertGenerics();
final _paint = _paints[paintId];
if (_paint == null) {
@ -40,16 +40,29 @@ mixin HasPaint<T extends Object> 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<Paint> get paintLayers {
if (!hasPaintLayers) {
return paintLayersInternal = [];
}
return paintLayersInternal!;
}
set paintLayers(List<Paint> 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<T extends Object> 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<T?>? paintIds}) {
OpacityProvider opacityProviderOfList({
List<T?>? paintIds,
bool includeLayers = true,
}) {
return _MultiPaintOpacityProvider(
paintIds ?? (List<T?>.from(_paints.keys)..add(null)),
includeLayers,
this,
);
}
@ -152,19 +169,25 @@ class _ProxyOpacityProvider<T extends Object> implements OpacityProvider {
}
class _MultiPaintOpacityProvider<T extends Object> implements OpacityProvider {
_MultiPaintOpacityProvider(this.paintIds, this.target) {
_MultiPaintOpacityProvider(this.paintIds, this.includeLayers, this.target) {
final maxOpacity = opacity;
_opacityRatios = List<double>.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<T?> paintIds;
final HasPaint<T> target;
final bool includeLayers;
late final List<double> _opacityRatios;
late final List<double>? _layerOpacityRatios;
@override
double get opacity {
@ -173,6 +196,11 @@ class _MultiPaintOpacityProvider<T extends Object> 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<T extends Object> 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]);
}
}
}
}

View File

@ -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<void> 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].

View File

@ -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<Paint>? 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,8 +174,14 @@ class PolygonComponent extends ShapeComponent {
@override
void render(Canvas canvas) {
if (renderShape) {
if (hasPaintLayers) {
for (final paint in paintLayers) {
canvas.drawPath(_path, paint);
}
} else {
canvas.drawPath(_path, paint);
}
}
}
@override

View File

@ -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<Paint>? paintLayers,
}) {
return RectangleComponent(
position: anchor == Anchor.topLeft
@ -67,6 +71,7 @@ class RectangleComponent extends PolygonComponent {
anchor: anchor,
priority: priority,
paint: paint,
paintLayers: paintLayers,
);
}

View File

@ -16,8 +16,14 @@ abstract class ShapeComponent extends PositionComponent with HasPaint {
super.children,
super.priority,
Paint? paint,
List<Paint>? 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;

View File

@ -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(
'getPaint throws exception when retrieving a paint that does not exists',
() {
final comp = _MyComponentWithType();
test('paintLayers defaults to empty list', () {
final component = _MyComponent();
const color = Color(0xFFE5E5E5);
component.paint = Paint()..color = color;
expect(
() => comp.getPaint(_MyComponentKeys.background),
component.paintLayers,
equals(<Paint>[]),
);
});
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(<Paint>[]),
);
expect(
circle.paint,
equals(firstPaint),
);
});
test(
'getPaint throws exception when retrieving a paint that does not exist',
() {
final component = _MyComponentWithType();
expect(
() => 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),
);
},