Initial commit

This commit is contained in:
2023-09-05 20:11:15 -07:00
commit 4bbf42db88
138 changed files with 5555 additions and 0 deletions

33
lib/assets.dart Normal file
View File

@ -0,0 +1,33 @@
import 'dart:developer';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
class Assets {
final Map<String, ui.Image> _loaded = {};
final Set<String> _waiting = {};
Future<ui.Image> getImageAsync(String name) async {
final assetImageByteData = await rootBundle.load(name);
final codec =
await ui.instantiateImageCodec(assetImageByteData.buffer.asUint8List());
final image = (await codec.getNextFrame()).image;
_loaded[name] = image;
return image;
}
ui.Image? getImageIfAvailable(String name) {
if (!_waiting.contains(name)) {
_waiting.add(name);
getImageAsync(name);
}
return _loaded[name];
}
}
final assets = Assets();
ui.Image? getImageIfAvailable(String name) {
return assets.getImageIfAvailable(name);
}

307
lib/cp437.dart Normal file
View File

@ -0,0 +1,307 @@
typedef Cp437 = int;
bool initialized = false;
final List<String> _fromCp437 = [
"\u0000",
"\u0001",
"\u0002",
"\u0003",
"\u0004",
"\u0005",
"\u0006",
"\u0007",
"\b",
"\t",
"\n",
"\u000b",
"\f",
"\r",
"\u000e",
"\u000f",
"\u0010",
"\u0011",
"\u0012",
"\u0013",
"\u0014",
"\u0015",
"\u0016",
"\u0017",
"\u0018",
"\u0019",
"\u001a",
"\u001b",
"\u001c",
"\u001d",
"\u001e",
"\u001f",
" ",
"!",
"\"",
"#",
"\$",
"%",
"&",
"'",
"(",
")",
"*",
"+",
",",
"-",
".",
"/",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
":",
";",
"<",
"=",
">",
"?",
"@",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"[",
"\\",
"]",
"^",
"_",
"`",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"{",
"|",
"}",
"~",
"\u007f",
"\u00c7",
"\u00fc",
"\u00e9",
"\u00e2",
"\u00e4",
"\u00e0",
"\u00e5",
"\u00e7",
"\u00ea",
"\u00eb",
"\u00e8",
"\u00ef",
"\u00ee",
"\u00ec",
"\u00c4",
"\u00c5",
"\u00c9",
"\u00e6",
"\u00c6",
"\u00f4",
"\u00f6",
"\u00f2",
"\u00fb",
"\u00f9",
"\u00ff",
"\u00d6",
"\u00dc",
"\u00a2",
"\u00a3",
"\u00a5",
"\u20a7",
"\u0192",
"\u00e1",
"\u00ed",
"\u00f3",
"\u00fa",
"\u00f1",
"\u00d1",
"\u00aa",
"\u00ba",
"\u00bf",
"\u2310",
"\u00ac",
"\u00bd",
"\u00bc",
"\u00a1",
"\u00ab",
"\u00bb",
"\u2591",
"\u2592",
"\u2593",
"\u2502",
"\u2524",
"\u2561",
"\u2562",
"\u2556",
"\u2555",
"\u2563",
"\u2551",
"\u2557",
"\u255d",
"\u255c",
"\u255b",
"\u2510",
"\u2514",
"\u2534",
"\u252c",
"\u251c",
"\u2500",
"\u253c",
"\u255e",
"\u255f",
"\u255a",
"\u2554",
"\u2569",
"\u2566",
"\u2560",
"\u2550",
"\u256c",
"\u2567",
"\u2568",
"\u2564",
"\u2565",
"\u2559",
"\u2558",
"\u2552",
"\u2553",
"\u256b",
"\u256a",
"\u2518",
"\u250c",
"\u2588",
"\u2584",
"\u258c",
"\u2590",
"\u2580",
"\u03b1",
"\u00df",
"\u0393",
"\u03c0",
"\u03a3",
"\u03c3",
"\u00b5",
"\u03c4",
"\u03a6",
"\u0398",
"\u03a9",
"\u03b4",
"\u221e",
"\u03c6",
"\u03b5",
"\u2229",
"\u2261",
"\u00b1",
"\u2265",
"\u2264",
"\u2320",
"\u2321",
"\u00f7",
"\u2248",
"\u00b0",
"\u2219",
"\u00b7",
"\u221a",
"\u207f",
"\u00b2",
"\u25a0",
"\u00a0"
];
final Map<String, Cp437> _toCp437 = {};
void _init() {
if (initialized) {
return;
}
for (final (i, c) in _fromCp437.indexed) {
_toCp437[c] = i;
}
initialized = true;
}
String fromCp437Char(Cp437 c) {
_init();
if (c < 0 || c > _fromCp437.length) {
return "?";
}
return _fromCp437[c];
}
Cp437 toCp437Char(String c) {
_init();
var cp = _toCp437[c];
if (cp == null) {
return toCp437Char("?");
}
return cp;
}
String fromCp437String(List<Cp437> s) {
var out = "";
for (final c in s) {
out += fromCp437Char(c);
}
return out;
}
List<Cp437> toCp437String(String s) {
List<Cp437> out = [];
for (final c in s.runes) {
out.add(c);
}
return out;
}

