react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
368 lines (327 loc) • 10.4 kB
text/typescript
;
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;
}
}