Skip to content

Commit

Permalink
feat: Adding ClipComponent (#1769)
Browse files Browse the repository at this point in the history
Adds a new component called ClipComponent that clips the canvas area based on its size and shape.
  • Loading branch information
erickzanardo authored Sep 13, 2022
1 parent 96be840 commit f34d86d
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 2 deletions.
23 changes: 21 additions & 2 deletions doc/flame/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ void main() {
The `Component()` here could of course be any subclass of `Component`.

Every `Component` has a few methods that you can optionally implement, which are used by the
`FlameGame` class.
`FlameGame` class.


### Component lifecycle
Expand Down Expand Up @@ -201,7 +201,7 @@ an assertion error will be thrown.

### Ensuring a component has a given ancestor

When a component requires to have a specific ancestor type somewhere in the
When a component requires to have a specific ancestor type somewhere in the
component tree, `HasAncestor` mixin can be used to enforce that relationship.

The mixin exposes the `ancestor` field that will be of the given type.
Expand Down Expand Up @@ -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

Expand Down
87 changes: 87 additions & 0 deletions examples/lib/stories/components/clip_component_example.dart
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
7 changes: 7 additions & 0 deletions examples/lib/stories/components/components.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
);
}
1 change: 1 addition & 0 deletions packages/flame/lib/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
139 changes: 139 additions & 0 deletions packages/flame/lib/src/components/clip_component.dart
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 68 additions & 0 deletions packages/flame/test/components/clip_component_test.dart
Original file line number Diff line number Diff line change
@@ -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',
);
});
});
}

0 comments on commit f34d86d

Please sign in to comment.