Skip to content

Commit b79223d

Browse files
authored
[Web] Stylus support (#3107)
## 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.
1 parent 53fc8e8 commit b79223d

File tree

12 files changed

+355
-28
lines changed

12 files changed

+355
-28
lines changed

example/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import PagerAndDrawer from './src/basic/pagerAndDrawer';
5454
import ForceTouch from './src/basic/forcetouch';
5555
import Fling from './src/basic/fling';
5656
import WebStylesResetExample from './src/release_tests/webStylesReset';
57+
import StylusData from './src/release_tests/StylusData';
5758

5859
import ReanimatedSimple from './src/new_api/reanimated';
5960
import Camera from './src/new_api/camera';
@@ -154,6 +155,7 @@ const EXAMPLES: ExamplesSection[] = [
154155
{ name: 'RectButton (borders)', component: RectButtonBorders },
155156
{ name: 'Gesturized pressable', component: GesturizedPressable },
156157
{ name: 'Web styles reset', component: WebStylesResetExample },
158+
{ name: 'Stylus data', component: StylusData },
157159
],
158160
},
159161
{
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React from 'react';
2+
import { StyleSheet, View, Image } from 'react-native';
3+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
4+
import Animated, {
5+
useAnimatedStyle,
6+
useSharedValue,
7+
withTiming,
8+
} from 'react-native-reanimated';
9+
10+
const GH = require('../../new_api/hoverable_icons/gh.png');
11+
12+
export default function StylusData() {
13+
const scaleFactor = useSharedValue(0);
14+
const rotationXFactor = useSharedValue(0);
15+
const rotationYFactor = useSharedValue(0);
16+
17+
const pan = Gesture.Pan()
18+
.onBegin((e) => {
19+
if (!e.stylusData) {
20+
return;
21+
}
22+
23+
scaleFactor.value = e.stylusData.pressure;
24+
rotationYFactor.value = e.stylusData.tiltX;
25+
rotationXFactor.value = e.stylusData.tiltY;
26+
})
27+
.onStart(() => {})
28+
.onChange((e) => {
29+
if (!e.stylusData) {
30+
return;
31+
}
32+
33+
scaleFactor.value = e.stylusData.pressure;
34+
rotationYFactor.value = e.stylusData.tiltX;
35+
rotationXFactor.value = e.stylusData.tiltY;
36+
})
37+
.onFinalize((e) => {
38+
if (!e.stylusData) {
39+
return;
40+
}
41+
42+
scaleFactor.value = withTiming(0, { duration: 250 });
43+
rotationXFactor.value = withTiming(0, { duration: 250 });
44+
rotationYFactor.value = withTiming(0, { duration: 250 });
45+
})
46+
.minDistance(0);
47+
48+
const animatedStyle = useAnimatedStyle(() => {
49+
return {
50+
transform: [
51+
{ scale: 1 + scaleFactor.value },
52+
{ rotateY: `${rotationYFactor.value}deg` },
53+
{ rotateX: `${rotationXFactor.value}deg` },
54+
],
55+
};
56+
});
57+
58+
return (
59+
<View style={styles.container}>
60+
<GestureDetector gesture={pan}>
61+
<Animated.View style={[styles.ball, animatedStyle]}>
62+
<Image
63+
source={GH}
64+
// @ts-ignore pointerEvents exists
65+
style={{ width: 180, height: 180, pointerEvents: 'none' }}
66+
/>
67+
</Animated.View>
68+
</GestureDetector>
69+
</View>
70+
);
71+
}
72+
73+
const styles = StyleSheet.create({
74+
container: {
75+
flex: 1,
76+
justifyContent: 'center',
77+
alignItems: 'center',
78+
backgroundColor: '#F5FCFF',
79+
},
80+
81+
ball: {
82+
width: 200,
83+
height: 200,
84+
borderWidth: 2,
85+
borderRadius: 100,
86+
backgroundColor: '#c8e3f7',
87+
88+
display: 'flex',
89+
justifyContent: 'space-around',
90+
alignItems: 'center',
91+
},
92+
});

src/components/Pressable/utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Insets } from 'react-native';
2-
import { LongPressGestureHandlerEventPayload } from '../../handlers/GestureHandlerEventPayload';
2+
import {
3+
HoverGestureHandlerEventPayload,
4+
LongPressGestureHandlerEventPayload,
5+
} from '../../handlers/GestureHandlerEventPayload';
36
import {
47
TouchData,
58
GestureStateChangeEvent,
69
GestureTouchEvent,
710
} from '../../handlers/gestureHandlerCommon';
8-
import { HoverGestureHandlerEventPayload } from '../../handlers/gestures/hoverGesture';
911
import { InnerPressableEvent, PressableEvent } from './PressableProps';
1012

