mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
## Summary - add inline profile photo capture with smooth avatar-to-viewfinder transitions - add close, flip, shutter, retry, and use-photo states with haptics and front-camera mirroring - polish iOS liquid-glass controls and avatar editor motion ### Related issue Follow-up to #6583. ### Testing - just mobile-check - just mobile-test (1,743 tests) - built, installed, and launched on Pixel 10 and iPhone Air --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Code Reviewer <037593536284cf40e221c96c931e9877d4166d54f6bb84e5341a86d7fd5d05a4@buzz.block.builderlab.xyz> Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Code Reviewer <037593536284cf40e221c96c931e9877d4166d54f6bb84e5341a86d7fd5d05a4@buzz.block.builderlab.xyz> Co-authored-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
111 lines
3.1 KiB
Dart
111 lines
3.1 KiB
Dart
part of '../animated_avatar_capture.dart';
|
|
|
|
/// Creates an isolated workspace for one capture's segmentation frames.
|
|
@visibleForTesting
|
|
Future<Directory> createAnimatedAvatarFrameDirectory({
|
|
Directory? parent,
|
|
}) async => (parent ?? await getTemporaryDirectory()).createTemp(
|
|
'buzz-avatar-capture-',
|
|
);
|
|
|
|
Future<List<Uint8List>> _removeBackgrounds(List<Uint8List> frames) async {
|
|
final segmenter = SelfieSegmenter(
|
|
mode: SegmenterMode.stream,
|
|
enableRawSizeMask: false,
|
|
);
|
|
Directory? captureDirectory;
|
|
final results = <Uint8List>[];
|
|
try {
|
|
captureDirectory = await createAnimatedAvatarFrameDirectory();
|
|
for (var index = 0; index < frames.length; index++) {
|
|
final file = File('${captureDirectory.path}/frame-$index.png');
|
|
await file.writeAsBytes(frames[index], flush: false);
|
|
final mask = await segmenter.processImage(
|
|
InputImage.fromFilePath(file.path),
|
|
);
|
|
if (mask == null) {
|
|
results.add(frames[index]);
|
|
continue;
|
|
}
|
|
results.add(
|
|
await compute(
|
|
_applySegmentationMask,
|
|
_MaskRequest(
|
|
frame: frames[index],
|
|
maskWidth: mask.width,
|
|
maskHeight: mask.height,
|
|
confidences: Float32List.fromList(mask.confidences),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
await segmenter.close();
|
|
final directory = captureDirectory;
|
|
if (directory != null && await directory.exists()) {
|
|
try {
|
|
await directory.delete(recursive: true);
|
|
} on FileSystemException {
|
|
// Temporary capture cleanup is best effort.
|
|
}
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
@immutable
|
|
class _MaskRequest {
|
|
const _MaskRequest({
|
|
required this.frame,
|
|
required this.maskWidth,
|
|
required this.maskHeight,
|
|
required this.confidences,
|
|
});
|
|
|
|
final Uint8List frame;
|
|
final int maskWidth;
|
|
final int maskHeight;
|
|
final Float32List confidences;
|
|
}
|
|
|
|
Uint8List _applySegmentationMask(_MaskRequest request) {
|
|
final result = image.decodePng(request.frame)!.convert(numChannels: 4);
|
|
for (var y = 0; y < result.height; y++) {
|
|
final maskY = (y * request.maskHeight / result.height).floor().clamp(
|
|
0,
|
|
request.maskHeight - 1,
|
|
);
|
|
for (var x = 0; x < result.width; x++) {
|
|
final maskX = (x * request.maskWidth / result.width).floor().clamp(
|
|
0,
|
|
request.maskWidth - 1,
|
|
);
|
|
final confidence = request.confidences[maskY * request.maskWidth + maskX];
|
|
final alpha = ((confidence - 0.28) / (0.72 - 0.28)).clamp(0, 1);
|
|
final pixel = result.getPixel(x, y);
|
|
pixel.a = (alpha * 255).round();
|
|
}
|
|
}
|
|
return image.encodePng(result, level: 4);
|
|
}
|
|
|
|
/// Encodes one poster frame for validating animated-avatar framing parity.
|
|
@visibleForTesting
|
|
Uint8List encodeAnimatedAvatarPoster({
|
|
required Uint8List frame,
|
|
required double scale,
|
|
}) => _encodeAvatar(
|
|
_EncodeRequest(
|
|
frames: [frame],
|
|
posterIndex: 0,
|
|
scale: scale,
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
backdropColor: 0xff0000ff,
|
|
personOutline: false,
|
|
shapeScale: 1,
|
|
shapeOffsetX: 0,
|
|
shapeOffsetY: 0,
|
|
),
|
|
).poster;
|