Skip to content

Commit

Permalink
[Web] Stylus support (#3107)
Browse files Browse the repository at this point in the history
## Description

This PR adds more information about `stylus` to event. `Pan` and `Hover` events now have `stylusData` field, which contains the following information:

- `tiltX`
- `tiltY`
- `altitudeAngle`
- `azimuthAngle`
- `pressure`


>[!IMPORTANT]
> Because we receive only one set of data (either `tiltX/tiltY` or `altitudeAngle/azimuthAngle`, we use [this conversion algorithm](https://w3c.github.io/pointerevents/#converting-between-tiltx-tilty-and-altitudeangle-azimuthangle) to calculate second set.

>[!NOTE]
> This PR adds `stylusData` only on web, native platforms will be handled in another PRs
## Test plan

Tested on newly added example.
  • Loading branch information
m-bert authored Sep 19, 2024
1 parent 53fc8e8 commit b79223d
Show file tree
Hide file tree
Showing 12 changed files with 355 additions and 28 deletions.
2 changes: 2 additions & 0 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import PagerAndDrawer from './src/basic/pagerAndDrawer';
import ForceTouch from './src/basic/forcetouch';
import Fling from './src/basic/fling';
import WebStylesResetExample from './src/release_tests/webStylesReset';
import StylusData from './src/release_tests/StylusData';

import ReanimatedSimple from './src/new_api/reanimated';
import Camera from './src/new_api/camera';
Expand Down Expand Up @@ -154,6 +155,7 @@ const EXAMPLES: ExamplesSection[] = [
{ name: 'RectButton (borders)', component: RectButtonBorders },
{ name: 'Gesturized pressable', component: GesturizedPressable },
{ name: 'Web styles reset', component: WebStylesResetExample },
{ name: 'Stylus data', component: StylusData },
],
},
{
Expand Down
92 changes: 92 additions & 0 deletions example/src/release_tests/StylusData/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import { StyleSheet, View, Image } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';

const GH = require('../../new_api/hoverable_icons/gh.png');

export default function StylusData() {
const scaleFactor = useSharedValue(0);
const rotationXFactor = useSharedValue(0);
const rotationYFactor = useSharedValue(0);

const pan = Gesture.Pan()
.onBegin((e) => {
if (!e.stylusData) {
return;
}

scaleFactor.value = e.stylusData.pressure;
rotationYFactor.value = e.stylusData.tiltX;
rotationXFactor.value = e.stylusData.tiltY;
})
.onStart(() => {})
.onChange((e) => {
if (!e.stylusData) {
return;
}

scaleFactor.value = e.stylusData.pressure;
rotationYFactor.value = e.stylusData.tiltX;
rotationXFactor.value = e.stylusData.tiltY;
})
.onFinalize((e) => {
if (!e.stylusData) {
return;
}

scaleFactor.value = withTiming(0, { duration: 250 });
rotationXFactor.value = withTiming(0, { duration: 250 });
rotationYFactor.value = withTiming(0, { duration: 250 });
})
.minDistance(0);

const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ scale: 1 + scaleFactor.value },
{ rotateY: `${rotationYFactor.value}deg` },
{ rotateX: `${rotationXFactor.value}deg` },
],
};
});

return (
<View style={styles.container}>
<GestureDetector gesture={pan}>
<Animated.View style={[styles.ball, animatedStyle]}>
<Image
source={GH}
// @ts-ignore pointerEvents exists
style={{ width: 180, height: 180, pointerEvents: 'none' }}
/>
</Animated.View>
</GestureDetector>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},

ball: {
width: 200,
height: 200,
borderWidth: 2,
borderRadius: 100,
backgroundColor: '#c8e3f7',

display: 'flex',
justifyContent: 'space-around',
alignItems: 'center',
},
});
6 changes: 4 additions & 2 deletions src/components/Pressable/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Insets } from 'react-native';
import { LongPressGestureHandlerEventPayload } from '../../handlers/GestureHandlerEventPayload';
import {
HoverGestureHandlerEventPayload,
LongPressGestureHandlerEventPayload,
} from '../../handlers/GestureHandlerEventPayload';
import {
TouchData,
GestureStateChangeEvent,
GestureTouchEvent,
} from '../../handlers/gestureHandlerCommon';
import { HoverGestureHandlerEventPayload } from '../../handlers/gestures/hoverGesture';
import { InnerPressableEvent, PressableEvent } from './PressableProps';

