Skip to content

Commit

Permalink
feat: Added Shadow3DDecorator (#1812)
Browse files Browse the repository at this point in the history
  • Loading branch information
st-pasha committed Jul 24, 2022
1 parent a5598a8 commit 0a41b2d
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 21 deletions.
70 changes: 70 additions & 0 deletions doc/flame/examples/lib/decorator_shadow3d.dart
@@ -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
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
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
@@ -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
@@ -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);
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 0a41b2d

Please sign in to comment.