Compare commits
5 Commits
8d2c97269a
...
638ea22562
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
638ea22562 | ||
|
|
38f6e0c0c9 | ||
|
|
e2a584d4dc | ||
|
|
409942b4af | ||
|
|
5e6a4877f4 |
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Added TexturePacker frame, manual source-region, and nine-slice image rendering fields for image-capable nodes.
|
||||||
|
- Added Runtime text shadow fields for text-capable nodes.
|
||||||
- Fixed Runtime node color alpha composition so `#AARRGGBB` alpha now multiplies with node/runtime alpha instead of being overwritten.
|
- Fixed Runtime node color alpha composition so `#AARRGGBB` alpha now multiplies with node/runtime alpha instead of being overwritten.
|
||||||
|
|
||||||
## 0.1.0
|
## 0.1.0
|
||||||
|
|||||||
@@ -44,6 +44,35 @@ Final opacity = color alpha × node alpha × runtime animation alpha
|
|||||||
|
|
||||||
`#RRGGBB` colors behave as fully opaque colors. `#00000000` is fully transparent.
|
`#RRGGBB` colors behave as fully opaque colors. `#00000000` is fully transparent.
|
||||||
|
|
||||||
|
### Text shadow
|
||||||
|
|
||||||
|
Text-capable nodes may use flat shadow fields:
|
||||||
|
|
||||||
|
- `textShadowColor`: `#RRGGBB` or `#AARRGGBB` shadow color.
|
||||||
|
- `textShadowOffsetX` / `textShadowOffsetY`: shadow offset in runtime pixels.
|
||||||
|
- `textShadowBlur`: non-negative blur radius.
|
||||||
|
|
||||||
|
The shadow color alpha is multiplied by `RuntimeNode.alpha` and any runtime animation alpha.
|
||||||
|
|
||||||
|
### Image regions and nine-slice
|
||||||
|
|
||||||
|
Image-capable nodes (`image`, `sprite`, and image-backed `button`) may draw only part of an asset.
|
||||||
|
|
||||||
|
For manual atlas regions, set source fields in image pixels:
|
||||||
|
|
||||||
|
- `sourceX` / `sourceY`: top-left source position inside the loaded image.
|
||||||
|
- `sourceWidth` / `sourceHeight`: source region size.
|
||||||
|
|
||||||
|
For TexturePacker JSON atlases, declare `atlas` on an image resource and set `frame` on the node. Image-backed buttons may also use `pressedFrame` and `disabledFrame`. The runtime supports TexturePacker JSON Hash and JSON Array `frames` formats with non-rotated frames. `rotated: true` frames are rejected.
|
||||||
|
|
||||||
|
When frame/source fields are omitted, the full image is used.
|
||||||
|
|
||||||
|
Image-capable nodes may also use nine-slice scaling with source-pixel insets:
|
||||||
|
|
||||||
|
- `sliceLeft` / `sliceTop` / `sliceRight` / `sliceBottom`.
|
||||||
|
|
||||||
|
Nine-slice keeps corners unscaled, stretches edges on one axis, and stretches the center on both axes. Insets are clamped to the selected source region and destination size.
|
||||||
|
|
||||||
## RuntimeCommand
|
## RuntimeCommand
|
||||||
|
|
||||||
Runtime commands request generic side effects owned by Dart/Flame.
|
Runtime commands request generic side effects owned by Dart/Flame.
|
||||||
|
|||||||
@@ -86,6 +86,17 @@
|
|||||||
---@field type RuntimeNodeType
|
---@field type RuntimeNodeType
|
||||||
---@field parent? string
|
---@field parent? string
|
||||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||||
|
---@field frame? string TexturePacker frame name within the asset atlas.
|
||||||
|
---@field pressedFrame? string Button pressed-state TexturePacker frame name.
|
||||||
|
---@field disabledFrame? string Button disabled-state TexturePacker frame name.
|
||||||
|
---@field sourceX? number Source atlas region x in image pixels.
|
||||||
|
---@field sourceY? number Source atlas region y in image pixels.
|
||||||
|
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||||
|
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||||
|
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||||
|
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||||
|
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||||
|
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||||
---@field pressedAsset? string Button pressed-state image asset key.
|
---@field pressedAsset? string Button pressed-state image asset key.
|
||||||
---@field disabledAsset? string Button disabled-state image asset key.
|
---@field disabledAsset? string Button disabled-state image asset key.
|
||||||
---@field animation? string
|
---@field animation? string
|
||||||
@@ -109,6 +120,10 @@
|
|||||||
---@field color? string
|
---@field color? string
|
||||||
---@field fontSize? number
|
---@field fontSize? number
|
||||||
---@field textAlign? RuntimeTextAlign
|
---@field textAlign? RuntimeTextAlign
|
||||||
|
---@field textShadowColor? string
|
||||||
|
---@field textShadowOffsetX? number
|
||||||
|
---@field textShadowOffsetY? number
|
||||||
|
---@field textShadowBlur? number
|
||||||
---@field radius? number
|
---@field radius? number
|
||||||
---@field strokeWidth? number
|
---@field strokeWidth? number
|
||||||
---@field value? number
|
---@field value? number
|
||||||
@@ -143,6 +158,17 @@
|
|||||||
---@field type? RuntimeNodeType
|
---@field type? RuntimeNodeType
|
||||||
---@field parent? string
|
---@field parent? string
|
||||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||||
|
---@field frame? string TexturePacker frame name within the asset atlas.
|
||||||
|
---@field pressedFrame? string Button pressed-state TexturePacker frame name.
|
||||||
|
---@field disabledFrame? string Button disabled-state TexturePacker frame name.
|
||||||
|
---@field sourceX? number Source atlas region x in image pixels.
|
||||||
|
---@field sourceY? number Source atlas region y in image pixels.
|
||||||
|
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||||
|
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||||
|
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||||
|
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||||
|
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||||
|
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||||
---@field pressedAsset? string Button pressed-state image asset key.
|
---@field pressedAsset? string Button pressed-state image asset key.
|
||||||
---@field disabledAsset? string Button disabled-state image asset key.
|
---@field disabledAsset? string Button disabled-state image asset key.
|
||||||
---@field animation? string
|
---@field animation? string
|
||||||
@@ -166,6 +192,10 @@
|
|||||||
---@field color? string
|
---@field color? string
|
||||||
---@field fontSize? number
|
---@field fontSize? number
|
||||||
---@field textAlign? RuntimeTextAlign
|
---@field textAlign? RuntimeTextAlign
|
||||||
|
---@field textShadowColor? string
|
||||||
|
---@field textShadowOffsetX? number
|
||||||
|
---@field textShadowOffsetY? number
|
||||||
|
---@field textShadowBlur? number
|
||||||
---@field radius? number
|
---@field radius? number
|
||||||
---@field strokeWidth? number
|
---@field strokeWidth? number
|
||||||
---@field value? number
|
---@field value? number
|
||||||
|
|||||||
@@ -86,6 +86,17 @@
|
|||||||
---@field type RuntimeNodeType
|
---@field type RuntimeNodeType
|
||||||
---@field parent? string
|
---@field parent? string
|
||||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||||
|
---@field frame? string TexturePacker frame name within the asset atlas.
|
||||||
|
---@field pressedFrame? string Button pressed-state TexturePacker frame name.
|
||||||
|
---@field disabledFrame? string Button disabled-state TexturePacker frame name.
|
||||||
|
---@field sourceX? number Source atlas region x in image pixels.
|
||||||
|
---@field sourceY? number Source atlas region y in image pixels.
|
||||||
|
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||||
|
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||||
|
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||||
|
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||||
|
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||||
|
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||||
---@field pressedAsset? string Button pressed-state image asset key.
|
---@field pressedAsset? string Button pressed-state image asset key.
|
||||||
---@field disabledAsset? string Button disabled-state image asset key.
|
---@field disabledAsset? string Button disabled-state image asset key.
|
||||||
---@field animation? string
|
---@field animation? string
|
||||||
@@ -109,6 +120,10 @@
|
|||||||
---@field color? string
|
---@field color? string
|
||||||
---@field fontSize? number
|
---@field fontSize? number
|
||||||
---@field textAlign? RuntimeTextAlign
|
---@field textAlign? RuntimeTextAlign
|
||||||
|
---@field textShadowColor? string
|
||||||
|
---@field textShadowOffsetX? number
|
||||||
|
---@field textShadowOffsetY? number
|
||||||
|
---@field textShadowBlur? number
|
||||||
---@field radius? number
|
---@field radius? number
|
||||||
---@field strokeWidth? number
|
---@field strokeWidth? number
|
||||||
---@field value? number
|
---@field value? number
|
||||||
@@ -143,6 +158,17 @@
|
|||||||
---@field type? RuntimeNodeType
|
---@field type? RuntimeNodeType
|
||||||
---@field parent? string
|
---@field parent? string
|
||||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||||
|
---@field frame? string TexturePacker frame name within the asset atlas.
|
||||||
|
---@field pressedFrame? string Button pressed-state TexturePacker frame name.
|
||||||
|
---@field disabledFrame? string Button disabled-state TexturePacker frame name.
|
||||||
|
---@field sourceX? number Source atlas region x in image pixels.
|
||||||
|
---@field sourceY? number Source atlas region y in image pixels.
|
||||||
|
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||||
|
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||||
|
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||||
|
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||||
|
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||||
|
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||||
---@field pressedAsset? string Button pressed-state image asset key.
|
---@field pressedAsset? string Button pressed-state image asset key.
|
||||||
---@field disabledAsset? string Button disabled-state image asset key.
|
---@field disabledAsset? string Button disabled-state image asset key.
|
||||||
---@field animation? string
|
---@field animation? string
|
||||||
@@ -166,6 +192,10 @@
|
|||||||
---@field color? string
|
---@field color? string
|
||||||
---@field fontSize? number
|
---@field fontSize? number
|
||||||
---@field textAlign? RuntimeTextAlign
|
---@field textAlign? RuntimeTextAlign
|
||||||
|
---@field textShadowColor? string
|
||||||
|
---@field textShadowOffsetX? number
|
||||||
|
---@field textShadowOffsetY? number
|
||||||
|
---@field textShadowBlur? number
|
||||||
---@field radius? number
|
---@field radius? number
|
||||||
---@field strokeWidth? number
|
---@field strokeWidth? number
|
||||||
---@field value? number
|
---@field value? number
|
||||||
|
|||||||
@@ -86,6 +86,17 @@
|
|||||||
---@field type RuntimeNodeType
|
---@field type RuntimeNodeType
|
||||||
---@field parent? string
|
---@field parent? string
|
||||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||||
|
---@field frame? string TexturePacker frame name within the asset atlas.
|
||||||
|
---@field pressedFrame? string Button pressed-state TexturePacker frame name.
|
||||||
|
---@field disabledFrame? string Button disabled-state TexturePacker frame name.
|
||||||
|
---@field sourceX? number Source atlas region x in image pixels.
|
||||||
|
---@field sourceY? number Source atlas region y in image pixels.
|
||||||
|
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||||
|
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||||
|
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||||
|
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||||
|
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||||
|
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||||
---@field pressedAsset? string Button pressed-state image asset key.
|
---@field pressedAsset? string Button pressed-state image asset key.
|
||||||
---@field disabledAsset? string Button disabled-state image asset key.
|
---@field disabledAsset? string Button disabled-state image asset key.
|
||||||
---@field animation? string
|
---@field animation? string
|
||||||
@@ -109,6 +120,10 @@
|
|||||||
---@field color? string
|
---@field color? string
|
||||||
---@field fontSize? number
|
---@field fontSize? number
|
||||||
---@field textAlign? RuntimeTextAlign
|
---@field textAlign? RuntimeTextAlign
|
||||||
|
---@field textShadowColor? string
|
||||||
|
---@field textShadowOffsetX? number
|
||||||
|
---@field textShadowOffsetY? number
|
||||||
|
---@field textShadowBlur? number
|
||||||
---@field radius? number
|
---@field radius? number
|
||||||
---@field strokeWidth? number
|
---@field strokeWidth? number
|
||||||
---@field value? number
|
---@field value? number
|
||||||
@@ -143,6 +158,17 @@
|
|||||||
---@field type? RuntimeNodeType
|
---@field type? RuntimeNodeType
|
||||||
---@field parent? string
|
---@field parent? string
|
||||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||||
|
---@field frame? string TexturePacker frame name within the asset atlas.
|
||||||
|
---@field pressedFrame? string Button pressed-state TexturePacker frame name.
|
||||||
|
---@field disabledFrame? string Button disabled-state TexturePacker frame name.
|
||||||
|
---@field sourceX? number Source atlas region x in image pixels.
|
||||||
|
---@field sourceY? number Source atlas region y in image pixels.
|
||||||
|
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||||
|
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||||
|
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||||
|
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||||
|
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||||
|
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||||
---@field pressedAsset? string Button pressed-state image asset key.
|
---@field pressedAsset? string Button pressed-state image asset key.
|
||||||
---@field disabledAsset? string Button disabled-state image asset key.
|
---@field disabledAsset? string Button disabled-state image asset key.
|
||||||
---@field animation? string
|
---@field animation? string
|
||||||
@@ -166,6 +192,10 @@
|
|||||||
---@field color? string
|
---@field color? string
|
||||||
---@field fontSize? number
|
---@field fontSize? number
|
||||||
---@field textAlign? RuntimeTextAlign
|
---@field textAlign? RuntimeTextAlign
|
||||||
|
---@field textShadowColor? string
|
||||||
|
---@field textShadowOffsetX? number
|
||||||
|
---@field textShadowOffsetY? number
|
||||||
|
---@field textShadowBlur? number
|
||||||
---@field radius? number
|
---@field radius? number
|
||||||
---@field strokeWidth? number
|
---@field strokeWidth? number
|
||||||
---@field value? number
|
---@field value? number
|
||||||
|
|||||||
@@ -86,6 +86,17 @@
|
|||||||
---@field type RuntimeNodeType
|
---@field type RuntimeNodeType
|
||||||
---@field parent? string
|
---@field parent? string
|
||||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||||
|
---@field frame? string TexturePacker frame name within the asset atlas.
|
||||||
|
---@field pressedFrame? string Button pressed-state TexturePacker frame name.
|
||||||
|
---@field disabledFrame? string Button disabled-state TexturePacker frame name.
|
||||||
|
---@field sourceX? number Source atlas region x in image pixels.
|
||||||
|
---@field sourceY? number Source atlas region y in image pixels.
|
||||||
|
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||||
|
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||||
|
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||||
|
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||||
|
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||||
|
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||||
---@field pressedAsset? string Button pressed-state image asset key.
|
---@field pressedAsset? string Button pressed-state image asset key.
|
||||||
---@field disabledAsset? string Button disabled-state image asset key.
|
---@field disabledAsset? string Button disabled-state image asset key.
|
||||||
---@field animation? string
|
---@field animation? string
|
||||||
@@ -109,6 +120,10 @@
|
|||||||
---@field color? string
|
---@field color? string
|
||||||
---@field fontSize? number
|
---@field fontSize? number
|
||||||
---@field textAlign? RuntimeTextAlign
|
---@field textAlign? RuntimeTextAlign
|
||||||
|
---@field textShadowColor? string
|
||||||
|
---@field textShadowOffsetX? number
|
||||||
|
---@field textShadowOffsetY? number
|
||||||
|
---@field textShadowBlur? number
|
||||||
---@field radius? number
|
---@field radius? number
|
||||||
---@field strokeWidth? number
|
---@field strokeWidth? number
|
||||||
---@field value? number
|
---@field value? number
|
||||||
@@ -143,6 +158,17 @@
|
|||||||
---@field type? RuntimeNodeType
|
---@field type? RuntimeNodeType
|
||||||
---@field parent? string
|
---@field parent? string
|
||||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||||
|
---@field frame? string TexturePacker frame name within the asset atlas.
|
||||||
|
---@field pressedFrame? string Button pressed-state TexturePacker frame name.
|
||||||
|
---@field disabledFrame? string Button disabled-state TexturePacker frame name.
|
||||||
|
---@field sourceX? number Source atlas region x in image pixels.
|
||||||
|
---@field sourceY? number Source atlas region y in image pixels.
|
||||||
|
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||||
|
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||||
|
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||||
|
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||||
|
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||||
|
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||||
---@field pressedAsset? string Button pressed-state image asset key.
|
---@field pressedAsset? string Button pressed-state image asset key.
|
||||||
---@field disabledAsset? string Button disabled-state image asset key.
|
---@field disabledAsset? string Button disabled-state image asset key.
|
||||||
---@field animation? string
|
---@field animation? string
|
||||||
@@ -166,6 +192,10 @@
|
|||||||
---@field color? string
|
---@field color? string
|
||||||
---@field fontSize? number
|
---@field fontSize? number
|
||||||
---@field textAlign? RuntimeTextAlign
|
---@field textAlign? RuntimeTextAlign
|
||||||
|
---@field textShadowColor? string
|
||||||
|
---@field textShadowOffsetX? number
|
||||||
|
---@field textShadowOffsetY? number
|
||||||
|
---@field textShadowBlur? number
|
||||||
---@field radius? number
|
---@field radius? number
|
||||||
---@field strokeWidth? number
|
---@field strokeWidth? number
|
||||||
---@field value? number
|
---@field value? number
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ class RuntimeNode {
|
|||||||
required this.type,
|
required this.type,
|
||||||
this.parent,
|
this.parent,
|
||||||
this.asset,
|
this.asset,
|
||||||
|
this.frame,
|
||||||
|
this.pressedFrame,
|
||||||
|
this.disabledFrame,
|
||||||
|
this.sourceX,
|
||||||
|
this.sourceY,
|
||||||
|
this.sourceWidth,
|
||||||
|
this.sourceHeight,
|
||||||
|
this.sliceLeft,
|
||||||
|
this.sliceTop,
|
||||||
|
this.sliceRight,
|
||||||
|
this.sliceBottom,
|
||||||
this.pressedAsset,
|
this.pressedAsset,
|
||||||
this.disabledAsset,
|
this.disabledAsset,
|
||||||
this.animation,
|
this.animation,
|
||||||
@@ -31,6 +42,10 @@ class RuntimeNode {
|
|||||||
this.color,
|
this.color,
|
||||||
this.fontSize,
|
this.fontSize,
|
||||||
this.textAlign = RuntimeTextAlignValue.center,
|
this.textAlign = RuntimeTextAlignValue.center,
|
||||||
|
this.textShadowColor,
|
||||||
|
this.textShadowOffsetX,
|
||||||
|
this.textShadowOffsetY,
|
||||||
|
this.textShadowBlur,
|
||||||
this.radius,
|
this.radius,
|
||||||
this.strokeWidth,
|
this.strokeWidth,
|
||||||
this.value,
|
this.value,
|
||||||
@@ -66,6 +81,17 @@ class RuntimeNode {
|
|||||||
final String type;
|
final String type;
|
||||||
final String? parent;
|
final String? parent;
|
||||||
final String? asset;
|
final String? asset;
|
||||||
|
final String? frame;
|
||||||
|
final String? pressedFrame;
|
||||||
|
final String? disabledFrame;
|
||||||
|
final double? sourceX;
|
||||||
|
final double? sourceY;
|
||||||
|
final double? sourceWidth;
|
||||||
|
final double? sourceHeight;
|
||||||
|
final double? sliceLeft;
|
||||||
|
final double? sliceTop;
|
||||||
|
final double? sliceRight;
|
||||||
|
final double? sliceBottom;
|
||||||
final String? pressedAsset;
|
final String? pressedAsset;
|
||||||
final String? disabledAsset;
|
final String? disabledAsset;
|
||||||
final String? animation;
|
final String? animation;
|
||||||
@@ -89,6 +115,10 @@ class RuntimeNode {
|
|||||||
final Color? color;
|
final Color? color;
|
||||||
final double? fontSize;
|
final double? fontSize;
|
||||||
final String textAlign;
|
final String textAlign;
|
||||||
|
final Color? textShadowColor;
|
||||||
|
final double? textShadowOffsetX;
|
||||||
|
final double? textShadowOffsetY;
|
||||||
|
final double? textShadowBlur;
|
||||||
final double? radius;
|
final double? radius;
|
||||||
final double? strokeWidth;
|
final double? strokeWidth;
|
||||||
final double? value;
|
final double? value;
|
||||||
@@ -205,6 +235,36 @@ class RuntimeNode {
|
|||||||
type: nextType,
|
type: nextType,
|
||||||
parent: _parentProp(props, currentParent: parent, nodeId: id),
|
parent: _parentProp(props, currentParent: parent, nodeId: id),
|
||||||
asset: _stringProp(props, RuntimeProtocolField.asset) ?? asset,
|
asset: _stringProp(props, RuntimeProtocolField.asset) ?? asset,
|
||||||
|
frame: _stringProp(props, RuntimeProtocolField.frame) ?? frame,
|
||||||
|
pressedFrame:
|
||||||
|
_stringProp(props, RuntimeProtocolField.pressedFrame) ?? pressedFrame,
|
||||||
|
disabledFrame:
|
||||||
|
_stringProp(props, RuntimeProtocolField.disabledFrame) ??
|
||||||
|
disabledFrame,
|
||||||
|
sourceX:
|
||||||
|
_nonNegativeDoubleProp(props, RuntimeProtocolField.sourceX) ??
|
||||||
|
sourceX,
|
||||||
|
sourceY:
|
||||||
|
_nonNegativeDoubleProp(props, RuntimeProtocolField.sourceY) ??
|
||||||
|
sourceY,
|
||||||
|
sourceWidth:
|
||||||
|
_positiveDoubleProp(props, RuntimeProtocolField.sourceWidth) ??
|
||||||
|
sourceWidth,
|
||||||
|
sourceHeight:
|
||||||
|
_positiveDoubleProp(props, RuntimeProtocolField.sourceHeight) ??
|
||||||
|
sourceHeight,
|
||||||
|
sliceLeft:
|
||||||
|
_nonNegativeDoubleProp(props, RuntimeProtocolField.sliceLeft) ??
|
||||||
|
sliceLeft,
|
||||||
|
sliceTop:
|
||||||
|
_nonNegativeDoubleProp(props, RuntimeProtocolField.sliceTop) ??
|
||||||
|
sliceTop,
|
||||||
|
sliceRight:
|
||||||
|
_nonNegativeDoubleProp(props, RuntimeProtocolField.sliceRight) ??
|
||||||
|
sliceRight,
|
||||||
|
sliceBottom:
|
||||||
|
_nonNegativeDoubleProp(props, RuntimeProtocolField.sliceBottom) ??
|
||||||
|
sliceBottom,
|
||||||
pressedAsset:
|
pressedAsset:
|
||||||
_stringProp(props, RuntimeProtocolField.pressedAsset) ?? pressedAsset,
|
_stringProp(props, RuntimeProtocolField.pressedAsset) ?? pressedAsset,
|
||||||
disabledAsset:
|
disabledAsset:
|
||||||
@@ -232,6 +292,18 @@ class RuntimeNode {
|
|||||||
color: _colorProp(props, RuntimeProtocolField.color) ?? color,
|
color: _colorProp(props, RuntimeProtocolField.color) ?? color,
|
||||||
fontSize: _doubleProp(props, RuntimeProtocolField.fontSize) ?? fontSize,
|
fontSize: _doubleProp(props, RuntimeProtocolField.fontSize) ?? fontSize,
|
||||||
textAlign: nextTextAlign,
|
textAlign: nextTextAlign,
|
||||||
|
textShadowColor:
|
||||||
|
_colorProp(props, RuntimeProtocolField.textShadowColor) ??
|
||||||
|
textShadowColor,
|
||||||
|
textShadowOffsetX:
|
||||||
|
_doubleProp(props, RuntimeProtocolField.textShadowOffsetX) ??
|
||||||
|
textShadowOffsetX,
|
||||||
|
textShadowOffsetY:
|
||||||
|
_doubleProp(props, RuntimeProtocolField.textShadowOffsetY) ??
|
||||||
|
textShadowOffsetY,
|
||||||
|
textShadowBlur:
|
||||||
|
_nonNegativeDoubleProp(props, RuntimeProtocolField.textShadowBlur) ??
|
||||||
|
textShadowBlur,
|
||||||
radius: _doubleProp(props, RuntimeProtocolField.radius) ?? radius,
|
radius: _doubleProp(props, RuntimeProtocolField.radius) ?? radius,
|
||||||
strokeWidth:
|
strokeWidth:
|
||||||
_doubleProp(props, RuntimeProtocolField.strokeWidth) ?? strokeWidth,
|
_doubleProp(props, RuntimeProtocolField.strokeWidth) ?? strokeWidth,
|
||||||
@@ -325,6 +397,20 @@ class RuntimeNode {
|
|||||||
nodeId: _requiredString(map, RuntimeProtocolField.id),
|
nodeId: _requiredString(map, RuntimeProtocolField.id),
|
||||||
),
|
),
|
||||||
asset: _stringProp(map, RuntimeProtocolField.asset),
|
asset: _stringProp(map, RuntimeProtocolField.asset),
|
||||||
|
frame: _stringProp(map, RuntimeProtocolField.frame),
|
||||||
|
pressedFrame: _stringProp(map, RuntimeProtocolField.pressedFrame),
|
||||||
|
disabledFrame: _stringProp(map, RuntimeProtocolField.disabledFrame),
|
||||||
|
sourceX: _nonNegativeDoubleProp(map, RuntimeProtocolField.sourceX),
|
||||||
|
sourceY: _nonNegativeDoubleProp(map, RuntimeProtocolField.sourceY),
|
||||||
|
sourceWidth: _positiveDoubleProp(map, RuntimeProtocolField.sourceWidth),
|
||||||
|
sourceHeight: _positiveDoubleProp(map, RuntimeProtocolField.sourceHeight),
|
||||||
|
sliceLeft: _nonNegativeDoubleProp(map, RuntimeProtocolField.sliceLeft),
|
||||||
|
sliceTop: _nonNegativeDoubleProp(map, RuntimeProtocolField.sliceTop),
|
||||||
|
sliceRight: _nonNegativeDoubleProp(map, RuntimeProtocolField.sliceRight),
|
||||||
|
sliceBottom: _nonNegativeDoubleProp(
|
||||||
|
map,
|
||||||
|
RuntimeProtocolField.sliceBottom,
|
||||||
|
),
|
||||||
pressedAsset: _stringProp(map, RuntimeProtocolField.pressedAsset),
|
pressedAsset: _stringProp(map, RuntimeProtocolField.pressedAsset),
|
||||||
disabledAsset: _stringProp(map, RuntimeProtocolField.disabledAsset),
|
disabledAsset: _stringProp(map, RuntimeProtocolField.disabledAsset),
|
||||||
animation: _stringProp(map, RuntimeProtocolField.animation),
|
animation: _stringProp(map, RuntimeProtocolField.animation),
|
||||||
@@ -352,6 +438,19 @@ class RuntimeNode {
|
|||||||
color: _colorProp(map, RuntimeProtocolField.color),
|
color: _colorProp(map, RuntimeProtocolField.color),
|
||||||
fontSize: _doubleProp(map, RuntimeProtocolField.fontSize),
|
fontSize: _doubleProp(map, RuntimeProtocolField.fontSize),
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
|
textShadowColor: _colorProp(map, RuntimeProtocolField.textShadowColor),
|
||||||
|
textShadowOffsetX: _doubleProp(
|
||||||
|
map,
|
||||||
|
RuntimeProtocolField.textShadowOffsetX,
|
||||||
|
),
|
||||||
|
textShadowOffsetY: _doubleProp(
|
||||||
|
map,
|
||||||
|
RuntimeProtocolField.textShadowOffsetY,
|
||||||
|
),
|
||||||
|
textShadowBlur: _nonNegativeDoubleProp(
|
||||||
|
map,
|
||||||
|
RuntimeProtocolField.textShadowBlur,
|
||||||
|
),
|
||||||
radius: _doubleProp(map, RuntimeProtocolField.radius),
|
radius: _doubleProp(map, RuntimeProtocolField.radius),
|
||||||
strokeWidth: _doubleProp(map, RuntimeProtocolField.strokeWidth),
|
strokeWidth: _doubleProp(map, RuntimeProtocolField.strokeWidth),
|
||||||
value: _normalizedValueProp(map, RuntimeProtocolField.value),
|
value: _normalizedValueProp(map, RuntimeProtocolField.value),
|
||||||
@@ -497,6 +596,17 @@ class RuntimeNode {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static double? _positiveDoubleProp(Map<String, Object?> map, String key) {
|
||||||
|
final value = _doubleProp(map, key);
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value <= 0) {
|
||||||
|
throw FormatException('RuntimeNode.$key must be > 0');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
static double? _scrollProp(
|
static double? _scrollProp(
|
||||||
Map<String, Object?> map,
|
Map<String, Object?> map,
|
||||||
String key, {
|
String key, {
|
||||||
|
|||||||
@@ -222,8 +222,15 @@ class GameResource {
|
|||||||
'spine resource.skeleton must be a non-empty string',
|
'spine resource.skeleton must be a non-empty string',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (path is! String || path.isEmpty) {
|
} else {
|
||||||
throw const FormatException('resource.path must be a non-empty string');
|
if (path is! String || path.isEmpty) {
|
||||||
|
throw const FormatException('resource.path must be a non-empty string');
|
||||||
|
}
|
||||||
|
if (atlas != null && (atlas is! String || atlas.isEmpty)) {
|
||||||
|
throw const FormatException(
|
||||||
|
'image resource.atlas must be a non-empty string',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final preload = map['preload'] as String? ?? GameResourcePreload.required;
|
final preload = map['preload'] as String? ?? GameResourcePreload.required;
|
||||||
if (!GameResourcePreload.isSupported(preload)) {
|
if (!GameResourcePreload.isSupported(preload)) {
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ class PackageVerifier {
|
|||||||
if (resource.type == GameResourceType.spine) {
|
if (resource.type == GameResourceType.spine) {
|
||||||
return [resource.atlas!, resource.skeleton!];
|
return [resource.atlas!, resource.skeleton!];
|
||||||
}
|
}
|
||||||
|
if (resource.type == GameResourceType.image && resource.atlas != null) {
|
||||||
|
return [resource.path, resource.atlas!];
|
||||||
|
}
|
||||||
return [resource.path];
|
return [resource.path];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,6 +138,17 @@ class RuntimeProtocolField {
|
|||||||
static const target = 'target';
|
static const target = 'target';
|
||||||
static const parent = 'parent';
|
static const parent = 'parent';
|
||||||
static const asset = 'asset';
|
static const asset = 'asset';
|
||||||
|
static const frame = 'frame';
|
||||||
|
static const pressedFrame = 'pressedFrame';
|
||||||
|
static const disabledFrame = 'disabledFrame';
|
||||||
|
static const sourceX = 'sourceX';
|
||||||
|
static const sourceY = 'sourceY';
|
||||||
|
static const sourceWidth = 'sourceWidth';
|
||||||
|
static const sourceHeight = 'sourceHeight';
|
||||||
|
static const sliceLeft = 'sliceLeft';
|
||||||
|
static const sliceTop = 'sliceTop';
|
||||||
|
static const sliceRight = 'sliceRight';
|
||||||
|
static const sliceBottom = 'sliceBottom';
|
||||||
static const pressedAsset = 'pressedAsset';
|
static const pressedAsset = 'pressedAsset';
|
||||||
static const disabledAsset = 'disabledAsset';
|
static const disabledAsset = 'disabledAsset';
|
||||||
static const animation = 'animation';
|
static const animation = 'animation';
|
||||||
@@ -161,6 +172,10 @@ class RuntimeProtocolField {
|
|||||||
static const color = 'color';
|
static const color = 'color';
|
||||||
static const fontSize = 'fontSize';
|
static const fontSize = 'fontSize';
|
||||||
static const textAlign = 'textAlign';
|
static const textAlign = 'textAlign';
|
||||||
|
static const textShadowColor = 'textShadowColor';
|
||||||
|
static const textShadowOffsetX = 'textShadowOffsetX';
|
||||||
|
static const textShadowOffsetY = 'textShadowOffsetY';
|
||||||
|
static const textShadowBlur = 'textShadowBlur';
|
||||||
static const radius = 'radius';
|
static const radius = 'radius';
|
||||||
static const strokeWidth = 'strokeWidth';
|
static const strokeWidth = 'strokeWidth';
|
||||||
static const value = 'value';
|
static const value = 'value';
|
||||||
@@ -221,6 +236,17 @@ class RuntimeProtocolSchema {
|
|||||||
RuntimeProtocolField.type,
|
RuntimeProtocolField.type,
|
||||||
RuntimeProtocolField.parent,
|
RuntimeProtocolField.parent,
|
||||||
RuntimeProtocolField.asset,
|
RuntimeProtocolField.asset,
|
||||||
|
RuntimeProtocolField.frame,
|
||||||
|
RuntimeProtocolField.pressedFrame,
|
||||||
|
RuntimeProtocolField.disabledFrame,
|
||||||
|
RuntimeProtocolField.sourceX,
|
||||||
|
RuntimeProtocolField.sourceY,
|
||||||
|
RuntimeProtocolField.sourceWidth,
|
||||||
|
RuntimeProtocolField.sourceHeight,
|
||||||
|
RuntimeProtocolField.sliceLeft,
|
||||||
|
RuntimeProtocolField.sliceTop,
|
||||||
|
RuntimeProtocolField.sliceRight,
|
||||||
|
RuntimeProtocolField.sliceBottom,
|
||||||
RuntimeProtocolField.pressedAsset,
|
RuntimeProtocolField.pressedAsset,
|
||||||
RuntimeProtocolField.disabledAsset,
|
RuntimeProtocolField.disabledAsset,
|
||||||
RuntimeProtocolField.animation,
|
RuntimeProtocolField.animation,
|
||||||
@@ -244,6 +270,10 @@ class RuntimeProtocolSchema {
|
|||||||
RuntimeProtocolField.color,
|
RuntimeProtocolField.color,
|
||||||
RuntimeProtocolField.fontSize,
|
RuntimeProtocolField.fontSize,
|
||||||
RuntimeProtocolField.textAlign,
|
RuntimeProtocolField.textAlign,
|
||||||
|
RuntimeProtocolField.textShadowColor,
|
||||||
|
RuntimeProtocolField.textShadowOffsetX,
|
||||||
|
RuntimeProtocolField.textShadowOffsetY,
|
||||||
|
RuntimeProtocolField.textShadowBlur,
|
||||||
RuntimeProtocolField.radius,
|
RuntimeProtocolField.radius,
|
||||||
RuntimeProtocolField.strokeWidth,
|
RuntimeProtocolField.strokeWidth,
|
||||||
RuntimeProtocolField.value,
|
RuntimeProtocolField.value,
|
||||||
@@ -286,6 +316,17 @@ class RuntimeProtocolSchema {
|
|||||||
RuntimeProtocolField.type,
|
RuntimeProtocolField.type,
|
||||||
RuntimeProtocolField.parent,
|
RuntimeProtocolField.parent,
|
||||||
RuntimeProtocolField.asset,
|
RuntimeProtocolField.asset,
|
||||||
|
RuntimeProtocolField.frame,
|
||||||
|
RuntimeProtocolField.pressedFrame,
|
||||||
|
RuntimeProtocolField.disabledFrame,
|
||||||
|
RuntimeProtocolField.sourceX,
|
||||||
|
RuntimeProtocolField.sourceY,
|
||||||
|
RuntimeProtocolField.sourceWidth,
|
||||||
|
RuntimeProtocolField.sourceHeight,
|
||||||
|
RuntimeProtocolField.sliceLeft,
|
||||||
|
RuntimeProtocolField.sliceTop,
|
||||||
|
RuntimeProtocolField.sliceRight,
|
||||||
|
RuntimeProtocolField.sliceBottom,
|
||||||
RuntimeProtocolField.pressedAsset,
|
RuntimeProtocolField.pressedAsset,
|
||||||
RuntimeProtocolField.disabledAsset,
|
RuntimeProtocolField.disabledAsset,
|
||||||
RuntimeProtocolField.animation,
|
RuntimeProtocolField.animation,
|
||||||
@@ -309,6 +350,10 @@ class RuntimeProtocolSchema {
|
|||||||
RuntimeProtocolField.color,
|
RuntimeProtocolField.color,
|
||||||
RuntimeProtocolField.fontSize,
|
RuntimeProtocolField.fontSize,
|
||||||
RuntimeProtocolField.textAlign,
|
RuntimeProtocolField.textAlign,
|
||||||
|
RuntimeProtocolField.textShadowColor,
|
||||||
|
RuntimeProtocolField.textShadowOffsetX,
|
||||||
|
RuntimeProtocolField.textShadowOffsetY,
|
||||||
|
RuntimeProtocolField.textShadowBlur,
|
||||||
RuntimeProtocolField.radius,
|
RuntimeProtocolField.radius,
|
||||||
RuntimeProtocolField.strokeWidth,
|
RuntimeProtocolField.strokeWidth,
|
||||||
RuntimeProtocolField.value,
|
RuntimeProtocolField.value,
|
||||||
|
|||||||
@@ -17,6 +17,100 @@ Color composeRuntimeColorAlpha(Color color, double alpha) {
|
|||||||
return color.withValues(alpha: color.a * alpha.clamp(0.0, 1.0));
|
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,
|
||||||
|
}) {
|
||||||
|
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 sourcePart = Rect.fromLTRB(
|
||||||
|
sourceXs[x],
|
||||||
|
sourceYs[y],
|
||||||
|
sourceXs[x + 1],
|
||||||
|
sourceYs[y + 1],
|
||||||
|
);
|
||||||
|
final destPart = Rect.fromLTRB(
|
||||||
|
destXs[x],
|
||||||
|
destYs[y],
|
||||||
|
destXs[x + 1],
|
||||||
|
destYs[y + 1],
|
||||||
|
);
|
||||||
|
if (sourcePart.width <= 0 ||
|
||||||
|
sourcePart.height <= 0 ||
|
||||||
|
destPart.width <= 0 ||
|
||||||
|
destPart.height <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parts.add((source: sourcePart, destination: destPart));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
class RuntimeComponent extends PositionComponent
|
class RuntimeComponent extends PositionComponent
|
||||||
with HasVisibility, TapCallbacks {
|
with HasVisibility, TapCallbacks {
|
||||||
RuntimeComponent({
|
RuntimeComponent({
|
||||||
@@ -67,7 +161,12 @@ class RuntimeComponent extends PositionComponent
|
|||||||
double get renderAlpha => _runtimeAlpha ?? _node.alpha;
|
double get renderAlpha => _runtimeAlpha ?? _node.alpha;
|
||||||
|
|
||||||
void setRuntimeAlpha(double value) {
|
void setRuntimeAlpha(double value) {
|
||||||
_runtimeAlpha = value.clamp(0, 1).toDouble();
|
final next = value.clamp(0, 1).toDouble();
|
||||||
|
if (_runtimeAlpha == next) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_runtimeAlpha = next;
|
||||||
|
_syncTextStyle(_node);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setParentScroll({
|
void setParentScroll({
|
||||||
@@ -426,12 +525,14 @@ class RuntimeComponent extends PositionComponent
|
|||||||
(_node.type == RuntimeNodeType.sprite ||
|
(_node.type == RuntimeNodeType.sprite ||
|
||||||
_node.type == RuntimeNodeType.image ||
|
_node.type == RuntimeNodeType.image ||
|
||||||
_node.type == RuntimeNodeType.button)) {
|
_node.type == RuntimeNodeType.button)) {
|
||||||
canvas.drawImageRect(
|
final imagePaint = Paint()
|
||||||
image,
|
..color = composeRuntimeColorAlpha(Colors.white, renderAlpha);
|
||||||
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
|
final source = _imageSourceRect(image, _currentImageFrame(_node));
|
||||||
rect,
|
if (_usesNineSlice(source, rect)) {
|
||||||
Paint()..color = composeRuntimeColorAlpha(Colors.white, renderAlpha),
|
_drawNineSliceImage(canvas, image, source, rect, imagePaint);
|
||||||
);
|
} else {
|
||||||
|
canvas.drawImageRect(image, source, rect, imagePaint);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,6 +544,56 @@ class RuntimeComponent extends PositionComponent
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
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) {
|
void _applyBase(RuntimeNode node) {
|
||||||
_syncVisibility();
|
_syncVisibility();
|
||||||
size = Vector2(node.width ?? 40, node.height ?? 40);
|
size = Vector2(node.width ?? 40, node.height ?? 40);
|
||||||
@@ -519,6 +670,19 @@ class RuntimeComponent extends PositionComponent
|
|||||||
return node.asset;
|
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) {
|
void _releaseRetainedImage(String asset, int generation, ui.Image? image) {
|
||||||
if (image == null) {
|
if (image == null) {
|
||||||
return;
|
return;
|
||||||
@@ -961,20 +1125,11 @@ class RuntimeComponent extends PositionComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
final text = label ?? '';
|
final text = label ?? '';
|
||||||
final color = _textColor(node);
|
|
||||||
final style = TextStyle(
|
|
||||||
color: composeRuntimeColorAlpha(color, renderAlpha),
|
|
||||||
fontSize: node.fontSize ?? 18,
|
|
||||||
fontWeight: node.type == RuntimeNodeType.button
|
|
||||||
? FontWeight.w600
|
|
||||||
: FontWeight.normal,
|
|
||||||
);
|
|
||||||
|
|
||||||
final component = _textComponent;
|
final component = _textComponent;
|
||||||
if (component == null) {
|
if (component == null) {
|
||||||
_textComponent = TextComponent(
|
_textComponent = TextComponent(
|
||||||
text: text,
|
text: text,
|
||||||
textRenderer: TextPaint(style: style),
|
textRenderer: TextPaint(style: _textStyle(node)),
|
||||||
anchor: _textAnchor(node),
|
anchor: _textAnchor(node),
|
||||||
position: _textPosition(node),
|
position: _textPosition(node),
|
||||||
priority: priority + 1,
|
priority: priority + 1,
|
||||||
@@ -985,10 +1140,30 @@ class RuntimeComponent extends PositionComponent
|
|||||||
|
|
||||||
component
|
component
|
||||||
..text = text
|
..text = text
|
||||||
..textRenderer = TextPaint(style: style)
|
|
||||||
..anchor = _textAnchor(node)
|
..anchor = _textAnchor(node)
|
||||||
..position = _textPosition(node)
|
..position = _textPosition(node)
|
||||||
..priority = priority + 1;
|
..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
|
@override
|
||||||
@@ -1033,6 +1208,23 @@ class RuntimeComponent extends PositionComponent
|
|||||||
return (node.text ?? '').contains('\n');
|
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) {
|
Color _textColor(RuntimeNode node) {
|
||||||
if (node.type == RuntimeNodeType.button) {
|
if (node.type == RuntimeNodeType.button) {
|
||||||
return Colors.white;
|
return Colors.white;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flame_spine/flame_spine.dart';
|
import 'package:flame_spine/flame_spine.dart';
|
||||||
@@ -32,6 +33,7 @@ class GameResourceManager {
|
|||||||
final RuntimeAsyncGate _asyncGate = RuntimeAsyncGate(initiallyClosed: true);
|
final RuntimeAsyncGate _asyncGate = RuntimeAsyncGate(initiallyClosed: true);
|
||||||
GamePackage? _package;
|
GamePackage? _package;
|
||||||
final Map<String, _ImageResourceRecord> _images = {};
|
final Map<String, _ImageResourceRecord> _images = {};
|
||||||
|
final Map<String, RuntimeTextureAtlas> _textureAtlases = {};
|
||||||
int _cacheBytes = 0;
|
int _cacheBytes = 0;
|
||||||
int _accessCounter = 0;
|
int _accessCounter = 0;
|
||||||
|
|
||||||
@@ -49,8 +51,10 @@ class GameResourceManager {
|
|||||||
|
|
||||||
Future<void> mount(GamePackage package) async {
|
Future<void> mount(GamePackage package) async {
|
||||||
_releaseCachedImages();
|
_releaseCachedImages();
|
||||||
|
_textureAtlases.clear();
|
||||||
_asyncGate.activate();
|
_asyncGate.activate();
|
||||||
_package = package;
|
_package = package;
|
||||||
|
await loadDeclaredTextureAtlases(package.manifest);
|
||||||
await preloadDeclaredImages(package.manifest);
|
await preloadDeclaredImages(package.manifest);
|
||||||
await preloadDeclaredSpines(package.manifest);
|
await preloadDeclaredSpines(package.manifest);
|
||||||
}
|
}
|
||||||
@@ -58,6 +62,7 @@ class GameResourceManager {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_asyncGate.close();
|
_asyncGate.close();
|
||||||
_releaseCachedImages();
|
_releaseCachedImages();
|
||||||
|
_textureAtlases.clear();
|
||||||
_package = null;
|
_package = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +151,17 @@ class GameResourceManager {
|
|||||||
return _loadImage(keyOrPath, failOnError: false, retain: retain);
|
return _loadImage(keyOrPath, failOnError: false, retain: retain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RuntimeTextureFrame? textureFrame(String keyOrPath, String? frameName) {
|
||||||
|
if (frameName == null || frameName.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final path = _tryResolve(keyOrPath);
|
||||||
|
if (path == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return _textureAtlases[path]?.frames[frameName];
|
||||||
|
}
|
||||||
|
|
||||||
Future<SpineComponent?> createSpineComponent(String? keyOrPath) {
|
Future<SpineComponent?> createSpineComponent(String? keyOrPath) {
|
||||||
return _createSpineComponent(keyOrPath);
|
return _createSpineComponent(keyOrPath);
|
||||||
}
|
}
|
||||||
@@ -223,6 +239,24 @@ class GameResourceManager {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadDeclaredTextureAtlases(GamePackageManifest manifest) async {
|
||||||
|
final activePackage = _package;
|
||||||
|
if (activePackage == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (final entry in manifest.resources.entries) {
|
||||||
|
final resource = entry.value;
|
||||||
|
final atlas = resource.atlas;
|
||||||
|
if (resource.type != GameResourceType.image || atlas == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final imagePath = activePackage.resolveResourcePath(entry.key);
|
||||||
|
final atlasPath = activePackage.resolveResourcePath(atlas);
|
||||||
|
final source = await activePackage.readText(atlasPath);
|
||||||
|
_textureAtlases[imagePath] = RuntimeTextureAtlas.fromJsonString(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> preloadDeclaredImages(GamePackageManifest manifest) async {
|
Future<void> preloadDeclaredImages(GamePackageManifest manifest) async {
|
||||||
final futures = <Future<void>>[];
|
final futures = <Future<void>>[];
|
||||||
for (final entry in manifest.resources.entries) {
|
for (final entry in manifest.resources.entries) {
|
||||||
@@ -254,6 +288,100 @@ class GameResourceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RuntimeTextureAtlas {
|
||||||
|
const RuntimeTextureAtlas({required this.frames});
|
||||||
|
|
||||||
|
final Map<String, RuntimeTextureFrame> frames;
|
||||||
|
|
||||||
|
factory RuntimeTextureAtlas.fromJsonString(String source) {
|
||||||
|
final value = jsonDecode(source);
|
||||||
|
if (value is! Map) {
|
||||||
|
throw const FormatException('Texture atlas JSON must be an object');
|
||||||
|
}
|
||||||
|
final framesValue = value['frames'];
|
||||||
|
final frames = <String, RuntimeTextureFrame>{};
|
||||||
|
if (framesValue is Map) {
|
||||||
|
for (final entry in framesValue.entries) {
|
||||||
|
if (entry.key is! String || entry.value is! Map) {
|
||||||
|
throw const FormatException('Texture atlas frames must be objects');
|
||||||
|
}
|
||||||
|
frames[entry.key as String] = RuntimeTextureFrame.fromMap(
|
||||||
|
Map<String, Object?>.from(entry.value as Map),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (framesValue is List) {
|
||||||
|
for (final item in framesValue) {
|
||||||
|
if (item is! Map) {
|
||||||
|
throw const FormatException('Texture atlas frames must be objects');
|
||||||
|
}
|
||||||
|
final map = Map<String, Object?>.from(item);
|
||||||
|
final filename = map['filename'];
|
||||||
|
if (filename is! String || filename.isEmpty) {
|
||||||
|
throw const FormatException(
|
||||||
|
'Texture atlas array frames require filename',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
frames[filename] = RuntimeTextureFrame.fromMap(map);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw const FormatException('Texture atlas frames must be a map or list');
|
||||||
|
}
|
||||||
|
return RuntimeTextureAtlas(frames: frames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RuntimeTextureFrame {
|
||||||
|
const RuntimeTextureFrame({
|
||||||
|
required this.x,
|
||||||
|
required this.y,
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double x;
|
||||||
|
final double y;
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
ui.Rect get rect => ui.Rect.fromLTWH(x, y, width, height);
|
||||||
|
|
||||||
|
factory RuntimeTextureFrame.fromMap(Map<String, Object?> map) {
|
||||||
|
final rotated = map['rotated'];
|
||||||
|
if (rotated == true) {
|
||||||
|
throw const FormatException(
|
||||||
|
'Rotated TexturePacker frames are unsupported',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final frame = map['frame'];
|
||||||
|
if (frame is! Map) {
|
||||||
|
throw const FormatException('TexturePacker frame must be an object');
|
||||||
|
}
|
||||||
|
final frameMap = Map<String, Object?>.from(frame);
|
||||||
|
return RuntimeTextureFrame(
|
||||||
|
x: _number(frameMap, 'x'),
|
||||||
|
y: _number(frameMap, 'y'),
|
||||||
|
width: _positiveNumber(frameMap, 'w'),
|
||||||
|
height: _positiveNumber(frameMap, 'h'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static double _number(Map<String, Object?> map, String key) {
|
||||||
|
final value = map[key];
|
||||||
|
if (value is num) {
|
||||||
|
return value.toDouble();
|
||||||
|
}
|
||||||
|
throw FormatException('TexturePacker frame.$key must be a number');
|
||||||
|
}
|
||||||
|
|
||||||
|
static double _positiveNumber(Map<String, Object?> map, String key) {
|
||||||
|
final value = _number(map, key);
|
||||||
|
if (value <= 0) {
|
||||||
|
throw FormatException('TexturePacker frame.$key must be > 0');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum GameResourceState { idle, loading, ready, failed, disposed }
|
enum GameResourceState { idle, loading, ready, failed, disposed }
|
||||||
|
|
||||||
class ResourceLoadException implements Exception {
|
class ResourceLoadException implements Exception {
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ void main() {
|
|||||||
'type': 'button',
|
'type': 'button',
|
||||||
'parent': 'top_bar',
|
'parent': 'top_bar',
|
||||||
'asset': 'dice_normal',
|
'asset': 'dice_normal',
|
||||||
|
'frame': 'dice_idle.png',
|
||||||
|
'pressedFrame': 'dice_pressed.png',
|
||||||
|
'disabledFrame': 'dice_disabled.png',
|
||||||
|
'sourceX': 4,
|
||||||
|
'sourceY': 5,
|
||||||
|
'sourceWidth': 64,
|
||||||
|
'sourceHeight': 32,
|
||||||
|
'sliceLeft': 6,
|
||||||
|
'sliceTop': 7,
|
||||||
|
'sliceRight': 8,
|
||||||
|
'sliceBottom': 9,
|
||||||
'pressedAsset': 'dice_pressed',
|
'pressedAsset': 'dice_pressed',
|
||||||
'disabledAsset': 'dice_disabled',
|
'disabledAsset': 'dice_disabled',
|
||||||
'animation': 'idle',
|
'animation': 'idle',
|
||||||
@@ -33,6 +44,10 @@ void main() {
|
|||||||
'color': '#112233',
|
'color': '#112233',
|
||||||
'fontSize': 18,
|
'fontSize': 18,
|
||||||
'textAlign': 'left',
|
'textAlign': 'left',
|
||||||
|
'textShadowColor': '#80000000',
|
||||||
|
'textShadowOffsetX': 2,
|
||||||
|
'textShadowOffsetY': 3,
|
||||||
|
'textShadowBlur': 4,
|
||||||
'radius': 10,
|
'radius': 10,
|
||||||
'strokeWidth': 3,
|
'strokeWidth': 3,
|
||||||
'value': 0.6,
|
'value': 0.6,
|
||||||
@@ -68,6 +83,17 @@ void main() {
|
|||||||
expect(node.type, 'button');
|
expect(node.type, 'button');
|
||||||
expect(node.parent, 'top_bar');
|
expect(node.parent, 'top_bar');
|
||||||
expect(node.asset, 'dice_normal');
|
expect(node.asset, 'dice_normal');
|
||||||
|
expect(node.frame, 'dice_idle.png');
|
||||||
|
expect(node.pressedFrame, 'dice_pressed.png');
|
||||||
|
expect(node.disabledFrame, 'dice_disabled.png');
|
||||||
|
expect(node.sourceX, 4);
|
||||||
|
expect(node.sourceY, 5);
|
||||||
|
expect(node.sourceWidth, 64);
|
||||||
|
expect(node.sourceHeight, 32);
|
||||||
|
expect(node.sliceLeft, 6);
|
||||||
|
expect(node.sliceTop, 7);
|
||||||
|
expect(node.sliceRight, 8);
|
||||||
|
expect(node.sliceBottom, 9);
|
||||||
expect(node.pressedAsset, 'dice_pressed');
|
expect(node.pressedAsset, 'dice_pressed');
|
||||||
expect(node.disabledAsset, 'dice_disabled');
|
expect(node.disabledAsset, 'dice_disabled');
|
||||||
expect(node.animation, 'idle');
|
expect(node.animation, 'idle');
|
||||||
@@ -91,6 +117,10 @@ void main() {
|
|||||||
expect(node.color, const Color(0xff112233));
|
expect(node.color, const Color(0xff112233));
|
||||||
expect(node.fontSize, 18);
|
expect(node.fontSize, 18);
|
||||||
expect(node.textAlign, 'left');
|
expect(node.textAlign, 'left');
|
||||||
|
expect(node.textShadowColor, const Color(0x80000000));
|
||||||
|
expect(node.textShadowOffsetX, 2);
|
||||||
|
expect(node.textShadowOffsetY, 3);
|
||||||
|
expect(node.textShadowBlur, 4);
|
||||||
expect(node.radius, 10);
|
expect(node.radius, 10);
|
||||||
expect(node.strokeWidth, 3);
|
expect(node.strokeWidth, 3);
|
||||||
expect(node.value, 0.6);
|
expect(node.value, 0.6);
|
||||||
@@ -135,6 +165,21 @@ void main() {
|
|||||||
expect(node.rotation, 0);
|
expect(node.rotation, 0);
|
||||||
expect(node.loop, isTrue);
|
expect(node.loop, isTrue);
|
||||||
expect(node.textAlign, 'center');
|
expect(node.textAlign, 'center');
|
||||||
|
expect(node.textShadowColor, isNull);
|
||||||
|
expect(node.textShadowOffsetX, isNull);
|
||||||
|
expect(node.textShadowOffsetY, isNull);
|
||||||
|
expect(node.textShadowBlur, isNull);
|
||||||
|
expect(node.frame, isNull);
|
||||||
|
expect(node.pressedFrame, isNull);
|
||||||
|
expect(node.disabledFrame, isNull);
|
||||||
|
expect(node.sourceX, isNull);
|
||||||
|
expect(node.sourceY, isNull);
|
||||||
|
expect(node.sourceWidth, isNull);
|
||||||
|
expect(node.sourceHeight, isNull);
|
||||||
|
expect(node.sliceLeft, isNull);
|
||||||
|
expect(node.sliceTop, isNull);
|
||||||
|
expect(node.sliceRight, isNull);
|
||||||
|
expect(node.sliceBottom, isNull);
|
||||||
expect(node.scrollbarVisible, isTrue);
|
expect(node.scrollbarVisible, isTrue);
|
||||||
expect(node.paddingLeft, 0);
|
expect(node.paddingLeft, 0);
|
||||||
expect(node.paddingTop, 0);
|
expect(node.paddingTop, 0);
|
||||||
@@ -170,11 +215,26 @@ void main() {
|
|||||||
'paddingBottom': 11,
|
'paddingBottom': 11,
|
||||||
'contentWidth': 120,
|
'contentWidth': 120,
|
||||||
'contentHeight': 100,
|
'contentHeight': 100,
|
||||||
|
'sourceX': 3,
|
||||||
|
'sourceY': 4,
|
||||||
|
'sourceWidth': 40,
|
||||||
|
'sourceHeight': 41,
|
||||||
|
'frame': 'piece.png',
|
||||||
|
'pressedFrame': 'piece_down.png',
|
||||||
|
'disabledFrame': 'piece_disabled.png',
|
||||||
|
'sliceLeft': 5,
|
||||||
|
'sliceTop': 6,
|
||||||
|
'sliceRight': 7,
|
||||||
|
'sliceBottom': 8,
|
||||||
'pressedAsset': 'button_pressed',
|
'pressedAsset': 'button_pressed',
|
||||||
'disabledAsset': 'button_disabled',
|
'disabledAsset': 'button_disabled',
|
||||||
'scrollX': 90,
|
'scrollX': 90,
|
||||||
'scrollY': 80,
|
'scrollY': 80,
|
||||||
'textAlign': 'right',
|
'textAlign': 'right',
|
||||||
|
'textShadowColor': '#40000000',
|
||||||
|
'textShadowOffsetX': 1,
|
||||||
|
'textShadowOffsetY': 2,
|
||||||
|
'textShadowBlur': 3,
|
||||||
'preset': 'trail',
|
'preset': 'trail',
|
||||||
'count': 12,
|
'count': 12,
|
||||||
});
|
});
|
||||||
@@ -197,11 +257,26 @@ void main() {
|
|||||||
expect(updated.paddingBottom, 11);
|
expect(updated.paddingBottom, 11);
|
||||||
expect(updated.contentWidth, 120);
|
expect(updated.contentWidth, 120);
|
||||||
expect(updated.contentHeight, 100);
|
expect(updated.contentHeight, 100);
|
||||||
|
expect(updated.sourceX, 3);
|
||||||
|
expect(updated.sourceY, 4);
|
||||||
|
expect(updated.sourceWidth, 40);
|
||||||
|
expect(updated.sourceHeight, 41);
|
||||||
|
expect(updated.frame, 'piece.png');
|
||||||
|
expect(updated.pressedFrame, 'piece_down.png');
|
||||||
|
expect(updated.disabledFrame, 'piece_disabled.png');
|
||||||
|
expect(updated.sliceLeft, 5);
|
||||||
|
expect(updated.sliceTop, 6);
|
||||||
|
expect(updated.sliceRight, 7);
|
||||||
|
expect(updated.sliceBottom, 8);
|
||||||
expect(updated.pressedAsset, 'button_pressed');
|
expect(updated.pressedAsset, 'button_pressed');
|
||||||
expect(updated.disabledAsset, 'button_disabled');
|
expect(updated.disabledAsset, 'button_disabled');
|
||||||
expect(updated.scrollX, 68);
|
expect(updated.scrollX, 68);
|
||||||
expect(updated.scrollY, 60);
|
expect(updated.scrollY, 60);
|
||||||
expect(updated.textAlign, 'right');
|
expect(updated.textAlign, 'right');
|
||||||
|
expect(updated.textShadowColor, const Color(0x40000000));
|
||||||
|
expect(updated.textShadowOffsetX, 1);
|
||||||
|
expect(updated.textShadowOffsetY, 2);
|
||||||
|
expect(updated.textShadowBlur, 3);
|
||||||
expect(updated.preset, 'trail');
|
expect(updated.preset, 'trail');
|
||||||
expect(updated.count, 12);
|
expect(updated.count, 12);
|
||||||
});
|
});
|
||||||
@@ -235,6 +310,16 @@ void main() {
|
|||||||
() => RuntimeNode.fromMap({'id': 'a', 'type': 'progress', 'value': 2}),
|
() => RuntimeNode.fromMap({'id': 'a', 'type': 'progress', 'value': 2}),
|
||||||
throwsFormatException,
|
throwsFormatException,
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
() =>
|
||||||
|
RuntimeNode.fromMap({'id': 'a', 'type': 'image', 'sourceWidth': 0}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
() =>
|
||||||
|
RuntimeNode.fromMap({'id': 'a', 'type': 'image', 'sliceLeft': -1}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
() => RuntimeNode.fromMap({
|
() => RuntimeNode.fromMap({
|
||||||
'id': 'a',
|
'id': 'a',
|
||||||
@@ -243,6 +328,14 @@ void main() {
|
|||||||
}),
|
}),
|
||||||
throwsFormatException,
|
throwsFormatException,
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
() => RuntimeNode.fromMap({
|
||||||
|
'id': 'a',
|
||||||
|
'type': 'text',
|
||||||
|
'textShadowBlur': -1,
|
||||||
|
}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
() =>
|
() =>
|
||||||
RuntimeNode.fromMap({'id': 'a', 'type': 'listView', 'scrollY': -1}),
|
RuntimeNode.fromMap({'id': 'a', 'type': 'listView', 'scrollY': -1}),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ void main() {
|
|||||||
'board': {
|
'board': {
|
||||||
'type': 'image',
|
'type': 'image',
|
||||||
'path': 'assets/board.png',
|
'path': 'assets/board.png',
|
||||||
|
'atlas': 'assets/board.json',
|
||||||
'preload': 'lazy',
|
'preload': 'lazy',
|
||||||
'group': 'board',
|
'group': 'board',
|
||||||
},
|
},
|
||||||
@@ -53,6 +54,7 @@ void main() {
|
|||||||
expect(manifest.display.scaleMode, 'fit');
|
expect(manifest.display.scaleMode, 'fit');
|
||||||
expect(manifest.resources['board']?.type, 'image');
|
expect(manifest.resources['board']?.type, 'image');
|
||||||
expect(manifest.resources['board']?.path, 'assets/board.png');
|
expect(manifest.resources['board']?.path, 'assets/board.png');
|
||||||
|
expect(manifest.resources['board']?.atlas, 'assets/board.json');
|
||||||
expect(manifest.resources['board']?.preload, GameResourcePreload.lazy);
|
expect(manifest.resources['board']?.preload, GameResourcePreload.lazy);
|
||||||
expect(manifest.resources['board']?.group, 'board');
|
expect(manifest.resources['board']?.group, 'board');
|
||||||
expect(manifest.resources['roll']?.type, GameResourceType.audio);
|
expect(manifest.resources['roll']?.type, GameResourceType.audio);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flame_lua_runtime/runtime/protocol/runtime_protocol.dart';
|
|||||||
import 'package:flame_lua_runtime/runtime/rendering/runtime_component.dart';
|
import 'package:flame_lua_runtime/runtime/rendering/runtime_component.dart';
|
||||||
import 'package:flame_lua_runtime/runtime/resources/game_resource_manager.dart';
|
import 'package:flame_lua_runtime/runtime/resources/game_resource_manager.dart';
|
||||||
import 'package:flutter/material.dart' show Color;
|
import 'package:flutter/material.dart' show Color;
|
||||||
|
import 'package:flutter/rendering.dart' show Rect;
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -138,6 +139,125 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('computes atlas source region and clamps to image bounds', () {
|
||||||
|
expect(
|
||||||
|
runtimeImageSourceRect(
|
||||||
|
imageWidth: 100,
|
||||||
|
imageHeight: 80,
|
||||||
|
sourceX: 10,
|
||||||
|
sourceY: 12,
|
||||||
|
sourceWidth: 30,
|
||||||
|
sourceHeight: 20,
|
||||||
|
),
|
||||||
|
const Rect.fromLTWH(10, 12, 30, 20),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
runtimeImageSourceRect(
|
||||||
|
imageWidth: 100,
|
||||||
|
imageHeight: 80,
|
||||||
|
sourceX: 90,
|
||||||
|
sourceY: 70,
|
||||||
|
sourceWidth: 30,
|
||||||
|
sourceHeight: 20,
|
||||||
|
),
|
||||||
|
const Rect.fromLTWH(90, 70, 10, 10),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computes nine-slice source and destination rects', () {
|
||||||
|
final parts = runtimeNineSliceRects(
|
||||||
|
source: const Rect.fromLTWH(10, 20, 30, 40),
|
||||||
|
destination: const Rect.fromLTWH(0, 0, 90, 120),
|
||||||
|
sliceLeft: 5,
|
||||||
|
sliceTop: 6,
|
||||||
|
sliceRight: 7,
|
||||||
|
sliceBottom: 8,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parts, hasLength(9));
|
||||||
|
expect(parts.first.source, const Rect.fromLTRB(10, 20, 15, 26));
|
||||||
|
expect(parts.first.destination, const Rect.fromLTRB(0, 0, 5, 6));
|
||||||
|
expect(parts[4].source, const Rect.fromLTRB(15, 26, 33, 52));
|
||||||
|
expect(parts[4].destination, const Rect.fromLTRB(5, 6, 83, 112));
|
||||||
|
expect(parts.last.source, const Rect.fromLTRB(33, 52, 40, 60));
|
||||||
|
expect(parts.last.destination, const Rect.fromLTRB(83, 112, 90, 120));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updates text alpha style without rebuilding text component', () {
|
||||||
|
final component = RuntimeComponent(
|
||||||
|
node: const RuntimeNode(
|
||||||
|
id: 'text',
|
||||||
|
type: RuntimeNodeType.text,
|
||||||
|
text: 'Fade me',
|
||||||
|
alpha: 0,
|
||||||
|
color: Color(0xffffffff),
|
||||||
|
),
|
||||||
|
resources: GameResourceManager(),
|
||||||
|
onNodeTap: (_, __) {},
|
||||||
|
);
|
||||||
|
|
||||||
|
final text = component.children.whereType<TextComponent>().single;
|
||||||
|
expect(((text.textRenderer as TextPaint).style.color!).a, 0);
|
||||||
|
|
||||||
|
component.setRuntimeAlpha(1);
|
||||||
|
|
||||||
|
final updatedText = component.children.whereType<TextComponent>().single;
|
||||||
|
expect(identical(updatedText, text), isTrue);
|
||||||
|
expect(((updatedText.textRenderer as TextPaint).style.color!).a, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'updates button text alpha style without rebuilding text component',
|
||||||
|
() {
|
||||||
|
final component = RuntimeComponent(
|
||||||
|
node: const RuntimeNode(
|
||||||
|
id: 'button',
|
||||||
|
type: RuntimeNodeType.button,
|
||||||
|
text: 'Fade me',
|
||||||
|
alpha: 0,
|
||||||
|
),
|
||||||
|
resources: GameResourceManager(),
|
||||||
|
onNodeTap: (_, __) {},
|
||||||
|
);
|
||||||
|
|
||||||
|
final text = component.children.whereType<TextComponent>().single;
|
||||||
|
expect(((text.textRenderer as TextPaint).style.color!).a, 0);
|
||||||
|
|
||||||
|
component.setRuntimeAlpha(1);
|
||||||
|
|
||||||
|
final updatedText = component.children
|
||||||
|
.whereType<TextComponent>()
|
||||||
|
.single;
|
||||||
|
expect(identical(updatedText, text), isTrue);
|
||||||
|
expect(((updatedText.textRenderer as TextPaint).style.color!).a, 1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('applies text shadow style', () {
|
||||||
|
final component = RuntimeComponent(
|
||||||
|
node: const RuntimeNode(
|
||||||
|
id: 'text',
|
||||||
|
type: RuntimeNodeType.text,
|
||||||
|
text: 'Shadowed',
|
||||||
|
alpha: 0.5,
|
||||||
|
textShadowColor: Color(0x80000000),
|
||||||
|
textShadowOffsetX: 2,
|
||||||
|
textShadowOffsetY: 3,
|
||||||
|
textShadowBlur: 4,
|
||||||
|
),
|
||||||
|
resources: GameResourceManager(),
|
||||||
|
onNodeTap: (_, __) {},
|
||||||
|
);
|
||||||
|
|
||||||
|
final text = component.children.whereType<TextComponent>().single;
|
||||||
|
final style = (text.textRenderer as TextPaint).style;
|
||||||
|
final shadow = style.shadows!.single;
|
||||||
|
expect(shadow.color.a, closeTo(0.25, 0.003));
|
||||||
|
expect(shadow.offset.dx, 2);
|
||||||
|
expect(shadow.offset.dy, 3);
|
||||||
|
expect(shadow.blurRadius, 4);
|
||||||
|
});
|
||||||
|
|
||||||
test('multi-line non-button text is top aligned', () {
|
test('multi-line non-button text is top aligned', () {
|
||||||
final component = RuntimeComponent(
|
final component = RuntimeComponent(
|
||||||
node: const RuntimeNode(
|
node: const RuntimeNode(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async' as async;
|
import 'dart:async' as async;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui' show Rect;
|
||||||
|
|
||||||
import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
|
import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
|
||||||
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
|
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
|
||||||
@@ -61,6 +62,19 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test('loads TexturePacker atlas frames for image resources', () async {
|
||||||
|
final resources = GameResourceManager();
|
||||||
|
final package = await _createTextureAtlasPackage('texture_atlas');
|
||||||
|
|
||||||
|
await resources.mount(package);
|
||||||
|
|
||||||
|
final idle = resources.textureFrame('ui', 'button_idle.png');
|
||||||
|
final pressed = resources.textureFrame('ui', 'button_pressed.png');
|
||||||
|
expect(idle?.rect, Rect.fromLTWH(2, 3, 40, 20));
|
||||||
|
expect(pressed?.rect, Rect.fromLTWH(44, 3, 40, 20));
|
||||||
|
expect(resources.textureFrame('ui', 'missing.png'), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
test('exports image debug json and evicts failed records', () async {
|
test('exports image debug json and evicts failed records', () async {
|
||||||
final resources = GameResourceManager();
|
final resources = GameResourceManager();
|
||||||
final package = await _createPackage('debug_json');
|
final package = await _createPackage('debug_json');
|
||||||
@@ -332,6 +346,87 @@ Future<GamePackage> _createPackage(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<GamePackage> _createTextureAtlasPackage(String name) async {
|
||||||
|
final root = await Directory.systemTemp.createTemp('resource_${name}_');
|
||||||
|
Directory('${root.path}/assets').createSync(recursive: true);
|
||||||
|
File('${root.path}/assets/ui.png').writeAsBytesSync(_pngBytes);
|
||||||
|
File('${root.path}/assets/ui.json').writeAsStringSync('''
|
||||||
|
{
|
||||||
|
"frames": {
|
||||||
|
"button_idle.png": {
|
||||||
|
"frame": { "x": 2, "y": 3, "w": 40, "h": 20 },
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
|
||||||
|
addTearDown(() {
|
||||||
|
if (root.existsSync()) {
|
||||||
|
root.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final hashAtlas = RuntimeTextureAtlas.fromJsonString(
|
||||||
|
File('${root.path}/assets/ui.json').readAsStringSync(),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
hashAtlas.frames['button_idle.png']?.rect,
|
||||||
|
Rect.fromLTWH(2, 3, 40, 20),
|
||||||
|
);
|
||||||
|
final arrayAtlas = RuntimeTextureAtlas.fromJsonString('''
|
||||||
|
{
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"filename": "button_pressed.png",
|
||||||
|
"frame": { "x": 44, "y": 3, "w": 40, "h": 20 },
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
final mergedAtlas =
|
||||||
|
'''
|
||||||
|
{
|
||||||
|
"frames": {
|
||||||
|
"button_idle.png": {
|
||||||
|
"frame": { "x": 2, "y": 3, "w": 40, "h": 20 },
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false
|
||||||
|
},
|
||||||
|
"button_pressed.png": {
|
||||||
|
"frame": { "x": ${arrayAtlas.frames['button_pressed.png']!.x}, "y": 3, "w": 40, "h": 20 },
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
File('${root.path}/assets/ui.json').writeAsStringSync(mergedAtlas);
|
||||||
|
|
||||||
|
return GamePackage.file(
|
||||||
|
rootPath: root.path,
|
||||||
|
manifest: GamePackageManifest(
|
||||||
|
gameId: 'test',
|
||||||
|
name: 'Test',
|
||||||
|
version: '0.1.0',
|
||||||
|
runtimeApiVersion: 1,
|
||||||
|
entry: 'scripts/main.lua',
|
||||||
|
assetsBase: 'assets',
|
||||||
|
resources: const {
|
||||||
|
'ui': GameResource(
|
||||||
|
type: GameResourceType.image,
|
||||||
|
path: 'assets/ui.png',
|
||||||
|
atlas: 'assets/ui.json',
|
||||||
|
preload: GameResourcePreload.lazy,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<GamePackage> _createMultiImagePackage(String name) async {
|
Future<GamePackage> _createMultiImagePackage(String name) async {
|
||||||
final root = await Directory.systemTemp.createTemp('resource_${name}_');
|
final root = await Directory.systemTemp.createTemp('resource_${name}_');
|
||||||
Directory('${root.path}/assets').createSync(recursive: true);
|
Directory('${root.path}/assets').createSync(recursive: true);
|
||||||
|
|||||||
@@ -86,6 +86,17 @@
|
|||||||
---@field type RuntimeNodeType
|
---@field type RuntimeNodeType
|
||||||
---@field parent? string
|
---@field parent? string
|
||||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||||
|
---@field frame? string TexturePacker frame name within the asset atlas.
|
||||||
|
---@field pressedFrame? string Button pressed-state TexturePacker frame name.
|
||||||
|
---@field disabledFrame? string Button disabled-state TexturePacker frame name.
|
||||||
|
---@field sourceX? number Source atlas region x in image pixels.
|
||||||
|
---@field sourceY? number Source atlas region y in image pixels.
|
||||||
|
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||||
|
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||||
|
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||||
|
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||||
|
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||||
|
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||||
---@field pressedAsset? string Button pressed-state image asset key.
|
---@field pressedAsset? string Button pressed-state image asset key.
|
||||||
---@field disabledAsset? string Button disabled-state image asset key.
|
---@field disabledAsset? string Button disabled-state image asset key.
|
||||||
---@field animation? string
|
---@field animation? string
|
||||||
@@ -109,6 +120,10 @@
|
|||||||
---@field color? string
|
---@field color? string
|
||||||
---@field fontSize? number
|
---@field fontSize? number
|
||||||
---@field textAlign? RuntimeTextAlign
|
---@field textAlign? RuntimeTextAlign
|
||||||
|
---@field textShadowColor? string
|
||||||
|
---@field textShadowOffsetX? number
|
||||||
|
---@field textShadowOffsetY? number
|
||||||
|
---@field textShadowBlur? number
|
||||||
---@field radius? number
|
---@field radius? number
|
||||||
---@field strokeWidth? number
|
---@field strokeWidth? number
|
||||||
---@field value? number
|
---@field value? number
|
||||||
@@ -143,6 +158,17 @@
|
|||||||
---@field type? RuntimeNodeType
|
---@field type? RuntimeNodeType
|
||||||
---@field parent? string
|
---@field parent? string
|
||||||
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
|
||||||
|
---@field frame? string TexturePacker frame name within the asset atlas.
|
||||||
|
---@field pressedFrame? string Button pressed-state TexturePacker frame name.
|
||||||
|
---@field disabledFrame? string Button disabled-state TexturePacker frame name.
|
||||||
|
---@field sourceX? number Source atlas region x in image pixels.
|
||||||
|
---@field sourceY? number Source atlas region y in image pixels.
|
||||||
|
---@field sourceWidth? number Source atlas region width in image pixels.
|
||||||
|
---@field sourceHeight? number Source atlas region height in image pixels.
|
||||||
|
---@field sliceLeft? number Left nine-slice inset in source pixels.
|
||||||
|
---@field sliceTop? number Top nine-slice inset in source pixels.
|
||||||
|
---@field sliceRight? number Right nine-slice inset in source pixels.
|
||||||
|
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
|
||||||
---@field pressedAsset? string Button pressed-state image asset key.
|
---@field pressedAsset? string Button pressed-state image asset key.
|
||||||
---@field disabledAsset? string Button disabled-state image asset key.
|
---@field disabledAsset? string Button disabled-state image asset key.
|
||||||
---@field animation? string
|
---@field animation? string
|
||||||
@@ -166,6 +192,10 @@
|
|||||||
---@field color? string
|
---@field color? string
|
||||||
---@field fontSize? number
|
---@field fontSize? number
|
||||||
---@field textAlign? RuntimeTextAlign
|
---@field textAlign? RuntimeTextAlign
|
||||||
|
---@field textShadowColor? string
|
||||||
|
---@field textShadowOffsetX? number
|
||||||
|
---@field textShadowOffsetY? number
|
||||||
|
---@field textShadowBlur? number
|
||||||
---@field radius? number
|
---@field radius? number
|
||||||
---@field strokeWidth? number
|
---@field strokeWidth? number
|
||||||
---@field value? number
|
---@field value? number
|
||||||
|
|||||||
Reference in New Issue
Block a user