const numberAsInset = (value: number): Insets => ({
Expand Down
42 changes: 42 additions & 0 deletions src/handlers/GestureHandlerEventPayload.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { StylusData } from '../web/interfaces';

export type FlingGestureHandlerEventPayload = {
x: number;
y: number;
Expand Down Expand Up @@ -120,6 +122,11 @@ export type PanGestureHandlerEventPayload = {
* value is expressed in point units per second.
*/
velocityY: number;

/**
* Object containing additional stylus data.
*/
stylusData: StylusData | undefined;
};

export type PinchGestureHandlerEventPayload = {
Expand Down Expand Up @@ -182,3 +189,38 @@ export type RotationGestureHandlerEventPayload = {
*/
velocity: number;
};

export type HoverGestureHandlerEventPayload = {
/**
* X coordinate of the current position of the pointer relative to the view
* attached to the handler. Expressed in point units.
*/
x: number;

/**
* Y coordinate of the current position of the pointer relative to the view
* attached to the handler. Expressed in point units.
*/
y: number;

/**
* X coordinate of the current position of the pointer relative to the window.
* The value is expressed in point units. It is recommended to use it instead
* of `x` in cases when the original view can be transformed as an
* effect of the gesture.
*/
absoluteX: number;

/**
* Y coordinate of the current position of the pointer relative to the window.
* The value is expressed in point units. It is recommended to use it instead
* of `y` in cases when the original view can be transformed as an
* effect of the gesture.
*/
absoluteY: number;

/**
* Object containing additional stylus data.
*/
stylusData: StylusData | undefined;
};
4 changes: 3 additions & 1 deletion src/handlers/gestures/gesture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
RotationGestureHandlerEventPayload,
TapGestureHandlerEventPayload,
NativeViewGestureHandlerPayload,
HoverGestureHandlerEventPayload,
} from '../GestureHandlerEventPayload';
import { isRemoteDebuggingEnabled } from '../../utils';

Expand All @@ -31,7 +32,8 @@ export type GestureType =
| BaseGesture<PinchGestureHandlerEventPayload>
| BaseGesture<FlingGestureHandlerEventPayload>
| BaseGesture<ForceTouchGestureHandlerEventPayload>
| BaseGesture<NativeViewGestureHandlerPayload>;
| BaseGesture<NativeViewGestureHandlerPayload>
| BaseGesture<HoverGestureHandlerEventPayload>;

export type GestureRef =
| number
Expand Down
8 changes: 1 addition & 7 deletions src/handlers/gestures/hoverGesture.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { BaseGestureConfig, ContinousBaseGesture } from './gesture';
import { GestureUpdateEvent } from '../gestureHandlerCommon';

export type HoverGestureHandlerEventPayload = {
x: number;
y: number;
absoluteX: number;
absoluteY: number;
};
import type { HoverGestureHandlerEventPayload } from '../GestureHandlerEventPayload';

export type HoverGestureChangeEventPayload = {
changeX: number;
Expand Down
1 change: 1 addition & 0 deletions src/jestUtils/jestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const handlersDefaultEvents: DefaultEventsMapping = {
velocityX: 3,
velocityY: 0,
numberOfPointers: 1,
stylusData: undefined,
},
[pinchHandlerName]: {
focalX: 0,
Expand Down
16 changes: 15 additions & 1 deletion src/web/handlers/HoverGestureHandler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { State } from '../../State';
import { AdaptedEvent, Config } from '../interfaces';
import { AdaptedEvent, Config, StylusData } from '../interfaces';
import GestureHandlerOrchestrator from '../tools/GestureHandlerOrchestrator';
import GestureHandler from './GestureHandler';

export default class HoverGestureHandler extends GestureHandler {
private stylusData: StylusData | undefined;

public init(ref: number, propsRef: React.RefObject<unknown>) {
super.init(ref, propsRef);
}

protected transformNativeEvent(): Record<string, unknown> {
return {
...super.transformNativeEvent(),
stylusData: this.stylusData,
};
}

public updateGestureConfig({ enabled = true, ...props }: Config): void {
super.updateGestureConfig({ enabled: enabled, ...props });
}
Expand All @@ -16,6 +25,7 @@ export default class HoverGestureHandler extends GestureHandler {
GestureHandlerOrchestrator.getInstance().recordHandlerIfNotPresent(this);

this.tracker.addToTracker(event);
this.stylusData = event.stylusData;
super.onPointerMoveOver(event);

if (this.getState() === State.UNDETERMINED) {
Expand All @@ -26,13 +36,17 @@ export default class HoverGestureHandler extends GestureHandler {

protected onPointerMoveOut(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
this.stylusData = event.stylusData;

super.onPointerMoveOut(event);

this.end();
}

protected onPointerMove(event: AdaptedEvent): void {
this.tracker.track(event);
this.stylusData = event.stylusData;

super.onPointerMove(event);
}

Expand Down
11 changes: 10 additions & 1 deletion src/web/handlers/PanGestureHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { State } from '../../State';
import { DEFAULT_TOUCH_SLOP } from '../constants';
import { AdaptedEvent, Config } from '../interfaces';
import { AdaptedEvent, Config, StylusData } from '../interfaces';

import GestureHandler from './GestureHandler';

Expand Down Expand Up @@ -52,6 +52,8 @@ export default class PanGestureHandler extends GestureHandler {
private lastX = 0;
private lastY = 0;

private stylusData: StylusData | undefined;

private activateAfterLongPress = 0;
private activationTimeout = 0;

Expand Down Expand Up @@ -196,6 +198,7 @@ export default class PanGestureHandler extends GestureHandler {
translationY: isNaN(translationY) ? 0 : translationY,
velocityX: this.velocityX,
velocityY: this.velocityY,
stylusData: this.stylusData,
};
}

Expand All @@ -217,6 +220,8 @@ export default class PanGestureHandler extends GestureHandler {
}

this.tracker.addToTracker(event);
this.stylusData = event.stylusData;

super.onPointerDown(event);

const lastCoords = this.tracker.getAbsoluteCoordsAverage();
Expand Down Expand Up @@ -259,6 +264,8 @@ export default class PanGestureHandler extends GestureHandler {
}

protected onPointerUp(event: AdaptedEvent): void {
this.stylusData = event.stylusData;

super.onPointerUp(event);
if (this.currentState === State.ACTIVE) {
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
Expand Down Expand Up @@ -306,6 +313,7 @@ export default class PanGestureHandler extends GestureHandler {

protected onPointerMove(event: AdaptedEvent): void {
this.tracker.track(event);
this.stylusData = event.stylusData;

const lastCoords = this.tracker.getAbsoluteCoordsAverage();
this.lastX = lastCoords.x;
Expand All @@ -326,6 +334,7 @@ export default class PanGestureHandler extends GestureHandler {
}

this.tracker.track(event);
this.stylusData = event.stylusData;

const lastCoords = this.tracker.getAbsoluteCoordsAverage();
this.lastX = lastCoords.x;
Expand Down
9 changes: 9 additions & 0 deletions src/web/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ export interface PropsRef {
onGestureHandlerStateChange: () => void;
}

export interface StylusData {
tiltX: number;
tiltY: number;
azimuthAngle: number;
altitudeAngle: number;
pressure: number;
}

export interface AdaptedEvent {
x: number;
y: number;
Expand All @@ -142,6 +150,7 @@ export interface AdaptedEvent {
pointerType: PointerType;
time: number;
button?: MouseButton;
stylusData?: StylusData;
}

export enum EventTypes {
Expand Down
17 changes: 2 additions & 15 deletions src/web/tools/PointerEventManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@ import { AdaptedEvent, EventTypes, Point } from '../interfaces';
import {
PointerTypeMapping,
calculateViewScale,
tryExtractStylusData,
isPointerInBounds,
} from '../utils';
import { PointerType } from '../../PointerType';

const POINTER_CAPTURE_EXCLUDE_LIST = new Set<string>(['SELECT', 'INPUT']);
const PointerTypes = {
Touch: 'touch',
Stylus: 'pen',
};

export default class PointerEventManager extends EventManager<HTMLElement> {
private trackedPointers = new Set<number>();
Expand Down Expand Up @@ -85,17 +82,6 @@ export default class PointerEventManager extends EventManager<HTMLElement> {
};

private pointerMoveCallback = (event: PointerEvent) => {
// Stylus triggers `pointermove` event when it detects changes in pressure. Since it is very sensitive to those changes,
// it constantly sends events, even though there was no change in position. To fix that we check whether
// pointer has actually moved and if not, we do not send event.
if (
event.pointerType === PointerTypes.Stylus &&
event.x === this.lastPosition.x &&
event.y === this.lastPosition.y
) {
return;
}

const adaptedEvent: AdaptedEvent = this.mapEvent(event, EventTypes.MOVE);
const target = event.target as HTMLElement;

Expand Down Expand Up @@ -229,6 +215,7 @@ export default class PointerEventManager extends EventManager<HTMLElement> {
PointerTypeMapping.get(event.pointerType) ?? PointerType.OTHER,
button: this.mouseButtonsMapper.get(event.button),
time: event.timeStamp,
stylusData: tryExtractStylusData(event),
};
}

Expand Down
Loading

0 comments on commit b79223d

Please sign in to comment.