Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added Shadow3DDecorator #1812

Merged
merged 11 commits into from
Jul 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions doc/flame/examples/lib/decorator_shadow3d.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'dart:ui';

import 'package:doc_flame_examples/flower.dart';
import 'package:flame/components.dart';
import 'package:flame/experimental.dart';
import 'package:flame/game.dart';
import 'package:flame/rendering.dart';

class DecoratorShadowGame extends FlameGame with HasTappableComponents {
@override
Color backgroundColor() => const Color(0xFFC7C7C7);

@override
Future<void> onLoad() async {
var step = 0;
add(Grid());
add(
Flower(
size: 100,
position: canvasSize / 2,
decorator: Shadow3DDecorator(
base: canvasSize / 2 + Vector2(0, 50),
),
onTap: (flower) {
step++;
final decorator = flower.decorator! as Shadow3DDecorator;
if (step == 1) {
decorator.xShift = 200;
decorator.opacity = 0.5;
} else if (step == 2) {
decorator.xShift = 400;
decorator.yScale = 3;
decorator.blur = 1;
} else if (step == 3) {
decorator.angle = 1.7;
decorator.blur = 2;
} else if (step == 4) {
decorator.ascent = 20;
decorator.angle = 1.7;
decorator.blur = 2;
flower.y -= 20;
} else {
decorator.ascent = 0;
decorator.xShift = 0;
decorator.yScale = 1;
decorator.angle = -1.4;
decorator.opacity = 0.8;
decorator.blur = 0;
flower.y += 20;
step = 0;
}
},
)..onTapUp(),
);
}
}

