Massively improved the ASCII renderer
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -4,6 +4,34 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
class AsciiTheme {
|
||||||
|
/// The character ramp, ordered from most dense (index 0) to least dense (last index).
|
||||||
|
final String ramp;
|
||||||
|
|
||||||
|
const AsciiTheme(this.ramp);
|
||||||
|
|
||||||
|
/// Always returns the densest character (e.g., for walls, UI, floors)
|
||||||
|
String get solid => ramp[0];
|
||||||
|
|
||||||
|
/// Always returns the completely empty character (e.g., for pitch black darkness)
|
||||||
|
String get empty => ramp[ramp.length - 1];
|
||||||
|
|
||||||
|
/// Returns a character based on a 0.0 to 1.0 brightness scale.
|
||||||
|
/// 1.0 returns the [solid] character, 0.0 returns the [empty] character.
|
||||||
|
String getByBrightness(double brightness) {
|
||||||
|
double b = brightness.clamp(0.0, 1.0);
|
||||||
|
int index = ((1.0 - b) * (ramp.length - 1)).round();
|
||||||
|
return ramp[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A collection of pre-defined character sets
|
||||||
|
abstract class AsciiThemes {
|
||||||
|
static const AsciiTheme blocks = AsciiTheme("█▓▒░ ");
|
||||||
|
static const AsciiTheme classic = AsciiTheme("@%#*+=-:. ");
|
||||||
|
static const AsciiTheme dense = AsciiTheme("█▇▆▅▄▃▂ ");
|
||||||
|
}
|
||||||
|
|
||||||
class ColoredChar {
|
class ColoredChar {
|
||||||
final String char;
|
final String char;
|
||||||
final Color color;
|
final Color color;
|
||||||
@@ -11,7 +39,7 @@ class ColoredChar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AsciiRasterizer extends Rasterizer {
|
class AsciiRasterizer extends Rasterizer {
|
||||||
static const String _charset = "@%#*+=-:. ";
|
final AsciiTheme activeTheme = AsciiThemes.blocks;
|
||||||
|
|
||||||
late List<List<ColoredChar>> _screen;
|
late List<List<ColoredChar>> _screen;
|
||||||
late WolfEngine _engine;
|
late WolfEngine _engine;
|
||||||
@@ -22,15 +50,11 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
double get aspectMultiplier => 0.6;
|
double get aspectMultiplier => 0.6;
|
||||||
|
|
||||||
// --- HELPER: Color Conversion ---
|
// --- HELPER: Color Conversion ---
|
||||||
Color _vgaToColor(int vgaColor, {double brightnessBoost = 2.0}) {
|
Color _vgaToColor(int vgaColor) {
|
||||||
int r = vgaColor & 0xFF;
|
int r = vgaColor & 0xFF;
|
||||||
int g = (vgaColor >> 8) & 0xFF;
|
int g = (vgaColor >> 8) & 0xFF;
|
||||||
int b = (vgaColor >> 16) & 0xFF;
|
int b = (vgaColor >> 16) & 0xFF;
|
||||||
|
|
||||||
r = (r * brightnessBoost).toInt().clamp(0, 255);
|
|
||||||
g = (g * brightnessBoost).toInt().clamp(0, 255);
|
|
||||||
b = (b * brightnessBoost).toInt().clamp(0, 255);
|
|
||||||
|
|
||||||
return Color.fromARGB(255, r, g, b);
|
return Color.fromARGB(255, r, g, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,9 +77,11 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
for (int y = 0; y < height; y++) {
|
for (int y = 0; y < height; y++) {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
if (y < viewHeight / 2) {
|
if (y < viewHeight / 2) {
|
||||||
_screen[y][x] = ColoredChar(' ', ceilingColor);
|
// Fetch the solid character from the theme
|
||||||
|
_screen[y][x] = ColoredChar(activeTheme.solid, ceilingColor);
|
||||||
} else if (y < viewHeight) {
|
} else if (y < viewHeight) {
|
||||||
_screen[y][x] = ColoredChar('.', floorColor);
|
// Fetch the solid character from the theme
|
||||||
|
_screen[y][x] = ColoredChar(activeTheme.solid, floorColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,12 +98,8 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
double perpWallDist,
|
double perpWallDist,
|
||||||
int side,
|
int side,
|
||||||
) {
|
) {
|
||||||
double brightness = (1.5 / (perpWallDist + 1.0)).clamp(0.0, 1.0);
|
double brightness = (4.0 / (perpWallDist + 1.0));
|
||||||
String wallChar =
|
String wallChar = activeTheme.getByBrightness(brightness);
|
||||||
_charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp(
|
|
||||||
0,
|
|
||||||
_charset.length - 1,
|
|
||||||
)];
|
|
||||||
|
|
||||||
for (int y = drawStart; y < drawEnd; y++) {
|
for (int y = drawStart; y < drawEnd; y++) {
|
||||||
double relativeY =
|
double relativeY =
|
||||||
@@ -111,12 +133,8 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
int texX,
|
int texX,
|
||||||
double transformY,
|
double transformY,
|
||||||
) {
|
) {
|
||||||
double brightness = (1.5 / (transformY + 1.0)).clamp(0.0, 1.0);
|
double brightness = (4.0 / (transformY + 1.0)).clamp(0.0, 1.0);
|
||||||
String spriteChar =
|
String spriteChar = activeTheme.getByBrightness(brightness);
|
||||||
_charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp(
|
|
||||||
0,
|
|
||||||
_charset.length - 1,
|
|
||||||
)];
|
|
||||||
|
|
||||||
for (
|
for (
|
||||||
int y = math.max(0, drawStartY);
|
int y = math.max(0, drawStartY);
|
||||||
@@ -161,7 +179,7 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
int drawY = startY + dy;
|
int drawY = startY + dy;
|
||||||
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) {
|
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) {
|
||||||
_screen[drawY][drawX] = ColoredChar(
|
_screen[drawY][drawX] = ColoredChar(
|
||||||
'@',
|
activeTheme.solid,
|
||||||
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
|
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -294,11 +312,8 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
if (colorByte != 255) {
|
if (colorByte != 255) {
|
||||||
// Using '█' for UI to make it look solid
|
// Using '█' for UI to make it look solid
|
||||||
_screen[drawY][drawX] = ColoredChar(
|
_screen[drawY][drawX] = ColoredChar(
|
||||||
'█',
|
activeTheme.solid,
|
||||||
_vgaToColor(
|
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
|
||||||
ColorPalette.vga32Bit[colorByte],
|
|
||||||
brightnessBoost: 1.5,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,29 +104,51 @@ class AsciiFrameWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FittedBox(
|
return AspectRatio(
|
||||||
fit: BoxFit.contain,
|
aspectRatio: 4 / 3,
|
||||||
child: Container(
|
child: FittedBox(
|
||||||
padding: const EdgeInsets.all(16.0),
|
fit: BoxFit.fill,
|
||||||
child: Column(
|
child: Container(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
padding: const EdgeInsets.all(16.0),
|
||||||
children: frameData.map((row) {
|
child: Column(
|
||||||
return RichText(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
text: TextSpan(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: const TextStyle(
|
children: frameData.map((row) {
|
||||||
fontFamily: 'monospace',
|
List<TextSpan> optimizedSpans = [];
|
||||||
height: 1.0,
|
if (row.isNotEmpty) {
|
||||||
letterSpacing: 2.0,
|
Color currentColor = row[0].color;
|
||||||
|
StringBuffer currentSegment = StringBuffer(row[0].char);
|
||||||
|
|
||||||
|
for (int i = 1; i < row.length; i++) {
|
||||||
|
if (row[i].color == currentColor) {
|
||||||
|
currentSegment.write(row[i].char);
|
||||||
|
} else {
|
||||||
|
optimizedSpans.add(
|
||||||
|
TextSpan(
|
||||||
|
text: currentSegment.toString(),
|
||||||
|
style: TextStyle(color: currentColor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
currentColor = row[i].color;
|
||||||
|
currentSegment = StringBuffer(row[i].char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
optimizedSpans.add(
|
||||||
|
TextSpan(
|
||||||
|
text: currentSegment.toString(),
|
||||||
|
style: TextStyle(color: currentColor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: const TextStyle(fontFamily: 'monospace', height: 1.0),
|
||||||
|
children: optimizedSpans,
|
||||||
),
|
),
|
||||||
children: row.map((cell) {
|
);
|
||||||
return TextSpan(
|
}).toList(),
|
||||||
text: cell.char,
|
),
|
||||||
style: TextStyle(color: cell.color),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user