createTextSprites method

Future<List<TxSprite>> createTextSprites(
  1. String text
)

Since the Paragraph rasterizing to the Canvas, and the getting of the Image bytes are async functions, there needs to be an async function not just the constructor. Plus we want the caller to decide how many lines of a long paragraph to rasterize, and when. Text lines as TxSprites are returned as a List, and the caller can decide how many to send to Frame, and when.

Implementation

Future<List<TxSprite>> createTextSprites(String text) async {
  final List<TxSprite> sprites = [];
  final double dpr = ui.window.devicePixelRatio;

  // 1. Force a massive line height to space lines far apart.
  // This guarantees ascenders/descenders from adjacent lines
  // won't bleed into the current line's isolated bounding box.
  final paragraphBuilder = ui.ParagraphBuilder(ui.ParagraphStyle(
    textAlign: textAlign,
    textDirection: textDirection,
    fontFamily: fontFamily,
    fontSize: _fontSize.toDouble(),
    height: 5.0, // Massive multiplier to isolate lines
  ));

  paragraphBuilder.addText(text);
  final ui.Paragraph paragraph = paragraphBuilder.build();

  paragraph.layout(ui.ParagraphConstraints(width: width.toDouble()));
  List<ui.LineMetrics> lineMetrics = paragraph.computeLineMetrics();

  if (lineMetrics.isEmpty) {
    return sprites;
  }

  // 2. Choose a fixed baseline for all lines in this block.
  // Placing the baseline at 80% of the line height is standard.
  // For a 16px line height, this puts the baseline at Y=13,
  // leaving 13px for ascenders and 3px for descenders.
  final double fixedBaseline = (_lineHeight * 0.8).roundToDouble();

  for (var line in lineMetrics) {
    final int lineWidth = line.width.ceil();

    // check for non-blank lines
    if (lineWidth > 0) {
      final pictureRecorder = ui.PictureRecorder();
      final canvas = ui.Canvas(pictureRecorder);

      // Scale the canvas by DPR so the text is rendered at high fidelity
      canvas.scale(dpr);

      // Clip the canvas exactly to our sprite dimensions
      canvas.clipRect(Rect.fromLTWH(0, 0, lineWidth.toDouble(), _lineHeight.toDouble()));

      // 3. Align the line's specific baseline exactly to our fixedBaseline
      canvas.translate(-line.left, fixedBaseline - line.baseline);

      canvas.drawParagraph(paragraph, ui.Offset.zero);
      final ui.Picture picture = pictureRecorder.endRecording();

      final int scaledWidth = (lineWidth * dpr).round();
      final int scaledHeight = (_lineHeight * dpr).round();
      final ui.Image image = await picture.toImage(scaledWidth, scaledHeight);

      // Force RGBA for predictable byte-access on Android/iOS
      final ByteData? byteData = (await image.toByteData(format: ui.ImageByteFormat.rawStraightRgba))!;
      if (byteData == null) continue;

      final Uint8List pixels = byteData.buffer.asUint8List();
      final Uint8List linePixelData = Uint8List(lineWidth * _lineHeight);

      // 4. Downsample: Map the high-res buffer back to your 1-bit grid
      for (int y = 0; y < _lineHeight; y++) {
        for (int x = 0; x < lineWidth; x++) {
          // Calculate the "center" pixel of the DPR-scaled block
          // This ensures we aren't sampling an anti-aliased edge pixel
          int centerX = (x * dpr + dpr / 2).floor();
          int centerY = (y * dpr + dpr / 2).floor();

          // Index in the RGBA8888 byte array
          int pos = (centerY * scaledWidth + centerX) * 4;

          // Thresholding logic:
          // iOS Impeller might need a slightly lower threshold (e.g., 100)
          // if your fonts appear too thin.
          int threshold = 110;
          linePixelData[y * lineWidth + x] = pixels[pos] >= threshold ? 1 : 0;
        }
      }

      sprites.add(TxSprite(
        width: lineWidth,
        height: _lineHeight,
        numColors: 2,
        paletteData: _getPalette().data,
        pixelData: linePixelData,
      ));
    } else {
      // zero-width line, a blank line in the text block
      sprites.add(TxSprite(
        width: 1,
        height: 1,
        numColors: 2,
        paletteData: _getPalette().data,
        pixelData: Uint8List(1)
      ));
    }
  }

  return sprites;
}