fix dot grid fragment shader for android

This commit is contained in:
2025-08-16 07:57:57 +00:00
parent 3337e204cc
commit 066ab24ca0
2 changed files with 111 additions and 26 deletions
+93 -14
View File
@@ -40,6 +40,8 @@ class DotGridBackground extends CanvasBackground {
final double spacing; // grid spacing in logical pixels at scale = 1 final double spacing; // grid spacing in logical pixels at scale = 1
final Color dotColor; final Color dotColor;
final Color backgroundColor; 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 shader program cache shared across instances
static ui.FragmentProgram? _program; static ui.FragmentProgram? _program;
@@ -50,6 +52,7 @@ class DotGridBackground extends CanvasBackground {
this.spacing = 50.0, this.spacing = 50.0,
this.dotColor = Colors.black12, this.dotColor = Colors.black12,
this.backgroundColor = Colors.white, this.backgroundColor = Colors.white,
this.naturalPan = true,
}) : super(); }) : super();
// Kick off async load once // Kick off async load once
@@ -91,20 +94,22 @@ class DotGridBackground extends CanvasBackground {
return; return;
} }
// Device pixel ratio for converting logical -> physical pixels used by FlutterFragCoord // Compute uniforms in logical pixels to match FlutterFragCoord and CPU path
final dpr = RendererBinding.instance.renderView.configuration.devicePixelRatio; final scaledSpacingPx = (spacing * scale).abs();
final radiusPx = (size * scale).abs();
// 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 // Phase to align grid with world origin under pan/zoom
double modPositive(double a, double m) => ((a % m) + m) % m; double modPositive(double a, double m) => ((a % m) + m) % m;
final phaseXPx = modPositive(canvasOffset.dx * scale * dpr, scaledSpacingPx == 0 ? 1 : scaledSpacingPx); final sign = naturalPan ? 1.0 : -1.0;
final phaseYPx = modPositive(canvasOffset.dy * scale * dpr, scaledSpacingPx == 0 ? 1 : scaledSpacingPx); final phaseXPx = modPositive(sign * canvasOffset.dx * scale, scaledSpacingPx == 0 ? 1 : scaledSpacingPx);
final phaseYPx = modPositive(sign * canvasOffset.dy * scale, scaledSpacingPx == 0 ? 1 : scaledSpacingPx);
final originXPx = screenOffset.dx * dpr; // Snap origin: use exact logical origin (no rounding) to match CPU path
final originYPx = screenOffset.dy * dpr; 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 // Build fragment shader and set uniforms in declaration order
final shader = program.fragmentShader(); final shader = program.fragmentShader();
@@ -119,14 +124,15 @@ class DotGridBackground extends CanvasBackground {
} }
void set4c(Color c) { void set4c(Color c) {
shader.setFloat(i++, c.r); shader.setFloat(i++, c.red / 255.0);
shader.setFloat(i++, c.g); shader.setFloat(i++, c.green / 255.0);
shader.setFloat(i++, c.b); shader.setFloat(i++, c.blue / 255.0);
shader.setFloat(i++, c.a); shader.setFloat(i++, c.opacity);
} }
set2(originXPx, originYPx); set2(originXPx, originYPx);
set1(scaledSpacingPx); set1(scaledSpacingPx);
set1(invScaledSpacing);
set2(phaseXPx, phaseYPx); set2(phaseXPx, phaseYPx);
set1(radiusPx); set1(radiusPx);
set4c(dotColor); set4c(dotColor);
@@ -138,3 +144,76 @@ class DotGridBackground extends CanvasBackground {
canvas.drawRect(rect, paint); 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);
}
}
}
}
+18 -12
View File
@@ -1,17 +1,19 @@
#include <flutter/runtime_effect.glsl> #include <flutter/runtime_effect.glsl>
precision mediump float; precision mediump float;
uniform vec2 uScreenOrigin; // screenOffset in pixels uniform vec2 uScreenOrigin; // screenOffset in logical pixels
uniform float uScaledSpacing; // spacing * scale in pixels uniform float uScaledSpacing; // spacing * scale in logical pixels
uniform vec2 uPhasePx; // phase shift in pixels: mod(-canvasOffset * scale, scaledSpacing) uniform float uInvScaledSpacing; // 1.0 / uScaledSpacing
uniform float uRadiusPx; // dot radius in pixels: size * scale uniform vec2 uPhasePx; // phase shift in logical pixels
uniform float uRadiusPx; // dot radius in logical pixels
uniform vec4 uDotColor; // RGBA 0..1 uniform vec4 uDotColor; // RGBA 0..1
uniform vec4 uBgColor; // RGBA 0..1 uniform vec4 uBgColor; // RGBA 0..1
out vec4 fragColor; out vec4 fragColor;
void main() { void main() {
vec2 p = FlutterFragCoord().xy - uScreenOrigin; // local pixel coords within the painted rect // Work entirely in logical pixels
vec2 p = FlutterFragCoord().xy - uScreenOrigin; // local logical pixel coords within the painted rect
// LOD: if dots become sub-pixel or spacing too dense, draw only background // LOD: if dots become sub-pixel or spacing too dense, draw only background
if (uScaledSpacing < 1.0 || uRadiusPx < 0.25) { if (uScaledSpacing < 1.0 || uRadiusPx < 0.25) {
@@ -19,14 +21,18 @@ void main() {
return; return;
} }
// Periodic grid in screen space with phase to account for canvasOffset // Fractional position within the lattice, centered around each grid point
vec2 rem = mod(p + uPhasePx, uScaledSpacing); vec2 cell = (p + uPhasePx) * uInvScaledSpacing; // multiply by inverse spacing to avoid division
vec2 d = min(rem, vec2(uScaledSpacing) - rem); vec2 f = fract(cell) - 0.5; // [-0.5, 0.5)
float dist = length(d);
// Analytic AA around the circle edge (~1px transition) // Use squared distance to avoid sqrt
float edge = uRadiusPx; float s2 = uScaledSpacing * uScaledSpacing;
float alpha = 1.0 - smoothstep(edge - 1.0, edge + 1.0, dist); float dist2 = dot(f, f) * s2;
// Analytic AA using squared thresholds (~1 logical px transition)
float e0 = max(0.0, uRadiusPx - 1.0);
float e1 = uRadiusPx + 1.0;
float alpha = 1.0 - smoothstep(e0 * e0, e1 * e1, dist2);
fragColor = mix(uBgColor, uDotColor, alpha); fragColor = mix(uBgColor, uDotColor, alpha);
} }