Match scanner scrim corners to the scan-frame outline

Replaces the four-rect scrim with a single SVG path that punches a
rounded-rect cutout, so the dark scrim's inner corners line up with the
colored frame above it instead of being square.
This commit is contained in:
vineyardbovines
2026-06-09 12:55:43 -04:00
parent 27ab4af139
commit 19f04777c9
@@ -1,6 +1,7 @@
import {useCallback, useState} from 'react'
import {Pressable, StyleSheet, View} from 'react-native'
import {Pressable, StyleSheet, useWindowDimensions, View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Svg, {Path} from 'react-native-svg'
import {
type BarcodeScanningResult,
CameraView,
@@ -19,6 +20,7 @@ import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
const SCAN_FRAME_SIZE = 333
const SCAN_FRAME_RADIUS = 16
export function InviteScannerScreen() {
const {t: l} = useLingui()
@@ -131,6 +133,23 @@ export function InviteScannerScreen() {
<ScannerScrim />
{/* scan-frame outline (the colored border around the cutout) */}
<View
style={{
position: 'absolute',
top: '50%',
left: '50%',
width: SCAN_FRAME_SIZE,
height: SCAN_FRAME_SIZE,
marginTop: -SCAN_FRAME_SIZE / 2,
marginLeft: -SCAN_FRAME_SIZE / 2,
borderWidth: 2,
borderColor: '#006aff',
borderRadius: SCAN_FRAME_RADIUS,
}}
pointerEvents="none"
/>
<View
style={{
position: 'absolute',
@@ -214,82 +233,32 @@ export function InviteScannerScreen() {
}
/**
* Renders a dark scrim covering the camera view with a transparent 333x333
* cutout in the middle. We use four absolute Views forming a window-frame
* shape because React Native doesn't support CSS clip-path / mix-blend modes.
* Renders a dark scrim covering the camera view with a rounded-rect cutout in
* the middle. Implemented as a single SVG Path (outer screen rect + inner
* rounded rect, even-odd fill) so the cutout's corners match the colored
* outline above it.
*/
function ScannerScrim() {
const SCRIM_BG = 'rgba(0, 0, 0, 0.55)'
const {width, height} = useWindowDimensions()
const r = SCAN_FRAME_RADIUS
const half = SCAN_FRAME_SIZE / 2
const x = width / 2 - half
const y = height / 2 - half
const right = x + SCAN_FRAME_SIZE
const bottom = y + SCAN_FRAME_SIZE
const d =
`M0 0 H${width} V${height} H0 Z ` +
`M${x + r} ${y} H${right - r} A${r} ${r} 0 0 1 ${right} ${y + r} ` +
`V${bottom - r} A${r} ${r} 0 0 1 ${right - r} ${bottom} ` +
`H${x + r} A${r} ${r} 0 0 1 ${x} ${bottom - r} ` +
`V${y + r} A${r} ${r} 0 0 1 ${x + r} ${y} Z`
return (
<>
{/* top */}
<View
style={{
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: '50%',
marginBottom: SCAN_FRAME_SIZE / 2,
backgroundColor: SCRIM_BG,
}}
/>
{/* bottom */}
<View
style={{
position: 'absolute',
left: 0,
right: 0,
top: '50%',
bottom: 0,
marginTop: SCAN_FRAME_SIZE / 2,
backgroundColor: SCRIM_BG,
}}
/>
{/* left */}
<View
style={{
position: 'absolute',
top: '50%',
left: 0,
width: '50%',
height: SCAN_FRAME_SIZE,
marginTop: -SCAN_FRAME_SIZE / 2,
marginRight: SCAN_FRAME_SIZE / 2,
backgroundColor: SCRIM_BG,
transform: [{translateX: -SCAN_FRAME_SIZE / 2}],
}}
/>
{/* right */}
<View
style={{
position: 'absolute',
top: '50%',
right: 0,
width: '50%',
height: SCAN_FRAME_SIZE,
marginTop: -SCAN_FRAME_SIZE / 2,
marginLeft: SCAN_FRAME_SIZE / 2,
backgroundColor: SCRIM_BG,
transform: [{translateX: SCAN_FRAME_SIZE / 2}],
}}
/>
{/* scan-frame outline (the colored border around the cutout) */}
<View
style={{
position: 'absolute',
top: '50%',
left: '50%',
width: SCAN_FRAME_SIZE,
height: SCAN_FRAME_SIZE,
marginTop: -SCAN_FRAME_SIZE / 2,
marginLeft: -SCAN_FRAME_SIZE / 2,
borderWidth: 2,
borderColor: '#006aff',
borderRadius: 16,
}}
pointerEvents="none"
/>
</>
<Svg
style={StyleSheet.absoluteFill}
width={width}
height={height}
pointerEvents="none">
<Path d={d} fill="rgba(0, 0, 0, 0.55)" fillRule="evenodd" />
</Svg>
)
}