1import Ionicons from '@expo/vector-icons/build/Ionicons';
2import * as FaceDetector from 'expo-face-detector';
3import React from 'react';
4import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
5
6const pictureSize = 150;
7
8interface State {
9  selected: boolean;
10  faces: FaceDetector.FaceFeature[];
11  image?: FaceDetector.Image;
12}
13
14export default class Photo extends React.Component<
15  { uri: string; onSelectionToggle: (uri: string, selected: boolean) => void },
16  State
17> {
18  readonly state: State = {
19    selected: false,
20    faces: [],
21  };
22  _mounted = false;
23
24  componentDidMount() {
25    this._mounted = true;
26  }
27
28  componentWillUnmount() {
29    this._mounted = false;
30  }
31
32  toggleSelection = () => {
33    this.setState(
34      (state) => ({ selected: !state.selected }),
35      () => this.props.onSelectionToggle(this.props.uri, this.state.selected)
36    );
37  };
38
39  detectFace = () =>
40    FaceDetector.detectFacesAsync(this.props.uri, {
41      detectLandmarks: FaceDetector.FaceDetectorLandmarks.none,
42      runClassifications: FaceDetector.FaceDetectorClassifications.all,
43    })
44      .then(this.facesDetected)
45      .catch(this.handleFaceDetectionError);
46
47  facesDetected = ({
48    faces,
49    image,
50  }: {
51    faces: FaceDetector.FaceFeature[];
52    image: FaceDetector.Image;
53  }) => {
54    this.setState({
55      faces,
56      image,
57    });
58  };
59
60  getImageDimensions = ({ width, height }: FaceDetector.Image) => {
61    if (width > height) {
62      const scaledHeight = (pictureSize * height) / width;
63      return {
64        width: pictureSize,
65        height: scaledHeight,
66
67        scaleX: pictureSize / width,
68        scaleY: scaledHeight / height,
69
70        offsetX: 0,
71        offsetY: (pictureSize - scaledHeight) / 2,
72      };
73    } else {
74      const scaledWidth = (pictureSize * width) / height;
75      return {
76        width: scaledWidth,
77        height: pictureSize,
78
79        scaleX: scaledWidth / width,
80        scaleY: pictureSize / height,
81
82        offsetX: (pictureSize - scaledWidth) / 2,
83        offsetY: 0,
84      };
85    }
86  };
87
88  // tslint:disable-next-line no-console
89  handleFaceDetectionError = (error: any) => console.warn(error);
90
91  renderFaces = () => this.state.image && this.state.faces && this.state.faces.map(this.renderFace);
92
93  renderFace = (face: FaceDetector.FaceFeature, index: number) => {
94    const { scaleX, scaleY, offsetX, offsetY } = this.getImageDimensions(this.state.image!);
95    const layout = {
96      top: offsetY + face.bounds.origin.y * scaleY,
97      left: offsetX + face.bounds.origin.x * scaleX,
98      width: face.bounds.size.width * scaleX,
99      height: face.bounds.size.height * scaleY,
100    };
101
102    return (
103      <View
104        key={index}
105        style={[
106          styles.face,
107          layout,
108          {
109            transform: [
110              { perspective: 600 },
111              { rotateZ: `${(face.rollAngle || 0).toFixed(0)}deg` },
112              { rotateY: `${(face.yawAngle || 0).toFixed(0)}deg` },
113            ],
114          },
115        ]}>
116        {face.smilingProbability && (
117          <Text style={styles.faceText}>�� {(face.smilingProbability * 100).toFixed(0)}%</Text>
118        )}
119      </View>
120    );
121  };
122
123  render() {
124    const { uri } = this.props;
125    return (
126      <TouchableOpacity
127        style={styles.pictureWrapper}
128        onLongPress={this.detectFace}
129        onPress={this.toggleSelection}
130        activeOpacity={1}>
131        <Image style={styles.picture} source={{ uri }} />
132        {this.state.selected && <Ionicons name="md-checkmark-circle" size={30} color="#4630EB" />}
133        <View style={styles.facesContainer}>{this.renderFaces()}</View>
134      </TouchableOpacity>
135    );
136  }
137}
138
139const styles = StyleSheet.create({
140  picture: {
141    position: 'absolute',
142    bottom: 0,
143    right: 0,
144    left: 0,
145    top: 0,
146    resizeMode: 'contain',
147  },
148  pictureWrapper: {
149    width: pictureSize,
150    height: pictureSize,
151    alignItems: 'center',
152    justifyContent: 'center',
153    margin: 5,
154  },
155  facesContainer: {
156    position: 'absolute',
157    bottom: 0,
158    right: 0,
159    left: 0,
160    top: 0,
161  },
162  face: {
163    borderWidth: 2,
164    borderRadius: 2,
165    position: 'absolute',
166    borderColor: '#FFD700',
167    justifyContent: 'center',
168    backgroundColor: 'rgba(0, 0, 0, 0.5)',
169  },
170  faceText: {
171    color: '#FFD700',
172    fontWeight: 'bold',
173    textAlign: 'center',
174    margin: 2,
175    fontSize: 10,
176    backgroundColor: 'transparent',
177  },
178});
179