Fix nine-slice seams and inherited alpha
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
## Unreleased
|
||||
|
||||
- Added TexturePacker frame, manual source-region, and nine-slice image rendering fields for image-capable nodes.
|
||||
- Fixed nine-slice image seams by overlapping destination slices during runtime rendering.
|
||||
- Fixed Runtime alpha inheritance so parent fade commands apply to the full child subtree.
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -327,6 +327,7 @@ class RenderTreeController {
|
||||
contentOffsetY: parentContentOffset.y,
|
||||
);
|
||||
component.setViewportCulled(_isCulledByParentListView(component, parent));
|
||||
component.setInheritedAlpha(parent?.renderAlpha ?? 1);
|
||||
if (component.parent == target) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({
|
||||
double sliceTop = 0,
|
||||
double sliceRight = 0,
|
||||
double sliceBottom = 0,
|
||||
double destinationOverlap = 0,
|
||||
}) {
|
||||
if (source.width <= 0 ||
|
||||
source.height <= 0 ||
|
||||
@@ -93,7 +94,7 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({
|
||||
sourceXs[x + 1],
|
||||
sourceYs[y + 1],
|
||||
);
|
||||
final destPart = Rect.fromLTRB(
|
||||
final rawDestPart = Rect.fromLTRB(
|
||||
destXs[x],
|
||||
destYs[y],
|
||||
destXs[x + 1],
|
||||
@@ -101,16 +102,41 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({
|
||||
);
|
||||
if (sourcePart.width <= 0 ||
|
||||
sourcePart.height <= 0 ||
|
||||
destPart.width <= 0 ||
|
||||
destPart.height <= 0) {
|
||||
rawDestPart.width <= 0 ||
|
||||
rawDestPart.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
final destPart = _overlapNineSliceDestinationRect(
|
||||
rawDestPart,
|
||||
x: x,
|
||||
y: y,
|
||||
bounds: destination,
|
||||
overlap: destinationOverlap,
|
||||
);
|
||||
parts.add((source: sourcePart, destination: destPart));
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
Rect _overlapNineSliceDestinationRect(
|
||||
Rect rect, {
|
||||
required int x,
|
||||
required int y,
|
||||
required Rect bounds,
|
||||
required double overlap,
|
||||
}) {
|
||||
if (overlap <= 0) {
|
||||
return rect;
|
||||
}
|
||||
return Rect.fromLTRB(
|
||||
x == 0 ? rect.left : math.max(bounds.left, rect.left - overlap),
|
||||
y == 0 ? rect.top : math.max(bounds.top, rect.top - overlap),
|
||||
x == 2 ? rect.right : math.min(bounds.right, rect.right + overlap),
|
||||
y == 2 ? rect.bottom : math.min(bounds.bottom, rect.bottom + overlap),
|
||||
);
|
||||
}
|
||||
|
||||
class RuntimeComponent extends PositionComponent
|
||||
with HasVisibility, TapCallbacks {
|
||||
RuntimeComponent({
|
||||
@@ -157,8 +183,11 @@ class RuntimeComponent extends PositionComponent
|
||||
double _parentContentOffsetY = 0;
|
||||
bool _viewportCulled = false;
|
||||
bool _pressed = false;
|
||||
double _inheritedAlpha = 1;
|
||||
|
||||
double get renderAlpha => _runtimeAlpha ?? _node.alpha;
|
||||
double get localAlpha => _runtimeAlpha ?? _node.alpha;
|
||||
|
||||
double get renderAlpha => (_inheritedAlpha * localAlpha).clamp(0.0, 1.0);
|
||||
|
||||
void setRuntimeAlpha(double value) {
|
||||
final next = value.clamp(0, 1).toDouble();
|
||||
@@ -166,7 +195,23 @@ class RuntimeComponent extends PositionComponent
|
||||
return;
|
||||
}
|
||||
_runtimeAlpha = next;
|
||||
_refreshInheritedAlphaSubtree();
|
||||
}
|
||||
|
||||
void setInheritedAlpha(double value) {
|
||||
final next = value.clamp(0, 1).toDouble();
|
||||
if (_inheritedAlpha == next) {
|
||||
return;
|
||||
}
|
||||
_inheritedAlpha = next;
|
||||
_refreshInheritedAlphaSubtree();
|
||||
}
|
||||
|
||||
void _refreshInheritedAlphaSubtree() {
|
||||
_syncTextStyle(_node);
|
||||
for (final child in children.whereType<RuntimeComponent>()) {
|
||||
child.setInheritedAlpha(renderAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
void setParentScroll({
|
||||
@@ -194,6 +239,7 @@ class RuntimeComponent extends PositionComponent
|
||||
_syncImage(node);
|
||||
_syncSpine(node);
|
||||
_syncParticle(node);
|
||||
_refreshInheritedAlphaSubtree();
|
||||
}
|
||||
|
||||
bool containsVisualPoint(Vector2 point) {
|
||||
@@ -584,6 +630,7 @@ class RuntimeComponent extends PositionComponent
|
||||
final parts = runtimeNineSliceRects(
|
||||
source: source,
|
||||
destination: destination,
|
||||
destinationOverlap: 0.5,
|
||||
sliceLeft: _node.sliceLeft ?? 0,
|
||||
sliceTop: _node.sliceTop ?? 0,
|
||||
sliceRight: _node.sliceRight ?? 0,
|
||||
@@ -1174,6 +1221,7 @@ class RuntimeComponent extends PositionComponent
|
||||
_releaseLoadedSpine();
|
||||
_releaseParticle();
|
||||
_runtimeAlpha = null;
|
||||
_inheritedAlpha = 1;
|
||||
_pressed = false;
|
||||
_textComponent = null;
|
||||
super.onRemove();
|
||||
|
||||
@@ -116,6 +116,90 @@ void main() {
|
||||
expect(component.renderAlpha, 1);
|
||||
});
|
||||
|
||||
test('inherits parent alpha for prefab-like subtrees', () async {
|
||||
final parent = RuntimeComponent(
|
||||
node: const RuntimeNode(
|
||||
id: 'parent',
|
||||
type: RuntimeNodeType.panel,
|
||||
alpha: 0.8,
|
||||
),
|
||||
resources: GameResourceManager(),
|
||||
onNodeTap: (_, __) {},
|
||||
);
|
||||
final child = RuntimeComponent(
|
||||
node: const RuntimeNode(
|
||||
id: 'child',
|
||||
type: RuntimeNodeType.text,
|
||||
text: 'Child',
|
||||
alpha: 0.5,
|
||||
color: Color(0xffffffff),
|
||||
),
|
||||
resources: GameResourceManager(),
|
||||
onNodeTap: (_, __) {},
|
||||
);
|
||||
parent.add(child);
|
||||
parent.updateTree(0);
|
||||
child.setInheritedAlpha(parent.renderAlpha);
|
||||
|
||||
expect(child.renderAlpha, closeTo(0.4, 0.001));
|
||||
final text = child.children.whereType<TextComponent>().single;
|
||||
expect(
|
||||
((text.textRenderer as TextPaint).style.color!).a,
|
||||
closeTo(0.4, 0.003),
|
||||
);
|
||||
|
||||
parent.setRuntimeAlpha(0.25);
|
||||
|
||||
expect(child.renderAlpha, closeTo(0.125, 0.001));
|
||||
final updatedText = child.children.whereType<TextComponent>().single;
|
||||
expect(identical(updatedText, text), isTrue);
|
||||
expect(
|
||||
((updatedText.textRenderer as TextPaint).style.color!).a,
|
||||
closeTo(0.125, 0.003),
|
||||
);
|
||||
});
|
||||
|
||||
test('propagates parent node alpha updates to child subtree', () {
|
||||
final parent = RuntimeComponent(
|
||||
node: const RuntimeNode(
|
||||
id: 'parent',
|
||||
type: RuntimeNodeType.panel,
|
||||
alpha: 0.8,
|
||||
),
|
||||
resources: GameResourceManager(),
|
||||
onNodeTap: (_, __) {},
|
||||
);
|
||||
final child = RuntimeComponent(
|
||||
node: const RuntimeNode(
|
||||
id: 'child',
|
||||
type: RuntimeNodeType.text,
|
||||
text: 'Child',
|
||||
alpha: 0.5,
|
||||
color: Color(0xffffffff),
|
||||
),
|
||||
resources: GameResourceManager(),
|
||||
onNodeTap: (_, __) {},
|
||||
);
|
||||
parent.add(child);
|
||||
parent.updateTree(0);
|
||||
child.setInheritedAlpha(parent.renderAlpha);
|
||||
|
||||
parent.updateNode(
|
||||
const RuntimeNode(
|
||||
id: 'parent',
|
||||
type: RuntimeNodeType.panel,
|
||||
alpha: 0.6,
|
||||
),
|
||||
);
|
||||
|
||||
final text = child.children.whereType<TextComponent>().single;
|
||||
expect(child.renderAlpha, closeTo(0.3, 0.001));
|
||||
expect(
|
||||
((text.textRenderer as TextPaint).style.color!).a,
|
||||
closeTo(0.3, 0.003),
|
||||
);
|
||||
});
|
||||
|
||||
test('multiplies color alpha with node and runtime alpha', () {
|
||||
expect(
|
||||
composeRuntimeColorAlpha(const Color(0xffffffff), 1).a,
|
||||
@@ -183,6 +267,23 @@ void main() {
|
||||
expect(parts.last.destination, const Rect.fromLTRB(83, 112, 90, 120));
|
||||
});
|
||||
|
||||
test('overlaps nine-slice destination seams without changing bounds', () {
|
||||
final parts = runtimeNineSliceRects(
|
||||
source: const Rect.fromLTWH(0, 0, 30, 30),
|
||||
destination: const Rect.fromLTWH(0, 0, 90, 90),
|
||||
sliceLeft: 10,
|
||||
sliceTop: 10,
|
||||
sliceRight: 10,
|
||||
sliceBottom: 10,
|
||||
destinationOverlap: 0.5,
|
||||
);
|
||||
|
||||
expect(parts.first.destination, const Rect.fromLTRB(0, 0, 10.5, 10.5));
|
||||
expect(parts[4].destination, const Rect.fromLTRB(9.5, 9.5, 80.5, 80.5));
|
||||
expect(parts.last.destination, const Rect.fromLTRB(79.5, 79.5, 90, 90));
|
||||
expect(parts[4].source, const Rect.fromLTRB(10, 10, 20, 20));
|
||||
});
|
||||
|
||||
test('updates text alpha style without rebuilding text component', () {
|
||||
final component = RuntimeComponent(
|
||||
node: const RuntimeNode(
|
||||
|
||||
Reference in New Issue
Block a user