UNPKG

react-native-gesture-handler

Version:

Declarative API exposing native platform touch and gesture system to React Native

294 lines (251 loc) 8.98 kB
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) { // If `ref` has `rngh` field, it means that component comes from Gesture Handler. This is a special case for // `Text` component, which is present in `RNSVGElements` set, yet we don't want to treat it as SVG. if (node.ref?.rngh) { return false; } return ( Object.getPrototypeOf(node?.type)?.name === 'WebShape' || RNSVGElements.has(node?.type?.displayName) ); }