class Grid extends Component {
final paint = Paint()
..color = const Color(0xffa9a9a9)
..style = PaintingStyle.stroke
..strokeWidth = 1;
@override
void render(Canvas canvas) {
for (var i = 0; i < 50; i++) {
canvas.drawLine(Offset(0, i * 25), Offset(500, i * 25), paint);
canvas.drawLine(Offset(i * 25, 0), Offset(i * 25, 500), paint);
}
}
}
32 changes: 11 additions & 21 deletions doc/flame/examples/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:html'; // ignore: avoid_web_libraries_in_flutter
import 'package:doc_flame_examples/decorator_blur.dart';
import 'package:doc_flame_examples/decorator_grayscale.dart';
import 'package:doc_flame_examples/decorator_rotate3d.dart';
import 'package:doc_flame_examples/decorator_shadow3d.dart';
import 'package:doc_flame_examples/decorator_tint.dart';
import 'package:doc_flame_examples/drag_events.dart';
import 'package:doc_flame_examples/tap_events.dart';
Expand All @@ -14,27 +15,16 @@ void main() {
if (page.startsWith('?')) {
page = page.substring(1);
}
Game? game;
switch (page) {
case 'tap_events':
game = TapEventsGame();
break;
case 'drag_events':
game = DragEventsGame();
break;
case 'decorator_blur':
game = DecoratorBlurGame();
break;
case 'decorator_grayscale':
game = DecoratorGrayscaleGame();
break;
case 'decorator_rotate3d':
game = DecoratorRotate3DGame();
break;
case 'decorator_tint':
game = DecoratorTintGame();
break;
}
final routes = <String, Game Function()>{
'decorator_blur': DecoratorBlurGame.new,
'decorator_grayscale': DecoratorGrayscaleGame.new,
'decorator_rotate3d': DecoratorRotate3DGame.new,
'decorator_shadow3d': DecoratorShadowGame.new,
'decorator_tint': DecoratorTintGame.new,
'drag_events': DragEventsGame.new,
'tap_events': TapEventsGame.new,
};
final game = routes[page]?.call();
if (game != null) {
runApp(GameWidget(game: game));
} else {
Expand Down
33 changes: 33 additions & 0 deletions doc/flame/rendering/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,39 @@ Possible uses:
- 3d falling particles such as snowflakes or leaves.


### Shadow3DDecorator

```{flutter-app}
:sources: ../flame/examples
:page: decorator_shadow3d
:show: widget code infobox
:width: 180
:height: 160
```

This decorator renders a shadow underneath the component, as if the component was a 3D object
standing on a plane. This effect works best for games that use isometric camera projection.

The shadow produced by this generator is quite flexible: you can control its angle, length, opacity,
blur, etc. For a full description of what properties this decorator has and their meaning, see the
class documentation.

```dart
final decorator = Shadow3DDecorator(
base: Vector2(100, 150),
angle: -1.4,
xShift: 200,
yScale: 1.5,
opacity: 0.5,
blur: 1.5,
);
```

The primary purpose of this decorator is to add shadows on the ground to your components. The main
limitation is that the shadows are flat and cannot interact with the environment. For example, this
decorator cannot handle shadows that fall onto walls or other vertical structures.


## Using decorators

### HasDecorator mixin
Expand Down
1 change: 1 addition & 0 deletions packages/flame/lib/rendering.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'src/rendering/decorator.dart' show Decorator;
export 'src/rendering/paint_decorator.dart' show PaintDecorator;
export 'src/rendering/rotate3d_decorator.dart' show Rotate3DDecorator;
export 'src/rendering/shadow3d_decorator.dart' show Shadow3DDecorator;
153 changes: 153 additions & 0 deletions packages/flame/lib/src/rendering/shadow3d_decorator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import 'dart:ui';

import 'package:flame/src/rendering/decorator.dart';
import 'package:vector_math/vector_math_64.dart';

/// [Shadow3DDecorator] casts a realistic-looking shadow from the component
/// onto the ground.
///
/// This decorator is suitable for games that use an isometric projection.
///
/// The shadows are very flexible, allowing for different positions of sun in
/// the sky, and even supporting airborne objects.
///
/// Still, these are not real 3D shadows cast by real 3D objects on a real 3D
/// terrain, so many limitations apply. For example, the shadow must fall on
/// the flat ground, having the sun too high in the sky is undesirable as it
/// would betray the fact that the component is really flat, etc.
class Shadow3DDecorator extends Decorator {
Shadow3DDecorator({
Vector2? base,
double? ascent,
double? angle,
double? xShift,
double? yScale,
double? blur,
double? opacity,
}) : _base = base?.clone() ?? Vector2.zero(),
_ascent = ascent ?? 0,
_angle = angle ?? -1.4,
_shift = xShift ?? 100.0,
_scale = yScale ?? 1.0,
_blur = blur ?? 0,
_opacity = opacity ?? 0.6;

/// Coordinates of the point where the component "touches the ground". If the
/// component is airborne (i.e. [ascent] is non-zero), then this should be the
/// coordinate of the point where the component would have touched the ground
/// if it landed.
///
/// This point is in the parent's coordinate space.
Vector2 get base => _base;
final Vector2 _base;
set base(Vector2 value) {
_base.setFrom(value);
_transformMatrix = null;
}

/// How high is the component above the ground.
double get ascent => _ascent;
double _ascent;
set ascent(double value) {
_ascent = value;
_transformMatrix = null;
}

/// The amount of skew the shadow is experiencing. The value of 0 corresponds
/// to the shadow being right behind (or in front of) the object. Positive
/// shift skews the shadow to the right if it's behind the object, or to the
/// left if the shadow is in front of the object. Negative shift skews in the
/// opposite direction.
///
/// This property should be determined by the meridian position of the sun.
double get xShift => _shift;
double _shift;
set xShift(double value) {
_shift = value;
_transformMatrix = null;
}

/// The length of the shadow relative to the height of the object. If the sun
/// is 45º above the horizon, this scale will be 1. When the sun is higher in
/// the sky, the scale factor should be less than 1, and when the sun is
/// lower, the scale factor ought to be greater than 1.
double get yScale => _scale;
double _scale;
set yScale(double value) {
_scale = value;
_transformMatrix = null;
}

/// Visual angle between a vertically standing component and the ground. This
/// angle is determined by your isometric projection. Use negative values
/// smaller than τ/4 (1.57) in magnitude to create shadows that are behind the
/// objects. Use positive angles that are slightly above τ/4 to make shadows
/// that are in front of the objects.
double get angle => _angle;
double _angle;
set angle(double value) {
_angle = value;
_transformMatrix = null;
}

/// The amount of blur to apply to the shadow. The value of 0 produces crisp
/// shadows with sharp edges, whereas positive [blur] produces softer-looking
/// shadows.
///
/// Strictly speaking, the parts of the object that are closer to the ground
/// ought to experience less blur than those that are higher up. However, this
/// is not supported by this decorator. Still, you can try setting the amount
/// of blur proportional to the height of the object, or dependent on its
/// ascent above the ground.
double get blur => _blur;
double _blur;
set blur(double value) {
_blur = value;
_paint = null;
}

/// Shadow's intensity. The value of 1 will create a hard pitch-black shadow,
/// which can only happen when there are no ambient sources of light (e.g. in
/// a cave). Values close to 0 will make the shadow barely visible, such as
/// on a cloudy day.
double get opacity => _opacity;
double _opacity;
set opacity(double value) {
_opacity = value;
_paint = null;
}

Paint? _paint;
Paint _makePaint() {
final paint = Paint();
final color = Color.fromRGBO(0, 0, 0, opacity);
paint.colorFilter = ColorFilter.mode(color, BlendMode.srcIn);
if (_blur > 0) {
paint.imageFilter = ImageFilter.blur(sigmaX: blur, sigmaY: blur / _scale);
}
return paint;
}

Matrix4? _transformMatrix;
Matrix4 _makeTransform() {
return Matrix4.identity()
..translate(0.0, 0.0, _scale * _ascent)
..setEntry(3, 2, 0.001)
..rotateX(_angle)
..scale(1.0, _scale)
..translate(-base.x - _shift, -base.y);
}

@override
void apply(void Function(Canvas) draw, Canvas canvas) {
_transformMatrix ??= _makeTransform();
_paint ??= _makePaint();

canvas.saveLayer(null, _paint!);
canvas.translate(base.x + _shift, base.y);
canvas.transform(_transformMatrix!.storage);
draw(canvas);
canvas.restore();
draw(canvas);
}
}
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.
Loading