mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 16:36:57 +08:00
feat: Adding ClipComponent (#1769)
Adds a new component called ClipComponent that clips the canvas area based on its size and shape.
This commit is contained in:
@ -989,6 +989,25 @@ Check the example app
|
||||
[custom_painter_component](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/widgets/custom_painter_example.dart)
|
||||
for details on how to use it.
|
||||
|
||||
## ClipComponent
|
||||
|
||||
A `ClipComponent` is a component that will clip the canvas to its size and shape. This means that
|
||||
if the component itself or any child of the `ClipComponent` renders outside of the
|
||||
`ClipComponent`'s boundaries, the part that is not inside the area will not be shown.
|
||||
|
||||
A `ClipComponent` receives a builder function that should return the `Shape` that will define the
|
||||
clipped area, based on its size.
|
||||
|
||||
To make it easier to use that component, there are three factories that offers common shapes:
|
||||
|
||||
- `ClipComponent.rectangle`: Clips the area in the form a rectangle based on its size.
|
||||
- `ClipComponent.circle`: Clips the area in the form of a circle based on its size.
|
||||
- `ClipComponent.polygon`: Clips the area in the form of a polygon based on the points received
|
||||
in the constructor.
|
||||
|
||||
Check the example app
|
||||
[clip_component](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/components/clip_component_example.dart)
|
||||
for details on how to use it.
|
||||
|
||||
## Effects
|
||||
|
||||
|
||||
87
examples/lib/stories/components/clip_component_example.dart
Normal file
87
examples/lib/stories/components/clip_component_example.dart
Normal file
@ -0,0 +1,87 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:flutter/material.dart' hide Gradient;
|
||||
|
||||
class _Rectangle extends RectangleComponent {
|
||||
_Rectangle()
|
||||
: super(
|
||||
size: Vector2(200, 200),
|
||||
anchor: Anchor.center,
|
||||
paint: Paint()
|
||||
..shader = Gradient.linear(
|
||||
Offset.zero,
|
||||
const Offset(0, 100),
|
||||
[Colors.orange, Colors.blue],
|
||||
),
|
||||
children: [
|
||||
SequenceEffect(
|
||||
[
|
||||
RotateEffect.by(
|
||||
pi * 2,
|
||||
LinearEffectController(.4),
|
||||
),
|
||||
RotateEffect.by(
|
||||
0,
|
||||
LinearEffectController(.4),
|
||||
),
|
||||
],
|
||||
infinite: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class ClipComponentExample extends FlameGame with TapDetector {
|
||||
static String description = 'Tap on the objects to increase their size.';
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
addAll(
|
||||
[
|
||||
ClipComponent.circle(
|
||||
position: Vector2(100, 100),
|
||||
size: Vector2.all(50),
|
||||
children: [_Rectangle()],
|
||||
),
|
||||
ClipComponent.rectangle(
|
||||
position: Vector2(200, 100),
|
||||
size: Vector2.all(50),
|
||||
children: [_Rectangle()],
|
||||
),
|
||||
ClipComponent.polygon(
|
||||
points: [
|
||||
Vector2(1, 0),
|
||||
Vector2(1, 1),
|
||||
Vector2(0, 1),
|
||||
Vector2(1, 0),
|
||||
],
|
||||
position: Vector2(200, 200),
|
||||
size: Vector2.all(50),
|
||||
children: [_Rectangle()],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTapUp(TapUpInfo info) {
|
||||
final position = info.eventPosition.game;
|
||||
final hit = children
|
||||
.whereType<PositionComponent>()
|
||||
.where(
|
||||
(component) => component.containsLocalPoint(
|
||||
position - component.position,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
hit.forEach((component) {
|
||||
component.size += Vector2.all(10);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:examples/commons/commons.dart';
|
||||
import 'package:examples/stories/components/clip_component_example.dart';
|
||||
import 'package:examples/stories/components/composability_example.dart';
|
||||
import 'package:examples/stories/components/debug_example.dart';
|
||||
import 'package:examples/stories/components/game_in_game_example.dart';
|
||||
@ -31,5 +32,11 @@ void addComponentsStories(Dashbook dashbook) {
|
||||
(_) => GameWidget(game: GameInGameExample()),
|
||||
codeLink: baseLink('components/game_in_game_example.dart'),
|
||||
info: GameInGameExample.description,
|
||||
)
|
||||
..add(
|
||||
'ClipComponent',
|
||||
(context) => GameWidget(game: ClipComponentExample()),
|
||||
codeLink: baseLink('components/clip_component_example.dart'),
|
||||
info: ClipComponentExample.description,
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
export 'src/anchor.dart';
|
||||
export 'src/collisions/has_collision_detection.dart';
|
||||
export 'src/collisions/hitboxes/screen_hitbox.dart';
|
||||
export 'src/components/clip_component.dart';
|
||||
export 'src/components/core/component.dart';
|
||||
export 'src/components/core/component_set.dart';
|
||||
export 'src/components/core/position_type.dart';
|
||||
|
||||
139
packages/flame/lib/src/components/clip_component.dart
Normal file
139
packages/flame/lib/src/components/clip_component.dart
Normal file
@ -0,0 +1,139 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/experimental.dart';
|
||||
|
||||
/// A function that creates a shape based on a size represented by a [Vector2]
|
||||
typedef ShapeBuilder = Shape Function(Vector2 size);
|
||||
|
||||
/// {@template clip_component}
|
||||
/// A component that will clip its content.
|
||||
/// {@endtemplate}
|
||||
class ClipComponent extends PositionComponent {
|
||||
/// {@macro clip_component}
|
||||
///
|
||||
/// Clips the canvas based its shape and size.
|
||||
ClipComponent({
|
||||
required ShapeBuilder builder,
|
||||
super.position,
|
||||
super.size,
|
||||
super.scale,
|
||||
super.angle,
|
||||
super.anchor,
|
||||
super.children,
|
||||
super.priority,
|
||||
}) : _builder = builder;
|
||||
|
||||
/// {@macro circle_clip_component}
|
||||
///
|
||||
/// Clips the canvas in the form of a circle based on its size.
|
||||
factory ClipComponent.circle({
|
||||
Vector2? position,
|
||||
Vector2? size,
|
||||
Vector2? scale,
|
||||
double? angle,
|
||||
Anchor? anchor,
|
||||
Iterable<Component>? children,
|
||||
int? priority,
|
||||
}) {
|
||||
return ClipComponent(
|
||||
builder: (size) => Circle(size / 2, size.x / 2),
|
||||
position: position,
|
||||
size: size,
|
||||
scale: scale,
|
||||
angle: angle,
|
||||
anchor: anchor,
|
||||
children: children,
|
||||
priority: priority,
|
||||
);
|
||||
}
|
||||
|
||||
/// {@macro rectangle_clip_component}
|
||||
///
|
||||
/// Clips the canvas in the form of a rectangle based on its size.
|
||||
factory ClipComponent.rectangle({
|
||||
Vector2? position,
|
||||
Vector2? size,
|
||||
Vector2? scale,
|
||||
double? angle,
|
||||
Anchor? anchor,
|
||||
Iterable<Component>? children,
|
||||
int? priority,
|
||||
}) {
|
||||
return ClipComponent(
|
||||
builder: (size) => Rectangle.fromRect(size.toRect()),
|
||||
position: position,
|
||||
size: size,
|
||||
scale: scale,
|
||||
angle: angle,
|
||||
anchor: anchor,
|
||||
children: children,
|
||||
priority: priority,
|
||||
);
|
||||
}
|
||||
|
||||
/// {@macro polygon_clip_component}
|
||||
///
|
||||
/// Clips the canvas in the form of a polygon based on its size.
|
||||
factory ClipComponent.polygon({
|
||||
required List<Vector2> points,
|
||||
Vector2? position,
|
||||
Vector2? size,
|
||||
Vector2? scale,
|
||||
double? angle,
|
||||
Anchor? anchor,
|
||||
Iterable<Component>? children,
|
||||
int? priority,
|
||||
}) {
|
||||
assert(
|
||||
points.length > 2,
|
||||
'PolygonClipComponent requires at least 3 points.',
|
||||
);
|
||||
|
||||
return ClipComponent(
|
||||
builder: (size) {
|
||||
final translatedPoints = points
|
||||
.map(
|
||||
(p) => p.clone()..multiply(size),
|
||||
)
|
||||
.toList();
|
||||
return Polygon(translatedPoints);
|
||||
},
|
||||
position: position,
|
||||
size: size,
|
||||
scale: scale,
|
||||
angle: angle,
|
||||
anchor: anchor,
|
||||
children: children,
|
||||
priority: priority,
|
||||
);
|
||||
}
|
||||
|
||||
late Path _path;
|
||||
late Shape _shape;
|
||||
final ShapeBuilder _builder;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
_prepare();
|
||||
size.addListener(_prepare);
|
||||
}
|
||||
|
||||
void _prepare() {
|
||||
_shape = _builder(size);
|
||||
_path = _shape.asPath();
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) => canvas.clipPath(_path);
|
||||
|
||||
@override
|
||||
bool containsPoint(Vector2 point) {
|
||||
return _shape.containsPoint(point - position);
|
||||
}
|
||||
|
||||
@override
|
||||
bool containsLocalPoint(Vector2 point) {
|
||||
return _shape.containsPoint(point);
|
||||
}
|
||||
}
|
||||
BIN
packages/flame/test/_goldens/clip_component_circle.png
Normal file
BIN
packages/flame/test/_goldens/clip_component_circle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
packages/flame/test/_goldens/clip_component_polygon.png
Normal file
BIN
packages/flame/test/_goldens/clip_component_polygon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
packages/flame/test/_goldens/clip_component_rect.png
Normal file
BIN
packages/flame/test/_goldens/clip_component_rect.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
68
packages/flame/test/components/clip_component_test.dart
Normal file
68
packages/flame/test/components/clip_component_test.dart
Normal file
@ -0,0 +1,68 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class _Rectangle extends RectangleComponent {
|
||||
_Rectangle()
|
||||
: super(
|
||||
size: Vector2(200, 200),
|
||||
anchor: Anchor.center,
|
||||
paint: Paint()..color = Colors.blue,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('ClipComponent', () {
|
||||
group('RectangleClipComponent', () {
|
||||
testGolden(
|
||||
'renders correctly',
|
||||
(game) async {
|
||||
await game.add(
|
||||
ClipComponent.rectangle(
|
||||
size: Vector2(100, 100),
|
||||
children: [_Rectangle()],
|
||||
),
|
||||
);
|
||||
},
|
||||
goldenFile: '../_goldens/clip_component_rect.png',
|
||||
);
|
||||
});
|
||||
|
||||
group('CircleClipComponent', () {
|
||||
testGolden(
|
||||
'renders correctly',
|
||||
(game) async {
|
||||
await game.add(
|
||||
ClipComponent.circle(
|
||||
size: Vector2(100, 100),
|
||||
children: [_Rectangle()],
|
||||
),
|
||||
);
|
||||
},
|
||||
goldenFile: '../_goldens/clip_component_circle.png',
|
||||
);
|
||||
});
|
||||
|
||||
group('PolygonClipComponent', () {
|
||||
testGolden(
|
||||
'renders correctly',
|
||||
(game) async {
|
||||
await game.add(
|
||||
ClipComponent.polygon(
|
||||
points: [
|
||||
Vector2(1, 0),
|
||||
Vector2(1, 1),
|
||||
Vector2(0, 1),
|
||||
Vector2(1, 0),
|
||||
],
|
||||
size: Vector2(100, 100),
|
||||
children: [_Rectangle()],
|
||||
),
|
||||
);
|
||||
},
|
||||
goldenFile: '../_goldens/clip_component_polygon.png',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user