feat: Implement ASCII theme cycling and add quadrant theme support
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -125,9 +125,36 @@ class CliGameLoop {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bytes.contains(116) || bytes.contains(84)) {
|
||||||
|
_cycleAsciiTheme();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
input.handleKey(bytes);
|
input.handleKey(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _cycleAsciiTheme() {
|
||||||
|
final List<AsciiRenderer> asciiRenderers = <AsciiRenderer>[
|
||||||
|
if (primaryRenderer is AsciiRenderer) primaryRenderer as AsciiRenderer,
|
||||||
|
if (secondaryRenderer is AsciiRenderer)
|
||||||
|
secondaryRenderer as AsciiRenderer,
|
||||||
|
];
|
||||||
|
if (asciiRenderers.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final AsciiTheme nextTheme = AsciiThemes.nextOf(
|
||||||
|
asciiRenderers.first.activeTheme,
|
||||||
|
);
|
||||||
|
for (final renderer in asciiRenderers) {
|
||||||
|
renderer.activeTheme = nextTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout.hasTerminal) {
|
||||||
|
stdout.write('\x1b[2J\x1b[H');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _tick(Timer timer) {
|
void _tick(Timer timer) {
|
||||||
if (!_isRunning) {
|
if (!_isRunning) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ library;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
|
||||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||||
import 'package:wolf_3d_renderer/wolf_3d_ascii_renderer.dart';
|
import 'package:wolf_3d_renderer/wolf_3d_ascii_renderer.dart';
|
||||||
import 'package:wolf_3d_renderer/wolf_3d_flutter_renderer.dart';
|
import 'package:wolf_3d_renderer/wolf_3d_flutter_renderer.dart';
|
||||||
@@ -33,6 +34,7 @@ class GameScreen extends StatefulWidget {
|
|||||||
class _GameScreenState extends State<GameScreen> {
|
class _GameScreenState extends State<GameScreen> {
|
||||||
late final WolfEngine _engine;
|
late final WolfEngine _engine;
|
||||||
_RendererMode _rendererMode = _RendererMode.software;
|
_RendererMode _rendererMode = _RendererMode.software;
|
||||||
|
AsciiTheme _asciiTheme = AsciiThemes.blocks;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -104,7 +106,7 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
top: 16,
|
top: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
child: Text(
|
child: Text(
|
||||||
'TAB: ${_modeLabel(_rendererMode)} `: FPS ${_engine.showFpsCounter ? 'On' : 'Off'}',
|
'TAB: ${_modeLabel(_rendererMode)} T: ${_asciiTheme.name} `: FPS ${_engine.showFpsCounter ? 'On' : 'Off'}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.5),
|
color: Colors.white.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
@@ -131,6 +133,7 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
case _RendererMode.ascii:
|
case _RendererMode.ascii:
|
||||||
return WolfAsciiRenderer(
|
return WolfAsciiRenderer(
|
||||||
engine: _engine,
|
engine: _engine,
|
||||||
|
theme: _asciiTheme,
|
||||||
onKeyEvent: _handleRendererKeyEvent,
|
onKeyEvent: _handleRendererKeyEvent,
|
||||||
);
|
);
|
||||||
case _RendererMode.glsl:
|
case _RendererMode.glsl:
|
||||||
@@ -155,6 +158,13 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
if (event.logicalKey == LogicalKeyboardKey.backquote ||
|
if (event.logicalKey == LogicalKeyboardKey.backquote ||
|
||||||
event.character == '`') {
|
event.character == '`') {
|
||||||
setState(_toggleFpsCounter);
|
setState(_toggleFpsCounter);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.keyT ||
|
||||||
|
event.character == 't' ||
|
||||||
|
event.character == 'T') {
|
||||||
|
setState(_cycleAsciiTheme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +195,10 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
_engine.showFpsCounter = !_engine.showFpsCounter;
|
_engine.showFpsCounter = !_engine.showFpsCounter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _cycleAsciiTheme() {
|
||||||
|
_asciiTheme = AsciiThemes.nextOf(_asciiTheme);
|
||||||
|
}
|
||||||
|
|
||||||
String _modeLabel(_RendererMode mode) {
|
String _modeLabel(_RendererMode mode) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case _RendererMode.software:
|
case _RendererMode.software:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer' show log;
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:arcane_helper_utils/arcane_helper_utils.dart';
|
import 'package:arcane_helper_utils/arcane_helper_utils.dart';
|
||||||
@@ -35,9 +36,9 @@ class AsciiTheme {
|
|||||||
/// A collection of pre-defined character sets
|
/// A collection of pre-defined character sets
|
||||||
abstract class AsciiThemes {
|
abstract class AsciiThemes {
|
||||||
static const AsciiTheme blocks = AsciiTheme('Blocks', "█▓▒░ ");
|
static const AsciiTheme blocks = AsciiTheme('Blocks', "█▓▒░ ");
|
||||||
static const AsciiTheme classic = AsciiTheme('Classic', "@%#*+=-:. ");
|
static const AsciiTheme quadrant = AsciiTheme('Quadrant', "▛▜▟▙▚▞▖ ");
|
||||||
|
|
||||||
static const List<AsciiTheme> values = [blocks, classic];
|
static const List<AsciiTheme> values = [blocks, quadrant];
|
||||||
|
|
||||||
static AsciiTheme nextOf(AsciiTheme current) {
|
static AsciiTheme nextOf(AsciiTheme current) {
|
||||||
final int currentIndex = values.indexOf(current);
|
final int currentIndex = values.indexOf(current);
|
||||||
@@ -111,6 +112,26 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
|
|
||||||
late List<List<ColoredChar>> _screen;
|
late List<List<ColoredChar>> _screen;
|
||||||
late List<List<int>> _scenePixels;
|
late List<List<int>> _scenePixels;
|
||||||
|
String? _lastLoggedThemeName;
|
||||||
|
|
||||||
|
static const List<String> _quadrantByMask = <String>[
|
||||||
|
' ',
|
||||||
|
'▘',
|
||||||
|
'▝',
|
||||||
|
'▀',
|
||||||
|
'▖',
|
||||||
|
'▌',
|
||||||
|
'▞',
|
||||||
|
'▛',
|
||||||
|
'▗',
|
||||||
|
'▚',
|
||||||
|
'▐',
|
||||||
|
'▜',
|
||||||
|
'▄',
|
||||||
|
'▙',
|
||||||
|
'▟',
|
||||||
|
'█',
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final double aspectMultiplier;
|
final double aspectMultiplier;
|
||||||
@@ -148,6 +169,10 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
|
|
||||||
int get _terminalPixelHeight => _usesTerminalLayout ? height * 2 : height;
|
int get _terminalPixelHeight => _usesTerminalLayout ? height * 2 : height;
|
||||||
|
|
||||||
|
int get _terminalSceneWidth => width;
|
||||||
|
|
||||||
|
bool get _usesQuadrantCompose => activeTheme == AsciiThemes.quadrant;
|
||||||
|
|
||||||
int get _viewportRightX => projectionOffsetX + projectionWidth;
|
int get _viewportRightX => projectionOffsetX + projectionWidth;
|
||||||
|
|
||||||
int get _terminalBackdropColor =>
|
int get _terminalBackdropColor =>
|
||||||
@@ -157,6 +182,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
@override
|
@override
|
||||||
/// Initializes the character grid before running the shared render pipeline.
|
/// Initializes the character grid before running the shared render pipeline.
|
||||||
dynamic render(WolfEngine engine) {
|
dynamic render(WolfEngine engine) {
|
||||||
|
_logThemeIfChanged();
|
||||||
_screen = List.generate(
|
_screen = List.generate(
|
||||||
engine.frameBuffer.height,
|
engine.frameBuffer.height,
|
||||||
(_) => List.filled(
|
(_) => List.filled(
|
||||||
@@ -1224,7 +1250,10 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
for (int dx = 0; dx < w; dx++) {
|
for (int dx = 0; dx < w; dx++) {
|
||||||
int x = startX + dx;
|
int x = startX + dx;
|
||||||
int y = startY + dy;
|
int y = startY + dy;
|
||||||
if (x >= 0 && x < width && y >= 0 && y < _terminalPixelHeight) {
|
if (x >= 0 &&
|
||||||
|
x < _terminalSceneWidth &&
|
||||||
|
y >= 0 &&
|
||||||
|
y < _terminalPixelHeight) {
|
||||||
_scenePixels[y][x] = color;
|
_scenePixels[y][x] = color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1309,8 +1338,13 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
int topY = y * 2;
|
int topY = y * 2;
|
||||||
int bottomY = math.min(topY + 1, _terminalPixelHeight - 1);
|
int bottomY = math.min(topY + 1, _terminalPixelHeight - 1);
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
int topColor = _scenePixels[topY][x];
|
final int leftX = x;
|
||||||
int bottomColor = _scenePixels[bottomY][x];
|
final int rightX = _usesQuadrantCompose
|
||||||
|
? math.min(x + 1, _terminalSceneWidth - 1)
|
||||||
|
: leftX;
|
||||||
|
|
||||||
|
int topColor = _scenePixels[topY][leftX];
|
||||||
|
int bottomColor = _scenePixels[bottomY][leftX];
|
||||||
|
|
||||||
ColoredChar overlay = _screen[y][x];
|
ColoredChar overlay = _screen[y][x];
|
||||||
if (overlay.char != ' ') {
|
if (overlay.char != ' ') {
|
||||||
@@ -1322,7 +1356,12 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
_screen[y][x] = ColoredChar(
|
_screen[y][x] = ColoredChar(
|
||||||
overlay.char,
|
overlay.char,
|
||||||
overlay.rawColor,
|
overlay.rawColor,
|
||||||
bottomColor,
|
_usesQuadrantCompose
|
||||||
|
? _blendPackedColors(
|
||||||
|
_scenePixels[bottomY][leftX],
|
||||||
|
_scenePixels[bottomY][rightX],
|
||||||
|
)
|
||||||
|
: bottomColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -1331,13 +1370,109 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
// Pack two scene rows into one cell for both terminal and Flutter grid
|
// Pack two scene rows into one cell for both terminal and Flutter grid
|
||||||
// modes. Overlay characters above keep a null background in Flutter
|
// modes. Overlay characters above keep a null background in Flutter
|
||||||
// mode, so this does not introduce text background artifacts.
|
// mode, so this does not introduce text background artifacts.
|
||||||
_screen[y][x] = topColor == bottomColor
|
if (_usesQuadrantCompose) {
|
||||||
? ColoredChar('█', topColor)
|
final (char, fgColor, bgColor) = _composeQuadrantCell(
|
||||||
: ColoredChar('▀', topColor, bottomColor);
|
_scenePixels[topY][leftX],
|
||||||
|
_scenePixels[topY][rightX],
|
||||||
|
_scenePixels[bottomY][leftX],
|
||||||
|
_scenePixels[bottomY][rightX],
|
||||||
|
);
|
||||||
|
_screen[y][x] = bgColor == null
|
||||||
|
? ColoredChar(char, fgColor)
|
||||||
|
: ColoredChar(char, fgColor, bgColor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topColor == bottomColor) {
|
||||||
|
_screen[y][x] = ColoredChar('█', topColor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_screen[y][x] = ColoredChar('▀', topColor, bottomColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(String char, int fgColor, int? bgColor) _composeQuadrantCell(
|
||||||
|
int topLeft,
|
||||||
|
int topRight,
|
||||||
|
int bottomLeft,
|
||||||
|
int bottomRight,
|
||||||
|
) {
|
||||||
|
final List<int> samples = <int>[topLeft, topRight, bottomLeft, bottomRight];
|
||||||
|
int bgColor = samples.first;
|
||||||
|
int bgCount = 0;
|
||||||
|
for (final candidate in samples) {
|
||||||
|
final int count = samples.where((value) => value == candidate).length;
|
||||||
|
if (count > bgCount) {
|
||||||
|
bgColor = candidate;
|
||||||
|
bgCount = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int fgColor = bgColor;
|
||||||
|
double fgDistance = -1.0;
|
||||||
|
for (final candidate in samples) {
|
||||||
|
final double distance = _packedColorDistance(candidate, bgColor);
|
||||||
|
if (distance > fgDistance) {
|
||||||
|
fgDistance = distance;
|
||||||
|
fgColor = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fgColor == bgColor) {
|
||||||
|
return ('█', fgColor, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
int mask = 0;
|
||||||
|
if (_packedColorDistance(topLeft, fgColor) <=
|
||||||
|
_packedColorDistance(topLeft, bgColor)) {
|
||||||
|
mask |= 1;
|
||||||
|
}
|
||||||
|
if (_packedColorDistance(topRight, fgColor) <=
|
||||||
|
_packedColorDistance(topRight, bgColor)) {
|
||||||
|
mask |= 2;
|
||||||
|
}
|
||||||
|
if (_packedColorDistance(bottomLeft, fgColor) <=
|
||||||
|
_packedColorDistance(bottomLeft, bgColor)) {
|
||||||
|
mask |= 4;
|
||||||
|
}
|
||||||
|
if (_packedColorDistance(bottomRight, fgColor) <=
|
||||||
|
_packedColorDistance(bottomRight, bgColor)) {
|
||||||
|
mask |= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask == 0) {
|
||||||
|
return (' ', bgColor, null);
|
||||||
|
}
|
||||||
|
if (mask == 15) {
|
||||||
|
return ('█', fgColor, null);
|
||||||
|
}
|
||||||
|
return (_quadrantByMask[mask], fgColor, bgColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _logThemeIfChanged() {
|
||||||
|
if (_lastLoggedThemeName == activeTheme.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastLoggedThemeName = activeTheme.name;
|
||||||
|
log('ASCII renderer theme: ${activeTheme.name}', name: 'AsciiRenderer');
|
||||||
|
}
|
||||||
|
|
||||||
|
int _blendPackedColors(int a, int b) {
|
||||||
|
final int r = (((a & 0xFF) + (b & 0xFF)) / 2).round();
|
||||||
|
final int g = ((((a >> 8) & 0xFF) + ((b >> 8) & 0xFF)) / 2).round();
|
||||||
|
final int blue = ((((a >> 16) & 0xFF) + ((b >> 16) & 0xFF)) / 2).round();
|
||||||
|
return 0xFF000000 | (blue << 16) | (g << 8) | r;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _packedColorDistance(int a, int b) {
|
||||||
|
final int dr = (a & 0xFF) - (b & 0xFF);
|
||||||
|
final int dg = ((a >> 8) & 0xFF) - ((b >> 8) & 0xFF);
|
||||||
|
final int db = ((a >> 16) & 0xFF) - ((b >> 16) & 0xFF);
|
||||||
|
return (dr * dr + dg * dg + db * db).toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts the current frame to a single printable ANSI string
|
/// Converts the current frame to a single printable ANSI string
|
||||||
StringBuffer toAnsiString() {
|
StringBuffer toAnsiString() {
|
||||||
StringBuffer buffer = StringBuffer();
|
StringBuffer buffer = StringBuffer();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ library;
|
|||||||
export 'src/raycasting/projection.dart';
|
export 'src/raycasting/projection.dart';
|
||||||
export 'src/raycasting/raycaster.dart';
|
export 'src/raycasting/raycaster.dart';
|
||||||
export 'src/rendering/ascii_renderer.dart'
|
export 'src/rendering/ascii_renderer.dart'
|
||||||
show AsciiRenderer, AsciiRendererMode, ColoredChar;
|
show AsciiRenderer, AsciiRendererMode, AsciiTheme, AsciiThemes, ColoredChar;
|
||||||
export 'src/rendering/cli_renderer_backend.dart';
|
export 'src/rendering/cli_renderer_backend.dart';
|
||||||
export 'src/rendering/renderer_backend.dart';
|
export 'src/rendering/renderer_backend.dart';
|
||||||
export 'src/rendering/sixel_renderer.dart';
|
export 'src/rendering/sixel_renderer.dart';
|
||||||
|
|||||||
20
packages/wolf_3d_dart/test/rendering/ascii_themes_test.dart
Normal file
20
packages/wolf_3d_dart/test/rendering/ascii_themes_test.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('AsciiThemes', () {
|
||||||
|
test('contains quadrant as a selectable style', () {
|
||||||
|
expect(AsciiThemes.values, contains(AsciiThemes.quadrant));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cycles through blocks and quadrant', () {
|
||||||
|
expect(AsciiThemes.nextOf(AsciiThemes.blocks), AsciiThemes.quadrant);
|
||||||
|
expect(AsciiThemes.nextOf(AsciiThemes.quadrant), AsciiThemes.blocks);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('defaults to first style when unknown style is provided', () {
|
||||||
|
const custom = AsciiTheme('Custom', 'Xx ');
|
||||||
|
expect(AsciiThemes.nextOf(custom), AsciiThemes.blocks);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -7,9 +7,12 @@ import 'package:wolf_3d_renderer/base_renderer.dart';
|
|||||||
|
|
||||||
/// Displays the game using a text-mode approximation of the original renderer.
|
/// Displays the game using a text-mode approximation of the original renderer.
|
||||||
class WolfAsciiRenderer extends BaseWolfRenderer {
|
class WolfAsciiRenderer extends BaseWolfRenderer {
|
||||||
|
final AsciiTheme theme;
|
||||||
|
|
||||||
/// Creates an ASCII renderer bound to [engine].
|
/// Creates an ASCII renderer bound to [engine].
|
||||||
const WolfAsciiRenderer({
|
const WolfAsciiRenderer({
|
||||||
required super.engine,
|
required super.engine,
|
||||||
|
this.theme = AsciiThemes.blocks,
|
||||||
super.onKeyEvent,
|
super.onKeyEvent,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
@@ -30,6 +33,7 @@ class _WolfAsciiRendererState extends BaseWolfRendererState<WolfAsciiRenderer> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_asciiRenderer.activeTheme = widget.theme;
|
||||||
// ASCII output uses a reduced logical framebuffer because glyph rendering
|
// ASCII output uses a reduced logical framebuffer because glyph rendering
|
||||||
// expands the final view significantly once laid out in Flutter text.
|
// expands the final view significantly once laid out in Flutter text.
|
||||||
if (widget.engine.frameBuffer.width != _renderWidth ||
|
if (widget.engine.frameBuffer.width != _renderWidth ||
|
||||||
@@ -38,6 +42,14 @@ class _WolfAsciiRendererState extends BaseWolfRendererState<WolfAsciiRenderer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant WolfAsciiRenderer oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.theme != widget.theme) {
|
||||||
|
_asciiRenderer.activeTheme = widget.theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color get scaffoldColor => widget.engine.difficulty == null
|
Color get scaffoldColor => widget.engine.difficulty == null
|
||||||
? _colorFromRgb(widget.engine.menuBackgroundRgb)
|
? _colorFromRgb(widget.engine.menuBackgroundRgb)
|
||||||
|
|||||||
Reference in New Issue
Block a user