diff --git a/lib/components/animation_component.dart b/lib/components/animation_component.dart index 207c4a5e6..45d4e4bfc 100644 --- a/lib/components/animation_component.dart +++ b/lib/components/animation_component.dart @@ -36,16 +36,12 @@ class AnimationComponent extends PositionComponent { } @override - bool loaded() { - return this.animation.loaded(); - } + bool loaded() => animation.loaded(); @override void render(Canvas canvas) { - if (loaded()) { - prepareCanvas(canvas); - animation.getSprite().render(canvas, width, height); - } + prepareCanvas(canvas); + animation.getSprite().render(canvas, width, height); } @override diff --git a/lib/components/component.dart b/lib/components/component.dart index 74e86e924..c17825a25 100644 --- a/lib/components/component.dart +++ b/lib/components/component.dart @@ -1,9 +1,11 @@ import 'dart:math'; import 'dart:ui'; +import 'package:flutter/painting.dart'; + import '../sprite.dart'; import '../position.dart'; -import 'package:flutter/painting.dart'; +import '../anchor.dart'; /// This represents a Component for your game. /// @@ -27,7 +29,7 @@ abstract class Component { /// You can use the [Resizable] mixin if you want an implementation of this hook that keeps track of the current size. void resize(Size size) {} - /// Wether this component has been loaded yet. If not loaded, [BaseGame] will not try to render it. + /// Whether this component has been loaded yet. If not loaded, [BaseGame] will not try to render it. /// /// Sprite based components can use this to let [BaseGame] know not to try to render when the [Sprite] has not been loaded yet. /// Note that for a more consistent experience, you can pre-load all your assets beforehand with Flame.images.loadAll. @@ -38,7 +40,7 @@ abstract class Component { /// It will be called once per component per loop, and if it returns true, [BaseGame] will mark your component for deletion and remove it before the next loop. bool destroy() => false; - /// Wether this component is HUD object or not. + /// Whether this component is HUD object or not. /// /// HUD objects ignore the [BaseGame.camera] when rendered (so their position coordinates are considered relative to the device screen). bool isHud() => false; @@ -52,12 +54,14 @@ abstract class Component { int priority() => 0; } -/// A [Component] implementation that represents a component that has a specific, possibly mutatable position on the screen. +/// A [Component] implementation that represents a component that has a specific, possibly dynamic position on the screen. /// /// It represents a rectangle of dimension ([width], [height]), on the position ([x], [y]), rotate around its center with angle [angle]. +/// It also uses the [anchor] property to properly position itself. abstract class PositionComponent extends Component { double x = 0.0, y = 0.0, angle = 0.0; double width = 0.0, height = 0.0; + Anchor anchor = Anchor.topLeft; Position toPosition() => new Position(x, y); void setByPosition(Position position) { @@ -88,27 +92,24 @@ abstract class PositionComponent extends Component { } void prepareCanvas(Canvas canvas) { - canvas.translate(x, y); - - // rotate around center - canvas.translate(width / 2, height / 2); + double ax = x - anchor.relativePosition.dx * width; + double ay = y - anchor.relativePosition.dy * height; + canvas.translate(ax, ay); canvas.rotate(angle); - canvas.translate(-width / 2, -height / 2); } } +/// A [PositionComponent] that renders a single [Sprite] at the designated position, scaled to have the designated size and rotated to the designated angle. +/// +/// This is the most commonly used child of [Component]. class SpriteComponent extends PositionComponent { Sprite sprite; - final Paint paint = new Paint()..color = new Color(0xffffffff); - SpriteComponent(); - SpriteComponent.square(double size, String imagePath) - : this.rectangle(size, size, imagePath); + SpriteComponent.square(double size, String imagePath) : this.rectangle(size, size, imagePath); - SpriteComponent.rectangle(double width, double height, String imagePath) - : this.fromSprite(width, height, new Sprite(imagePath)); + SpriteComponent.rectangle(double width, double height, String imagePath) : this.fromSprite(width, height, new Sprite(imagePath)); SpriteComponent.fromSprite(double width, double height, this.sprite) { this.width = width; @@ -117,15 +118,13 @@ class SpriteComponent extends PositionComponent { @override render(Canvas canvas) { - if (sprite != null && sprite.loaded() && x != null && y != null) { - prepareCanvas(canvas); - sprite.render(canvas, width, height); - } + prepareCanvas(canvas); + sprite.render(canvas, width, height); } @override bool loaded() { - return this.sprite.loaded(); + return sprite != null && sprite.loaded() && x != null && y != null; } @override diff --git a/lib/components/composed_component.dart b/lib/components/composed_component.dart index 8518940c8..5631c3e15 100644 --- a/lib/components/composed_component.dart +++ b/lib/components/composed_component.dart @@ -1,4 +1,3 @@ - import 'dart:ui'; import 'package:flame/components/component.dart'; @@ -10,7 +9,8 @@ import 'package:ordered_set/ordered_set.dart'; /// A component that lets your component be composed by others /// It resembles [BaseGame]. It has an [components] property and an [add] method mixin ComposedComponent on Component { - OrderedSet components = new OrderedSet(Comparing.on((c) => c.priority())); + OrderedSet components = + new OrderedSet(Comparing.on((c) => c.priority())); @override render(Canvas canvas) { @@ -45,4 +45,4 @@ mixin ComposedComponent on Component { List children() => this.components.where((r) => r is Resizable).cast().toList(); -} \ No newline at end of file +} diff --git a/lib/components/debug_component.dart b/lib/components/debug_component.dart index 321c075bc..e1a33d06e 100644 --- a/lib/components/debug_component.dart +++ b/lib/components/debug_component.dart @@ -19,7 +19,7 @@ class DebugComponent extends PositionComponent { /// Don't do anything (change as desired) void update(double t) {} - /// Renders the recatangle + /// Renders the rectangle void render(Canvas c) { prepareCanvas(c); c.drawRect(new Rect.fromLTWH(0.0, 0.0, width, height), paint); diff --git a/lib/components/text_box_component.dart b/lib/components/text_box_component.dart new file mode 100644 index 000000000..fda5140d6 --- /dev/null +++ b/lib/components/text_box_component.dart @@ -0,0 +1,160 @@ +import 'dart:ui'; +import 'dart:math' as math; + +import 'package:flutter/src/painting/text_painter.dart'; + +import 'component.dart'; +import 'resizable.dart'; +import '../text_config.dart'; +import '../palette.dart'; +import '../position.dart'; + +class TextBoxConfig { + final double maxWidth; + final double margin; + final double timePerChar; + final double dismissDelay; + + const TextBoxConfig({ + this.maxWidth: 200.0, + this.margin: 8.0, + this.timePerChar: 0.0, + this.dismissDelay: 0.0, + }); +} + +class TextBoxComponent extends PositionComponent with Resizable { + Position p = Position.empty(); + + String _text; + TextConfig _config; + TextBoxConfig _boxConfig; + + List _lines; + double _maxLineWidth = 0.0; + double _lineHeight; + + double _lifeTime = 0.0; + Image _cache; + + String get text => _text; + + TextConfig get config => _config; + + TextBoxConfig get boxConfig => _boxConfig; + + TextBoxComponent(String text, {TextConfig config = const TextConfig(), TextBoxConfig boxConfig = const TextBoxConfig()}) { + _boxConfig = boxConfig; + _config = config; + _text = text; + _lines = ['']; + text.split(' ').forEach((word) { + String possibleLine = _lines.last + ' ' + word; + TextPainter p = config.toTextPainter(possibleLine); + if (_lineHeight == null) { + _lineHeight = p.height; + } + if (p.width <= _boxConfig.maxWidth - 2 * _boxConfig.margin) { + _lines.last = possibleLine; + _updateMaxWidth(p.width); + } else { + _lines.add(word); + _updateMaxWidth(config.toTextPainter(word).width); + } + }); + + _cache = _redrawCache(); + } + + void _updateMaxWidth(double w) { + if (w > _maxLineWidth) { + _maxLineWidth = w; + } + } + + double get totalCharTime => _text.length * _boxConfig.timePerChar; + + bool get finished => _lifeTime > totalCharTime + _boxConfig.dismissDelay; + + int get currentChar => _boxConfig.timePerChar == 0.0 ? _text.length - 1 : math.min(_lifeTime ~/ _boxConfig.timePerChar, _text.length - 1); + + int get currentLine { + int totalCharCount = 0; + int _currentChar = currentChar; + for (int i = 0; i < _lines.length; i++) { + totalCharCount += _lines[i].length; + if (totalCharCount > _currentChar) { + return i; + } + } + return _lines.length - 1; + } + + double _withMargins(double size) => size + 2 * _boxConfig.margin; + + @override + double get width => currentWidth; + + @override + double get height => currentHeight; + + double get totalWidth => _withMargins(_maxLineWidth); + + double get totalHeight => _withMargins(_lineHeight * _lines.length); + + double getLineWidth(String line, int charCount) { + return _withMargins(_config.toTextPainter(line.substring(0, math.min(charCount, line.length))).width); + } + + double get currentWidth { + int i = 0; + int totalCharCount = 0; + int _currentChar = currentChar; + int _currentLine = currentLine; + return _lines.sublist(0, _currentLine + 1).map((line) { + int charCount = (i < _currentLine) ? line.length : (_currentChar - totalCharCount); + totalCharCount += line.length; + i++; + return getLineWidth(line, charCount); + }).reduce(math.max); + } + + double get currentHeight => _withMargins((currentLine + 1) * _lineHeight); + + void render(Canvas c) { + prepareCanvas(c); + c.drawImage(_cache, Offset.zero, BasicPalette.white.paint); + } + + Image _redrawCache() { + PictureRecorder recorder = new PictureRecorder(); + Canvas c = new Canvas(recorder, new Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble())); + _fullRender(c); + return recorder.endRecording().toImage(width.toInt(), height.toInt()); + } + + void drawBackground(Canvas c) {} + + void _fullRender(Canvas c) { + drawBackground(c); + + int _currentLine = currentLine; + int charCount = 0; + double dy = _boxConfig.margin; + for (int line = 0; line < _currentLine; line++) { + charCount += _lines[line].length; + _config.toTextPainter(_lines[line]).paint(c, new Offset(_boxConfig.margin, dy)); + dy += _lineHeight; + } + int max = math.min(currentChar - charCount, _lines[_currentLine].length); + _config.toTextPainter(_lines[_currentLine].substring(0, max)).paint(c, new Offset(_boxConfig.margin, dy)); + } + + void update(double dt) { + int prevCurrentChar = currentChar; + _lifeTime += dt; + if (prevCurrentChar != currentChar) { + _cache = _redrawCache(); + } + } +} diff --git a/lib/components/text_component.dart b/lib/components/text_component.dart new file mode 100644 index 000000000..780690088 --- /dev/null +++ b/lib/components/text_component.dart @@ -0,0 +1,46 @@ +import 'dart:ui'; + +import 'package:flutter/src/painting/text_painter.dart'; + +import 'component.dart'; +import '../position.dart'; +import '../text_config.dart'; + +class TextComponent extends PositionComponent { + String _text; + TextConfig _config; + + get text => _text; + + set text(String text) { + _text = text; + _updateBox(); + } + + get config => _config; + + set config(TextConfig config) { + _config = config; + _updateBox(); + } + + TextComponent(this._text, { TextConfig config = const TextConfig() }) { + this._config = config; + _updateBox(); + } + + void _updateBox() { + TextPainter tp = config.toTextPainter(text); + this.width = tp.width; + this.height = tp.height; + } + + @override + void render(Canvas c) { + prepareCanvas(c); + config.render(c, text, Position.empty()); + } + + @override + void update(double t) {} +} diff --git a/lib/sprite.dart b/lib/sprite.dart index 30c4b78c9..d8f3ff54e 100644 --- a/lib/sprite.dart +++ b/lib/sprite.dart @@ -1,17 +1,15 @@ import 'dart:ui'; -import 'dart:async'; -import 'package:flame/position.dart'; -import 'package:flame/flame.dart'; -import 'package:flutter/material.dart' show Colors; +import 'dart:async'; +import 'flame.dart'; +import 'position.dart'; +import 'palette.dart'; class Sprite { - Paint paint = whitePaint; + Paint paint = BasicPalette.white.paint; Image image; Rect src; - static final Paint whitePaint = new Paint()..color = Colors.white; - Sprite( String fileName, { double x = 0.0, @@ -69,6 +67,7 @@ class Sprite { } double get _imageWidth => this.image.width.toDouble(); + double get _imageHeight => this.image.height.toDouble(); Position get originalSize { @@ -127,7 +126,6 @@ class Sprite { return; } size ??= this.size; - renderRect(canvas, - new Rect.fromLTWH(p.x - size.x / 2, p.y - size.y / 2, size.x, size.y)); + renderRect(canvas, new Rect.fromLTWH(p.x - size.x / 2, p.y - size.y / 2, size.x, size.y)); } } diff --git a/lib/util.dart b/lib/util.dart index 90d074522..8e75b38ab 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:ui'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' as material; import 'package:flutter/services.dart'; import 'position.dart'; @@ -49,43 +48,6 @@ class Util { }); } - /// Returns a [material.TextPainter] that allows for text rendering and size measuring. - /// - /// Rendering text on the Canvas is not as trivial as it should. - /// This methods exposes all possible parameters you might want to pass to render text, with sensible defaults. - /// Only the [text] is mandatory. - /// It returns a [material.TextPainter]. that have the properties: paint, width and height. - /// Example usage: - /// - /// final tp = Flame.util.text('Score: $score', fontSize: 48.0, fontFamily: 'Awesome Font'); - /// tp.paint(c, Offset(size.width - p.width - 10, size.height - p.height - 10)); - /// - material.TextPainter text( - String text, { - double fontSize: 24.0, - Color color: material.Colors.black, - String fontFamily: 'Arial', - TextAlign textAlign: TextAlign.left, - TextDirection textDirection: TextDirection.ltr, - }) { - material.TextStyle style = new material.TextStyle( - color: color, - fontSize: fontSize, - fontFamily: fontFamily, - ); - material.TextSpan span = new material.TextSpan( - style: style, - text: text, - ); - material.TextPainter tp = new material.TextPainter( - text: span, - textAlign: textAlign, - textDirection: textDirection, - ); - tp.layout(); - return tp; - } - /// This properly binds a gesture recognizer to your game. /// /// Use this in order to get it to work in case your app also contains other widgets.