1113
const numberAsInset = (value: number): Insets => ({

src/handlers/GestureHandlerEventPayload.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { StylusData } from '../web/interfaces';
2+
13
export type FlingGestureHandlerEventPayload = {
24
x: number;
35
y: number;
@@ -120,6 +122,11 @@ export type PanGestureHandlerEventPayload = {
120122
* value is expressed in point units per second.
121123
*/
122124
velocityY: number;
125+
126+
/**
127+
* Object containing additional stylus data.
128+
*/
129+
stylusData: StylusData | undefined;
123130
};
124131

125132
export type PinchGestureHandlerEventPayload = {
@@ -182,3 +189,38 @@ export type RotationGestureHandlerEventPayload = {
182189
*/
183190
velocity: number;
184191
};
192+
193+
export type HoverGestureHandlerEventPayload = {
194+
/**
195+
* X coordinate of the current position of the pointer relative to the view
196+
* attached to the handler. Expressed in point units.
197+
*/
198+
x: number;
199+
200+
/**
201+
* Y coordinate of the current position of the pointer relative to the view
202+
* attached to the handler. Expressed in point units.
203+
*/
204+
y: number;
205+
206+
/**
207+
* X coordinate of the current position of the pointer relative to the window.
208+
* The value is expressed in point units. It is recommended to use it instead
209+
* of `x` in cases when the original view can be transformed as an
210+
* effect of the gesture.
211+
*/
212+
absoluteX: number;
213+
214+
/**
215+
* Y coordinate of the current position of the pointer relative to the window.
216+
* The value is expressed in point units. It is recommended to use it instead
217+
* of `y` in cases when the original view can be transformed as an
218+
* effect of the gesture.
219+
*/
220+
absoluteY: number;
221+
222+
/**
223+
* Object containing additional stylus data.
224+
*/
225+
stylusData: StylusData | undefined;
226+
};

src/handlers/gestures/gesture.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
RotationGestureHandlerEventPayload,
1919
TapGestureHandlerEventPayload,
2020
NativeViewGestureHandlerPayload,
21+
HoverGestureHandlerEventPayload,
2122
} from '../GestureHandlerEventPayload';
2223
import { isRemoteDebuggingEnabled } from '../../utils';
2324

@@ -31,7 +32,8 @@ export type GestureType =
3132
| BaseGesture<PinchGestureHandlerEventPayload>
3233
| BaseGesture<FlingGestureHandlerEventPayload>
3334
| BaseGesture<ForceTouchGestureHandlerEventPayload>
34-
| BaseGesture<NativeViewGestureHandlerPayload>;
35+
| BaseGesture<NativeViewGestureHandlerPayload>
36+
| BaseGesture<HoverGestureHandlerEventPayload>;
3537

3638
export type GestureRef =
3739
| number

src/handlers/gestures/hoverGesture.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { BaseGestureConfig, ContinousBaseGesture } from './gesture';
22
import { GestureUpdateEvent } from '../gestureHandlerCommon';
3-
4-
export type HoverGestureHandlerEventPayload = {
5-
x: number;
6-
y: number;
7-
absoluteX: number;
8-
absoluteY: number;
9-
};
3+
import type { HoverGestureHandlerEventPayload } from '../GestureHandlerEventPayload';
104

115
export type HoverGestureChangeEventPayload = {
126
changeX: number;

src/jestUtils/jestUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ const handlersDefaultEvents: DefaultEventsMapping = {
138138
velocityX: 3,
139139
velocityY: 0,
140140
numberOfPointers: 1,
141+
stylusData: undefined,
141142
},
142143
[pinchHandlerName]: {
143144
focalX: 0,

src/web/handlers/HoverGestureHandler.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import { State } from '../../State';
2-
import { AdaptedEvent, Config } from '../interfaces';
2+
import { AdaptedEvent, Config, StylusData } from '../interfaces';
33
import GestureHandlerOrchestrator from '../tools/GestureHandlerOrchestrator';
44
import GestureHandler from './GestureHandler';
55

66
export default class HoverGestureHandler extends GestureHandler {
7+
private stylusData: StylusData | undefined;
8+
79
public init(ref: number, propsRef: React.RefObject<unknown>) {
810
super.init(ref, propsRef);
911
}
1012

13+
protected transformNativeEvent(): Record<string, unknown> {
14+
return {
15+
...super.transformNativeEvent(),
16+
stylusData: this.stylusData,
17+
};
18+
}
19+
1120
public updateGestureConfig({ enabled = true, ...props }: Config): void {
1221
super.updateGestureConfig({ enabled: enabled, ...props });
1322
}
@@ -16,6 +25,7 @@ export default class HoverGestureHandler extends GestureHandler {
1625
GestureHandlerOrchestrator.getInstance().recordHandlerIfNotPresent(this);
1726

1827
this.tracker.addToTracker(event);
28+
this.stylusData = event.stylusData;
1929
super.onPointerMoveOver(event);
2030

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

2737
protected onPointerMoveOut(event: AdaptedEvent): void {
2838
this.tracker.addToTracker(event);
39+
this.stylusData = event.stylusData;
40+
2941
super.onPointerMoveOut(event);
3042

3143
this.end();
3244
}
3345

3446
protected onPointerMove(event: AdaptedEvent): void {
3547
this.tracker.track(event);
48+
this.stylusData = event.stylusData;
49+
3650
super.onPointerMove(event);
3751
}
3852

src/web/handlers/PanGestureHandler.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { State } from '../../State';
22
import { DEFAULT_TOUCH_SLOP } from '../constants';
3-
import { AdaptedEvent, Config } from '../interfaces';
3+
import { AdaptedEvent, Config, StylusData } from '../interfaces';
44

55
import GestureHandler from './GestureHandler';
66

@@ -52,6 +52,8 @@ export default class PanGestureHandler extends GestureHandler {
5252
private lastX = 0;
5353
private lastY = 0;
5454

55+
private stylusData: StylusData | undefined;
56+
5557
private activateAfterLongPress = 0;
5658
private activationTimeout = 0;
5759

@@ -196,6 +198,7 @@ export default class PanGestureHandler extends GestureHandler {
196198
translationY: isNaN(translationY) ? 0 : translationY,
197199
velocityX: this.velocityX,
198200
velocityY: this.velocityY,
201+
stylusData: this.stylusData,
199202
};
200203
}
201204

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

219222
this.tracker.addToTracker(event);
223+
this.stylusData = event.stylusData;
224+
220225
super.onPointerDown(event);
221226

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

261266
protected onPointerUp(event: AdaptedEvent): void {
267+
this.stylusData = event.stylusData;
268+
262269
super.onPointerUp(event);
263270
if (this.currentState === State.ACTIVE) {
264271
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
@@ -306,6 +313,7 @@ export default class PanGestureHandler extends GestureHandler {
306313

307314
protected onPointerMove(event: AdaptedEvent): void {
308315
this.tracker.track(event);
316+
this.stylusData = event.stylusData;
309317

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

328336
this.tracker.track(event);
337+
this.stylusData = event.stylusData;
329338

330339
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
331340
this.lastX = lastCoords.x;

src/web/interfaces.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ export interface PropsRef {
132132
onGestureHandlerStateChange: () => void;
133133
}
134134

135+
export interface StylusData {
136+
tiltX: number;
137+
tiltY: number;
138+
azimuthAngle: number;
139+
altitudeAngle: number;
140+
pressure: number;
141+
}
142+
135143
export interface AdaptedEvent {
136144
x: number;
137145
y: number;
@@ -142,6 +150,7 @@ export interface AdaptedEvent {
142150
pointerType: PointerType;
143151
time: number;
144152
button?: MouseButton;
153+
stylusData?: StylusData;
145154
}
146155

147156
export enum EventTypes {

0 commit comments

Comments
 (0)