1360 lines
38 KiB
Dart
1360 lines
38 KiB
Dart
import 'dart:math' as math;
|
|
import 'dart:typed_data';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:flame/components.dart';
|
|
import 'package:flame/events.dart';
|
|
import 'package:flame/particles.dart';
|
|
import 'package:flame_spine/flame_spine.dart' as spine;
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../models/runtime_node.dart';
|
|
import '../protocol/runtime_protocol.dart';
|
|
import '../resources/game_resource_manager.dart';
|
|
|
|
@visibleForTesting
|
|
Color composeRuntimeColorAlpha(Color color, double alpha) {
|
|
return color.withValues(alpha: color.a * alpha.clamp(0.0, 1.0));
|
|
}
|
|
|
|
@visibleForTesting
|
|
Rect runtimeImageSourceRect({
|
|
required double imageWidth,
|
|
required double imageHeight,
|
|
double? sourceX,
|
|
double? sourceY,
|
|
double? sourceWidth,
|
|
double? sourceHeight,
|
|
}) {
|
|
final x = (sourceX ?? 0).clamp(0.0, imageWidth).toDouble();
|
|
final y = (sourceY ?? 0).clamp(0.0, imageHeight).toDouble();
|
|
final maxWidth = imageWidth - x;
|
|
final maxHeight = imageHeight - y;
|
|
final width = (sourceWidth ?? maxWidth).clamp(0.0, maxWidth).toDouble();
|
|
final height = (sourceHeight ?? maxHeight).clamp(0.0, maxHeight).toDouble();
|
|
return Rect.fromLTWH(x, y, width, height);
|
|
}
|
|
|
|
@visibleForTesting
|
|
List<({Rect source, Rect destination})> runtimeNineSliceRects({
|
|
required Rect source,
|
|
required Rect destination,
|
|
double sliceLeft = 0,
|
|
double sliceTop = 0,
|
|
double sliceRight = 0,
|
|
double sliceBottom = 0,
|
|
double destinationOverlap = 0,
|
|
double sourceInset = 0,
|
|
}) {
|
|
if (source.width <= 0 ||
|
|
source.height <= 0 ||
|
|
destination.width <= 0 ||
|
|
destination.height <= 0) {
|
|
return const [];
|
|
}
|
|
final left = sliceLeft.clamp(0.0, source.width).toDouble();
|
|
final top = sliceTop.clamp(0.0, source.height).toDouble();
|
|
final right = sliceRight.clamp(0.0, source.width - left).toDouble();
|
|
final bottom = sliceBottom.clamp(0.0, source.height - top).toDouble();
|
|
final destLeft = left.clamp(0.0, destination.width).toDouble();
|
|
final destTop = top.clamp(0.0, destination.height).toDouble();
|
|
final destRight = right.clamp(0.0, destination.width - destLeft).toDouble();
|
|
final destBottom = bottom.clamp(0.0, destination.height - destTop).toDouble();
|
|
|
|
final sourceXs = [
|
|
source.left,
|
|
source.left + left,
|
|
source.right - right,
|
|
source.right,
|
|
];
|
|
final sourceYs = [
|
|
source.top,
|
|
source.top + top,
|
|
source.bottom - bottom,
|
|
source.bottom,
|
|
];
|
|
final destXs = [
|
|
destination.left,
|
|
destination.left + destLeft,
|
|
destination.right - destRight,
|
|
destination.right,
|
|
];
|
|
final destYs = [
|
|
destination.top,
|
|
destination.top + destTop,
|
|
destination.bottom - destBottom,
|
|
destination.bottom,
|
|
];
|
|
|
|
final parts = <({Rect source, Rect destination})>[];
|
|
for (var y = 0; y < 3; y++) {
|
|
for (var x = 0; x < 3; x++) {
|
|
final rawSourcePart = Rect.fromLTRB(
|
|
sourceXs[x],
|
|
sourceYs[y],
|
|
sourceXs[x + 1],
|
|
sourceYs[y + 1],
|
|
);
|
|
final rawDestPart = Rect.fromLTRB(
|
|
destXs[x],
|
|
destYs[y],
|
|
destXs[x + 1],
|
|
destYs[y + 1],
|
|
);
|
|
if (rawSourcePart.width <= 0 ||
|
|
rawSourcePart.height <= 0 ||
|
|
rawDestPart.width <= 0 ||
|
|
rawDestPart.height <= 0) {
|
|
continue;
|
|
}
|
|
final sourcePart = _insetNineSliceSourceRect(
|
|
rawSourcePart,
|
|
bounds: source,
|
|
inset: sourceInset,
|
|
);
|
|
final destPart = _overlapNineSliceDestinationRect(
|
|
rawDestPart,
|
|
x: x,
|
|
y: y,
|
|
bounds: destination,
|
|
overlap: destinationOverlap,
|
|
);
|
|
parts.add((source: sourcePart, destination: destPart));
|
|
}
|
|
}
|
|
return parts;
|
|
}
|
|
|
|
Rect _insetNineSliceSourceRect(
|
|
Rect rect, {
|
|
required Rect bounds,
|
|
required double inset,
|
|
}) {
|
|
if (inset <= 0 || rect.width <= inset * 2 || rect.height <= inset * 2) {
|
|
return rect;
|
|
}
|
|
return Rect.fromLTRB(
|
|
math.min(rect.right, math.max(bounds.left, rect.left + inset)),
|
|
math.min(rect.bottom, math.max(bounds.top, rect.top + inset)),
|
|
math.max(rect.left, math.min(bounds.right, rect.right - inset)),
|
|
math.max(rect.top, math.min(bounds.bottom, rect.bottom - inset)),
|
|
);
|
|
}
|
|
|
|
Rect _overlapNineSliceDestinationRect(
|
|
Rect rect, {
|
|
required int x,
|
|
required int y,
|
|
required Rect bounds,
|
|
required double overlap,
|
|
}) {
|
|
if (overlap <= 0) {
|
|
return rect;
|
|
}
|
|
return Rect.fromLTRB(
|
|
x == 0 ? rect.left : math.max(bounds.left, rect.left - overlap),
|
|
y == 0 ? rect.top : math.max(bounds.top, rect.top - overlap),
|
|
x == 2 ? rect.right : math.min(bounds.right, rect.right + overlap),
|
|
y == 2 ? rect.bottom : math.min(bounds.bottom, rect.bottom + overlap),
|
|
);
|
|
}
|
|
|
|
class RuntimeComponent extends PositionComponent
|
|
with HasVisibility, TapCallbacks {
|
|
RuntimeComponent({
|
|
required RuntimeNode node,
|
|
required GameResourceManager resources,
|
|
required this.onNodeTap,
|
|
}) : _node = node,
|
|
_resources = resources {
|
|
priority = node.layer;
|
|
_applyBase(node);
|
|
_syncImage(node);
|
|
_syncSpine(node);
|
|
_syncParticle(node);
|
|
}
|
|
|
|
RuntimeNode get node => _node;
|
|
|
|
final GameResourceManager _resources;
|
|
final void Function(RuntimeNode node, Vector2 localPosition) onNodeTap;
|
|
RuntimeNode _node;
|
|
TextComponent? _textComponent;
|
|
ui.Image? _image;
|
|
String? _loadedAsset;
|
|
int? _loadedGeneration;
|
|
int _imageLoadToken = 0;
|
|
spine.SpineComponent? _spineComponent;
|
|
ParticleSystemComponent? _particleComponent;
|
|
String? _particleSignature;
|
|
String? _loadedSpineAsset;
|
|
int? _loadedSpineGeneration;
|
|
int _spineLoadToken = 0;
|
|
String? _appliedSpineAnimation;
|
|
bool? _appliedSpineLoop;
|
|
String? _pendingSpineAnimation;
|
|
int _pendingSpineTrack = 0;
|
|
bool _pendingSpineLoop = true;
|
|
bool _pendingSpineQueue = false;
|
|
double _pendingSpineDelay = 0;
|
|
String? _appliedSpineSkin;
|
|
double? _runtimeAlpha;
|
|
double _parentScrollX = 0;
|
|
double _parentScrollY = 0;
|
|
double _parentContentOffsetX = 0;
|
|
double _parentContentOffsetY = 0;
|
|
bool _viewportCulled = false;
|
|
bool _pressed = false;
|
|
double _inheritedAlpha = 1;
|
|
|
|
double get localAlpha => _runtimeAlpha ?? _node.alpha;
|
|
|
|
double get renderAlpha => (_inheritedAlpha * localAlpha).clamp(0.0, 1.0);
|
|
|
|
void setRuntimeAlpha(double value) {
|
|
final next = value.clamp(0, 1).toDouble();
|
|
if (_runtimeAlpha == next) {
|
|
return;
|
|
}
|
|
_runtimeAlpha = next;
|
|
_refreshInheritedAlphaSubtree();
|
|
}
|
|
|
|
void setInheritedAlpha(double value) {
|
|
final next = value.clamp(0, 1).toDouble();
|
|
if (_inheritedAlpha == next) {
|
|
return;
|
|
}
|
|
_inheritedAlpha = next;
|
|
_refreshInheritedAlphaSubtree();
|
|
}
|
|
|
|
void _refreshInheritedAlphaSubtree() {
|
|
_syncTextStyle(_node);
|
|
for (final child in children.whereType<RuntimeComponent>()) {
|
|
child.setInheritedAlpha(renderAlpha);
|
|
}
|
|
}
|
|
|
|
void setParentScroll({
|
|
double x = 0,
|
|
double y = 0,
|
|
double contentOffsetX = 0,
|
|
double contentOffsetY = 0,
|
|
}) {
|
|
_parentScrollX = x < 0 ? 0 : x;
|
|
_parentScrollY = y < 0 ? 0 : y;
|
|
_parentContentOffsetX = contentOffsetX < 0 ? 0 : contentOffsetX;
|
|
_parentContentOffsetY = contentOffsetY < 0 ? 0 : contentOffsetY;
|
|
_syncPosition();
|
|
}
|
|
|
|
void setViewportCulled(bool value) {
|
|
_viewportCulled = value;
|
|
_syncVisibility();
|
|
}
|
|
|
|
void updateNode(RuntimeNode node) {
|
|
_node = node;
|
|
priority = node.layer;
|
|
_applyBase(node);
|
|
_syncImage(node);
|
|
_syncSpine(node);
|
|
_syncParticle(node);
|
|
_refreshInheritedAlphaSubtree();
|
|
}
|
|
|
|
bool containsVisualPoint(Vector2 point) {
|
|
if (!_node.visible) {
|
|
return false;
|
|
}
|
|
final local = absoluteToLocal(point);
|
|
return local.x >= 0 &&
|
|
local.y >= 0 &&
|
|
local.x <= size.x &&
|
|
local.y <= size.y;
|
|
}
|
|
|
|
@override
|
|
bool containsLocalPoint(Vector2 point) {
|
|
if (!_node.visible || !_node.interactive) {
|
|
return false;
|
|
}
|
|
return point.x >= 0 &&
|
|
point.y >= 0 &&
|
|
point.x <= size.x &&
|
|
point.y <= size.y;
|
|
}
|
|
|
|
@override
|
|
void onTapDown(TapDownEvent event) {
|
|
if (!_node.visible || !_node.interactive) {
|
|
return;
|
|
}
|
|
if (_node.type == RuntimeNodeType.button) {
|
|
_pressed = true;
|
|
_syncImage(_node);
|
|
}
|
|
onNodeTap(_node, event.localPosition);
|
|
}
|
|
|
|
@override
|
|
void onTapUp(TapUpEvent event) {
|
|
_releasePressedState();
|
|
}
|
|
|
|
@override
|
|
void onTapCancel(TapCancelEvent event) {
|
|
_releasePressedState();
|
|
}
|
|
|
|
void _releasePressedState() {
|
|
if (!_pressed) {
|
|
return;
|
|
}
|
|
_pressed = false;
|
|
_syncImage(_node);
|
|
}
|
|
|
|
@override
|
|
void renderTree(Canvas canvas) {
|
|
if (_node.type != RuntimeNodeType.listView) {
|
|
super.renderTree(canvas);
|
|
return;
|
|
}
|
|
|
|
if (!isVisible) {
|
|
return;
|
|
}
|
|
|
|
final paint = Paint()
|
|
..color = composeRuntimeColorAlpha(
|
|
_node.color ?? _defaultColor(),
|
|
renderAlpha,
|
|
);
|
|
|
|
canvas.save();
|
|
canvas.transform(Float64List.fromList(transform.transformMatrix.storage));
|
|
_renderBoxOrImage(canvas, paint);
|
|
canvas.save();
|
|
canvas.clipRRect(_listViewClipRRect());
|
|
for (final child in children) {
|
|
child.renderTree(canvas);
|
|
}
|
|
canvas.restore();
|
|
_renderListViewScrollbar(canvas);
|
|
if (debugMode) {
|
|
renderDebugMode(canvas);
|
|
}
|
|
canvas.restore();
|
|
}
|
|
|
|
@override
|
|
void render(Canvas canvas) {
|
|
final paint = Paint()
|
|
..color = composeRuntimeColorAlpha(
|
|
_node.color ?? _defaultColor(),
|
|
renderAlpha,
|
|
);
|
|
|
|
switch (_node.type) {
|
|
case RuntimeNodeType.circle:
|
|
canvas.drawCircle(Offset(size.x / 2, size.y / 2), size.x / 2, paint);
|
|
break;
|
|
case RuntimeNodeType.line:
|
|
_renderLine(canvas, paint);
|
|
break;
|
|
case RuntimeNodeType.progress:
|
|
_renderProgress(canvas, paint);
|
|
break;
|
|
case RuntimeNodeType.listView:
|
|
_renderListView(canvas, paint);
|
|
break;
|
|
case RuntimeNodeType.panel:
|
|
case RuntimeNodeType.button:
|
|
case RuntimeNodeType.rect:
|
|
case RuntimeNodeType.sprite:
|
|
case RuntimeNodeType.image:
|
|
_renderBoxOrImage(canvas, paint);
|
|
break;
|
|
case RuntimeNodeType.spine:
|
|
_renderSpinePlaceholder(canvas, paint);
|
|
break;
|
|
case RuntimeNodeType.particle:
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void _renderLine(Canvas canvas, Paint paint) {
|
|
final stroke = _node.strokeWidth ?? 2;
|
|
canvas.drawLine(
|
|
Offset.zero,
|
|
Offset(size.x, size.y),
|
|
paint
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = stroke,
|
|
);
|
|
}
|
|
|
|
void _renderProgress(Canvas canvas, Paint paint) {
|
|
final rect = Rect.fromLTWH(0, 0, size.x, size.y);
|
|
final radius = Radius.circular(_node.radius ?? 4);
|
|
final backgroundPaint = Paint()
|
|
..color = composeRuntimeColorAlpha(const Color(0x33475569), renderAlpha);
|
|
canvas.drawRRect(RRect.fromRectAndRadius(rect, radius), backgroundPaint);
|
|
|
|
final value = _node.value ?? 0;
|
|
if (value <= 0) {
|
|
return;
|
|
}
|
|
final fillRect = Rect.fromLTWH(0, 0, size.x * value, size.y);
|
|
canvas.drawRRect(RRect.fromRectAndRadius(fillRect, radius), paint);
|
|
}
|
|
|
|
void _renderSpinePlaceholder(Canvas canvas, Paint paint) {
|
|
if (_spineComponent != null) {
|
|
return;
|
|
}
|
|
final rect = Rect.fromLTWH(0, 0, size.x, size.y);
|
|
canvas.drawRRect(
|
|
RRect.fromRectAndRadius(rect, const Radius.circular(4)),
|
|
paint,
|
|
);
|
|
}
|
|
|
|
void _renderListView(Canvas canvas, Paint paint) {
|
|
_renderBoxOrImage(canvas, paint);
|
|
}
|
|
|
|
double get _scrollbarThickness =>
|
|
(_node.scrollbarThickness ?? 5).clamp(1.0, 16.0).toDouble();
|
|
|
|
double get _scrollbarGutter => _scrollbarThickness + 8;
|
|
|
|
({bool horizontal, bool vertical}) _listViewScrollbarVisibility() {
|
|
if (!_node.scrollbarVisible) {
|
|
return (horizontal: false, vertical: false);
|
|
}
|
|
final left = _node.paddingLeft.clamp(0.0, size.x).toDouble();
|
|
final top = _node.paddingTop.clamp(0.0, size.y).toDouble();
|
|
final right = _node.paddingRight.clamp(0.0, size.x).toDouble();
|
|
final bottom = _node.paddingBottom.clamp(0.0, size.y).toDouble();
|
|
var viewportWidth = size.x - left - right;
|
|
var viewportHeight = size.y - top - bottom;
|
|
var vertical =
|
|
(_node.contentHeight ?? 0) > viewportHeight && viewportHeight > 0;
|
|
var horizontal =
|
|
(_node.contentWidth ?? 0) > viewportWidth && viewportWidth > 0;
|
|
for (var i = 0; i < 2; i += 1) {
|
|
viewportWidth = size.x - left - right - (vertical ? _scrollbarGutter : 0);
|
|
viewportHeight =
|
|
size.y - top - bottom - (horizontal ? _scrollbarGutter : 0);
|
|
vertical =
|
|
(_node.contentHeight ?? 0) > viewportHeight && viewportHeight > 0;
|
|
horizontal =
|
|
(_node.contentWidth ?? 0) > viewportWidth && viewportWidth > 0;
|
|
}
|
|
return (horizontal: horizontal, vertical: vertical);
|
|
}
|
|
|
|
Rect _listViewContentRect() {
|
|
final scrollbars = _listViewScrollbarVisibility();
|
|
final left = _node.paddingLeft.clamp(0.0, size.x).toDouble();
|
|
final top = _node.paddingTop.clamp(0.0, size.y).toDouble();
|
|
final rightInset =
|
|
(_node.paddingRight + (scrollbars.vertical ? _scrollbarGutter : 0))
|
|
.clamp(0.0, size.x)
|
|
.toDouble();
|
|
final bottomInset =
|
|
(_node.paddingBottom + (scrollbars.horizontal ? _scrollbarGutter : 0))
|
|
.clamp(0.0, size.y)
|
|
.toDouble();
|
|
final width = (size.x - left - rightInset).clamp(0.0, size.x).toDouble();
|
|
final height = (size.y - top - bottomInset).clamp(0.0, size.y).toDouble();
|
|
return Rect.fromLTWH(left, top, width, height);
|
|
}
|
|
|
|
RRect _listViewClipRRect() {
|
|
final radius = Radius.circular((_node.radius ?? 0).clamp(0.0, 10000.0));
|
|
return RRect.fromRectAndRadius(_listViewContentRect(), radius);
|
|
}
|
|
|
|
Vector2 listViewContentOffset() {
|
|
if (_node.type != RuntimeNodeType.listView) {
|
|
return Vector2.zero();
|
|
}
|
|
final rect = _listViewContentRect();
|
|
return Vector2(rect.left, rect.top);
|
|
}
|
|
|
|
Vector2 listViewContentViewport() {
|
|
if (_node.type != RuntimeNodeType.listView) {
|
|
return Vector2.zero();
|
|
}
|
|
final rect = _listViewContentRect();
|
|
return Vector2(rect.width, rect.height);
|
|
}
|
|
|
|
void _renderListViewScrollbar(Canvas canvas) {
|
|
if (!_node.scrollbarVisible) {
|
|
return;
|
|
}
|
|
final thickness = _scrollbarThickness;
|
|
final contentRect = _listViewContentRect();
|
|
final scrollbars = _listViewScrollbarVisibility();
|
|
final trackPaint = Paint()
|
|
..color = composeRuntimeColorAlpha(
|
|
_node.scrollbarTrackColor ?? const Color(0x33475569),
|
|
renderAlpha,
|
|
);
|
|
final thumbPaint = Paint()
|
|
..color = composeRuntimeColorAlpha(
|
|
_node.scrollbarThumbColor ?? const Color(0xaa94a3b8),
|
|
renderAlpha,
|
|
);
|
|
|
|
final contentHeight = _node.contentHeight;
|
|
if (scrollbars.vertical &&
|
|
contentHeight != null &&
|
|
contentRect.height > 0) {
|
|
final trackHeight = (contentRect.height - 12).clamp(
|
|
4.0,
|
|
contentRect.height,
|
|
);
|
|
final maxScroll = contentHeight - contentRect.height;
|
|
final thumbHeight =
|
|
(contentRect.height * contentRect.height / contentHeight).clamp(
|
|
18.0,
|
|
contentRect.height,
|
|
);
|
|
final thumbY = maxScroll <= 0
|
|
? 0.0
|
|
: (_node.scrollY / maxScroll) * (contentRect.height - thumbHeight);
|
|
canvas.drawRRect(
|
|
RRect.fromRectAndRadius(
|
|
Rect.fromLTWH(size.x - thickness - 2, 6, thickness, trackHeight),
|
|
Radius.circular(thickness / 2),
|
|
),
|
|
trackPaint,
|
|
);
|
|
canvas.drawRRect(
|
|
RRect.fromRectAndRadius(
|
|
Rect.fromLTWH(
|
|
size.x - thickness - 2,
|
|
thumbY + 6,
|
|
thickness,
|
|
(thumbHeight - 12).clamp(4.0, contentRect.height),
|
|
),
|
|
Radius.circular(thickness / 2),
|
|
),
|
|
thumbPaint,
|
|
);
|
|
}
|
|
|
|
final contentWidth = _node.contentWidth;
|
|
if (scrollbars.horizontal &&
|
|
contentWidth != null &&
|
|
contentRect.width > 0) {
|
|
final trackWidth = (contentRect.width - 12).clamp(4.0, contentRect.width);
|
|
final maxScroll = contentWidth - contentRect.width;
|
|
final thumbWidth = (contentRect.width * contentRect.width / contentWidth)
|
|
.clamp(18.0, contentRect.width);
|
|
final thumbX = maxScroll <= 0
|
|
? 0.0
|
|
: (_node.scrollX / maxScroll) * (contentRect.width - thumbWidth);
|
|
canvas.drawRRect(
|
|
RRect.fromRectAndRadius(
|
|
Rect.fromLTWH(6, size.y - thickness - 2, trackWidth, thickness),
|
|
Radius.circular(thickness / 2),
|
|
),
|
|
trackPaint,
|
|
);
|
|
canvas.drawRRect(
|
|
RRect.fromRectAndRadius(
|
|
Rect.fromLTWH(
|
|
thumbX + 6,
|
|
size.y - thickness - 2,
|
|
(thumbWidth - 12).clamp(4.0, contentRect.width),
|
|
thickness,
|
|
),
|
|
Radius.circular(thickness / 2),
|
|
),
|
|
thumbPaint,
|
|
);
|
|
}
|
|
}
|
|
|
|
void _renderBoxOrImage(Canvas canvas, Paint paint) {
|
|
final image = _image;
|
|
final rect = Rect.fromLTWH(0, 0, size.x, size.y);
|
|
if (image != null &&
|
|
(_node.type == RuntimeNodeType.sprite ||
|
|
_node.type == RuntimeNodeType.image ||
|
|
_node.type == RuntimeNodeType.button)) {
|
|
final imagePaint = Paint()
|
|
..color = composeRuntimeColorAlpha(Colors.white, renderAlpha);
|
|
final source = _imageSourceRect(image, _currentImageFrame(_node));
|
|
if (_usesNineSlice(source, rect)) {
|
|
_drawNineSliceImage(
|
|
canvas,
|
|
image,
|
|
source,
|
|
rect,
|
|
imagePaint..filterQuality = FilterQuality.none,
|
|
);
|
|
} else {
|
|
canvas.drawImageRect(image, source, rect, imagePaint);
|
|
}
|
|
return;
|
|
}
|
|
|
|
final radius =
|
|
_node.radius ?? (_node.type == RuntimeNodeType.button ? 12.0 : 4.0);
|
|
canvas.drawRRect(
|
|
RRect.fromRectAndRadius(rect, Radius.circular(radius)),
|
|
paint,
|
|
);
|
|
}
|
|
|
|
Rect _imageSourceRect(ui.Image image, String? frameName) {
|
|
final frame = _loadedAsset == null
|
|
? null
|
|
: _resources.textureFrame(_loadedAsset!, frameName);
|
|
if (frame != null) {
|
|
return frame.rect;
|
|
}
|
|
return runtimeImageSourceRect(
|
|
imageWidth: image.width.toDouble(),
|
|
imageHeight: image.height.toDouble(),
|
|
sourceX: _node.sourceX,
|
|
sourceY: _node.sourceY,
|
|
sourceWidth: _node.sourceWidth,
|
|
sourceHeight: _node.sourceHeight,
|
|
);
|
|
}
|
|
|
|
bool _usesNineSlice(Rect source, Rect destination) {
|
|
if (source.width <= 0 ||
|
|
source.height <= 0 ||
|
|
destination.width <= 0 ||
|
|
destination.height <= 0) {
|
|
return false;
|
|
}
|
|
return (_node.sliceLeft ?? 0) > 0 ||
|
|
(_node.sliceTop ?? 0) > 0 ||
|
|
(_node.sliceRight ?? 0) > 0 ||
|
|
(_node.sliceBottom ?? 0) > 0;
|
|
}
|
|
|
|
void _drawNineSliceImage(
|
|
Canvas canvas,
|
|
ui.Image image,
|
|
Rect source,
|
|
Rect destination,
|
|
Paint paint,
|
|
) {
|
|
final parts = runtimeNineSliceRects(
|
|
source: source,
|
|
destination: destination,
|
|
destinationOverlap: 1,
|
|
sourceInset: 0.5,
|
|
sliceLeft: _node.sliceLeft ?? 0,
|
|
sliceTop: _node.sliceTop ?? 0,
|
|
sliceRight: _node.sliceRight ?? 0,
|
|
sliceBottom: _node.sliceBottom ?? 0,
|
|
);
|
|
for (final part in parts) {
|
|
canvas.drawImageRect(image, part.source, part.destination, paint);
|
|
}
|
|
}
|
|
|
|
void _applyBase(RuntimeNode node) {
|
|
_syncVisibility();
|
|
size = Vector2(node.width ?? 40, node.height ?? 40);
|
|
_syncPosition();
|
|
scale = Vector2.all(node.scale);
|
|
angle = node.rotation;
|
|
anchor = _anchorFromString(node.anchor);
|
|
_syncText(node);
|
|
_syncSpineLayout();
|
|
}
|
|
|
|
void _syncVisibility() {
|
|
isVisible = _node.visible && !_viewportCulled;
|
|
}
|
|
|
|
void _syncPosition() {
|
|
position = Vector2(
|
|
_parentContentOffsetX + _node.x - _parentScrollX,
|
|
_parentContentOffsetY + _node.y - _parentScrollY,
|
|
);
|
|
}
|
|
|
|
void _syncImage(RuntimeNode node) {
|
|
if (node.type != RuntimeNodeType.sprite &&
|
|
node.type != RuntimeNodeType.image &&
|
|
node.type != RuntimeNodeType.button) {
|
|
_imageLoadToken++;
|
|
_releaseLoadedImage();
|
|
return;
|
|
}
|
|
final asset = _currentImageAsset(node);
|
|
if (asset == null || asset.isEmpty) {
|
|
_imageLoadToken++;
|
|
_releaseLoadedImage();
|
|
return;
|
|
}
|
|
|
|
final requestedGeneration = _resources.generation;
|
|
if (asset == _loadedAsset && requestedGeneration == _loadedGeneration) {
|
|
return;
|
|
}
|
|
|
|
final requestToken = ++_imageLoadToken;
|
|
_releaseLoadedImage();
|
|
_loadedAsset = asset;
|
|
_loadedGeneration = requestedGeneration;
|
|
_resources.loadImage(asset, retain: true).then((image) {
|
|
if (_imageLoadToken != requestToken) {
|
|
_releaseRetainedImage(asset, requestedGeneration, image);
|
|
return;
|
|
}
|
|
if (_loadedAsset != asset || _loadedGeneration != requestedGeneration) {
|
|
_releaseRetainedImage(asset, requestedGeneration, image);
|
|
return;
|
|
}
|
|
if (_resources.generation != requestedGeneration) {
|
|
_releaseRetainedImage(asset, requestedGeneration, image);
|
|
return;
|
|
}
|
|
_image = image;
|
|
});
|
|
}
|
|
|
|
String? _currentImageAsset(RuntimeNode node) {
|
|
if (node.type != RuntimeNodeType.button) {
|
|
return node.asset;
|
|
}
|
|
if (!node.interactive && node.disabledAsset != null) {
|
|
return node.disabledAsset;
|
|
}
|
|
if (_pressed && node.pressedAsset != null) {
|
|
return node.pressedAsset;
|
|
}
|
|
return node.asset;
|
|
}
|
|
|
|
String? _currentImageFrame(RuntimeNode node) {
|
|
if (node.type != RuntimeNodeType.button) {
|
|
return node.frame;
|
|
}
|
|
if (!node.interactive && node.disabledFrame != null) {
|
|
return node.disabledFrame;
|
|
}
|
|
if (_pressed && node.pressedFrame != null) {
|
|
return node.pressedFrame;
|
|
}
|
|
return node.frame;
|
|
}
|
|
|
|
void _releaseRetainedImage(String asset, int generation, ui.Image? image) {
|
|
if (image == null) {
|
|
return;
|
|
}
|
|
_resources.releaseImage(asset, generation: generation);
|
|
}
|
|
|
|
void _syncParticle(RuntimeNode node) {
|
|
if (node.type != RuntimeNodeType.particle) {
|
|
_releaseParticle();
|
|
return;
|
|
}
|
|
|
|
final signature = _particleNodeSignature(node);
|
|
if (signature == _particleSignature && _particleComponent != null) {
|
|
_syncParticleLayout();
|
|
return;
|
|
}
|
|
|
|
_releaseParticle();
|
|
_particleSignature = signature;
|
|
final component = ParticleSystemComponent(
|
|
particle: _buildParticle(node),
|
|
position: Vector2.zero(),
|
|
size: size,
|
|
anchor: Anchor.topLeft,
|
|
priority: priority + 1,
|
|
);
|
|
_particleComponent = component;
|
|
add(component);
|
|
}
|
|
|
|
String _particleNodeSignature(RuntimeNode node) {
|
|
return [
|
|
node.preset,
|
|
node.count,
|
|
node.duration,
|
|
node.color,
|
|
node.colorTo,
|
|
node.radius,
|
|
node.radiusTo,
|
|
node.speedMin,
|
|
node.speedMax,
|
|
node.gravityX,
|
|
node.gravityY,
|
|
node.spread,
|
|
node.autoRemove,
|
|
node.fadeOut,
|
|
node.width,
|
|
node.height,
|
|
].join('|');
|
|
}
|
|
|
|
Particle _buildParticle(RuntimeNode node) {
|
|
final preset = node.preset ?? RuntimeParticlePresetValue.burst;
|
|
final count = (node.count ?? _defaultParticleCount(preset)).clamp(1, 300);
|
|
final duration = node.duration ?? _defaultParticleDuration(preset);
|
|
return Particle.generate(
|
|
count: count,
|
|
lifespan: node.autoRemove ? duration : 86400,
|
|
generator: (index) => ComputedParticle(
|
|
lifespan: node.autoRemove ? duration : 86400,
|
|
renderer: (canvas, particle) => _renderComputedParticle(
|
|
canvas,
|
|
particle,
|
|
node,
|
|
preset,
|
|
index,
|
|
count,
|
|
duration,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
int _defaultParticleCount(String preset) {
|
|
return switch (preset) {
|
|
RuntimeParticlePresetValue.snow => 80,
|
|
RuntimeParticlePresetValue.confetti => 72,
|
|
RuntimeParticlePresetValue.trail => 16,
|
|
_ => 36,
|
|
};
|
|
}
|
|
|
|
double _defaultParticleDuration(String preset) {
|
|
return switch (preset) {
|
|
RuntimeParticlePresetValue.snow => 8,
|
|
RuntimeParticlePresetValue.confetti => 1.2,
|
|
RuntimeParticlePresetValue.trail => 0.35,
|
|
_ => 0.55,
|
|
};
|
|
}
|
|
|
|
void _renderComputedParticle(
|
|
Canvas canvas,
|
|
Particle particle,
|
|
RuntimeNode node,
|
|
String preset,
|
|
int index,
|
|
int count,
|
|
double duration,
|
|
) {
|
|
final config = _particleConfig(node, preset, index, count);
|
|
final progress = node.autoRemove
|
|
? particle.progress
|
|
: ((particle.progress * 86400 / duration) % 1.0);
|
|
final x =
|
|
config.start.x +
|
|
config.velocity.x * progress * duration +
|
|
0.5 * config.gravity.x * progress * progress * duration * duration;
|
|
final y =
|
|
config.start.y +
|
|
config.velocity.y * progress * duration +
|
|
0.5 * config.gravity.y * progress * progress * duration * duration;
|
|
final radius = config.radius + (config.radiusTo - config.radius) * progress;
|
|
if (radius <= 0) {
|
|
return;
|
|
}
|
|
final color =
|
|
Color.lerp(config.color, config.colorTo, progress) ?? config.color;
|
|
final alpha = node.fadeOut ? (1 - progress).clamp(0.0, 1.0) : 1.0;
|
|
final paint = Paint()
|
|
..color = color.withValues(alpha: color.a * renderAlpha * alpha);
|
|
canvas.drawCircle(Offset(x, y), radius, paint);
|
|
}
|
|
|
|
_ParticleConfig _particleConfig(
|
|
RuntimeNode node,
|
|
String preset,
|
|
int index,
|
|
int count,
|
|
) {
|
|
final width = node.width ?? size.x;
|
|
final height = node.height ?? size.y;
|
|
final center = Vector2(width / 2, height / 2);
|
|
final baseColor = node.color ?? _defaultParticleColor(preset);
|
|
final colorTo = node.colorTo ?? baseColor.withValues(alpha: 0);
|
|
final radius = node.radius ?? _defaultParticleRadius(preset);
|
|
final radiusTo = node.radiusTo ?? 0;
|
|
final speedMin = node.speedMin ?? _defaultParticleSpeedMin(preset);
|
|
final speedMax = node.speedMax ?? _defaultParticleSpeedMax(preset);
|
|
final gravity = Vector2(
|
|
node.gravityX ?? 0,
|
|
node.gravityY ?? _defaultParticleGravityY(preset),
|
|
);
|
|
|
|
if (preset == RuntimeParticlePresetValue.snow) {
|
|
final x = width * _stableUnit(index, 3);
|
|
final y = height * _stableUnit(index, 7);
|
|
final drift = -12 + 24 * _stableUnit(index, 11);
|
|
final fall = speedMin + (speedMax - speedMin) * _stableUnit(index, 13);
|
|
return _ParticleConfig(
|
|
start: Vector2(x, y),
|
|
velocity: Vector2(drift, fall),
|
|
gravity: gravity,
|
|
color: baseColor,
|
|
colorTo: colorTo,
|
|
radius: radius * (0.6 + _stableUnit(index, 17)),
|
|
radiusTo: radiusTo,
|
|
);
|
|
}
|
|
|
|
if (preset == RuntimeParticlePresetValue.trail) {
|
|
final angle = -math.pi / 2 + (_stableUnit(index, 23) - 0.5) * math.pi;
|
|
final speed = speedMin + _stableUnit(index, 29) * (speedMax - speedMin);
|
|
return _ParticleConfig(
|
|
start: center,
|
|
velocity: Vector2(math.cos(angle), math.sin(angle)) * speed,
|
|
gravity: gravity,
|
|
color: baseColor,
|
|
colorTo: colorTo,
|
|
radius: radius,
|
|
radiusTo: radiusTo,
|
|
);
|
|
}
|
|
|
|
final spread = (node.spread ?? 360) * math.pi / 180;
|
|
final baseAngle = preset == RuntimeParticlePresetValue.confetti
|
|
? -math.pi / 2
|
|
: 0.0;
|
|
final angle =
|
|
baseAngle -
|
|
spread / 2 +
|
|
spread * (count <= 1 ? 0 : index / (count - 1));
|
|
final speed = speedMin + _stableUnit(index, 31) * (speedMax - speedMin);
|
|
return _ParticleConfig(
|
|
start: center,
|
|
velocity: Vector2(math.cos(angle), math.sin(angle)) * speed,
|
|
gravity: gravity,
|
|
color: baseColor,
|
|
colorTo: node.colorTo ?? _defaultParticleColorTo(preset, baseColor),
|
|
radius: radius,
|
|
radiusTo: radiusTo,
|
|
);
|
|
}
|
|
|
|
double _stableUnit(int index, int salt) {
|
|
final value = math.sin((index + 1) * (salt + 17) * 12.9898) * 43758.5453;
|
|
return value - value.floorToDouble();
|
|
}
|
|
|
|
Color _defaultParticleColor(String preset) {
|
|
return switch (preset) {
|
|
RuntimeParticlePresetValue.snow => const Color(0xccffffff),
|
|
RuntimeParticlePresetValue.confetti => const Color(0xffff4d6d),
|
|
RuntimeParticlePresetValue.trail => const Color(0xff38bdf8),
|
|
_ => const Color(0xffffcc33),
|
|
};
|
|
}
|
|
|
|
Color _defaultParticleColorTo(String preset, Color base) {
|
|
return switch (preset) {
|
|
RuntimeParticlePresetValue.confetti => const Color(0xfffacc15),
|
|
_ => base.withValues(alpha: 0),
|
|
};
|
|
}
|
|
|
|
double _defaultParticleRadius(String preset) {
|
|
return switch (preset) {
|
|
RuntimeParticlePresetValue.snow => 1.6,
|
|
RuntimeParticlePresetValue.confetti => 2.6,
|
|
RuntimeParticlePresetValue.trail => 2.0,
|
|
_ => 2.4,
|
|
};
|
|
}
|
|
|
|
double _defaultParticleSpeedMin(String preset) {
|
|
return switch (preset) {
|
|
RuntimeParticlePresetValue.snow => 12,
|
|
RuntimeParticlePresetValue.confetti => 120,
|
|
RuntimeParticlePresetValue.trail => 20,
|
|
_ => 80,
|
|
};
|
|
}
|
|
|
|
double _defaultParticleSpeedMax(String preset) {
|
|
return switch (preset) {
|
|
RuntimeParticlePresetValue.snow => 28,
|
|
RuntimeParticlePresetValue.confetti => 260,
|
|
RuntimeParticlePresetValue.trail => 70,
|
|
_ => 220,
|
|
};
|
|
}
|
|
|
|
double _defaultParticleGravityY(String preset) {
|
|
return switch (preset) {
|
|
RuntimeParticlePresetValue.snow => 4,
|
|
RuntimeParticlePresetValue.confetti => 240,
|
|
RuntimeParticlePresetValue.trail => 0,
|
|
_ => 80,
|
|
};
|
|
}
|
|
|
|
void _syncParticleLayout() {
|
|
final particle = _particleComponent;
|
|
if (particle == null) {
|
|
return;
|
|
}
|
|
particle
|
|
..position = Vector2.zero()
|
|
..size = size
|
|
..anchor = Anchor.topLeft
|
|
..priority = priority + 1;
|
|
}
|
|
|
|
void _releaseParticle() {
|
|
_particleComponent?.removeFromParent();
|
|
_particleComponent = null;
|
|
_particleSignature = null;
|
|
}
|
|
|
|
void _syncSpine(RuntimeNode node) {
|
|
if (node.type != RuntimeNodeType.spine) {
|
|
_spineLoadToken++;
|
|
_releaseLoadedSpine();
|
|
return;
|
|
}
|
|
final asset = node.asset;
|
|
if (asset == null || asset.isEmpty) {
|
|
_spineLoadToken++;
|
|
_releaseLoadedSpine();
|
|
return;
|
|
}
|
|
|
|
final requestedGeneration = _resources.generation;
|
|
if (asset == _loadedSpineAsset &&
|
|
requestedGeneration == _loadedSpineGeneration) {
|
|
_syncSpineLayout();
|
|
_syncSpineSkin(node);
|
|
_syncSpineAnimation(node);
|
|
return;
|
|
}
|
|
|
|
final requestToken = ++_spineLoadToken;
|
|
_releaseLoadedSpine();
|
|
_loadedSpineAsset = asset;
|
|
_loadedSpineGeneration = requestedGeneration;
|
|
_resources.createSpineComponent(asset).then((spine) {
|
|
if (_spineLoadToken != requestToken) {
|
|
spine?.dispose();
|
|
return;
|
|
}
|
|
if (_loadedSpineAsset != asset ||
|
|
_loadedSpineGeneration != requestedGeneration) {
|
|
spine?.dispose();
|
|
return;
|
|
}
|
|
if (_resources.generation != requestedGeneration) {
|
|
spine?.dispose();
|
|
return;
|
|
}
|
|
if (spine == null) {
|
|
return;
|
|
}
|
|
_spineComponent = spine;
|
|
add(spine);
|
|
_syncSpineLayout();
|
|
_syncSpineSkin(_node);
|
|
_syncSpineAnimation(_node);
|
|
});
|
|
}
|
|
|
|
bool playSpineAnimation(
|
|
String animation, {
|
|
int track = 0,
|
|
bool loop = true,
|
|
bool queue = false,
|
|
double delay = 0,
|
|
}) {
|
|
final spine = _spineComponent;
|
|
if (spine == null) {
|
|
if (_node.type != RuntimeNodeType.spine) {
|
|
return false;
|
|
}
|
|
_pendingSpineAnimation = animation;
|
|
_pendingSpineTrack = track;
|
|
_pendingSpineLoop = loop;
|
|
_pendingSpineQueue = queue;
|
|
_pendingSpineDelay = delay;
|
|
return true;
|
|
}
|
|
if (queue) {
|
|
spine.animationState.addAnimation(track, animation, loop, delay);
|
|
} else {
|
|
spine.animationState.setAnimation(track, animation, loop);
|
|
}
|
|
_appliedSpineAnimation = animation;
|
|
_appliedSpineLoop = loop;
|
|
_pendingSpineAnimation = null;
|
|
return true;
|
|
}
|
|
|
|
void _syncSpineLayout() {
|
|
final spine = _spineComponent;
|
|
if (spine == null) {
|
|
return;
|
|
}
|
|
spine
|
|
..position = Vector2.zero()
|
|
..anchor = Anchor.topLeft
|
|
..priority = priority;
|
|
final width = _node.width;
|
|
final height = _node.height;
|
|
if (width != null &&
|
|
height != null &&
|
|
spine.size.x > 0 &&
|
|
spine.size.y > 0) {
|
|
spine.scale = Vector2(width / spine.size.x, height / spine.size.y);
|
|
}
|
|
}
|
|
|
|
void _syncSpineSkin(RuntimeNode node) {
|
|
final skin = node.skin;
|
|
if (skin == null || skin == _appliedSpineSkin) {
|
|
return;
|
|
}
|
|
_spineComponent?.skeleton.setSkin(skin);
|
|
_appliedSpineSkin = skin;
|
|
}
|
|
|
|
void _syncSpineAnimation(RuntimeNode node) {
|
|
final pendingAnimation = _pendingSpineAnimation;
|
|
if (pendingAnimation != null) {
|
|
playSpineAnimation(
|
|
pendingAnimation,
|
|
track: _pendingSpineTrack,
|
|
loop: _pendingSpineLoop,
|
|
queue: _pendingSpineQueue,
|
|
delay: _pendingSpineDelay,
|
|
);
|
|
return;
|
|
}
|
|
|
|
final animation = node.animation;
|
|
if (animation == null) {
|
|
return;
|
|
}
|
|
if (animation == _appliedSpineAnimation && node.loop == _appliedSpineLoop) {
|
|
return;
|
|
}
|
|
playSpineAnimation(animation, loop: node.loop);
|
|
}
|
|
|
|
void _releaseLoadedSpine() {
|
|
final spine = _spineComponent;
|
|
if (spine != null) {
|
|
spine.removeFromParent();
|
|
spine.dispose();
|
|
}
|
|
_spineComponent = null;
|
|
_loadedSpineAsset = null;
|
|
_loadedSpineGeneration = null;
|
|
_appliedSpineAnimation = null;
|
|
_appliedSpineLoop = null;
|
|
_pendingSpineAnimation = null;
|
|
_pendingSpineTrack = 0;
|
|
_pendingSpineLoop = true;
|
|
_pendingSpineQueue = false;
|
|
_pendingSpineDelay = 0;
|
|
_appliedSpineSkin = null;
|
|
}
|
|
|
|
void _releaseLoadedImage() {
|
|
final asset = _loadedAsset;
|
|
final generation = _loadedGeneration;
|
|
if (_image != null && asset != null && generation != null) {
|
|
_resources.releaseImage(asset, generation: generation);
|
|
}
|
|
_image = null;
|
|
_loadedAsset = null;
|
|
_loadedGeneration = null;
|
|
}
|
|
|
|
void _syncText(RuntimeNode node) {
|
|
final label = node.text;
|
|
if (label == null && node.type != RuntimeNodeType.text) {
|
|
_textComponent?.removeFromParent();
|
|
_textComponent = null;
|
|
return;
|
|
}
|
|
|
|
final text = label ?? '';
|
|
final component = _textComponent;
|
|
if (component == null) {
|
|
_textComponent = TextComponent(
|
|
text: text,
|
|
textRenderer: TextPaint(style: _textStyle(node)),
|
|
anchor: _textAnchor(node),
|
|
position: _textPosition(node),
|
|
priority: priority + 1,
|
|
);
|
|
add(_textComponent!);
|
|
return;
|
|
}
|
|
|
|
component
|
|
..text = text
|
|
..anchor = _textAnchor(node)
|
|
..position = _textPosition(node)
|
|
..priority = priority + 1;
|
|
_syncTextStyle(node);
|
|
}
|
|
|
|
void _syncTextStyle(RuntimeNode node) {
|
|
final component = _textComponent;
|
|
if (component == null) {
|
|
return;
|
|
}
|
|
component.textRenderer = TextPaint(style: _textStyle(node));
|
|
}
|
|
|
|
TextStyle _textStyle(RuntimeNode node) {
|
|
final color = _textColor(node);
|
|
return TextStyle(
|
|
color: composeRuntimeColorAlpha(color, renderAlpha),
|
|
fontSize: node.fontSize ?? 18,
|
|
fontWeight: node.type == RuntimeNodeType.button
|
|
? FontWeight.w600
|
|
: FontWeight.normal,
|
|
shadows: _textShadows(node),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void onRemove() {
|
|
_imageLoadToken++;
|
|
_spineLoadToken++;
|
|
_releaseLoadedImage();
|
|
_releaseLoadedSpine();
|
|
_releaseParticle();
|
|
_runtimeAlpha = null;
|
|
_inheritedAlpha = 1;
|
|
_pressed = false;
|
|
_textComponent = null;
|
|
super.onRemove();
|
|
}
|
|
|
|
Anchor _textAnchor(RuntimeNode node) {
|
|
final topAligned = _usesTopAlignedText(node);
|
|
return switch (node.textAlign) {
|
|
RuntimeTextAlignValue.left =>
|
|
topAligned ? Anchor.topLeft : Anchor.centerLeft,
|
|
RuntimeTextAlignValue.right =>
|
|
topAligned ? Anchor.topRight : Anchor.centerRight,
|
|
_ => topAligned ? Anchor.topCenter : Anchor.center,
|
|
};
|
|
}
|
|
|
|
Vector2 _textPosition(RuntimeNode node) {
|
|
final topAligned = _usesTopAlignedText(node);
|
|
return switch (node.textAlign) {
|
|
RuntimeTextAlignValue.left =>
|
|
topAligned ? Vector2.zero() : Vector2(0, size.y / 2),
|
|
RuntimeTextAlignValue.right =>
|
|
topAligned ? Vector2(size.x, 0) : Vector2(size.x, size.y / 2),
|
|
_ => topAligned ? Vector2(size.x / 2, 0) : size / 2,
|
|
};
|
|
}
|
|
|
|
bool _usesTopAlignedText(RuntimeNode node) {
|
|
if (node.type == RuntimeNodeType.button) {
|
|
return false;
|
|
}
|
|
return (node.text ?? '').contains('\n');
|
|
}
|
|
|
|
List<Shadow>? _textShadows(RuntimeNode node) {
|
|
final color = node.textShadowColor;
|
|
if (color == null) {
|
|
return null;
|
|
}
|
|
return [
|
|
Shadow(
|
|
color: composeRuntimeColorAlpha(color, renderAlpha),
|
|
offset: Offset(
|
|
node.textShadowOffsetX ?? 0,
|
|
node.textShadowOffsetY ?? 0,
|
|
),
|
|
blurRadius: node.textShadowBlur ?? 0,
|
|
),
|
|
];
|
|
}
|
|
|
|
Color _textColor(RuntimeNode node) {
|
|
if (node.type == RuntimeNodeType.button) {
|
|
return Colors.white;
|
|
}
|
|
return node.color ?? Colors.white;
|
|
}
|
|
|
|
Color _defaultColor() {
|
|
return switch (_node.type) {
|
|
RuntimeNodeType.button => const Color(0xff3662d8),
|
|
RuntimeNodeType.panel => const Color(0xaa111827),
|
|
RuntimeNodeType.text => Colors.transparent,
|
|
RuntimeNodeType.circle => const Color(0xffef4444),
|
|
RuntimeNodeType.rect => const Color(0xff334155),
|
|
RuntimeNodeType.listView => const Color(0xff111827),
|
|
RuntimeNodeType.line => const Color(0xffffffff),
|
|
RuntimeNodeType.progress => const Color(0xff22c55e),
|
|
RuntimeNodeType.sprite ||
|
|
RuntimeNodeType.image ||
|
|
RuntimeNodeType.spine => const Color(0xff64748b),
|
|
_ => const Color(0xff94a3b8),
|
|
};
|
|
}
|
|
|
|
Anchor _anchorFromString(String value) {
|
|
return switch (value) {
|
|
RuntimeAnchorValue.center => Anchor.center,
|
|
RuntimeAnchorValue.topLeft => Anchor.topLeft,
|
|
RuntimeAnchorValue.topRight => Anchor.topRight,
|
|
RuntimeAnchorValue.bottomLeft => Anchor.bottomLeft,
|
|
RuntimeAnchorValue.bottomRight => Anchor.bottomRight,
|
|
_ => Anchor.topLeft,
|
|
};
|
|
}
|
|
}
|
|
|
|
class _ParticleConfig {
|
|
const _ParticleConfig({
|
|
required this.start,
|
|
required this.velocity,
|
|
required this.gravity,
|
|
required this.color,
|
|
required this.colorTo,
|
|
required this.radius,
|
|
required this.radiusTo,
|
|
});
|
|
|
|
final Vector2 start;
|
|
final Vector2 velocity;
|
|
final Vector2 gravity;
|
|
final Color color;
|
|
final Color colorTo;
|
|
final double radius;
|
|
final double radiusTo;
|
|
}
|