The Garage SDKs, in the open
garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme, docs under docs/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import "dart:ui" as ui;
|
||||
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/rendering.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
// Wraps the whole app in a RepaintBoundary we can snapshot on demand, so an
|
||||
// eyedropper has something to read pixels out of on platforms with no system
|
||||
// colour sampler (see platform/eyedropper.dart for the ones that do).
|
||||
//
|
||||
// This obviously only covers whats inside the flutter window - thats the
|
||||
// tradeoff for it working everywhere. Mounted once, in main.dart, just under
|
||||
// CustomCursorLayer so the fake cursor doesnt end up baked into the snapshot
|
||||
// and get sampled by the very thing thats drawing it.
|
||||
class AppFrameCapture extends StatelessWidget {
|
||||
const AppFrameCapture({super.key, required this.child});
|
||||
|
||||
static final GlobalKey _boundaryKey = GlobalKey(
|
||||
debugLabel: "app frame capture",
|
||||
);
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RepaintBoundary(key: _boundaryKey, child: child);
|
||||
}
|
||||
|
||||
/// Grabs the current frame. Returns null (and logs why) if theres nothing
|
||||
/// to grab - no boundary mounted yet, or the raster came back empty.
|
||||
static Future<AppFrameSnapshot?> capture() async {
|
||||
final object = _boundaryKey.currentContext?.findRenderObject();
|
||||
if (object is! RenderRepaintBoundary) {
|
||||
debugPrint(
|
||||
"AppFrameCapture.capture: no boundary mounted, is AppFrameCapture in "
|
||||
"the tree?",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
ui.Image? image;
|
||||
try {
|
||||
// pixelRatio 1 on purpose - it keeps image pixels and logical pixels the
|
||||
// same thing, so colorAt can index straight off a pointer position with
|
||||
// no dpr maths, and it keeps the byte buffer a quarter of the size on a
|
||||
// retina display.
|
||||
image = await object.toImage();
|
||||
final width = image.width;
|
||||
final height = image.height;
|
||||
final bytes = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
|
||||
if (bytes == null) {
|
||||
debugPrint("AppFrameCapture.capture: toByteData returned null");
|
||||
return null;
|
||||
}
|
||||
|
||||
return AppFrameSnapshot._(
|
||||
bytes.buffer.asUint8List(),
|
||||
width,
|
||||
height,
|
||||
object.localToGlobal(Offset.zero),
|
||||
);
|
||||
} catch (error, stack) {
|
||||
debugPrint("AppFrameCapture.capture failed: $error\n$stack");
|
||||
return null;
|
||||
} finally {
|
||||
image?.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A frozen copy of the app window you can read single pixels out of.
|
||||
class AppFrameSnapshot {
|
||||
AppFrameSnapshot._(this._pixels, this.width, this.height, this.origin);
|
||||
|
||||
/// straight-from-bytes ctor, only so the sampling maths can be probed
|
||||
/// without standing up a whole render tree.
|
||||
@visibleForTesting
|
||||
AppFrameSnapshot.fromRawRgba({
|
||||
required Uint8List pixels,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.origin = Offset.zero,
|
||||
}) : _pixels = pixels;
|
||||
|
||||
final Uint8List _pixels;
|
||||
final int width;
|
||||
final int height;
|
||||
|
||||
/// where the captured boundary sits in global coordinates. normally zero,
|
||||
/// but not worth assuming.
|
||||
final Offset origin;
|
||||
|
||||
/// The colour at [globalPosition], or null if thats outside the captured
|
||||
/// area or lands on a fully transparent pixel.
|
||||
Color? colorAt(Offset globalPosition) {
|
||||
final local = globalPosition - origin;
|
||||
final x = local.dx.floor();
|
||||
final y = local.dy.floor();
|
||||
if (x < 0 || y < 0 || x >= width || y >= height) return null;
|
||||
|
||||
final i = (y * width + x) * 4;
|
||||
final a = _pixels[i + 3];
|
||||
if (a == 0) return null;
|
||||
|
||||
// rawRgba is premultiplied, so anything drawn over a translucent layer
|
||||
// reads darker than it looks unless we undo that first.
|
||||
int channel(int offset) {
|
||||
final value = _pixels[i + offset];
|
||||
if (a == 255) return value;
|
||||
return (value * 255 / a).round().clamp(0, 255);
|
||||
}
|
||||
|
||||
return Color.fromARGB(255, channel(0), channel(1), channel(2));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user