diff --git a/apps/wolf_3d_gui/lib/screens/sprite_gallery.dart b/apps/wolf_3d_gui/lib/screens/sprite_gallery.dart index 3286b17..3338ffb 100644 --- a/apps/wolf_3d_gui/lib/screens/sprite_gallery.dart +++ b/apps/wolf_3d_gui/lib/screens/sprite_gallery.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_entities.dart'; import 'package:wolf_3d_flutter/wolf_3d.dart'; +import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart'; class SpriteGallery extends StatelessWidget { final List sprites; @@ -53,10 +54,7 @@ class SpriteGallery extends StatelessWidget { textAlign: TextAlign.center, ), Expanded( - child: CustomPaint( - painter: SingleSpritePainter(sprite: sprites[index]), - size: const Size(64, 64), - ), + child: WolfAssetPainter.sprite(sprites[index]), ), ], ); @@ -65,28 +63,3 @@ class SpriteGallery extends StatelessWidget { ); } } - -class SingleSpritePainter extends CustomPainter { - final Sprite sprite; - SingleSpritePainter({required this.sprite}); - - @override - void paint(Canvas canvas, Size size) { - double pixelSize = size.width / 64; - for (int x = 0; x < 64; x++) { - for (int y = 0; y < 64; y++) { - int colorByte = sprite.pixels[x * 64 + y]; - if (colorByte != 255) { - // Skip transparency - canvas.drawRect( - Rect.fromLTWH(x * pixelSize, y * pixelSize, pixelSize, pixelSize), - Paint()..color = Color(ColorPalette.vga32Bit[colorByte]), - ); - } - } - } - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) => false; -} diff --git a/apps/wolf_3d_gui/lib/screens/vga_gallery.dart b/apps/wolf_3d_gui/lib/screens/vga_gallery.dart index f2a1a24..e7850ea 100644 --- a/apps/wolf_3d_gui/lib/screens/vga_gallery.dart +++ b/apps/wolf_3d_gui/lib/screens/vga_gallery.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; +import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart'; class VgaGallery extends StatelessWidget { final List images; @@ -30,14 +31,7 @@ class VgaGallery extends StatelessWidget { const SizedBox(height: 8), Expanded( child: Center( - child: CustomPaint( - painter: VgaPainter(image: images[index]), - // Scale it up so tiny fonts are readable - size: Size( - images[index].width * 2.0, - images[index].height * 2.0, - ), - ), + child: WolfAssetPainter.vga(images[index]), ), ), ], @@ -47,49 +41,3 @@ class VgaGallery extends StatelessWidget { ); } } - -class VgaPainter extends CustomPainter { - final VgaImage image; - VgaPainter({required this.image}); - - @override - void paint(Canvas canvas, Size size) { - int planeWidth = image.width ~/ 4; - int planeSize = planeWidth * image.height; - - double pixelW = size.width / image.width; - double pixelH = size.height / image.height; - final Paint paint = Paint()..isAntiAlias = false; - - for (int y = 0; y < image.height; y++) { - for (int x = 0; x < image.width; x++) { - int plane = x % 4; - int sx = x ~/ 4; - int colorByte = - image.pixels[(plane * planeSize) + (y * planeWidth) + sx]; - - if (colorByte != 255) { - int abgr = ColorPalette.vga32Bit[colorByte]; - - // Extract the bytes - int r = abgr & 0xFF; - int g = (abgr >> 8) & 0xFF; - int b = (abgr >> 16) & 0xFF; - int a = (abgr >> 24) & 0xFF; - - // Repack them as ARGB for Flutter's Color object - int argb = (a << 24) | (r << 16) | (g << 8) | b; - - paint.color = Color(argb); - canvas.drawRect( - Rect.fromLTWH(x * pixelW, y * pixelH, pixelW + 0.5, pixelH + 0.5), - paint, - ); - } - } - } - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_asset_painter.dart b/packages/wolf_3d_renderer/lib/wolf_3d_asset_painter.dart new file mode 100644 index 0000000..29d763c --- /dev/null +++ b/packages/wolf_3d_renderer/lib/wolf_3d_asset_painter.dart @@ -0,0 +1,209 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; + +/// A unified widget to display and cache Wolf3D assets. +class WolfAssetPainter extends StatefulWidget { + final Sprite? sprite; + final VgaImage? vgaImage; + final ui.Image? frame; + + const WolfAssetPainter.sprite(this.sprite, {super.key}) + : vgaImage = null, + frame = null; + + const WolfAssetPainter.vga(this.vgaImage, {super.key}) + : sprite = null, + frame = null; + + const WolfAssetPainter.frame(this.frame, {super.key}) + : sprite = null, + vgaImage = null; + + @override + State createState() => _WolfAssetPainterState(); +} + +class _WolfAssetPainterState extends State { + ui.Image? _cachedImage; + + // Tracks if we should dispose the image to free native memory + bool _ownsImage = false; + + @override + void initState() { + super.initState(); + _prepareImage(); + } + + @override + void didUpdateWidget(covariant WolfAssetPainter oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.sprite != oldWidget.sprite || + widget.vgaImage != oldWidget.vgaImage || + widget.frame != oldWidget.frame) { + _prepareImage(); + } + } + + @override + void dispose() { + if (_ownsImage) { + _cachedImage?.dispose(); + } + super.dispose(); + } + + Future _prepareImage() async { + // Clean up previous internally generated image + if (_ownsImage && _cachedImage != null) { + _cachedImage!.dispose(); + _cachedImage = null; + } + + // If a pre-rendered frame is passed in, just use it directly + if (widget.frame != null) { + _ownsImage = false; + if (mounted) { + setState(() => _cachedImage = widget.frame); + } + return; + } + + ui.Image? newImage; + if (widget.sprite != null) { + newImage = await _buildSpriteImage(widget.sprite!); + } else if (widget.vgaImage != null) { + newImage = await _buildVgaImage(widget.vgaImage!); + } + + if (mounted) { + setState(() { + _ownsImage = true; + _cachedImage = newImage; + }); + } else { + // If the widget was unmounted while building, dispose the unused image + newImage?.dispose(); + } + } + + /// Converts a Sprite's 8-bit palette data to a 32-bit RGBA ui.Image + Future _buildSpriteImage(Sprite sprite) { + final completer = Completer(); + final pixels = Uint8List(64 * 64 * 4); // 4 bytes per pixel (RGBA) + + for (int x = 0; x < 64; x++) { + for (int y = 0; y < 64; y++) { + int colorByte = sprite.pixels[x * 64 + y]; + int offset = (y * 64 + x) * 4; // Row-major layout for ui.decodeImage + + if (colorByte != 255) { + // 255 is transparency + int abgr = ColorPalette.vga32Bit[colorByte]; + pixels[offset] = abgr & 0xFF; // R + pixels[offset + 1] = (abgr >> 8) & 0xFF; // G + pixels[offset + 2] = (abgr >> 16) & 0xFF; // B + pixels[offset + 3] = (abgr >> 24) & 0xFF; // A + } else { + pixels[offset + 3] = 0; // Alpha 0 + } + } + } + + ui.decodeImageFromPixels( + pixels, + 64, + 64, + ui.PixelFormat.rgba8888, + (img) => completer.complete(img), + ); + return completer.future; + } + + /// Converts a VgaImage's planar 8-bit palette data to a 32-bit RGBA ui.Image + Future _buildVgaImage(VgaImage image) { + final completer = Completer(); + final pixels = Uint8List(image.width * image.height * 4); + + int planeWidth = image.width ~/ 4; + int planeSize = planeWidth * image.height; + + for (int y = 0; y < image.height; y++) { + for (int x = 0; x < image.width; x++) { + int plane = x % 4; + int sx = x ~/ 4; + int colorByte = + image.pixels[(plane * planeSize) + (y * planeWidth) + sx]; + int offset = (y * image.width + x) * 4; + + if (colorByte != 255) { + int abgr = ColorPalette.vga32Bit[colorByte]; + pixels[offset] = abgr & 0xFF; // R + pixels[offset + 1] = (abgr >> 8) & 0xFF; // G + pixels[offset + 2] = (abgr >> 16) & 0xFF; // B + pixels[offset + 3] = (abgr >> 24) & 0xFF; // A + } else { + pixels[offset + 3] = 0; // Alpha 0 + } + } + } + + ui.decodeImageFromPixels( + pixels, + image.width, + image.height, + ui.PixelFormat.rgba8888, + (img) => completer.complete(img), + ); + return completer.future; + } + + @override + Widget build(BuildContext context) { + if (_cachedImage == null) { + return const Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: Colors.white24, + strokeWidth: 2, + ), + ), + ); + } + + return CustomPaint( + painter: _ImagePainter(_cachedImage!), + child: const SizedBox.expand(), + ); + } +} + +class _ImagePainter extends CustomPainter { + final ui.Image image; + _ImagePainter(this.image); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..filterQuality = FilterQuality.none; + final srcRect = Rect.fromLTWH( + 0, + 0, + image.width.toDouble(), + image.height.toDouble(), + ); + final dstRect = Rect.fromLTWH(0, 0, size.width, size.height); + + // Draw the entire cached image in one pass + canvas.drawImageRect(image, srcRect, dstRect, paint); + } + + @override + bool shouldRepaint(covariant _ImagePainter oldDelegate) => + oldDelegate.image != image; +} diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart index 50aef2c..b5764fd 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart'; import 'package:wolf_3d_renderer/base_renderer.dart'; +import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart'; class WolfFlutterRenderer extends BaseWolfRenderer { const WolfFlutterRenderer({ @@ -61,33 +62,8 @@ class _WolfFlutterRendererState padding: const EdgeInsets.all(16.0), child: AspectRatio( aspectRatio: 4 / 3, - child: CustomPaint(painter: BufferPainter(_renderedFrame)), + child: WolfAssetPainter.frame(_renderedFrame), ), ); } } - -class BufferPainter extends CustomPainter { - final ui.Image? frame; - BufferPainter(this.frame); - - @override - void paint(Canvas canvas, Size size) { - if (frame == null) return; - - final Paint paint = Paint()..filterQuality = FilterQuality.none; - final Rect srcRect = Rect.fromLTWH( - 0, - 0, - frame!.width.toDouble(), - frame!.height.toDouble(), - ); - final Rect dstRect = Rect.fromLTWH(0, 0, size.width, size.height); - - canvas.drawImageRect(frame!, srcRect, dstRect, paint); - } - - @override - bool shouldRepaint(covariant BufferPainter oldDelegate) => - oldDelegate.frame != frame; -}