diff --git a/examples/game/assets/checker.png b/examples/game/assets/checker.png new file mode 100644 index 00000000000..a39686bdb3a Binary files /dev/null and b/examples/game/assets/checker.png differ diff --git a/examples/game/assets/line_effects.png b/examples/game/assets/line_effects.png new file mode 100644 index 00000000000..198e73352b0 Binary files /dev/null and b/examples/game/assets/line_effects.png differ diff --git a/examples/game/test_sprite_mesh.dart b/examples/game/test_sprite_mesh.dart new file mode 100644 index 00000000000..2f0d9487597 --- /dev/null +++ b/examples/game/test_sprite_mesh.dart @@ -0,0 +1,166 @@ +import 'dart:sky' as sky; + +import 'package:sky/services.dart'; +import 'package:sky/rendering.dart'; +import 'package:sky/theme/colors.dart' as colors; +import 'package:sky/widgets.dart'; +import 'package:skysprites/skysprites.dart'; + +AssetBundle _initBundle() { + if (rootBundle != null) + return rootBundle; + return new NetworkAssetBundle(Uri.base); +} + +final AssetBundle _bundle = _initBundle(); + +ImageMap _images; +SpriteSheet _spriteSheet; +TestApp _app; + +main() async { + _images = new ImageMap(_bundle); + + await _images.load([ + 'assets/checker.png', + 'assets/line_effects.png' + ]); + + assert(_images["assets/checker.png"] != null); + + _app = new TestApp(); + runApp(_app); +} + +class TestApp extends App { + + TestApp() { + _testBed = new TestBed(_labelTexts[_selectedLine]); + } + + TestBed _testBed; + int _selectedLine = 0; + + List _labelTexts = [ + "Colored", + "Smoke", + "Electric" + ]; + + Widget build() { + ThemeData theme = new ThemeData( + brightness: ThemeBrightness.light, + primarySwatch: colors.Purple + ); + + return new Theme( + data: theme, + child: new Title( + title: 'Test drawAtlas', + child: _buildColumn() + ) + ); + } + + Column _buildColumn() { + return new Column([ + new Flexible(child: _buildSpriteWidget()), + _buildTabBar() + ]); + } + + TabBar _buildTabBar() { + return new TabBar( + labels: _buildTabLabels(), + selectedIndex: _selectedLine, + onChanged: (int selectedLine) { + setState(() { + _selectedLine = selectedLine; + }); + } + ); + } + + List _buildTabLabels() { + List labels = []; + for(String text in _labelTexts) { + labels.add(new TabLabel(text: text)); + } + return labels; + } + + SpriteWidget _buildSpriteWidget() { + _testBed.setupLine(_labelTexts[_selectedLine]); + + return new SpriteWidget( + _testBed, + SpriteBoxTransformMode.letterbox + ); + } +} + +class TestBed extends NodeWithSize { + EffectLine _line; + + TestBed(String lineType) : super(new Size(1024.0, 1024.0)) { + userInteractionEnabled = true; + setupLine(lineType); + } + + void setupLine(String lineType) { + if (_line != null) { + _line.removeFromParent(); + } + + if (lineType == "Colored") { + // Create a line with no texture and a color sequence + _line = new EffectLine( + texture: null, + colorSequence: new ColorSequence.fromStartAndEndColor(new Color(0xffff0000), new Color(0xff0000ff)), + widthMode: EffectLineWidthMode.barrel, + minWidth: 20.0, + maxWidth: 50.0, + animationMode: EffectLineAnimationMode.scroll, + fadeAfterDelay: 1.0, + fadeDuration: 1.0 + ); + } else if (lineType == "Smoke") { + Texture baseTexture = new Texture(_images['assets/line_effects.png']); + Texture smokyLineTexture = baseTexture.textureFromRect(new Rect.fromLTRB(0.0, 0.0, 1024.0, 128.0)); + + _line = new EffectLine( + texture: smokyLineTexture, + textureLoopLength: 300.0, + colorSequence: new ColorSequence.fromStartAndEndColor(new Color(0xffffffff), new Color(0x00ffffff)), + widthMode: EffectLineWidthMode.barrel, + minWidth: 20.0, + maxWidth: 80.0, + animationMode: EffectLineAnimationMode.scroll + ); + } else if (lineType == "Electric") { + Texture baseTexture = new Texture(_images['assets/line_effects.png']); + Texture electricLineTexture = baseTexture.textureFromRect(new Rect.fromLTRB(0.0, 384.0, 1024.0, 512.0)); + + _line = new EffectLine( + texture: electricLineTexture, + textureLoopLength: 300.0, + widthMode: EffectLineWidthMode.barrel, + minWidth: 20.0, + maxWidth: 100.0, + animationMode: EffectLineAnimationMode.random + ); + } + + addChild(_line); + } + + bool handleEvent(SpriteBoxEvent event) { + if (event.type == "pointerdown") _line.points = []; + + if (event.type == "pointerdown" || event.type == "pointermove") { + Point pos = convertPointToNodeSpace(event.boxPosition); + _line.addPoint(pos); + } + return true; + } +} diff --git a/packages/flutter_sprites/lib/color_secuence.dart b/packages/flutter_sprites/lib/color_secuence.dart index 8395bece233..5bf041b3513 100644 --- a/packages/flutter_sprites/lib/color_secuence.dart +++ b/packages/flutter_sprites/lib/color_secuence.dart @@ -37,6 +37,8 @@ class ColorSequence { Color colorAtPosition(double pos) { assert(pos >= 0.0 && pos <= 1.0); + if (pos == 0.0) return colors[0]; + double lastStop = colorStops[0]; Color lastColor = colors[0]; diff --git a/packages/flutter_sprites/lib/effect_line.dart b/packages/flutter_sprites/lib/effect_line.dart new file mode 100644 index 00000000000..8e6ba70793f --- /dev/null +++ b/packages/flutter_sprites/lib/effect_line.dart @@ -0,0 +1,159 @@ +part of skysprites; + +enum EffectLineWidthMode { + linear, + barrel, +} + +enum EffectLineAnimationMode { + none, + scroll, + random, +} + +class EffectLine extends Node { + + EffectLine({ + this.texture: null, + List points, + this.widthMode : EffectLineWidthMode.linear, + this.minWidth: 10.0, + this.maxWidth: 10.0, + this.animationMode: EffectLineAnimationMode.none, + this.scrollSpeed: 0.1, + this.fadeDuration: null, + this.fadeAfterDelay: null, + this.textureLoopLength: null, + this.simplify: true, + ColorSequence colorSequence + }) { + if (points == null) this.points = []; + else this.points = points; + + _colorSequence = colorSequence; + if (_colorSequence == null) + _colorSequence = new ColorSequence.fromStartAndEndColor( + new Color(0xffffffff), + new Color(0xffffffff)); + + _painter = new TexturedLinePainter(points, _colors, _widths, texture); + _painter.textureLoopLength = textureLoopLength; + } + + final Texture texture; + + final EffectLineWidthMode widthMode; + final double minWidth; + final double maxWidth; + + final EffectLineAnimationMode animationMode; + final double scrollSpeed; + ColorSequence _colorSequence; + ColorSequence get colorSequence => _colorSequence; + + List _points; + + List get points => _points; + + set points(List points) { + _points = points; + _pointAges = []; + for (int i = 0; i < _points.length; i++) { + _pointAges.add(0.0); + } + } + + List _pointAges; + List _colors; + List _widths; + + final double fadeDuration; + final double fadeAfterDelay; + + final double textureLoopLength; + + final bool simplify; + + TexturedLinePainter _painter; + double _offset = 0.0; + + void update(double dt) { + // Update scrolling position + if (animationMode == EffectLineAnimationMode.scroll) { + _offset += dt * scrollSpeed; + _offset %= 1.0; + } else if (animationMode == EffectLineAnimationMode.random) { + _offset = randomDouble(); + } + + // Update age of line points, and remove if neccessary + if (fadeDuration != null && fadeAfterDelay != null) { + for (int i = _points.length - 1; i >= 0; i--) { + _pointAges[i] += dt; + if (_pointAges[i] > (fadeDuration + fadeAfterDelay)) { + _pointAges.removeAt(i); + _points.removeAt(i); + } + } + } + } + + void paint(PaintingCanvas canvas) { + if (points.length < 2) return; + + //_painter.textureLoopLength = textureLoopLength; + + _painter.points = points; + + // Calculate colors + List stops = _painter.calculatedTextureStops; + + List colors = []; + for (int i = 0; i < stops.length; i++) { + double stop = stops[i]; + Color color = _colorSequence.colorAtPosition(stop); + + if (fadeDuration != null && fadeAfterDelay != null) { + double age = _pointAges[i]; + if (age > fadeAfterDelay) { + double fade = 1.0 - (age - fadeAfterDelay) / fadeDuration; + int alpha = (color.alpha * fade).toInt().clamp(0, 255); + color = new Color.fromARGB(alpha, color.red, color.green, color.blue); + } + } + colors.add(color); + } + _painter.colors = colors; + + // Calculate widths + List widths = []; + for (double stop in stops) { + if (widthMode == EffectLineWidthMode.linear) { + double width = minWidth + (maxWidth - minWidth) * stop; + widths.add(width); + } else if (widthMode == EffectLineWidthMode.barrel) { + double width = minWidth + math.sin(stop * math.PI) * (maxWidth - minWidth); + widths.add(width); + } + } + _painter.widths = widths; + + _painter.textureStopOffset = _offset; + + _painter.paint(canvas); + } + + void addPoint(Point point) { + // Skip duplicate points + if (points.length > 0 && point.x == points[points.length - 1].x && point.y == points[points.length - 1].y) + return; + + if (simplify) { + + } + + // Add point and point's age + _points.add(point); + _pointAges.add(0.0); + } +} diff --git a/packages/flutter_sprites/lib/skysprites.dart b/packages/flutter_sprites/lib/skysprites.dart index ea29ce5c05a..ffc31a518a8 100644 --- a/packages/flutter_sprites/lib/skysprites.dart +++ b/packages/flutter_sprites/lib/skysprites.dart @@ -21,9 +21,10 @@ import 'package:sky_services/media/media.mojom.dart'; import 'package:vector_math/vector_math.dart'; part 'action.dart'; -part 'constraint.dart'; part 'action_spline.dart'; part 'color_secuence.dart'; +part 'constraint.dart'; +part 'effect_line.dart'; part 'image_map.dart'; part 'label.dart'; part 'layer.dart'; @@ -38,5 +39,6 @@ part 'spritesheet.dart'; part 'sprite_box.dart'; part 'sprite_widget.dart'; part 'texture.dart'; +part 'textured_line.dart'; part 'util.dart'; part 'virtual_joystick.dart'; diff --git a/packages/flutter_sprites/lib/textured_line.dart b/packages/flutter_sprites/lib/textured_line.dart new file mode 100644 index 00000000000..e066daf125c --- /dev/null +++ b/packages/flutter_sprites/lib/textured_line.dart @@ -0,0 +1,269 @@ +part of skysprites; + +class TexturedLine extends Node { + TexturedLine(List points, List colors, List widths, [Texture texture, List textureStops]) { + painter = new TexturedLinePainter(points, colors, widths, texture, textureStops); + } + + TexturedLinePainter painter; + + void paint(PaintingCanvas canvas) { + painter.paint(canvas); + } +} + +class TexturedLinePainter { + TexturedLinePainter(this._points, this.colors, this.widths, [Texture texture, this.textureStops]) { + this.texture = texture; + } + + List _points; + + List get points => _points; + + set points(List points) { + _points = points; + _calculatedTextureStops = null; + } + + List colors; + List widths; + Texture _texture; + + Texture get texture => _texture; + + set texture(Texture texture) { + _texture = texture; + if (texture == null) { + _cachedPaint = new Paint(); + } else { + Matrix4 matrix = new Matrix4.identity(); + ImageShader shader = new ImageShader(texture.image, + TileMode.repeated, TileMode.repeated, matrix.storage); + + _cachedPaint = new Paint(); + _cachedPaint.setShader(shader); + } + } + + List textureStops; + + List _calculatedTextureStops; + + List get calculatedTextureStops { + if (_calculatedTextureStops == null) + _calculateTextureStops(); + return _calculatedTextureStops; + } + + double _length; + + double get length { + if (_calculatedTextureStops == null) + _calculateTextureStops(); + return _length; + } + + double textureStopOffset = 0.0; + + double _textureLoopLength; + + get textureLoopLength => textureLoopLength; + + set textureLoopLength(double textureLoopLength) { + _textureLoopLength = textureLoopLength; + _calculatedTextureStops = null; + } + + Paint _cachedPaint = new Paint(); + + void paint(PaintingCanvas canvas) { + // Check input values + assert(_points != null); + if (_points.length < 2) return; + + assert(_points.length == colors.length); + assert(_points.length == widths.length); + + // Calculate normals + List vectors = []; + for (Point pt in _points) { + vectors.add(new Vector2(pt.x, pt.y)); + } + List miters = _computeMiterList(vectors, false); + + List vertices = []; + List indicies = []; + List verticeColors = []; + List textureCoordinates; + double textureTop; + double textureBottom; + List stops; + + // Add first point + Point lastPoint = _points[0]; + Vector2 lastMiter = miters[0]; + + // Add vertices and colors + _addVerticesForPoint(vertices, lastPoint, lastMiter, widths[0]); + verticeColors.add(colors[0]); + verticeColors.add(colors[0]); + + if (texture != null) { + assert(texture.rotated == false); + + // Setup for calculating texture coordinates + textureTop = texture.frame.top; + textureBottom = texture.frame.bottom; + textureCoordinates = []; + + // Use correct stops + if (textureStops != null) { + assert(_points.length == textureStops.length); + stops = textureStops; + } else { + if (_calculatedTextureStops == null) _calculateTextureStops(); + stops = _calculatedTextureStops; + } + + // Texture coordinate points + double xPos = _xPosForStop(stops[0]); + textureCoordinates.add(new Point(xPos, textureTop)); + textureCoordinates.add(new Point(xPos, textureBottom)); + } + + // Add the rest of the points + for (int i = 1; i < _points.length; i++) { + // Add vertices + Point currentPoint = _points[i]; + Vector2 currentMiter = miters[i]; + _addVerticesForPoint(vertices, currentPoint, currentMiter, widths[i]); + + // Add references to the triangles + int lastIndex0 = (i - 1) * 2; + int lastIndex1 = (i - 1) * 2 + 1; + int currentIndex0 = i * 2; + int currentIndex1 = i * 2 + 1; + indicies.addAll([lastIndex0, lastIndex1, currentIndex0]); + indicies.addAll([lastIndex1, currentIndex1, currentIndex0]); + + // Add colors + verticeColors.add(colors[i]); + verticeColors.add(colors[i]); + + if (texture != null) { + // Texture coordinate points + double xPos = _xPosForStop(stops[i]); + textureCoordinates.add(new Point(xPos, textureTop)); + textureCoordinates.add(new Point(xPos, textureBottom)); + } + + // Update last values + lastPoint = currentPoint; + lastMiter = currentMiter; + } + + canvas.drawVertices(VertexMode.triangles, vertices, textureCoordinates, verticeColors, TransferMode.modulate, indicies, _cachedPaint); + } + + double _xPosForStop(double stop) { + if (_textureLoopLength == null) { + return texture.frame.left + texture.frame.width * (stop - textureStopOffset); + } else { + return texture.frame.left + texture.frame.width * (stop - textureStopOffset * (_textureLoopLength / length)) * (length / _textureLoopLength); + } + } + + void _addVerticesForPoint(List vertices, Point point, Vector2 miter, double width) { + double halfWidth = width / 2.0; + + Offset offset0 = new Offset(miter[0] * halfWidth, miter[1] * halfWidth); + Offset offset1 = new Offset(-miter[0] * halfWidth, -miter[1] * halfWidth); + + vertices.add(point + offset0); + vertices.add(point + offset1); + } + + void _calculateTextureStops() { + List stops = []; + double length = 0.0; + + // Add first stop + stops.add(0.0); + + // Calculate distance to each point from the first point along the line + for (int i = 1; i < _points.length; i++) { + Point lastPoint = _points[i - 1]; + Point currentPoint = _points[i]; + + double dist = GameMath.pointQuickDist(lastPoint, currentPoint); + length += dist; + stops.add(length); + } + + // Normalize the values in the range [0.0, 1.0] + for (int i = 1; i < points.length; i++) { + stops[i] = stops[i] / length; + new Point(512.0, 512.0); + } + + _calculatedTextureStops = stops; + _length = length; + } +} + +Vector2 _computeMiter(Vector2 lineA, Vector2 lineB) { + Vector2 miter = new Vector2(- (lineA[1] + lineB[1]), lineA[0] + lineB[0]); + miter.normalize(); + + double miterLength = 1.0 / dot2(miter, new Vector2(-lineA[1], lineA[0])); + miter = miter.scale(miterLength); + + return miter; +} + +Vector2 _vectorNormal(Vector2 v) { + return new Vector2(-v[1], v[0]); +} + +Vector2 _vectorDirection(Vector2 a, Vector2 b) { + Vector2 result = a - b; + return result.normalize(); +} + +List _computeMiterList(List points, bool closed) { + List out = []; + Vector2 curNormal = null; + + if (closed) { + points = new List.from(points); + points.add(points[0]); + } + + int total = points.length; + for (int i = 1; i < total; i++) { + Vector2 last = points[i - 1]; + Vector2 cur = points[i]; + Vector2 next = (i < total - 1) ? points[i + 1] : null; + + Vector2 lineA = _vectorDirection(cur, last); + if (curNormal == null) { + curNormal = _vectorNormal(lineA); + } + + if (i == 1) { + out.add(curNormal); + } + + if (next == null) { + curNormal = _vectorNormal(lineA); + out.add(curNormal); + } else { + Vector2 lineB = _vectorDirection(next, cur); + Vector2 miter = _computeMiter(lineA, lineB); + out.add(miter); + } + } + + return out; +}