UNPKG

react-native-reanimated

Version:

More powerful alternative to Animated library for React Native.

368 lines (327 loc) • 10.4 kB
'use strict'; import type { IWorkletsModule, SerializableRef, WorkletFunction, } from 'react-native-worklets'; import { WorkletsModule } from 'react-native-worklets'; import { IS_JEST, IS_WEB, IS_WINDOW_AVAILABLE, logger, ReanimatedError, } from '../../common'; import type { InternalHostInstance, SettledUpdate, ShadowNodeWrapper, StyleProps, Value3D, ValueRotation, } from '../../commonTypes'; import { SensorType } from '../../commonTypes'; import type { CSSAnimationUpdates, NormalizedCSSAnimationKeyframesConfig, NormalizedCSSTransitionConfig, } from '../../css/native'; import { assertWorkletsVersion } from '../../platform-specific/workletsVersion'; import type { IReanimatedModule } from '../reanimatedModuleProxy'; import type { WebSensor } from './WebSensor'; export function createJSReanimatedModule(): IReanimatedModule { return new JSReanimated(); } class JSReanimated implements IReanimatedModule { /** * We keep the instance of `WorkletsModule` here to keep correct coupling of * the modules and initialization order. */ // eslint-disable-next-line no-unused-private-class-members #workletsModule: IWorkletsModule = WorkletsModule; nextSensorId = 0; sensors = new Map<number, WebSensor>(); platform?: Platform = undefined; constructor() { if (__DEV__) { assertWorkletsVersion(); } } registerEventHandler<T>( _eventHandler: SerializableRef<T>, _eventName: string, _emitterReactTag: number ): number { throw new ReanimatedError( 'registerEventHandler is not available in JSReanimated.' ); } unregisterEventHandler(_: number): void { throw new ReanimatedError( 'unregisterEventHandler is not available in JSReanimated.' ); } configureLayoutAnimationBatch() { // no-op } setShouldAnimateExitingForTag() { // no-op } registerSensor( sensorType: SensorType, interval: number, _iosReferenceFrame: number, eventHandler: SerializableRef<(data: Value3D | ValueRotation) => void> ): number { if (!IS_WINDOW_AVAILABLE) { // the window object is unavailable when building the server portion of a site that uses SSG // this check is here to ensure that the server build won't fail return -1; } if (this.platform === undefined) { this.detectPlatform(); } if (!(this.getSensorName(sensorType) in window)) { // https://w3c.github.io/sensors/#secure-context logger.warn( 'Sensor is not available.' + (IS_WEB && location.protocol !== 'https:' ? ' Make sure you use secure origin with `npx expo start --web --https`.' : '') + (this.platform === Platform.WEB_IOS ? ' For iOS web, you will also have to also grant permission in the browser: https://dev.to/li/how-to-requestpermission-for-devicemotion-and-deviceorientation-events-in-ios-13-46g2.' : '') ); return -1; } if (this.platform === undefined) { this.detectPlatform(); } const sensor: WebSensor = this.initializeSensor(sensorType, interval); sensor.addEventListener( 'reading', this.getSensorCallback(sensor, sensorType, eventHandler) ); sensor.start(); this.sensors.set(this.nextSensorId, sensor); return this.nextSensorId++; } getSensorCallback = ( sensor: WebSensor, sensorType: SensorType, eventHandler: SerializableRef<(data: Value3D | ValueRotation) => void> ) => { switch (sensorType) { case SensorType.ACCELEROMETER: case SensorType.GRAVITY: return () => { let { x, y, z } = sensor; // Web Android sensors have a different coordinate system than iOS if (this.platform === Platform.WEB_ANDROID) { [x, y, z] = [-x, -y, -z]; } // TODO TYPESCRIPT on web SerializableRef is the value itself so we call it directly (eventHandler as any)({ x, y, z, interfaceOrientation: 0 }); }; case SensorType.GYROSCOPE: case SensorType.MAGNETIC_FIELD: return () => { const { x, y, z } = sensor; // TODO TYPESCRIPT on web SerializableRef is the value itself so we call it directly (eventHandler as any)({ x, y, z, interfaceOrientation: 0 }); }; case SensorType.ROTATION: return () => { const [qw, qx] = sensor.quaternion; let [, , qy, qz] = sensor.quaternion; // Android sensors have a different coordinate system than iOS if (this.platform === Platform.WEB_ANDROID) { [qy, qz] = [qz, -qy]; } // reference: https://stackoverflow.com/questions/5782658/extracting-yaw-from-a-quaternion const yaw = -Math.atan2( 2.0 * (qy * qz + qw * qx), qw * qw - qx * qx - qy * qy + qz * qz ); const pitch = Math.sin(-2.0 * (qx * qz - qw * qy)); const roll = -Math.atan2( 2.0 * (qx * qy + qw * qz), qw * qw + qx * qx - qy * qy - qz * qz ); // TODO TYPESCRIPT on web SerializableRef is the value itself so we call it directly (eventHandler as any)({ qw, qx, qy, qz, yaw, pitch, roll, interfaceOrientation: 0, }); }; } }; unregisterSensor(id: number): void { const sensor: WebSensor | undefined = this.sensors.get(id); if (sensor !== undefined) { sensor.stop(); this.sensors.delete(id); } } subscribeForKeyboardEvents(_: SerializableRef<WorkletFunction>): number { if (IS_WEB) { logger.warn('useAnimatedKeyboard is not available on web yet.'); } else if (IS_JEST) { logger.warn('useAnimatedKeyboard is not available when using Jest.'); } else { logger.warn( 'useAnimatedKeyboard is not available on this configuration.' ); } return -1; } unsubscribeFromKeyboardEvents(_: number): void { // noop } initializeSensor(sensorType: SensorType, interval: number): WebSensor { const config = interval <= 0 ? { referenceFrame: 'device' } : { frequency: 1000 / interval }; switch (sensorType) { case SensorType.ACCELEROMETER: return new window.Accelerometer(config); case SensorType.GYROSCOPE: return new window.Gyroscope(config); case SensorType.GRAVITY: return new window.GravitySensor(config); case SensorType.MAGNETIC_FIELD: return new window.Magnetometer(config); case SensorType.ROTATION: return new window.AbsoluteOrientationSensor(config); } } getSensorName(sensorType: SensorType): string { switch (sensorType) { case SensorType.ACCELEROMETER: return 'Accelerometer'; case SensorType.GRAVITY: return 'GravitySensor'; case SensorType.GYROSCOPE: return 'Gyroscope'; case SensorType.MAGNETIC_FIELD: return 'Magnetometer'; case SensorType.ROTATION: return 'AbsoluteOrientationSensor'; } } detectPlatform() { const userAgent = navigator.userAgent || navigator.vendor || window.opera; if (userAgent === undefined) { this.platform = Platform.UNKNOWN; } else if (/iPad|iPhone|iPod/.test(userAgent)) { this.platform = Platform.WEB_IOS; } else if (/android/i.test(userAgent)) { this.platform = Platform.WEB_ANDROID; } else { this.platform = Platform.WEB; } } getViewProp<T>( _viewTag: number, _propName: string, _component?: InternalHostInstance | null, _callback?: (result: T) => void ): Promise<T> { throw new ReanimatedError('getViewProp is not available in JSReanimated.'); } getStaticFeatureFlag(): boolean { // mock implementation return false; } setDynamicFeatureFlag(): void { // noop } setViewStyle(_viewTag: number, _style: StyleProps): void { throw new ReanimatedError('setViewStyle is not available in JSReanimated.'); } markNodeAsRemovable(_shadowNodeWrapper: ShadowNodeWrapper): void { throw new ReanimatedError( 'markNodeAsRemovable is not available in JSReanimated.' ); } unmarkNodeAsRemovable(_viewTag: number): void { throw new ReanimatedError( 'unmarkNodeAsRemovable is not available in JSReanimated.' ); } registerCSSKeyframes( _animationName: string, _viewName: string, _keyframesConfig: NormalizedCSSAnimationKeyframesConfig ): void { throw new ReanimatedError( '`registerCSSKeyframes` is not available in JSReanimated.' ); } unregisterCSSKeyframes(_animationName: string, _viewName: string): void { throw new ReanimatedError( '`unregisterCSSKeyframes` is not available in JSReanimated.' ); } applyCSSAnimations( _shadowNodeWrapper: ShadowNodeWrapper, _animationUpdates: CSSAnimationUpdates ) { throw new ReanimatedError( '`applyCSSAnimations` is not available in JSReanimated.' ); } unregisterCSSAnimations(_viewTag: number): void { throw new ReanimatedError( '`unregisterCSSAnimations` is not available in JSReanimated.' ); } registerCSSTransition( _shadowNodeWrapper: ShadowNodeWrapper, _transitionConfig: NormalizedCSSTransitionConfig ): void { throw new ReanimatedError( '`registerCSSTransition` is not available in JSReanimated.' ); } updateCSSTransition( _viewTag: number, _settingsUpdates: Partial<NormalizedCSSTransitionConfig> ): void { throw new ReanimatedError( '`updateCSSTransition` is not available in JSReanimated.' ); } unregisterCSSTransition(_viewTag: number): void { throw new ReanimatedError( '`unregisterCSSTransition` is not available in JSReanimated.' ); } getSettledUpdates(): SettledUpdate[] { throw new ReanimatedError( '`getSettledUpdates` is not available in JSReanimated.' ); } } // Lack of this export breaks TypeScript generation since // an enum transpiles into JavaScript code. /** @knipIgnore */ export enum Platform { WEB_IOS = 'web iOS', WEB_ANDROID = 'web Android', WEB = 'web', UNKNOWN = 'unknown', } declare global { interface Navigator { userAgent: string; vendor: string; } }