feat: Add intro splash screen with transition effects and rendering support

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-20 12:05:34 +01:00
parent 9733516693
commit b23c02f716
6 changed files with 367 additions and 14 deletions
@@ -419,6 +419,11 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
final art = WolfClassicMenuArt(engine.data);
if (engine.menuManager.activeMenu == WolfMenuScreen.introSplash) {
_drawIntroSplash(engine, art);
return;
}
if (engine.menuManager.activeMenu == WolfMenuScreen.mainMenu) {
_fillRect320(68, 52, 178, 136, panelColor);
@@ -703,6 +708,59 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
}
}
void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) {
final image = engine.menuManager.isIntroPg13Slide
? art.mappedPic(WolfMenuPic.pg13)
: art.mappedPic(WolfMenuPic.title);
int splashBg = _rgbToPaletteColor(engine.menuManager.introBackgroundRgb);
if (engine.menuManager.isIntroPg13Slide &&
image != null &&
image.pixels.isNotEmpty) {
splashBg = ColorPalette.vga32Bit[_dominantPaletteIndex(image)];
}
if (_usesTerminalLayout) {
_fillTerminalRect(0, 0, width, _terminalPixelHeight, splashBg);
} else {
_fillRect(0, 0, width, height, activeTheme.solid, splashBg);
}
if (image != null) {
final int x = engine.menuManager.isIntroPg13Slide
? (320 - image.width).clamp(0, 319)
: ((320 - image.width) ~/ 2).clamp(0, 319);
final int y = engine.menuManager.isIntroPg13Slide
? (200 - image.height).clamp(0, 199)
: ((200 - image.height) ~/ 2).clamp(0, 199);
_blitVgaImageAscii(image, x, y);
}
_applyMenuFade(
engine.menuManager.introOverlayAlpha,
_rgbToPaletteColor(0x000000),
);
}
int _dominantPaletteIndex(VgaImage image) {
final List<int> histogram = List<int>.filled(256, 0);
for (final int colorIndex in image.pixels) {
if (colorIndex >= 0 && colorIndex < histogram.length) {
histogram[colorIndex]++;
}
}
int bestIndex = 0;
int bestCount = -1;
for (int i = 0; i < histogram.length; i++) {
if (histogram[i] > bestCount) {
bestCount = histogram[i];
bestIndex = i;
}
}
return bestIndex;
}
void _applyMenuFade(double alpha, int fadeColor) {
if (alpha <= 0.0) {
return;
@@ -372,6 +372,11 @@ class SixelRenderer extends CliRendererBackend<String> {
// Draw footer first so menu panels can clip overlap in the center.
_drawMenuFooterArt(art);
if (engine.menuManager.activeMenu == WolfMenuScreen.introSplash) {
_drawIntroSplash(engine, art);
return;
}
if (engine.menuManager.activeMenu == WolfMenuScreen.mainMenu) {
_fillRect320(68, 52, 178, 136, panelColor);
@@ -555,6 +560,57 @@ class SixelRenderer extends CliRendererBackend<String> {
}
}
void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) {
final image = engine.menuManager.isIntroPg13Slide
? art.mappedPic(WolfMenuPic.pg13)
: art.mappedPic(WolfMenuPic.title);
int splashBg = _rgbToPaletteIndex(engine.menuManager.introBackgroundRgb);
if (engine.menuManager.isIntroPg13Slide &&
image != null &&
image.pixels.isNotEmpty) {
splashBg = _dominantPaletteIndex(image);
}
for (int i = 0; i < _screen.length; i++) {
_screen[i] = splashBg;
}
if (image != null) {
final int x = engine.menuManager.isIntroPg13Slide
? (320 - image.width).clamp(0, 319)
: ((320 - image.width) ~/ 2).clamp(0, 319);
final int y = engine.menuManager.isIntroPg13Slide
? (200 - image.height).clamp(0, 199)
: ((200 - image.height) ~/ 2).clamp(0, 199);
_blitVgaImage(image, x, y);
}
_applyMenuFade(
engine.menuManager.introOverlayAlpha,
_rgbToPaletteIndex(0x000000),
);
}
int _dominantPaletteIndex(VgaImage image) {
final List<int> histogram = List<int>.filled(256, 0);
for (final int colorIndex in image.pixels) {
if (colorIndex >= 0 && colorIndex < histogram.length) {
histogram[colorIndex]++;
}
}
int bestIndex = 0;
int bestCount = -1;
for (int i = 0; i < histogram.length; i++) {
if (histogram[i] > bestCount) {
bestCount = histogram[i];
bestIndex = i;
}
}
return bestIndex;
}
void _applyMenuFade(double alpha, int bgColor) {
if (alpha <= 0.0) {
return;
@@ -159,6 +159,9 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
_drawCenteredMenuFooter(art);
switch (engine.menuManager.activeMenu) {
case WolfMenuScreen.introSplash:
_drawIntroSplash(engine, art);
break;
case WolfMenuScreen.mainMenu:
_drawMainMenu(
engine,
@@ -205,6 +208,99 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
}
void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) {
final image = engine.menuManager.isIntroPg13Slide
? art.mappedPic(WolfMenuPic.pg13)
: art.mappedPic(WolfMenuPic.title);
int splashBgColor = _rgbToFrameColor(engine.menuManager.introBackgroundRgb);
int? matteIndex;
if (engine.menuManager.isIntroPg13Slide &&
image != null &&
image.pixels.isNotEmpty) {
matteIndex = _dominantPaletteIndex(image);
splashBgColor =
ColorPalette.vga32Bit[_pg13BackgroundPaletteIndex(image, matteIndex)];
}
for (int i = 0; i < _buffer.pixels.length; i++) {
_buffer.pixels[i] = splashBgColor;
}
if (image != null) {
final int x = engine.menuManager.isIntroPg13Slide
? (320 - image.width).clamp(0, 319)
: ((320 - image.width) ~/ 2).clamp(0, 319);
final int y = engine.menuManager.isIntroPg13Slide
? (200 - image.height).clamp(0, 199)
: ((200 - image.height) ~/ 2).clamp(0, 199);
if (engine.menuManager.isIntroPg13Slide && matteIndex != null) {
_blitVgaImage(image, x, y, transparentIndex: matteIndex);
} else {
_blitVgaImage(image, x, y);
}
}
_applyMenuFade(
engine.menuManager.introOverlayAlpha,
_rgbToFrameColor(0x000000),
);
}
int _dominantPaletteIndex(VgaImage image) {
final List<int> histogram = List<int>.filled(256, 0);
for (final int colorIndex in image.pixels) {
if (colorIndex >= 0 && colorIndex < histogram.length) {
histogram[colorIndex]++;
}
}
int bestIndex = 0;
int bestCount = -1;
for (int i = 0; i < histogram.length; i++) {
if (histogram[i] > bestCount) {
bestCount = histogram[i];
bestIndex = i;
}
}
return bestIndex;
}
int _pg13BackgroundPaletteIndex(VgaImage image, int matteIndex) {
final List<(int x, int y)> probes = <(int x, int y)>[
(0, 0),
(image.width - 1, 0),
(0, image.height - 1),
(image.width - 1, image.height - 1),
(image.width - 8, image.height - 8),
(image.width - 16, image.height - 16),
];
final Map<int, int> counts = <int, int>{};
for (final probe in probes) {
final int x = probe.$1.clamp(0, image.width - 1);
final int y = probe.$2.clamp(0, image.height - 1);
final int idx = image.decodePixel(x, y);
if (idx == matteIndex) {
continue;
}
counts[idx] = (counts[idx] ?? 0) + 1;
}
if (counts.isEmpty) {
return matteIndex;
}
int bestIndex = counts.keys.first;
int bestCount = counts[bestIndex] ?? 0;
for (final MapEntry<int, int> entry in counts.entries) {
if (entry.value > bestCount) {
bestCount = entry.value;
bestIndex = entry.key;
}
}
return bestIndex;
}
void _drawMainMenu(
WolfEngine engine,
WolfClassicMenuArt art,
@@ -663,7 +759,12 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
///
/// UI coordinates are expressed in canonical 320x200 space and scaled to the
/// current framebuffer so higher-resolution render targets preserve layout.
void _blitVgaImage(VgaImage image, int startX, int startY) {
void _blitVgaImage(
VgaImage image,
int startX,
int startY, {
int? transparentIndex,
}) {
final int destStartX = (startX * _uiScaleX).floor();
final int destStartY = (startY * _uiScaleY).floor();
final int destWidth = math.max(1, (image.width * _uiScaleX).ceil());
@@ -678,7 +779,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
final int srcX = (dx / _uiScaleX).toInt().clamp(0, image.width - 1);
final int srcY = (dy / _uiScaleY).toInt().clamp(0, image.height - 1);
final int colorByte = image.decodePixel(srcX, srcY);
if (colorByte != 255) {
if (colorByte != 255 && colorByte != transparentIndex) {
_buffer.pixels[drawY * width + drawX] =
ColorPalette.vga32Bit[colorByte];
}