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:
@@ -261,4 +261,32 @@ abstract class ColorPalette {
|
||||
0xFF6D6D00,
|
||||
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 {
|
||||
static const double _targetAspectRatio = 4 / 3;
|
||||
static const int _terminalBackdropArgb = 0xFF009688;
|
||||
static const int _minimumTerminalColumns = 80;
|
||||
static const int _minimumTerminalRows = 24;
|
||||
static const int _simpleHudMinWidth = 84;
|
||||
static const int _simpleHudMinRows = 7;
|
||||
|
||||
@@ -98,6 +100,19 @@ class AsciiRasterizer extends Rasterizer {
|
||||
@override
|
||||
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 _viewportRightX => projectionOffsetX + projectionWidth;
|
||||
@@ -615,7 +630,7 @@ class AsciiRasterizer extends Rasterizer {
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic finalizeFrame() {
|
||||
String finalizeFrame() {
|
||||
if (_engine.player.damageFlash > 0.0) {
|
||||
if (isTerminal) {
|
||||
_applyDamageFlashToScene();
|
||||
@@ -626,7 +641,7 @@ class AsciiRasterizer extends Rasterizer {
|
||||
if (isTerminal) {
|
||||
_composeTerminalScene();
|
||||
}
|
||||
return _screen;
|
||||
return toAnsiString();
|
||||
}
|
||||
|
||||
// --- PRIVATE HUD DRAWING HELPERS ---
|
||||
|
||||
@@ -31,6 +31,13 @@ abstract class Rasterizer {
|
||||
/// more vertical detail and collapse it into half-block glyphs.
|
||||
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.
|
||||
/// Orchestrates the mathematical rendering pipeline.
|
||||
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';
|
||||
|
||||
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 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
|
||||
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||
_engine = engine;
|
||||
final FrameBuffer scaledBuffer = _createScaledBuffer(buffer);
|
||||
// We only need 8-bit indices for the 256 VGA colors
|
||||
_screen = Uint8List(buffer.width * buffer.height);
|
||||
return super.render(engine, buffer);
|
||||
_screen = Uint8List(scaledBuffer.width * scaledBuffer.height);
|
||||
return super.render(engine, scaledBuffer);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -129,8 +206,12 @@ class SixelRasterizer extends Rasterizer {
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic finalizeFrame() {
|
||||
return toSixelString();
|
||||
String finalizeFrame() {
|
||||
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');
|
||||
}
|
||||
|
||||
// 2. Encode Image in 6-pixel vertical bands
|
||||
for (int band = 0; band < height; band += 6) {
|
||||
// 2. Encode scaled image in 6-pixel vertical bands.
|
||||
for (int band = 0; band < _outputHeight; band += 6) {
|
||||
Map<int, Uint8List> colorMap = {};
|
||||
|
||||
// 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++) {
|
||||
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)) {
|
||||
colorMap[colorIdx] = Uint8List(width);
|
||||
colorMap[colorIdx] = Uint8List(_outputWidth);
|
||||
}
|
||||
// Set the bit corresponding to the vertical position (0-5)
|
||||
colorMap[colorIdx]![x] |= (1 << yOffset);
|
||||
@@ -205,7 +286,7 @@ class SixelRasterizer extends Rasterizer {
|
||||
int runLength = 0;
|
||||
|
||||
// Run-Length Encoding (RLE) loop
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int x = 0; x < _outputWidth; x++) {
|
||||
int val = cols[x];
|
||||
if (val == currentVal) {
|
||||
runLength++;
|
||||
@@ -218,7 +299,9 @@ class SixelRasterizer extends Rasterizer {
|
||||
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
|
||||
@@ -226,6 +309,22 @@ class SixelRasterizer extends Rasterizer {
|
||||
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) {
|
||||
String char = String.fromCharCode(value + 63);
|
||||
// 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) {
|
||||
int planeWidth = image.width ~/ 4;
|
||||
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++) {
|
||||
for (int dx = 0; dx < image.width; dx++) {
|
||||
int drawX = startX + dx;
|
||||
int drawY = startY + dy;
|
||||
final int destStartX = (startX * scaleX).toInt();
|
||||
final int destStartY = (startY * scaleY).toInt();
|
||||
final int destWidth = math.max(1, (image.width * scaleX).toInt());
|
||||
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) {
|
||||
int srcX = dx.clamp(0, image.width - 1);
|
||||
int srcY = dy.clamp(0, image.height - 1);
|
||||
int srcX = (dx / scaleX).toInt().clamp(0, image.width - 1);
|
||||
int srcY = (dy / scaleY).toInt().clamp(0, image.height - 1);
|
||||
|
||||
int plane = srcX % 4;
|
||||
int sx = srcX ~/ 4;
|
||||
|
||||
Reference in New Issue
Block a user