-
-
Notifications
You must be signed in to change notification settings - Fork 1k
/
Copy pathutils.ts
287 lines (246 loc) · 8.52 KB
/
utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import { PointerType } from '../PointerType';
import type {
GestureHandlerRef,
Point,
StylusData,
SVGRef,
} from './interfaces';
export function isPointerInBounds(view: HTMLElement, { x, y }: Point): boolean {
const rect: DOMRect = view.getBoundingClientRect();
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
}
export const PointerTypeMapping = new Map<string, PointerType>([
['mouse', PointerType.MOUSE],
['touch', PointerType.TOUCH],
['pen', PointerType.STYLUS],
['none', PointerType.OTHER],
]);
export const degToRad = (degrees: number) => (degrees * Math.PI) / 180;
export const coneToDeviation = (degrees: number) =>
Math.cos(degToRad(degrees / 2));
export function calculateViewScale(view: HTMLElement) {
const styles = getComputedStyle(view);
const resultScales = {
scaleX: 1,
scaleY: 1,
};
// Get scales from scale property
if (styles.scale !== undefined && styles.scale !== 'none') {
const scales = styles.scale.split(' ');
if (scales[0]) {
resultScales.scaleX = parseFloat(scales[0]);
}
resultScales.scaleY = scales[1]
? parseFloat(scales[1])
: parseFloat(scales[0]);
}
// Get scales from transform property
const matrixElements = new RegExp(/matrix\((.+)\)/).exec(
styles.transform
)?.[1];
if (matrixElements) {
const matrixElementsArray = matrixElements.split(', ');
resultScales.scaleX *= parseFloat(matrixElementsArray[0]);
resultScales.scaleY *= parseFloat(matrixElementsArray[3]);
}
return resultScales;
}
export function tryExtractStylusData(
event: PointerEvent
): StylusData | undefined {
const pointerType = PointerTypeMapping.get(event.pointerType);
if (pointerType !== PointerType.STYLUS) {
return;
}
// @ts-ignore This property exists (https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent#instance_properties)
const eventAzimuthAngle: number | undefined = event.azimuthAngle;
// @ts-ignore This property exists (https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent#instance_properties)
const eventAltitudeAngle: number | undefined = event.altitudeAngle;
if (event.tiltX === 0 && event.tiltY === 0) {
// If we are in this branch, it means that either tilt properties are not supported and we have to calculate them from altitude and azimuth angles,
// or stylus is perpendicular to the screen and we can use altitude / azimuth instead of tilt
// If azimuth and altitude are undefined in this branch, it means that we are either perpendicular to the screen,
// or that none of the position sets is supported. In that case, we can treat stylus as perpendicular
if (eventAzimuthAngle === undefined || eventAltitudeAngle === undefined) {
return {
tiltX: 0,
tiltY: 0,
azimuthAngle: Math.PI / 2,
altitudeAngle: Math.PI / 2,
pressure: event.pressure,
};
}
const { tiltX, tiltY } = spherical2tilt(
eventAltitudeAngle,
eventAzimuthAngle
);
return {
tiltX,
tiltY,
azimuthAngle: eventAzimuthAngle,
altitudeAngle: eventAltitudeAngle,
pressure: event.pressure,
};
}
const { altitudeAngle, azimuthAngle } = tilt2spherical(
event.tiltX,
event.tiltY
);
return {
tiltX: event.tiltX,
tiltY: event.tiltY,
azimuthAngle,
altitudeAngle,
pressure: event.pressure,
};
}
// `altitudeAngle` and `azimuthAngle` are experimental properties, which are not supported on Firefox and Safari.
// Given that, we use `tilt` properties and algorithm that converts one value to another.
//
// Source: https://w3c.github.io/pointerevents/#converting-between-tiltx-tilty-and-altitudeangle-azimuthangle
function tilt2spherical(tiltX: number, tiltY: number) {
const tiltXrad = (tiltX * Math.PI) / 180;
const tiltYrad = (tiltY * Math.PI) / 180;
// calculate azimuth angle
let azimuthAngle = 0;
if (tiltX === 0) {
if (tiltY > 0) {
azimuthAngle = Math.PI / 2;
} else if (tiltY < 0) {
azimuthAngle = (3 * Math.PI) / 2;
}
} else if (tiltY === 0) {
if (tiltX < 0) {
azimuthAngle = Math.PI;
}
} else if (Math.abs(tiltX) === 90 || Math.abs(tiltY) === 90) {
// not enough information to calculate azimuth
azimuthAngle = 0;
} else {
// Non-boundary case: neither tiltX nor tiltY is equal to 0 or +-90
const tanX = Math.tan(tiltXrad);
const tanY = Math.tan(tiltYrad);
azimuthAngle = Math.atan2(tanY, tanX);
if (azimuthAngle < 0) {
azimuthAngle += 2 * Math.PI;
}
}
// calculate altitude angle
let altitudeAngle = 0;
if (Math.abs(tiltX) === 90 || Math.abs(tiltY) === 90) {
altitudeAngle = 0;
} else if (tiltX === 0) {
altitudeAngle = Math.PI / 2 - Math.abs(tiltYrad);
} else if (tiltY === 0) {
altitudeAngle = Math.PI / 2 - Math.abs(tiltXrad);
} else {
// Non-boundary case: neither tiltX nor tiltY is equal to 0 or +-90
altitudeAngle = Math.atan(
1.0 /
Math.sqrt(
Math.pow(Math.tan(tiltXrad), 2) + Math.pow(Math.tan(tiltYrad), 2)
)
);
}
return { altitudeAngle: altitudeAngle, azimuthAngle: azimuthAngle };
}
// If we are on a platform that doesn't support `tiltX` and `tiltY`, we have to calculate them from `altitude` and `azimuth` angles.
//
// Source: https://w3c.github.io/pointerevents/#converting-between-tiltx-tilty-and-altitudeangle-azimuthangle
function spherical2tilt(altitudeAngle: number, azimuthAngle: number) {
const radToDeg = 180 / Math.PI;
let tiltXrad = 0;
let tiltYrad = 0;
if (altitudeAngle === 0) {
// the pen is in the X-Y plane
if (azimuthAngle === 0 || azimuthAngle === 2 * Math.PI) {
// pen is on positive X axis
tiltXrad = Math.PI / 2;
}
if (azimuthAngle === Math.PI / 2) {
// pen is on positive Y axis
tiltYrad = Math.PI / 2;
}
if (azimuthAngle === Math.PI) {
// pen is on negative X axis
tiltXrad = -Math.PI / 2;
}
if (azimuthAngle === (3 * Math.PI) / 2) {
// pen is on negative Y axis
tiltYrad = -Math.PI / 2;
}
if (azimuthAngle > 0 && azimuthAngle < Math.PI / 2) {
tiltXrad = Math.PI / 2;
tiltYrad = Math.PI / 2;
}
if (azimuthAngle > Math.PI / 2 && azimuthAngle < Math.PI) {
tiltXrad = -Math.PI / 2;
tiltYrad = Math.PI / 2;
}
if (azimuthAngle > Math.PI && azimuthAngle < (3 * Math.PI) / 2) {
tiltXrad = -Math.PI / 2;
tiltYrad = -Math.PI / 2;
}
if (azimuthAngle > (3 * Math.PI) / 2 && azimuthAngle < 2 * Math.PI) {
tiltXrad = Math.PI / 2;
tiltYrad = -Math.PI / 2;
}
}
if (altitudeAngle !== 0) {
const tanAlt = Math.tan(altitudeAngle);
tiltXrad = Math.atan(Math.cos(azimuthAngle) / tanAlt);
tiltYrad = Math.atan(Math.sin(azimuthAngle) / tanAlt);
}
const tiltX = Math.round(tiltXrad * radToDeg);
const tiltY = Math.round(tiltYrad * radToDeg);
return { tiltX, tiltY };
}
export const RNSVGElements = new Set([
'Circle',
'ClipPath',
'Ellipse',
'ForeignObject',
'G',
'Image',
'Line',
'Marker',
'Mask',
'Path',
'Pattern',
'Polygon',
'Polyline',
'Rect',
'Svg',
'Symbol',
'TSpan',
'Text',
'TextPath',
'Use',
]);
// This function helps us determine whether given node is SVGElement or not. In our implementation of
// findNodeHandle, we can encounter such element in 2 forms - SVG tag or ref to SVG Element. Since Gesture Handler
// does not depend on SVG, we use our simplified SVGRef type that has `elementRef` field. This is something that is present
// in actual SVG ref object.
//
// In order to make sure that node passed into this function is in fact SVG element, first we check if its constructor name
// corresponds to one of the possible SVG elements. Then we also check if `elementRef` field exists.
// By doing both steps we decrease probability of detecting situations where, for example, user makes custom `Circle` and
// we treat it as SVG.
export function isRNSVGElement(viewRef: SVGRef | GestureHandlerRef) {
const componentClassName = Object.getPrototypeOf(viewRef).constructor.name;
return (
RNSVGElements.has(componentClassName) &&
Object.hasOwn(viewRef, 'elementRef')
);
}
// This function checks if given node is SVGElement. Unlike the function above, this one
// operates on React Nodes, not DOM nodes.
//
// Second condition was introduced to handle case where SVG element was wrapped with
// `createAnimatedComponent` from Reanimated.
export function isRNSVGNode(node: any) {
return (
Object.getPrototypeOf(node?.type)?.name === 'WebShape' ||
RNSVGElements.has(node?.type?.displayName)
);
}