Files
infinite_lazy_grid/lib/core/background.dart
2025-09-07 05:39:50 +00:00

285 lines
8.2 KiB
Dart

import 'package:flutter/material.dart';
import 'dart:ui' as ui show FragmentProgram; // for runtime shader
import 'package:flutter/rendering.dart'
show RendererBinding; // to mark repaint after async load
/// Abstract definition for a [LazyCanvas] background.
abstract class CanvasBackground {
/// the fill color of background
final Color bgColor;
const CanvasBackground({this.bgColor = Colors.white});
/// draw the backround on this context. Implement this to have
/// different kinds of backgrounds
/// [screenOffset] is the screen space offset for clipping
/// [canvasOffset] is the grid space offset from the controller
void paint(
Canvas canvas,
Offset screenOffset,
Offset canvasOffset,
double scale,
Size canvasSize,
);
}
class NoBackground extends CanvasBackground {
const NoBackground();
@override
void paint(
Canvas canvas,
Offset screenOffset,
Offset canvasOffset,
double scale,
Size canvasSize,
) {}
}
class SingleColorBackground extends CanvasBackground {
const SingleColorBackground(Color backgroundColor)
: super(bgColor: backgroundColor);
@override
void paint(
Canvas canvas,
Offset screenOffset,
Offset canvasOffset,
double scale,
Size canvasSize,
) {
final paint = Paint()
..color = bgColor
..style = PaintingStyle.fill;
canvas.drawRect(
Rect.fromLTWH(
screenOffset.dx,
screenOffset.dy,
canvasSize.width,
canvasSize.height,
),
paint,
);
}
}
class DotGridBackground extends CanvasBackground {
final double size; // radius in logical pixels at scale = 1
final double spacing; // grid spacing in logical pixels at scale = 1
final Color dotColor;
final Color backgroundColor;
// Allow choosing pan semantics: true => grid moves with content ("natural"), false => inverted
final bool naturalPan;
// Static shader program cache shared across instances
static ui.FragmentProgram? _program;
static Future<ui.FragmentProgram>? _programFuture;
const DotGridBackground({
this.size = 2.0,
this.spacing = 50.0,
this.dotColor = Colors.black12,
this.backgroundColor = Colors.white,
this.naturalPan = true,
}) : super();
// Kick off async load once
static void _ensureProgramLoaded() {
if (_program != null || _programFuture != null) return;
try {
_programFuture =
ui.FragmentProgram.fromAsset(
'packages/infinite_lazy_grid/shaders/dot_grid.frag',
)
..then((p) {
_program = p;
// Request a repaint when shader becomes available
try {
for (final renderView in RendererBinding.instance.renderViews) {
renderView.markNeedsPaint();
}
} catch (_) {}
}).catchError((_) {
// keep _program null; we'll fallback to CPU/simple paint
});
} catch (_) {
// fromAsset may throw synchronously on unsupported platforms
_programFuture = null;
_program = null;
}
}
@override
void paint(
Canvas canvas,
Offset screenOffset,
Offset canvasOffset,
double scale,
Size canvasSize,
) {
// Always draw background fill (also serves as fallback while shader loads)
final bgPaint = Paint()
..color = backgroundColor
..style = PaintingStyle.fill;
final rect = Rect.fromLTWH(
screenOffset.dx,
screenOffset.dy,
canvasSize.width,
canvasSize.height,
);
// Ensure shader load started
_ensureProgramLoaded();
final program = _program;
if (program == null) {
// Fallback: just fill background without dots until shader is ready
canvas.drawRect(rect, bgPaint);
return;
}
// Compute uniforms in logical pixels to match FlutterFragCoord and CPU path
final scaledSpacingPx = (spacing * scale).abs();
final radiusPx = (size * scale).abs();
// Phase to align grid with world origin under pan/zoom
double modPositive(double a, double m) => ((a % m) + m) % m;
final sign = naturalPan ? 1.0 : -1.0;
final phaseXPx = modPositive(
sign * canvasOffset.dx * scale,
scaledSpacingPx == 0 ? 1 : scaledSpacingPx,
);
final phaseYPx = modPositive(
sign * canvasOffset.dy * scale,
scaledSpacingPx == 0 ? 1 : scaledSpacingPx,
);
// Snap origin: use exact logical origin (no rounding) to match CPU path
final originXPx = screenOffset.dx;
final originYPx = screenOffset.dy;
// Precompute inverse spacing to avoid division in shader
final invScaledSpacing = scaledSpacingPx == 0 ? 0.0 : 1.0 / scaledSpacingPx;
// Build fragment shader and set uniforms in declaration order
final shader = program.fragmentShader();
int i = 0;
void set1(double a) {
shader.setFloat(i++, a);
}
void set2(double a, double b) {
shader.setFloat(i++, a);
shader.setFloat(i++, b);
}
void set4c(Color c) {
shader.setFloat(i++, c.r);
shader.setFloat(i++, c.g);
shader.setFloat(i++, c.b);
shader.setFloat(i++, c.a);
}
set2(originXPx, originYPx);
set1(scaledSpacingPx);
set1(invScaledSpacing);
set2(phaseXPx, phaseYPx);
set1(radiusPx);
set4c(dotColor);
set4c(backgroundColor);
final paint = Paint()..shader = shader;
// Draw once with the shader
canvas.drawRect(rect, paint);
}
}
class DotGridBackgroundCpu extends CanvasBackground {
final double size; // radius in logical pixels at scale = 1
final double spacing; // grid spacing in logical pixels at scale = 1
final Color dotColor;
final Color backgroundColor;
// true => grid tracks content ("natural"), false => inverted pan
final bool naturalPan;
const DotGridBackgroundCpu({
this.size = 2.0,
this.spacing = 50.0,
this.dotColor = Colors.black12,
this.backgroundColor = Colors.white,
this.naturalPan = true,
}) : super();
@override
void paint(
Canvas canvas,
Offset screenOffset,
Offset canvasOffset,
double scale,
Size canvasSize,
) {
// Fill background
final rect = Rect.fromLTWH(
screenOffset.dx,
screenOffset.dy,
canvasSize.width,
canvasSize.height,
);
final bgPaint = Paint()
..color = backgroundColor
..style = PaintingStyle.fill;
canvas.drawRect(rect, bgPaint);
// Early out if dots are too dense or too small
final spacingSS = (spacing * scale).abs();
final radiusSS = (size * scale).abs();
if (spacingSS < 1.0 || radiusSS < 0.25) {
return; // background already drawn
}
final dotPaint = Paint()
..color = dotColor
..style = PaintingStyle.fill;
// Choose pan semantics
final sign = naturalPan ? 1.0 : -1.0;
// Determine visible grid-space bounds to iterate
// Screen rect spans [screenOffset, screenOffset + canvasSize]
// xSS = screenOffset.x + (xGS - sign*canvasOffset.x) * scale
// Solve for xGS bounds to cover the screen rect (+radius margin)
final marginGSX = radiusSS / scale;
final marginGSY = radiusSS / scale;
final xGsMin = sign * canvasOffset.dx - marginGSX;
final xGsMax =
sign * canvasOffset.dx + (canvasSize.width + 2 * radiusSS) / scale;
final yGsMin = sign * canvasOffset.dy - marginGSY;
final yGsMax =
sign * canvasOffset.dy + (canvasSize.height + 2 * radiusSS) / scale;
int nStartX = (xGsMin / spacing).floor();
int nEndX = (xGsMax / spacing).ceil();
int nStartY = (yGsMin / spacing).floor();
int nEndY = (yGsMax / spacing).ceil();
// Iterate grid and draw circles
for (int nx = nStartX; nx <= nEndX; nx++) {
final xGS = nx * spacing;
final xSS = screenOffset.dx + (xGS - sign * canvasOffset.dx) * scale;
if (xSS + radiusSS < rect.left || xSS - radiusSS > rect.right)
continue; // skip out-of-bounds columns
for (int ny = nStartY; ny <= nEndY; ny++) {
final yGS = ny * spacing;
final ySS = screenOffset.dy + (yGS - sign * canvasOffset.dy) * scale;
if (ySS + radiusSS < rect.top || ySS - radiusSS > rect.bottom)
continue; // skip out-of-bounds rows
canvas.drawCircle(Offset(xSS, ySS), radiusSS, dotPaint);
}
}
}
}