Skip to content

Commit

Permalink
Improve hook to register events on canvas
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc committed Aug 30, 2023
1 parent 4f117f6 commit 2cae984
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 241 deletions.
16 changes: 5 additions & 11 deletions apps/storybook/src/Annotation.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import type { CanvasEvent } from '@h5web/lib';
import {
Annotation,
DefaultInteractions,
useCanvasEvents,
useCanvasEvent,
VisCanvas,
} from '@h5web/lib';
import { useRafState } from '@react-hookz/web';
import type { Meta, StoryObj } from '@storybook/react';
import type { ReactNode } from 'react';
import { useCallback } from 'react';

import FillHeight from './decorators/FillHeight';
import { formatCoord } from './utils';
Expand Down Expand Up @@ -158,15 +157,10 @@ function PointerTracker(props: {
const { children } = props;
const [coords, setCoords] = useRafState<[number, number]>();

const onPointerMove = useCallback(
(evt: CanvasEvent<PointerEvent>) => {
const { x, y } = evt.dataPt;
setCoords([x, y]);
},
[setCoords],
);

useCanvasEvents({ onPointerMove });
useCanvasEvent('pointermove', (evt: CanvasEvent<PointerEvent>) => {
const { x, y } = evt.dataPt;
setCoords([x, y]);
});

// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{coords ? children(...coords) : null}</>;
Expand Down
27 changes: 15 additions & 12 deletions apps/storybook/src/Utilities.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -184,21 +184,27 @@ getAxisDomain([4, 2, 0], ScaleType.Linear, 0.5); // [6, -2])
- **`useAxisDomain(..args): Domain | undefined`** - Memoised version of `getAxisDomain`.
- **`useValueToIndexScale(..args): ScaleThreshold<number, number>`** - Memoised version of `getValueToIndexScale`.
#### useCanvasEvents
#### useCanvasEvent
Register event listeners on the `canvas` element. Useful to implement custom interactions. Must be called from a child component of `VisCanvas`.
Register an event listener on the `canvas` element. Only events that extend from the `MouseEvent` interface are allowed: `pointer<down|up|move>`, `wheel`, `click`, `dblclick`, etc.)
The listener receives the coordinates of the mouse event in **HTML space** (`(offsetX, offsetY)`), Three.js **world space**, and **data space**,
which is convenient when implementing custom interactions.
```ts
useCanvasEvents(callbacks: CanvasEventCallbacks): void
useCanvasEvent(
mouseEventName: 'pointerdown' | 'pointerup' | 'pointermove' | 'wheel' | 'click' | ...,
listener: (evt: CanvasEvent<PointerEvent | WheelEvent | MouseEvent | DragEvent>) => void,
options: AddEventListenerOptions = {},
): void

const handlePointerDown = useCallback((evt: CanvasEvent<PointerEvent>) => {
useCanvasEvent('pointerdown', (evt: CanvasEvent<PointerEvent>) => {
const { htmlPt, worldPt, dataPt, sourceEvent } = evt;
// ...
}, []);

useCanvasEvents({ onPointerDown: handlePointerDown }); // also supported: `onPointerMove`, `onPointerUp` and `onWheel`
});
```
> Note that the hook must be called from a child component of `VisCanvas`.
#### useCameraState
Compute and update a state on every React Three Fiber frame. Useful to re-render React components when the user pans/zooms.
Expand Down Expand Up @@ -262,14 +268,11 @@ const shouldInteract = useInteraction('MyInteraction', {
modifierKey: 'Control',
})

const onPointerDown = useCallback((evt: CanvasEvent<PointerEvent>) => {
useCanvasEvent('pointerdown', (evt: CanvasEvent<PointerEvent>) => {
if (shouldInteract(evt.sourceEvent)) {
/* ... */
}
},
[shouldInteract]);

useCanvasEvents({ onPointerDown }};
});
```

#### useModifierKeyPressed
Expand Down
3 changes: 1 addition & 2 deletions packages/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export { useVisDomain, useSafeDomain } from './vis/heatmap/hooks';
export { scaleGamma } from './vis/scaleGamma';

export {
useCanvasEvents,
useCanvasEvent,
useInteraction,
useModifierKeyPressed,
useDrag,
Expand Down Expand Up @@ -172,7 +172,6 @@ export type {
Rect,
Selection,
CanvasEvent,
CanvasEventCallbacks,
InteractionInfo,
InteractionConfig,
CommonInteractionProps,
Expand Down
54 changes: 25 additions & 29 deletions packages/lib/src/interactions/Pan.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useThree } from '@react-three/fiber';
import { useCallback, useRef } from 'react';
import { useRef } from 'react';
import type { Vector3 } from 'three';

import {
useCanvasEvents,
useCanvasEvent,
useInteraction,
useModifierKeyPressed,
useMoveCameraTo,
Expand Down Expand Up @@ -32,41 +32,37 @@ function Pan(props: Props) {
const startOffsetPosition = useRef<Vector3>(); // `useRef` to avoid re-renders
const isModifierKeyPressed = useModifierKeyPressed(modifierKey);

const onPointerDown = useCallback(
(evt: CanvasEvent<PointerEvent>) => {
const { worldPt, sourceEvent } = evt;
const { target, pointerId } = sourceEvent;
function handlePointerDown(evt: CanvasEvent<PointerEvent>) {
const { worldPt, sourceEvent } = evt;
const { target, pointerId } = sourceEvent;

if (shouldInteract(sourceEvent)) {
(target as Element).setPointerCapture(pointerId); // https://stackoverflow.com/q/28900077/758806
startOffsetPosition.current = worldPt.clone();
}
}

function handlePointerMove(evt: CanvasEvent<PointerEvent>) {
if (!startOffsetPosition.current || !isModifierKeyPressed) {
return;
}

if (shouldInteract(sourceEvent)) {
(target as Element).setPointerCapture(pointerId); // https://stackoverflow.com/q/28900077/758806
startOffsetPosition.current = worldPt.clone();
}
},
[shouldInteract],
);
const { worldPt } = evt;
const delta = startOffsetPosition.current.clone().sub(worldPt);
moveCameraTo(camera.position.clone().add(delta));
}

const onPointerUp = useCallback((evt: CanvasEvent<PointerEvent>) => {
function handlePointerUp(evt: CanvasEvent<PointerEvent>) {
const { sourceEvent } = evt;
const { target, pointerId } = sourceEvent;
(target as Element).releasePointerCapture(pointerId); // https://stackoverflow.com/q/28900077/758806

startOffsetPosition.current = undefined;
}, []);

const onPointerMove = useCallback(
(evt: CanvasEvent<PointerEvent>) => {
if (!startOffsetPosition.current || !isModifierKeyPressed) {
return;
}

const { worldPt } = evt;
const delta = startOffsetPosition.current.clone().sub(worldPt);
moveCameraTo(camera.position.clone().add(delta));
},
[camera, isModifierKeyPressed, moveCameraTo],
);
}

useCanvasEvents({ onPointerDown, onPointerMove, onPointerUp });
useCanvasEvent('pointerdown', handlePointerDown);
useCanvasEvent('pointermove', handlePointerMove);
useCanvasEvent('pointerup', handlePointerUp);

return null;
}
Expand Down
81 changes: 35 additions & 46 deletions packages/lib/src/interactions/SelectionTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,12 @@ import {
} from '@react-hookz/web';
import { useThree } from '@react-three/fiber';
import type { ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import type { Camera } from 'three';

import type { VisCanvasContextValue } from '../vis/shared/VisCanvasProvider';
import { useVisCanvasContext } from '../vis/shared/VisCanvasProvider';
import {
useCanvasEvents,
useInteraction,
useModifierKeyPressed,
} from './hooks';
import { useCanvasEvent, useInteraction, useModifierKeyPressed } from './hooks';
import type {
CanvasEvent,
CommonInteractionProps,
Expand Down Expand Up @@ -87,55 +83,48 @@ function SelectionTool(props: Props) {
disabled,
});

const onPointerDown = useCallback(
(evt: CanvasEvent<PointerEvent>) => {
const { sourceEvent } = evt;
if (!shouldInteract(sourceEvent)) {
return;
}
function handlePointerDown(evt: CanvasEvent<PointerEvent>) {
const { sourceEvent } = evt;
if (!shouldInteract(sourceEvent)) {
return;
}

const { target, pointerId } = sourceEvent;
(target as Element).setPointerCapture(pointerId);
const { target, pointerId } = sourceEvent;
(target as Element).setPointerCapture(pointerId);

startEvtRef.current = evt;
},
[shouldInteract],
);
startEvtRef.current = evt;
}

const onPointerMove = useCallback(
(evt: CanvasEvent<PointerEvent>) => {
if (!startEvtRef.current) {
return;
}
function handlePointerMove(evt: CanvasEvent<PointerEvent>) {
if (!startEvtRef.current) {
return;
}

const { htmlPt: htmlStart } = startEvtRef.current;
const html: Rect = [htmlStart, canvasBox.clampPoint(evt.htmlPt)];
const world = html.map((pt) => htmlToWorld(camera, pt)) as Rect;
const data = world.map(worldToData) as Rect;
const { htmlPt: htmlStart } = startEvtRef.current;
const html: Rect = [htmlStart, canvasBox.clampPoint(evt.htmlPt)];
const world = html.map((pt) => htmlToWorld(camera, pt)) as Rect;
const data = world.map(worldToData) as Rect;

setRawSelection({ html, world, data });
},
[camera, canvasBox, htmlToWorld, worldToData, setRawSelection],
);
setRawSelection({ html, world, data });
}

const onPointerUp = useCallback(
(evt: CanvasEvent<PointerEvent>) => {
if (!startEvtRef.current) {
return;
}
function handlePointerUp(evt: CanvasEvent<PointerEvent>) {
if (!startEvtRef.current) {
return;
}

const { sourceEvent } = evt;
const { target, pointerId } = sourceEvent;
(target as Element).releasePointerCapture(pointerId);
const { sourceEvent } = evt;
const { target, pointerId } = sourceEvent;
(target as Element).releasePointerCapture(pointerId);

startEvtRef.current = undefined;
hasSuccessfullyEndedRef.current = shouldInteract(sourceEvent);
setRawSelection(undefined);
},
[setRawSelection, shouldInteract],
);
startEvtRef.current = undefined;
hasSuccessfullyEndedRef.current = shouldInteract(sourceEvent);
setRawSelection(undefined);
}

useCanvasEvents({ onPointerDown, onPointerMove, onPointerUp });
useCanvasEvent('pointerdown', handlePointerDown);
useCanvasEvent('pointermove', handlePointerMove);
useCanvasEvent('pointerup', handlePointerUp);

function cancelSelection() {
startEvtRef.current = undefined;
Expand Down
10 changes: 8 additions & 2 deletions packages/lib/src/interactions/XAxisZoom.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { useVisCanvasContext } from '../vis/shared/VisCanvasProvider';
import { useCanvasEvents, useInteraction, useZoomOnWheel } from './hooks';
import {
useCanvasEvent,
useInteraction,
useWheelCapture,
useZoomOnWheel,
} from './hooks';
import type { CommonInteractionProps } from './models';

type Props = CommonInteractionProps;
Expand All @@ -19,7 +24,8 @@ function XAxisZoom(props: Props) {
y: false,
});

useCanvasEvents({ onWheel: useZoomOnWheel(isZoomAllowed) });
useWheelCapture();
useCanvasEvent('wheel', useZoomOnWheel(isZoomAllowed));

return null;
}
Expand Down
10 changes: 8 additions & 2 deletions packages/lib/src/interactions/YAxisZoom.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { useVisCanvasContext } from '../vis/shared/VisCanvasProvider';
import { useCanvasEvents, useInteraction, useZoomOnWheel } from './hooks';
import {
useCanvasEvent,
useInteraction,
useWheelCapture,
useZoomOnWheel,
} from './hooks';
import type { CommonInteractionProps } from './models';

type Props = CommonInteractionProps;
Expand All @@ -19,7 +24,8 @@ function YAxisZoom(props: Props) {
y: shouldInteract(sourceEvent),
});

useCanvasEvents({ onWheel: useZoomOnWheel(isZoomAllowed) });
useWheelCapture();
useCanvasEvent('wheel', useZoomOnWheel(isZoomAllowed));

return null;
}
Expand Down
10 changes: 8 additions & 2 deletions packages/lib/src/interactions/Zoom.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { useCanvasEvents, useInteraction, useZoomOnWheel } from './hooks';
import {
useCanvasEvent,
useInteraction,
useWheelCapture,
useZoomOnWheel,
} from './hooks';
import type { CommonInteractionProps } from './models';

type Props = CommonInteractionProps;
Expand All @@ -17,7 +22,8 @@ function Zoom(props: Props) {
return { x: shouldZoom, y: shouldZoom };
};

useCanvasEvents({ onWheel: useZoomOnWheel(isZoomAllowed) });
useWheelCapture();
useCanvasEvent('wheel', useZoomOnWheel(isZoomAllowed));

return null;
}
Expand Down
Loading

0 comments on commit 2cae984

Please sign in to comment.