diff --git a/CHANGELOG.md b/CHANGELOG.md index c0cebf8..a5c3d3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- 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. ## 0.1.0 diff --git a/docs/protocol.md b/docs/protocol.md index 1293aa0..f0640fb 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -44,6 +44,16 @@ Final opacity = color alpha × node alpha × runtime animation alpha `#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. + ## RuntimeCommand Runtime commands request generic side effects owned by Dart/Flame. diff --git a/example/assets/games/flight/scripts/runtime_defs.lua b/example/assets/games/flight/scripts/runtime_defs.lua index 570b2ae..994f9dc 100644 --- a/example/assets/games/flight/scripts/runtime_defs.lua +++ b/example/assets/games/flight/scripts/runtime_defs.lua @@ -109,6 +109,10 @@ ---@field color? string ---@field fontSize? number ---@field textAlign? RuntimeTextAlign +---@field textShadowColor? string +---@field textShadowOffsetX? number +---@field textShadowOffsetY? number +---@field textShadowBlur? number ---@field radius? number ---@field strokeWidth? number ---@field value? number @@ -166,6 +170,10 @@ ---@field color? string ---@field fontSize? number ---@field textAlign? RuntimeTextAlign +---@field textShadowColor? string +---@field textShadowOffsetX? number +---@field textShadowOffsetY? number +---@field textShadowBlur? number ---@field radius? number ---@field strokeWidth? number ---@field value? number diff --git a/example/assets/games/ludo/scripts/runtime_defs.lua b/example/assets/games/ludo/scripts/runtime_defs.lua index 97ce774..443b779 100644 --- a/example/assets/games/ludo/scripts/runtime_defs.lua +++ b/example/assets/games/ludo/scripts/runtime_defs.lua @@ -109,6 +109,10 @@ ---@field color? string ---@field fontSize? number ---@field textAlign? RuntimeTextAlign +---@field textShadowColor? string +---@field textShadowOffsetX? number +---@field textShadowOffsetY? number +---@field textShadowBlur? number ---@field radius? number ---@field strokeWidth? number ---@field value? number @@ -166,6 +170,10 @@ ---@field color? string ---@field fontSize? number ---@field textAlign? RuntimeTextAlign +---@field textShadowColor? string +---@field textShadowOffsetX? number +---@field textShadowOffsetY? number +---@field textShadowBlur? number ---@field radius? number ---@field strokeWidth? number ---@field value? number diff --git a/example/assets/games/showcase/scripts/runtime_defs.lua b/example/assets/games/showcase/scripts/runtime_defs.lua index 4c9674d..35016f4 100644 --- a/example/assets/games/showcase/scripts/runtime_defs.lua +++ b/example/assets/games/showcase/scripts/runtime_defs.lua @@ -109,6 +109,10 @@ ---@field color? string ---@field fontSize? number ---@field textAlign? RuntimeTextAlign +---@field textShadowColor? string +---@field textShadowOffsetX? number +---@field textShadowOffsetY? number +---@field textShadowBlur? number ---@field radius? number ---@field strokeWidth? number ---@field value? number @@ -166,6 +170,10 @@ ---@field color? string ---@field fontSize? number ---@field textAlign? RuntimeTextAlign +---@field textShadowColor? string +---@field textShadowOffsetX? number +---@field textShadowOffsetY? number +---@field textShadowBlur? number ---@field radius? number ---@field strokeWidth? number ---@field value? number diff --git a/example/assets/games/template/scripts/runtime_defs.lua b/example/assets/games/template/scripts/runtime_defs.lua index ff1c21d..e2a852c 100644 --- a/example/assets/games/template/scripts/runtime_defs.lua +++ b/example/assets/games/template/scripts/runtime_defs.lua @@ -109,6 +109,10 @@ ---@field color? string ---@field fontSize? number ---@field textAlign? RuntimeTextAlign +---@field textShadowColor? string +---@field textShadowOffsetX? number +---@field textShadowOffsetY? number +---@field textShadowBlur? number ---@field radius? number ---@field strokeWidth? number ---@field value? number @@ -166,6 +170,10 @@ ---@field color? string ---@field fontSize? number ---@field textAlign? RuntimeTextAlign +---@field textShadowColor? string +---@field textShadowOffsetX? number +---@field textShadowOffsetY? number +---@field textShadowBlur? number ---@field radius? number ---@field strokeWidth? number ---@field value? number diff --git a/lib/runtime/models/runtime_node.dart b/lib/runtime/models/runtime_node.dart index e7c82b4..4bded4f 100644 --- a/lib/runtime/models/runtime_node.dart +++ b/lib/runtime/models/runtime_node.dart @@ -31,6 +31,10 @@ class RuntimeNode { this.color, this.fontSize, this.textAlign = RuntimeTextAlignValue.center, + this.textShadowColor, + this.textShadowOffsetX, + this.textShadowOffsetY, + this.textShadowBlur, this.radius, this.strokeWidth, this.value, @@ -89,6 +93,10 @@ class RuntimeNode { final Color? color; final double? fontSize; final String textAlign; + final Color? textShadowColor; + final double? textShadowOffsetX; + final double? textShadowOffsetY; + final double? textShadowBlur; final double? radius; final double? strokeWidth; final double? value; @@ -232,6 +240,18 @@ class RuntimeNode { color: _colorProp(props, RuntimeProtocolField.color) ?? color, fontSize: _doubleProp(props, RuntimeProtocolField.fontSize) ?? fontSize, 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, strokeWidth: _doubleProp(props, RuntimeProtocolField.strokeWidth) ?? strokeWidth, @@ -352,6 +372,19 @@ class RuntimeNode { color: _colorProp(map, RuntimeProtocolField.color), fontSize: _doubleProp(map, RuntimeProtocolField.fontSize), 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), strokeWidth: _doubleProp(map, RuntimeProtocolField.strokeWidth), value: _normalizedValueProp(map, RuntimeProtocolField.value), diff --git a/lib/runtime/protocol/runtime_protocol.dart b/lib/runtime/protocol/runtime_protocol.dart index fba464f..112360e 100644 --- a/lib/runtime/protocol/runtime_protocol.dart +++ b/lib/runtime/protocol/runtime_protocol.dart @@ -161,6 +161,10 @@ class RuntimeProtocolField { static const color = 'color'; static const fontSize = 'fontSize'; 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 strokeWidth = 'strokeWidth'; static const value = 'value'; @@ -244,6 +248,10 @@ class RuntimeProtocolSchema { RuntimeProtocolField.color, RuntimeProtocolField.fontSize, RuntimeProtocolField.textAlign, + RuntimeProtocolField.textShadowColor, + RuntimeProtocolField.textShadowOffsetX, + RuntimeProtocolField.textShadowOffsetY, + RuntimeProtocolField.textShadowBlur, RuntimeProtocolField.radius, RuntimeProtocolField.strokeWidth, RuntimeProtocolField.value, @@ -309,6 +317,10 @@ class RuntimeProtocolSchema { RuntimeProtocolField.color, RuntimeProtocolField.fontSize, RuntimeProtocolField.textAlign, + RuntimeProtocolField.textShadowColor, + RuntimeProtocolField.textShadowOffsetX, + RuntimeProtocolField.textShadowOffsetY, + RuntimeProtocolField.textShadowBlur, RuntimeProtocolField.radius, RuntimeProtocolField.strokeWidth, RuntimeProtocolField.value, diff --git a/lib/runtime/rendering/runtime_component.dart b/lib/runtime/rendering/runtime_component.dart index cd7e2c4..3dd2f90 100644 --- a/lib/runtime/rendering/runtime_component.dart +++ b/lib/runtime/rendering/runtime_component.dart @@ -968,6 +968,7 @@ class RuntimeComponent extends PositionComponent fontWeight: node.type == RuntimeNodeType.button ? FontWeight.w600 : FontWeight.normal, + shadows: _textShadows(node), ); final component = _textComponent; @@ -1033,6 +1034,23 @@ class RuntimeComponent extends PositionComponent return (node.text ?? '').contains('\n'); } + List? _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; diff --git a/test/runtime/models/runtime_node_test.dart b/test/runtime/models/runtime_node_test.dart index ee1492f..7670d52 100644 --- a/test/runtime/models/runtime_node_test.dart +++ b/test/runtime/models/runtime_node_test.dart @@ -33,6 +33,10 @@ void main() { 'color': '#112233', 'fontSize': 18, 'textAlign': 'left', + 'textShadowColor': '#80000000', + 'textShadowOffsetX': 2, + 'textShadowOffsetY': 3, + 'textShadowBlur': 4, 'radius': 10, 'strokeWidth': 3, 'value': 0.6, @@ -91,6 +95,10 @@ void main() { expect(node.color, const Color(0xff112233)); expect(node.fontSize, 18); 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.strokeWidth, 3); expect(node.value, 0.6); @@ -135,6 +143,10 @@ void main() { expect(node.rotation, 0); expect(node.loop, isTrue); expect(node.textAlign, 'center'); + expect(node.textShadowColor, isNull); + expect(node.textShadowOffsetX, isNull); + expect(node.textShadowOffsetY, isNull); + expect(node.textShadowBlur, isNull); expect(node.scrollbarVisible, isTrue); expect(node.paddingLeft, 0); expect(node.paddingTop, 0); @@ -175,6 +187,10 @@ void main() { 'scrollX': 90, 'scrollY': 80, 'textAlign': 'right', + 'textShadowColor': '#40000000', + 'textShadowOffsetX': 1, + 'textShadowOffsetY': 2, + 'textShadowBlur': 3, 'preset': 'trail', 'count': 12, }); @@ -202,6 +218,10 @@ void main() { expect(updated.scrollX, 68); expect(updated.scrollY, 60); 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.count, 12); }); @@ -243,6 +263,14 @@ void main() { }), throwsFormatException, ); + expect( + () => RuntimeNode.fromMap({ + 'id': 'a', + 'type': 'text', + 'textShadowBlur': -1, + }), + throwsFormatException, + ); expect( () => RuntimeNode.fromMap({'id': 'a', 'type': 'listView', 'scrollY': -1}), diff --git a/test/runtime/rendering/runtime_component_test.dart b/test/runtime/rendering/runtime_component_test.dart index 0b48c41..eb1bc8b 100644 --- a/test/runtime/rendering/runtime_component_test.dart +++ b/test/runtime/rendering/runtime_component_test.dart @@ -138,6 +138,31 @@ void main() { ); }); + 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().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', () { final component = RuntimeComponent( node: const RuntimeNode( diff --git a/tool/lua_runtime_defs_common.lua b/tool/lua_runtime_defs_common.lua index ff1c21d..e2a852c 100644 --- a/tool/lua_runtime_defs_common.lua +++ b/tool/lua_runtime_defs_common.lua @@ -109,6 +109,10 @@ ---@field color? string ---@field fontSize? number ---@field textAlign? RuntimeTextAlign +---@field textShadowColor? string +---@field textShadowOffsetX? number +---@field textShadowOffsetY? number +---@field textShadowBlur? number ---@field radius? number ---@field strokeWidth? number ---@field value? number @@ -166,6 +170,10 @@ ---@field color? string ---@field fontSize? number ---@field textAlign? RuntimeTextAlign +---@field textShadowColor? string +---@field textShadowOffsetX? number +---@field textShadowOffsetY? number +---@field textShadowBlur? number ---@field radius? number ---@field strokeWidth? number ---@field value? number