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_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 {
|
||||
final String char;
|
||||
final Color color;
|
||||
@@ -11,7 +39,7 @@ class ColoredChar {
|
||||
}
|
||||
|
||||
class AsciiRasterizer extends Rasterizer {
|
||||
static const String _charset = "@%#*+=-:. ";
|
||||
final AsciiTheme activeTheme = AsciiThemes.blocks;
|
||||
|
||||
late List<List<ColoredChar>> _screen;
|
||||
late WolfEngine _engine;
|
||||
@@ -22,15 +50,11 @@ class AsciiRasterizer extends Rasterizer {
|
||||
double get aspectMultiplier => 0.6;
|
||||
|
||||
// --- HELPER: Color Conversion ---
|
||||
Color _vgaToColor(int vgaColor, {double brightnessBoost = 2.0}) {
|
||||
Color _vgaToColor(int vgaColor) {
|
||||
int r = vgaColor & 0xFF;
|
||||
int g = (vgaColor >> 8) & 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);
|
||||
}
|
||||
|
||||
@@ -53,9 +77,11 @@ class AsciiRasterizer extends Rasterizer {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
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) {
|
||||
_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,
|
||||
int side,
|
||||
) {
|
||||
double brightness = (1.5 / (perpWallDist + 1.0)).clamp(0.0, 1.0);
|
||||
String wallChar =
|
||||
_charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp(
|
||||
0,
|
||||
_charset.length - 1,
|
||||
)];
|
||||
double brightness = (4.0 / (perpWallDist + 1.0));
|
||||
String wallChar = activeTheme.getByBrightness(brightness);
|
||||
|
||||
for (int y = drawStart; y < drawEnd; y++) {
|
||||
double relativeY =
|
||||
@@ -111,12 +133,8 @@ class AsciiRasterizer extends Rasterizer {
|
||||
int texX,
|
||||
double transformY,
|
||||
) {
|
||||
double brightness = (1.5 / (transformY + 1.0)).clamp(0.0, 1.0);
|
||||
String spriteChar =
|
||||
_charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp(
|
||||
0,
|
||||
_charset.length - 1,
|
||||
)];
|
||||
double brightness = (4.0 / (transformY + 1.0)).clamp(0.0, 1.0);
|
||||
String spriteChar = activeTheme.getByBrightness(brightness);
|
||||
|
||||
for (
|
||||
int y = math.max(0, drawStartY);
|
||||
@@ -161,7 +179,7 @@ class AsciiRasterizer extends Rasterizer {
|
||||
int drawY = startY + dy;
|
||||
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) {
|
||||
_screen[drawY][drawX] = ColoredChar(
|
||||
'@',
|
||||
activeTheme.solid,
|
||||
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
|
||||
);
|
||||
}
|
||||
@@ -294,11 +312,8 @@ class AsciiRasterizer extends Rasterizer {
|
||||
if (colorByte != 255) {
|
||||
// Using '█' for UI to make it look solid
|
||||
_screen[drawY][drawX] = ColoredChar(
|
||||
'█',
|
||||
_vgaToColor(
|
||||
ColorPalette.vga32Bit[colorByte],
|
||||
brightnessBoost: 1.5,
|
||||
),
|
||||
activeTheme.solid,
|
||||
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,29 +104,51 @@ class AsciiFrameWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: frameData.map((row) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
height: 1.0,
|
||||
letterSpacing: 2.0,
|
||||
return AspectRatio(
|
||||
aspectRatio: 4 / 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: frameData.map((row) {
|
||||
List<TextSpan> optimizedSpans = [];
|
||||
if (row.isNotEmpty) {
|
||||
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(
|
||||
text: cell.char,
|
||||
style: TextStyle(color: cell.color),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user