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:
@@ -1,6 +1,7 @@
|
|||||||
import {useCallback, useState} from 'react'
|
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 {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
import Svg, {Path} from 'react-native-svg'
|
||||||
import {
|
import {
|
||||||
type BarcodeScanningResult,
|
type BarcodeScanningResult,
|
||||||
CameraView,
|
CameraView,
|
||||||
@@ -19,6 +20,7 @@ import {Text} from '#/components/Typography'
|
|||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
const SCAN_FRAME_SIZE = 333
|
const SCAN_FRAME_SIZE = 333
|
||||||
|
const SCAN_FRAME_RADIUS = 16
|
||||||
|
|
||||||
export function InviteScannerScreen() {
|
export function InviteScannerScreen() {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
@@ -131,6 +133,23 @@ export function InviteScannerScreen() {
|
|||||||
|
|
||||||
<ScannerScrim />
|
<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
|
<View
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -214,82 +233,32 @@ export function InviteScannerScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a dark scrim covering the camera view with a transparent 333x333
|
* Renders a dark scrim covering the camera view with a rounded-rect cutout in
|
||||||
* cutout in the middle. We use four absolute Views forming a window-frame
|
* the middle. Implemented as a single SVG Path (outer screen rect + inner
|
||||||
* shape because React Native doesn't support CSS clip-path / mix-blend modes.
|
* rounded rect, even-odd fill) so the cutout's corners match the colored
|
||||||
|
* outline above it.
|
||||||
*/
|
*/
|
||||||
function ScannerScrim() {
|
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 (
|
return (
|
||||||
<>
|
<Svg
|
||||||
{/* top */}
|
style={StyleSheet.absoluteFill}
|
||||||
<View
|
width={width}
|
||||||
style={{
|
height={height}
|
||||||
position: 'absolute',
|
pointerEvents="none">
|
||||||
left: 0,
|
<Path d={d} fill="rgba(0, 0, 0, 0.55)" fillRule="evenodd" />
|
||||||
right: 0,
|
</Svg>
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user