Skip to content

Commit

Permalink
feat: Experimental integer viewport (#1866)
Browse files Browse the repository at this point in the history
  • Loading branch information
natebot13 committed Sep 5, 2022
1 parent 01b5949 commit 63822de
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/flame/lib/experimental.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export 'src/experimental/camera_component.dart' show CameraComponent;
export 'src/experimental/circular_viewport.dart' show CircularViewport;
export 'src/experimental/fixed_aspect_ratio_viewport.dart'
show FixedAspectRatioViewport;
export 'src/experimental/fixed_integer_resolution_viewport.dart'
show FixedIntegerResolutionViewport;
export 'src/experimental/fixed_size_viewport.dart' show FixedSizeViewport;
export 'src/experimental/follow_behavior.dart' show FollowBehavior;
export 'src/experimental/geometry/shapes/circle.dart' show Circle;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import 'dart:math' as math;

import 'package:flame/extensions.dart';
import 'package:flame/game.dart';

/// This viewport is a very similar to [FixedResolutionViewport], but allows
/// for better handling of viewing pixel art. The main point is it ensures
/// sprites align to the physical pixel grid and only scales at integer
/// intervals. This prevents artifacts or distortion happening to scaled up
/// pixel art as discussed in https://github.com/flame-engine/flame/issues/1152.
/// Remember to set [devicePixelRatio] before using the viewport; a good place
/// to update it is in handleResize of your game, directly from
/// WidgetBindings.instance.window.devicePixelRatio.
class FixedIntegerResolutionViewport extends Viewport {
/// By default, this viewport will clip anything rendered outside.
/// Use this variable to control that behaviour.
bool clip;
double devicePixelRatio = 1.0;

@override
late Vector2 effectiveSize;

final Vector2 _scaledSize = Vector2.zero();
Vector2 get scaledSize => _scaledSize.clone();

final Vector2 _resizeOffset = Vector2.zero();
Vector2 get resizeOffset =>
_resizeOffset.clone()..scale(1 / devicePixelRatio);

late double _scale;
double get scale => _scale / devicePixelRatio;

/// The matrix used for scaling and translating the canvas
final Matrix4 _transform = Matrix4.identity();

/// The Rect that is used to clip the canvas
late Rect _clipRect;

FixedIntegerResolutionViewport(this.effectiveSize, {this.clip = true});

@override
void resize(Vector2 newCanvasSize) {
canvasSize = newCanvasSize.clone();
final devicePixels = canvasSize!..scale(devicePixelRatio);

_scale = math
.min(
devicePixels.x / effectiveSize.x,
devicePixels.y / effectiveSize.y,
)
.ceil()
.toDouble();

_scaledSize
..setFrom(effectiveSize)
..scale(_scale);
_resizeOffset
..setFrom(devicePixels)
..sub(_scaledSize)
..scale(0.5)
..round();

_clipRect = _resizeOffset & _scaledSize;

_transform.setIdentity();
_transform.translate(resizeOffset.x, resizeOffset.y);
_transform.scale(scale, scale, 1);
}

@override
void apply(Canvas c) {
if (clip) {
c.clipRect(_clipRect);
}
c.transform(_transform.storage);
}

@override
Vector2 projectVector(Vector2 worldCoordinates) {
return (worldCoordinates * scale)..add(resizeOffset);
}

@override
Vector2 unprojectVector(Vector2 screenCoordinates) {
return (screenCoordinates - resizeOffset)..scale(1 / scale);
}

@override
Vector2 scaleVector(Vector2 worldCoordinates) {
return worldCoordinates * scale;
}

@override
Vector2 unscaleVector(Vector2 screenCoordinates) {
return screenCoordinates / scale;
}
}

0 comments on commit 63822de

Please sign in to comment.