Fix nine-slice seams and inherited alpha
This commit is contained in:
@@ -3,6 +3,8 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- Added TexturePacker frame, manual source-region, and nine-slice image rendering fields for image-capable nodes.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ class RenderTreeController {
|
|||||||
contentOffsetY: parentContentOffset.y,
|
contentOffsetY: parentContentOffset.y,
|
||||||
);
|
);
|
||||||
component.setViewportCulled(_isCulledByParentListView(component, parent));
|
component.setViewportCulled(_isCulledByParentListView(component, parent));
|
||||||
|
component.setInheritedAlpha(parent?.renderAlpha ?? 1);
|
||||||
if (component.parent == target) {
|
if (component.parent == target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({
|
|||||||
double sliceTop = 0,
|
double sliceTop = 0,
|
||||||
double sliceRight = 0,
|
double sliceRight = 0,
|
||||||
double sliceBottom = 0,
|
double sliceBottom = 0,
|
||||||
|
double destinationOverlap = 0,
|
||||||
}) {
|
}) {
|
||||||
if (source.width <= 0 ||
|
if (source.width <= 0 ||
|
||||||
source.height <= 0 ||
|
source.height <= 0 ||
|
||||||
@@ -93,7 +94,7 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({
|
|||||||
sourceXs[x + 1],
|
sourceXs[x + 1],
|
||||||
sourceYs[y + 1],
|
sourceYs[y + 1],
|
||||||
);
|
);
|
||||||
final destPart = Rect.fromLTRB(
|
final rawDestPart = Rect.fromLTRB(
|
||||||
destXs[x],
|
destXs[x],
|
||||||
destYs[y],
|
destYs[y],
|
||||||
destXs[x + 1],
|
destXs[x + 1],
|
||||||
@@ -101,16 +102,41 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({
|
|||||||
);
|
);
|
||||||
if (sourcePart.width <= 0 ||
|
if (sourcePart.width <= 0 ||
|
||||||
sourcePart.height <= 0 ||
|
sourcePart.height <= 0 ||
|
||||||
destPart.width <= 0 ||
|
rawDestPart.width <= 0 ||
|
||||||
destPart.height <= 0) {
|
rawDestPart.height <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
final destPart = _overlapNineSliceDestinationRect(
|
||||||
|
rawDestPart,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
bounds: destination,
|
||||||
|
overlap: destinationOverlap,
|
||||||
|
);
|
||||||
parts.add((source: sourcePart, destination: destPart));
|
parts.add((source: sourcePart, destination: destPart));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parts;
|
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
|
class RuntimeComponent extends PositionComponent
|
||||||
with HasVisibility, TapCallbacks {
|
with HasVisibility, TapCallbacks {
|
||||||
RuntimeComponent({
|
RuntimeComponent({
|
||||||
@@ -157,8 +183,11 @@ class RuntimeComponent extends PositionComponent
|
|||||||
double _parentContentOffsetY = 0;
|
double _parentContentOffsetY = 0;
|
||||||
bool _viewportCulled = false;
|
bool _viewportCulled = false;
|
||||||
bool _pressed = 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) {
|
void setRuntimeAlpha(double value) {
|
||||||
final next = value.clamp(0, 1).toDouble();
|
final next = value.clamp(0, 1).toDouble();
|
||||||
@@ -166,7 +195,23 @@ class RuntimeComponent extends PositionComponent
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_runtimeAlpha = next;
|
_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);
|
_syncTextStyle(_node);
|
||||||
|
for (final child in children.whereType<RuntimeComponent>()) {
|
||||||
|
child.setInheritedAlpha(renderAlpha);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setParentScroll({
|
void setParentScroll({
|
||||||
@@ -194,6 +239,7 @@ class RuntimeComponent extends PositionComponent
|
|||||||
_syncImage(node);
|
_syncImage(node);
|
||||||
_syncSpine(node);
|
_syncSpine(node);
|
||||||
_syncParticle(node);
|
_syncParticle(node);
|
||||||
|
_refreshInheritedAlphaSubtree();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool containsVisualPoint(Vector2 point) {
|
bool containsVisualPoint(Vector2 point) {
|
||||||
@@ -584,6 +630,7 @@ class RuntimeComponent extends PositionComponent
|
|||||||
final parts = runtimeNineSliceRects(
|
final parts = runtimeNineSliceRects(
|
||||||
source: source,
|
source: source,
|
||||||
destination: destination,
|
destination: destination,
|
||||||
|
destinationOverlap: 0.5,
|
||||||
sliceLeft: _node.sliceLeft ?? 0,
|
sliceLeft: _node.sliceLeft ?? 0,
|
||||||
sliceTop: _node.sliceTop ?? 0,
|
sliceTop: _node.sliceTop ?? 0,
|
||||||
sliceRight: _node.sliceRight ?? 0,
|
sliceRight: _node.sliceRight ?? 0,
|
||||||
@@ -1174,6 +1221,7 @@ class RuntimeComponent extends PositionComponent
|
|||||||
_releaseLoadedSpine();
|
_releaseLoadedSpine();
|
||||||
_releaseParticle();
|
_releaseParticle();
|
||||||
_runtimeAlpha = null;
|
_runtimeAlpha = null;
|
||||||
|
_inheritedAlpha = 1;
|
||||||
_pressed = false;
|
_pressed = false;
|
||||||
_textComponent = null;
|
_textComponent = null;
|
||||||
super.onRemove();
|
super.onRemove();
|
||||||
|
|||||||
@@ -116,6 +116,90 @@ void main() {
|
|||||||
expect(component.renderAlpha, 1);
|
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', () {
|
test('multiplies color alpha with node and runtime alpha', () {
|
||||||
expect(
|
expect(
|
||||||
composeRuntimeColorAlpha(const Color(0xffffffff), 1).a,
|
composeRuntimeColorAlpha(const Color(0xffffffff), 1).a,
|
||||||
@@ -183,6 +267,23 @@ void main() {
|
|||||||
expect(parts.last.destination, const Rect.fromLTRB(83, 112, 90, 120));
|
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', () {
|
test('updates text alpha style without rebuilding text component', () {
|
||||||
final component = RuntimeComponent(
|
final component = RuntimeComponent(
|
||||||
node: const RuntimeNode(
|
node: const RuntimeNode(
|
||||||
|
|||||||
Reference in New Issue
Block a user