13
lib/fonts.dart Normal file
View File

@ -0,0 +1,13 @@
class Font {
final String imageName;
final int cellWidth, cellHeight;
final int nCellsW, nCellsH;
const Font(this.imageName, this.cellWidth, this.cellHeight, this.nCellsW,
this.nCellsH);
static const Font bg = Font("assets/images/fonts/font_bg.png", 1, 1, 1, 1);
static const Font small =
Font("assets/images/fonts/font_small.png", 1, 1, 16, 16);
static const Font normal = Font("assets/images/fonts/font.png", 1, 2, 32, 8);
}

79
lib/main.dart Normal file
View File

@ -0,0 +1,79 @@
import 'package:dartterm/terminal.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'DARTTERM',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
useMaterial3: true,
),
builder: (BuildContext context, Widget? child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: child!);
},
home: const MyHomePage(title: 'DARTTERM'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Ticker? ticker;
@override
void initState() {
super.initState();
ticker = Ticker(tick);
ticker!.start();
}
@override
Widget build(BuildContext context) {
return Center(
child:
AspectRatio(aspectRatio: 16 / 9.0, child: terminal.toWidget(context)),
);
}
void tick(Duration elapsed) {
setState(() {/* state changed, force a redraw */});
}
}
/*
class TerminalCustomPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 4.0
..color = Colors.indigo;
canvas.drawRect(
Rect.fromLTWH(-20 + Random().nextDouble() * 20 - 10, -20, 40, 40),
paint);
}
@override
bool shouldRepaint(TerminalCustomPainter oldDelegate) => true;
}
*/

126
lib/main.dart.old Normal file
View File

@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a blue toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// backgroundColor: Colors.amber,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

87
lib/terminal.dart Normal file
View File

@ -0,0 +1,87 @@
import 'package:dartterm/cp437.dart';
import 'package:dartterm/fonts.dart';
import 'package:dartterm/terminal_painter.dart';
import 'package:flutter/material.dart';
class Terminal {
static const int width = 96;
static const int height = 54;
static const int cellW = 64;
static const int cellH = 64;
static const int nTiles = width * height;
late List<Tile> tiles;
Terminal() {
tiles = [for (var i = 0; i < nTiles; i += 1) Tile.empty()];
for (final (x, c) in toCp437String("Hello, world!").indexed) {
tiles[x].content = Content(Font.normal.imageName, c % 32, 2 * (c ~/ 32));
tiles[x + Terminal.width].content =
Content(Font.normal.imageName, c % 32, 2 * (c ~/ 32) + 1);
}
for (final (x, c) in toCp437String("BEWARE OF THE BAT!").indexed) {
tiles[x + Terminal.width * 2].content =
Content(Font.small.imageName, c % 16, c ~/ 16);
}
for (final (x, c) in toCp437String("BEWARE OF THE BAT!").indexed) {
tiles[Terminal.width * (x + 2)].content =
Content(Font.small.imageName, c % 16, c ~/ 16);
}
}
CustomPaint toWidget(BuildContext context) {
var scalingFactor = MediaQuery.devicePixelRatioOf(context);
return CustomPaint(painter: TerminalCustomPainter(this, scalingFactor));
}
(int, int)? toXY(int i) {
if (i < 0 || i > nTiles) {
return null;
}
return (i % width, i ~/ width);
}
int? fromXY(int x, int y) {
if (x < 0 || x >= width) {
return null;
}
if (y < 0 || y >= height) {
return null;
}
return x + y * width;
}
}
class Tile {
Content? content;
Color bg;
Color fg;
Tile(this.content, this.bg, this.fg);
static Tile empty() => Tile(null, Colors.black, Colors.white);
}
class Content {
final String sourceImage;
final int sourceCx;
final int sourceCy;
const Content(this.sourceImage, this.sourceCx, this.sourceCy);
}
// reexports
Terminal terminal = Terminal();
const int width = Terminal.width;
const int height = Terminal.height;
const int cellW = Terminal.cellW;
const int cellH = Terminal.cellH;
const int nTiles = Terminal.nTiles;
Widget toWidget(BuildContext context) {
return terminal.toWidget(context);
}

