mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-10-31 17:06:50 +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:
		| @ -27,7 +27,7 @@ void main() { | |||||||
| The `Component()` here could of course be any subclass of `Component`. | 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 | Every `Component` has a few methods that you can optionally implement, which are used by the | ||||||
| `FlameGame` class.  | `FlameGame` class. | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Component lifecycle | ### Component lifecycle | ||||||
| @ -201,7 +201,7 @@ an assertion error will be thrown. | |||||||
|  |  | ||||||
| ### Ensuring a component has a given ancestor | ### 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. | component tree, `HasAncestor` mixin can be used to enforce that relationship. | ||||||
|  |  | ||||||
| The mixin exposes the `ancestor` field that will be of the given type. | The mixin exposes the `ancestor` field that will be of the given type. | ||||||
| @ -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) | [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. | 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 | ## 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:dashbook/dashbook.dart'; | ||||||
| import 'package:examples/commons/commons.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/composability_example.dart'; | ||||||
| import 'package:examples/stories/components/debug_example.dart'; | import 'package:examples/stories/components/debug_example.dart'; | ||||||
| import 'package:examples/stories/components/game_in_game_example.dart'; | import 'package:examples/stories/components/game_in_game_example.dart'; | ||||||
| @ -31,5 +32,11 @@ void addComponentsStories(Dashbook dashbook) { | |||||||
|       (_) => GameWidget(game: GameInGameExample()), |       (_) => GameWidget(game: GameInGameExample()), | ||||||
|       codeLink: baseLink('components/game_in_game_example.dart'), |       codeLink: baseLink('components/game_in_game_example.dart'), | ||||||
|       info: GameInGameExample.description, |       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/anchor.dart'; | ||||||
| export 'src/collisions/has_collision_detection.dart'; | export 'src/collisions/has_collision_detection.dart'; | ||||||
| export 'src/collisions/hitboxes/screen_hitbox.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.dart'; | ||||||
| export 'src/components/core/component_set.dart'; | export 'src/components/core/component_set.dart'; | ||||||
| export 'src/components/core/position_type.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
	 Erick
					Erick