From 36ceef199b9de3499e1c7f9b581897492d615fa0 Mon Sep 17 00:00:00 2001 From: ruinivist Date: Thu, 14 Aug 2025 03:39:27 +0000 Subject: [PATCH] use fragment shader based dot grid background --- lib/core/background.dart | 124 ++++++++++++++++++++++++++++----------- pubspec.yaml | 60 ++++++++++--------- shaders/dot_grid.frag | 32 ++++++++++ 3 files changed, 154 insertions(+), 62 deletions(-) create mode 100644 shaders/dot_grid.frag diff --git a/lib/core/background.dart b/lib/core/background.dart index ba02f76..6e0d3fc 100644 --- a/lib/core/background.dart +++ b/lib/core/background.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import './render.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 { @@ -35,11 +36,15 @@ class SingleColorBackround extends CanvasBackground { } class DotGridBackground extends CanvasBackground { - final double size; - final double spacing; + 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; + // Static shader program cache shared across instances + static ui.FragmentProgram? _program; + static Future? _programFuture; + const DotGridBackground({ this.size = 2.0, this.spacing = 50.0, @@ -47,36 +52,89 @@ class DotGridBackground extends CanvasBackground { this.backgroundColor = Colors.white, }) : super(); - @override - void paint(Canvas canvas, Offset screenOffset, Offset canvasOffset, double scale, Size canvasSize) { - final paint = Paint() - ..color = backgroundColor - ..strokeWidth = 1.0 * scale; - double scaledSpacing = spacing * scale; - double scaledSize = size * scale; - - // Use canvasOffset to determine the grid position in grid space - // Calculate the grid origin in canvas space (0,0 should be at a grid point) - // Find the first grid point that aligns with the coordinate system - double firstGridX = (canvasOffset.dx / spacing).floor() * spacing; - double firstGridY = (canvasOffset.dy / spacing).floor() * spacing; - - // Convert grid space coordinates to screen space - double screenStartX = (firstGridX - canvasOffset.dx) * scale + screenOffset.dx; - double screenStartY = (firstGridY - canvasOffset.dy) * scale + screenOffset.dy; - - // Ensure we start drawing before the visible area to avoid gaps - while (screenStartX > screenOffset.dx) { - screenStartX -= scaledSpacing; - } - while (screenStartY > screenOffset.dy) { - screenStartY -= scaledSpacing; - } - - for (double x = screenStartX; x < screenOffset.dx + canvasSize.width + scaledSpacing; x += scaledSpacing) { - for (double y = screenStartY; y < screenOffset.dy + canvasSize.height + scaledSpacing; y += scaledSpacing) { - canvas.drawCircle(Offset(x, y), scaledSize, paint..color = dotColor); - } + // 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 { + RendererBinding.instance.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; + } + + // Device pixel ratio for converting logical -> physical pixels used by FlutterFragCoord + final dpr = RendererBinding.instance.renderView.configuration.devicePixelRatio; + + // Compute uniforms in physical pixels + final scaledSpacingPx = (spacing * scale * dpr).abs(); + final radiusPx = (size * scale * dpr).abs(); + + // Phase to align grid with world origin under pan/zoom + double modPositive(double a, double m) => ((a % m) + m) % m; + final phaseXPx = modPositive(canvasOffset.dx * scale * dpr, scaledSpacingPx == 0 ? 1 : scaledSpacingPx); + final phaseYPx = modPositive(canvasOffset.dy * scale * dpr, scaledSpacingPx == 0 ? 1 : scaledSpacingPx); + + final originXPx = screenOffset.dx * dpr; + final originYPx = screenOffset.dy * dpr; + + // 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); + set2(phaseXPx, phaseYPx); + set1(radiusPx); + set4c(dotColor); + set4c(backgroundColor); + + final paint = Paint()..shader = shader; + + // Draw once with the shader + canvas.drawRect(rect, paint); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 99fd587..75d4345 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,34 +22,36 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: + shaders: + - shaders/dot_grid.frag - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/to/asset-from-package - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/to/font-from-package + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/shaders/dot_grid.frag b/shaders/dot_grid.frag new file mode 100644 index 0000000..7b46dcc --- /dev/null +++ b/shaders/dot_grid.frag @@ -0,0 +1,32 @@ +#include +precision mediump float; + +uniform vec2 uScreenOrigin; // screenOffset in pixels +uniform float uScaledSpacing; // spacing * scale in pixels +uniform vec2 uPhasePx; // phase shift in pixels: mod(-canvasOffset * scale, scaledSpacing) +uniform float uRadiusPx; // dot radius in pixels: size * scale +uniform vec4 uDotColor; // RGBA 0..1 +uniform vec4 uBgColor; // RGBA 0..1 + +out vec4 fragColor; + +void main() { + vec2 p = FlutterFragCoord().xy - uScreenOrigin; // local pixel coords within the painted rect + + // LOD: if dots become sub-pixel or spacing too dense, draw only background + if (uScaledSpacing < 1.0 || uRadiusPx < 0.25) { + fragColor = uBgColor; + return; + } + + // Periodic grid in screen space with phase to account for canvasOffset + vec2 rem = mod(p + uPhasePx, uScaledSpacing); + vec2 d = min(rem, vec2(uScaledSpacing) - rem); + float dist = length(d); + + // Analytic AA around the circle edge (~1px transition) + float edge = uRadiusPx; + float alpha = 1.0 - smoothstep(edge - 1.0, edge + 1.0, dist); + + fragColor = mix(uBgColor, uDotColor, alpha); +}