154
lib/terminal_painter.dart Normal file
View File

@ -0,0 +1,154 @@
import 'dart:ui' as ui;
import 'package:dartterm/assets.dart';
import 'package:dartterm/fonts.dart';
import 'package:dartterm/terminal.dart';
import 'package:flutter/material.dart';
class TerminalCustomPainter extends CustomPainter {
Terminal t;
double scalingFactor;
TerminalCustomPainter(this.t, this.scalingFactor);
@override
bool shouldRepaint(TerminalCustomPainter oldDelegate) {
return true;
}
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..filterQuality = FilterQuality.none;
// == Fill backdrop ==
final paint2 = Paint()
..color = Colors.black
..style = PaintingStyle.fill;
canvas.drawRect(const Offset(0, 0) & size, paint2);
// == Estimate dimensions for scaling ==
var sx = size.width / Terminal.width;
var sy = size.height / Terminal.height;
var dpx = 0.0;
var dpy = 0.0;
var invScalingFactor = 1 / scalingFactor;
for (double preferredSx in [64, 56, 48, 40, 32, 24, 16, 8]) {
preferredSx *= invScalingFactor;
var preferredSy = preferredSx;
if (preferredSx < sx && preferredSy < sy) {
sx = preferredSx.toDouble();
sy = preferredSy.toDouble();
dpx = ((size.width - (sx * Terminal.width)) / 2);
dpy = ((size.height - (sy * Terminal.height)) / 2);
dpx /= invScalingFactor;
dpx = dpx.floorToDouble();
dpx *= invScalingFactor;
dpy /= invScalingFactor;
dpy = dpy.floorToDouble();
dpy *= invScalingFactor;
break;
}
}
for (var i = 0; i < nTiles; i++) {
final tile = t.tiles[i];
final (pcxDst, pcyDst) = terminal.toXY(i) ?? (0, 0);
final (pxDst, pyDst) = (dpx + pcxDst * sx, dpy + pcyDst * sy);
final rectDst = Rect.fromLTWH(pxDst, pyDst, sx, sy);
var content = tile.content;
if (content != null) {
var source = content.sourceImage;
var image = assets.getImageIfAvailable(source);
if (image != null) {
var fgRect = Rect.fromLTWH(
content.sourceCx.toDouble() * cellW,
content.sourceCy.toDouble() * cellH,
cellW.toDouble(),
cellH.toDouble());
canvas.drawImageRect(
image,
fgRect,
rectDst,
paint,
);
}
}
}
var todos = Todos();
// == Draw the background and foreground of every tile ==
for (var i = 0; i < nTiles; i++) {
final (pcxDst, pcyDst) = terminal.toXY(i) ?? (0, 0);
final (pxDst, pyDst) = (dpx + pcxDst * sx, dpy + pcyDst * sy);
final rectDst = Rect.fromLTWH(pxDst, pyDst, sx, sy);
final tile = t.tiles[i];
var bgRect = Rect.fromLTWH(0, 0, cellW.toDouble(), cellH.toDouble());
todos.add(Font.bg.imageName, bgRect, rectDst, tile.bg);
var c = tile.content;
if (c != null) {
var fgRect = Rect.fromLTWH(c.sourceCx.toDouble() * cellW,
c.sourceCy.toDouble() * cellH, cellW.toDouble(), cellH.toDouble());
todos.add(c.sourceImage, fgRect, rectDst, tile.fg);
}
}
for (var t in todos.imageTodos) {
final atlas = t.atlas;
if (atlas != null) {
canvas.drawAtlas(atlas, t.transforms, t.rects, t.colors,
BlendMode.modulate, null, paint);
}
}
}
}
class Todos {
List<ImageTodos> imageTodos = [];
Map<String, int> imageTodoIx = {};
void add(String source, Rect src, Rect dst, Color color) {
var ix = imageTodoIx[source];
if (ix == null) {
ix = imageTodos.length;
var image = assets.getImageIfAvailable(source);
imageTodos.add(ImageTodos(image));
imageTodoIx[source] = ix;
}
imageTodos[ix].add(src, dst, color);
}
}
class ImageTodos {
ui.Image? atlas;
List<RSTransform> transforms = [];
List<Rect> rects = [];
List<Color> colors = [];
ImageTodos(this.atlas);
void add(Rect src, Rect dst, Color color) {
// NOTE: Because main.dart uses aspectRatio, scaleX and scaleY should be close to the same
var scaleX = dst.width / src.width;
transforms.add(RSTransform.fromComponents(
rotation: 0.0,
scale: scaleX,
anchorX: 0.0,
anchorY: 0.0,
translateX: dst.left,
translateY: dst.top,
));
rects.add(src);
colors.add(color);
}
}