Files
buzz/mobile/lib/features/profile/animated_avatar_capture/frame_processing.dart
klopez4212 9aa332af03 Add inline profile camera capture (#6680)
## 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>
2026-08-25 14:58:55 +01:00

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;