Added ability to swap between ASCII and sixel renderers when pressing tab
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -36,12 +36,9 @@ void main() async {
|
|||||||
recursive: true,
|
recursive: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
final data = availableGames.values.first;
|
final AsciiRasterizer asciiRasterizer = AsciiRasterizer(isTerminal: true);
|
||||||
|
final SixelRasterizer sixelRasterizer = SixelRasterizer();
|
||||||
final input = CliInput();
|
Rasterizer rasterizer = sixelRasterizer;
|
||||||
final cliAudio = CliSilentAudio();
|
|
||||||
|
|
||||||
final rasterizer = AsciiRasterizer(isTerminal: true);
|
|
||||||
|
|
||||||
FrameBuffer buffer = FrameBuffer(
|
FrameBuffer buffer = FrameBuffer(
|
||||||
stdout.terminalColumns,
|
stdout.terminalColumns,
|
||||||
@@ -49,11 +46,11 @@ void main() async {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final engine = WolfEngine(
|
final engine = WolfEngine(
|
||||||
data: data,
|
data: availableGames.values.first,
|
||||||
difficulty: Difficulty.medium,
|
difficulty: Difficulty.medium,
|
||||||
startingEpisode: 0,
|
startingEpisode: 0,
|
||||||
audio: cliAudio,
|
audio: CliSilentAudio(),
|
||||||
input: input,
|
input: CliInput(),
|
||||||
onGameWon: () {
|
onGameWon: () {
|
||||||
exitCleanly(0);
|
exitCleanly(0);
|
||||||
print("YOU WON!");
|
print("YOU WON!");
|
||||||
@@ -67,7 +64,15 @@ void main() async {
|
|||||||
exitCleanly(0);
|
exitCleanly(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
input.handleKey(bytes);
|
if (bytes.contains(9)) {
|
||||||
|
rasterizer = identical(rasterizer, sixelRasterizer)
|
||||||
|
? asciiRasterizer
|
||||||
|
: sixelRasterizer;
|
||||||
|
stdout.write('\x1b[2J\x1b[H');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(engine.input as CliInput).handleKey(bytes);
|
||||||
});
|
});
|
||||||
|
|
||||||
Stopwatch stopwatch = Stopwatch()..start();
|
Stopwatch stopwatch = Stopwatch()..start();
|
||||||
@@ -78,13 +83,11 @@ void main() async {
|
|||||||
if (stdout.hasTerminal) {
|
if (stdout.hasTerminal) {
|
||||||
int cols = stdout.terminalColumns;
|
int cols = stdout.terminalColumns;
|
||||||
int rows = stdout.terminalLines;
|
int rows = stdout.terminalLines;
|
||||||
if (cols < 80 || rows < 24) {
|
if (!rasterizer.isTerminalSizeSupported(cols, rows)) {
|
||||||
// Clear the screen and print the warning at the top left
|
// Clear the screen and print the warning at the top left
|
||||||
stdout.write('\x1b[2J\x1b[H');
|
stdout.write('\x1b[2J\x1b[H');
|
||||||
stdout.write('\x1b[31m[ ERROR ] TERMINAL TOO SMALL\x1b[0m\n\n');
|
stdout.write('\x1b[31m[ ERROR ] TERMINAL TOO SMALL\x1b[0m\n\n');
|
||||||
stdout.write(
|
stdout.write('${rasterizer.terminalSizeRequirement}\n');
|
||||||
'Wolfenstein 3D requires a minimum resolution of 120x40.\n',
|
|
||||||
);
|
|
||||||
stdout.write(
|
stdout.write(
|
||||||
'Current size: \x1b[33m${stdout.terminalColumns}x${stdout.terminalLines}\x1b[0m\n\n',
|
'Current size: \x1b[33m${stdout.terminalColumns}x${stdout.terminalLines}\x1b[0m\n\n',
|
||||||
);
|
);
|
||||||
@@ -110,8 +113,7 @@ void main() async {
|
|||||||
|
|
||||||
engine.tick(elapsed);
|
engine.tick(elapsed);
|
||||||
rasterizer.render(engine, buffer);
|
rasterizer.render(engine, buffer);
|
||||||
rasterizer.finalizeFrame();
|
|
||||||
|
|
||||||
stdout.write(rasterizer.toAnsiString());
|
stdout.write(rasterizer.finalizeFrame());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,4 +261,32 @@ abstract class ColorPalette {
|
|||||||
0xFF6D6D00,
|
0xFF6D6D00,
|
||||||
0xFF890099,
|
0xFF890099,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
static int findClosestPaletteIndex(int argb) {
|
||||||
|
final int targetR = (argb >> 16) & 0xFF;
|
||||||
|
final int targetG = (argb >> 8) & 0xFF;
|
||||||
|
final int targetB = argb & 0xFF;
|
||||||
|
|
||||||
|
int bestIndex = 0;
|
||||||
|
int bestDistance = 1 << 30;
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
final int color = vga32Bit[i];
|
||||||
|
final int r = color & 0xFF;
|
||||||
|
final int g = (color >> 8) & 0xFF;
|
||||||
|
final int b = (color >> 16) & 0xFF;
|
||||||
|
|
||||||
|
final int dr = r - targetR;
|
||||||
|
final int dg = g - targetG;
|
||||||
|
final int db = b - targetB;
|
||||||
|
final int distance = dr * dr + dg * dg + db * db;
|
||||||
|
|
||||||
|
if (distance < bestDistance) {
|
||||||
|
bestDistance = distance;
|
||||||
|
bestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ class ColoredChar {
|
|||||||
class AsciiRasterizer extends Rasterizer {
|
class AsciiRasterizer extends Rasterizer {
|
||||||
static const double _targetAspectRatio = 4 / 3;
|
static const double _targetAspectRatio = 4 / 3;
|
||||||
static const int _terminalBackdropArgb = 0xFF009688;
|
static const int _terminalBackdropArgb = 0xFF009688;
|
||||||
|
static const int _minimumTerminalColumns = 80;
|
||||||
|
static const int _minimumTerminalRows = 24;
|
||||||
static const int _simpleHudMinWidth = 84;
|
static const int _simpleHudMinWidth = 84;
|
||||||
static const int _simpleHudMinRows = 7;
|
static const int _simpleHudMinRows = 7;
|
||||||
|
|
||||||
@@ -98,6 +100,19 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
@override
|
@override
|
||||||
int get projectionViewHeight => isTerminal ? viewHeight * 2 : viewHeight;
|
int get projectionViewHeight => isTerminal ? viewHeight * 2 : viewHeight;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isTerminalSizeSupported(int columns, int rows) {
|
||||||
|
if (!isTerminal) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return columns >= _minimumTerminalColumns && rows >= _minimumTerminalRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get terminalSizeRequirement =>
|
||||||
|
'ASCII renderer requires a minimum resolution of '
|
||||||
|
'${_minimumTerminalColumns}x$_minimumTerminalRows.';
|
||||||
|
|
||||||
int get _terminalPixelHeight => isTerminal ? height * 2 : height;
|
int get _terminalPixelHeight => isTerminal ? height * 2 : height;
|
||||||
|
|
||||||
int get _viewportRightX => projectionOffsetX + projectionWidth;
|
int get _viewportRightX => projectionOffsetX + projectionWidth;
|
||||||
@@ -615,7 +630,7 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
dynamic finalizeFrame() {
|
String finalizeFrame() {
|
||||||
if (_engine.player.damageFlash > 0.0) {
|
if (_engine.player.damageFlash > 0.0) {
|
||||||
if (isTerminal) {
|
if (isTerminal) {
|
||||||
_applyDamageFlashToScene();
|
_applyDamageFlashToScene();
|
||||||
@@ -626,7 +641,7 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
if (isTerminal) {
|
if (isTerminal) {
|
||||||
_composeTerminalScene();
|
_composeTerminalScene();
|
||||||
}
|
}
|
||||||
return _screen;
|
return toAnsiString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PRIVATE HUD DRAWING HELPERS ---
|
// --- PRIVATE HUD DRAWING HELPERS ---
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ abstract class Rasterizer {
|
|||||||
/// more vertical detail and collapse it into half-block glyphs.
|
/// more vertical detail and collapse it into half-block glyphs.
|
||||||
int get projectionViewHeight => viewHeight;
|
int get projectionViewHeight => viewHeight;
|
||||||
|
|
||||||
|
/// Whether the current terminal dimensions are supported by this renderer.
|
||||||
|
/// Default renderers accept all sizes.
|
||||||
|
bool isTerminalSizeSupported(int columns, int rows) => true;
|
||||||
|
|
||||||
|
/// Human-readable requirement text used by the host app when size checks fail.
|
||||||
|
String get terminalSizeRequirement => 'Please resize your terminal window.';
|
||||||
|
|
||||||
/// The main entry point called by the game loop.
|
/// The main entry point called by the game loop.
|
||||||
/// Orchestrates the mathematical rendering pipeline.
|
/// Orchestrates the mathematical rendering pipeline.
|
||||||
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||||
|
|||||||
@@ -5,15 +5,92 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
class SixelRasterizer extends Rasterizer {
|
class SixelRasterizer extends Rasterizer {
|
||||||
|
static const double _targetAspectRatio = 4 / 3;
|
||||||
|
static const int _defaultLineHeightPx = 18;
|
||||||
|
static const double _defaultCellWidthToHeight = 0.55;
|
||||||
|
static const int _minimumTerminalColumns = 117;
|
||||||
|
static const int _minimumTerminalRows = 34;
|
||||||
|
static const int _maxRenderWidth = 320;
|
||||||
|
static const int _maxRenderHeight = 240;
|
||||||
|
static const String _terminalTealBackground = '\x1b[48;2;0;150;136m';
|
||||||
|
|
||||||
late Uint8List _screen;
|
late Uint8List _screen;
|
||||||
late WolfEngine _engine;
|
late WolfEngine _engine;
|
||||||
|
int _offsetColumns = 0;
|
||||||
|
int _offsetRows = 0;
|
||||||
|
int _outputWidth = 1;
|
||||||
|
int _outputHeight = 1;
|
||||||
|
bool _needsBackgroundClear = true;
|
||||||
|
|
||||||
|
FrameBuffer _createScaledBuffer(FrameBuffer terminalBuffer) {
|
||||||
|
final int previousOffsetColumns = _offsetColumns;
|
||||||
|
final int previousOffsetRows = _offsetRows;
|
||||||
|
final int previousOutputWidth = _outputWidth;
|
||||||
|
final int previousOutputHeight = _outputHeight;
|
||||||
|
|
||||||
|
final double fitScale = math.min(
|
||||||
|
terminalBuffer.width / _minimumTerminalColumns,
|
||||||
|
terminalBuffer.height / _minimumTerminalRows,
|
||||||
|
);
|
||||||
|
|
||||||
|
final int targetColumns = math.max(
|
||||||
|
1,
|
||||||
|
(_minimumTerminalColumns * fitScale).floor(),
|
||||||
|
);
|
||||||
|
final int targetRows = math.max(
|
||||||
|
1,
|
||||||
|
(_minimumTerminalRows * fitScale).floor(),
|
||||||
|
);
|
||||||
|
|
||||||
|
_offsetColumns = math.max(0, (terminalBuffer.width - targetColumns) ~/ 2);
|
||||||
|
_offsetRows = math.max(0, (terminalBuffer.height - targetRows) ~/ 2);
|
||||||
|
|
||||||
|
final int boundsPixelWidth = math.max(
|
||||||
|
1,
|
||||||
|
(targetColumns * _defaultLineHeightPx * _defaultCellWidthToHeight)
|
||||||
|
.floor(),
|
||||||
|
);
|
||||||
|
final int boundsPixelHeight = math.max(
|
||||||
|
1,
|
||||||
|
targetRows * _defaultLineHeightPx,
|
||||||
|
);
|
||||||
|
|
||||||
|
final double boundsAspect = boundsPixelWidth / boundsPixelHeight;
|
||||||
|
if (boundsAspect > _targetAspectRatio) {
|
||||||
|
_outputHeight = boundsPixelHeight;
|
||||||
|
_outputWidth = math.max(1, (_outputHeight * _targetAspectRatio).floor());
|
||||||
|
} else {
|
||||||
|
_outputWidth = boundsPixelWidth;
|
||||||
|
_outputHeight = math.max(1, (_outputWidth / _targetAspectRatio).floor());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_offsetColumns != previousOffsetColumns ||
|
||||||
|
_offsetRows != previousOffsetRows ||
|
||||||
|
_outputWidth != previousOutputWidth ||
|
||||||
|
_outputHeight != previousOutputHeight) {
|
||||||
|
_needsBackgroundClear = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double renderScale = math.min(
|
||||||
|
1.0,
|
||||||
|
math.min(
|
||||||
|
_maxRenderWidth / _outputWidth,
|
||||||
|
_maxRenderHeight / _outputHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final int renderWidth = math.max(1, (_outputWidth * renderScale).floor());
|
||||||
|
final int renderHeight = math.max(1, (_outputHeight * renderScale).floor());
|
||||||
|
|
||||||
|
return FrameBuffer(renderWidth, renderHeight);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||||
_engine = engine;
|
_engine = engine;
|
||||||
|
final FrameBuffer scaledBuffer = _createScaledBuffer(buffer);
|
||||||
// We only need 8-bit indices for the 256 VGA colors
|
// We only need 8-bit indices for the 256 VGA colors
|
||||||
_screen = Uint8List(buffer.width * buffer.height);
|
_screen = Uint8List(scaledBuffer.width * scaledBuffer.height);
|
||||||
return super.render(engine, buffer);
|
return super.render(engine, scaledBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -129,8 +206,12 @@ class SixelRasterizer extends Rasterizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
dynamic finalizeFrame() {
|
String finalizeFrame() {
|
||||||
return toSixelString();
|
final String clearPrefix = _needsBackgroundClear
|
||||||
|
? '$_terminalTealBackground\x1b[2J\x1b[0m'
|
||||||
|
: '';
|
||||||
|
_needsBackgroundClear = false;
|
||||||
|
return '$clearPrefix\x1b[${_offsetRows + 1};${_offsetColumns + 1}H${toSixelString()}';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -169,19 +250,19 @@ class SixelRasterizer extends Rasterizer {
|
|||||||
sb.write('#$i;2;$sixelR;$sixelG;$sixelB');
|
sb.write('#$i;2;$sixelR;$sixelG;$sixelB');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Encode Image in 6-pixel vertical bands
|
// 2. Encode scaled image in 6-pixel vertical bands.
|
||||||
for (int band = 0; band < height; band += 6) {
|
for (int band = 0; band < _outputHeight; band += 6) {
|
||||||
Map<int, Uint8List> colorMap = {};
|
Map<int, Uint8List> colorMap = {};
|
||||||
|
|
||||||
// Map out which pixels use which color in this 6px high band
|
// Map out which pixels use which color in this 6px high band
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < _outputWidth; x++) {
|
||||||
for (int yOffset = 0; yOffset < 6; yOffset++) {
|
for (int yOffset = 0; yOffset < 6; yOffset++) {
|
||||||
int y = band + yOffset;
|
int y = band + yOffset;
|
||||||
if (y >= height) break;
|
if (y >= _outputHeight) break;
|
||||||
|
|
||||||
int colorIdx = _screen[y * width + x];
|
int colorIdx = _sampleScaledPixel(x, y);
|
||||||
if (!colorMap.containsKey(colorIdx)) {
|
if (!colorMap.containsKey(colorIdx)) {
|
||||||
colorMap[colorIdx] = Uint8List(width);
|
colorMap[colorIdx] = Uint8List(_outputWidth);
|
||||||
}
|
}
|
||||||
// Set the bit corresponding to the vertical position (0-5)
|
// Set the bit corresponding to the vertical position (0-5)
|
||||||
colorMap[colorIdx]![x] |= (1 << yOffset);
|
colorMap[colorIdx]![x] |= (1 << yOffset);
|
||||||
@@ -205,7 +286,7 @@ class SixelRasterizer extends Rasterizer {
|
|||||||
int runLength = 0;
|
int runLength = 0;
|
||||||
|
|
||||||
// Run-Length Encoding (RLE) loop
|
// Run-Length Encoding (RLE) loop
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < _outputWidth; x++) {
|
||||||
int val = cols[x];
|
int val = cols[x];
|
||||||
if (val == currentVal) {
|
if (val == currentVal) {
|
||||||
runLength++;
|
runLength++;
|
||||||
@@ -218,7 +299,9 @@ class SixelRasterizer extends Rasterizer {
|
|||||||
if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
|
if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.write('-'); // Move down to the next 6-pixel band
|
if (band + 6 < _outputHeight) {
|
||||||
|
sb.write('-');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// End Sixel sequence
|
// End Sixel sequence
|
||||||
@@ -226,6 +309,22 @@ class SixelRasterizer extends Rasterizer {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _sampleScaledPixel(int outX, int outY) {
|
||||||
|
final int srcX = ((((outX + 0.5) * width) / _outputWidth) - 0.5)
|
||||||
|
.round()
|
||||||
|
.clamp(
|
||||||
|
0,
|
||||||
|
width - 1,
|
||||||
|
);
|
||||||
|
final int srcY = ((((outY + 0.5) * height) / _outputHeight) - 0.5)
|
||||||
|
.round()
|
||||||
|
.clamp(
|
||||||
|
0,
|
||||||
|
height - 1,
|
||||||
|
);
|
||||||
|
return _screen[srcY * width + srcX];
|
||||||
|
}
|
||||||
|
|
||||||
void _writeSixelRle(StringBuffer sb, int value, int runLength) {
|
void _writeSixelRle(StringBuffer sb, int value, int runLength) {
|
||||||
String char = String.fromCharCode(value + 63);
|
String char = String.fromCharCode(value + 63);
|
||||||
// Sixel RLE format: !<count><char> (only worth it if count > 3)
|
// Sixel RLE format: !<count><char> (only worth it if count > 3)
|
||||||
@@ -243,15 +342,22 @@ class SixelRasterizer extends Rasterizer {
|
|||||||
void _blitVgaImage(VgaImage image, int startX, int startY) {
|
void _blitVgaImage(VgaImage image, int startX, int startY) {
|
||||||
int planeWidth = image.width ~/ 4;
|
int planeWidth = image.width ~/ 4;
|
||||||
int planeSize = planeWidth * image.height;
|
int planeSize = planeWidth * image.height;
|
||||||
|
final double scaleX = width / 320.0;
|
||||||
|
final double scaleY = height / 200.0;
|
||||||
|
|
||||||
for (int dy = 0; dy < image.height; dy++) {
|
final int destStartX = (startX * scaleX).toInt();
|
||||||
for (int dx = 0; dx < image.width; dx++) {
|
final int destStartY = (startY * scaleY).toInt();
|
||||||
int drawX = startX + dx;
|
final int destWidth = math.max(1, (image.width * scaleX).toInt());
|
||||||
int drawY = startY + dy;
|
final int destHeight = math.max(1, (image.height * scaleY).toInt());
|
||||||
|
|
||||||
|
for (int dy = 0; dy < destHeight; dy++) {
|
||||||
|
for (int dx = 0; dx < destWidth; dx++) {
|
||||||
|
int drawX = destStartX + dx;
|
||||||
|
int drawY = destStartY + dy;
|
||||||
|
|
||||||
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) {
|
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) {
|
||||||
int srcX = dx.clamp(0, image.width - 1);
|
int srcX = (dx / scaleX).toInt().clamp(0, image.width - 1);
|
||||||
int srcY = dy.clamp(0, image.height - 1);
|
int srcY = (dy / scaleY).toInt().clamp(0, image.height - 1);
|
||||||
|
|
||||||
int plane = srcX % 4;
|
int plane = srcX % 4;
|
||||||
int sx = srcX ~/ 4;
|
int sx = srcX ~/ 4;
|
||||||
|
|||||||
Reference in New Issue
Block a user