Skip to content

Commit

Permalink
Fix position of overflowing annotations by refactoring VisCanvas
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc committed Aug 21, 2023
1 parent 8fcf777 commit c6bd0a2
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 117 deletions.
2 changes: 1 addition & 1 deletion apps/storybook/src/Stacking.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A number of elements are rendered **on top of the WebGL canvas**: axis grid, annotations, SVG elements, etc.
Some of these elements must appear above or behind others, so it's important to control and understand their **stacking order**.

The closest container to all these elements, `canvasWrapper`, creates a new [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context).
The closest container to all these elements, `visCanvas`, creates a new [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context).
This allows its children (both direct and nested) to be stacked in relation with one another _within_ this stacking context (unless some of them define their own stacking context).

### Stacking order
Expand Down
9 changes: 2 additions & 7 deletions packages/lib/src/vis/shared/Axis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,8 @@ function Axis(props: Props) {
</svg>
)}
{showGrid && (
<svg className={styles.grid} style={canvasSize}>
<GridComponent
scale={scale}
width={width}
height={height}
{...ticksProp}
/>
<svg className={styles.grid} {...canvasSize}>
<GridComponent scale={scale} {...canvasSize} {...ticksProp} />
</svg>
)}
</>
Expand Down
36 changes: 6 additions & 30 deletions packages/lib/src/vis/shared/AxisSystem.module.css
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
.axisSystem {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
pointer-events: none;
display: grid;
grid-template-areas:
'. top-axis .'
'left-axis grid right-axis'
'. bottom-axis .';
z-index: var(--h5w-zi-axisSystem);
}

.axisSystem > svg {
display: block;
.axis,
.grid {
overflow: visible;
z-index: var(--h5w-zi-axisSystem);
pointer-events: none; /* let events through to canvas */
}

.axis[data-type='abscissa'] {
Expand All @@ -41,6 +28,7 @@
color: var(--h5w-tickLabels--color, #333);
font-family: var(--h5w-tickLabels--fontFamily, inherit);
font-size: var(--h5w-tickLabels--fontSize, 0.75em);
user-select: none; /* prevent unwanted selection during interaction */
}

.label {
Expand All @@ -52,20 +40,8 @@
text-anchor: middle;
}

.title {
display: flex;
justify-content: center;
align-items: center;
grid-area: top-axis;
margin: 0;
color: var(--h5w-plotTitle--color, inherit);
font-family: var(--h5w-plotTitle--fontFamily, inherit);
font-size: var(--h5w-plotTitle--fontSize, 1.125em);
font-weight: var(--h5w-plotTitle--fontWeight, inherit);
}

.grid {
grid-area: grid;
grid-area: canvas;
}

.grid line {
Expand Down
65 changes: 34 additions & 31 deletions packages/lib/src/vis/shared/AxisSystem.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,55 @@
import { assertDefined, assertNonNull } from '@h5web/shared';
import { useThree } from '@react-three/fiber';
import { createPortal } from 'react-dom';

import { useCameraState } from '../hooks';
import type { AxisOffsets } from '../models';
import Axis from './Axis';
import styles from './AxisSystem.module.css';
import Html from './Html';
import { useVisCanvasContext } from './VisCanvasProvider';

interface Props {
axisOffsets: AxisOffsets;
title?: string;
showAxes: boolean;
}

function AxisSystem(props: Props) {
const { axisOffsets, title, showAxes } = props;
const { axisOffsets, showAxes } = props;
const { canvasSize, abscissaConfig, ordinateConfig, getVisibleDomains } =
useVisCanvasContext();

const visCanvas = useThree(
(state) => state.gl.domElement.parentElement?.parentElement?.parentElement,
);
assertDefined(visCanvas);
assertNonNull(visCanvas);

const visibleDomains = useCameraState(getVisibleDomains, [getVisibleDomains]);

return (
// Append to `canvasWrapper` instead of `r3fRoot`
<Html overflowCanvas>
<div
className={styles.axisSystem}
style={{
gridTemplateColumns: `${axisOffsets.left}px 1fr ${axisOffsets.right}px`,
gridTemplateRows: `${axisOffsets.top}px 1fr ${axisOffsets.bottom}px`,
}}
>
{showAxes && title && <p className={styles.title}>{title}</p>}
<Axis
type="abscissa"
config={abscissaConfig}
domain={visibleDomains.xVisibleDomain}
canvasSize={canvasSize}
offset={axisOffsets.bottom}
showAxis={showAxes}
/>
<Axis
type="ordinate"
config={ordinateConfig}
domain={visibleDomains.yVisibleDomain}
canvasSize={canvasSize}
offset={axisOffsets.left}
showAxis={showAxes}
flipAxis
/>
</div>
<Html>
{createPortal(
<>
<Axis
type="abscissa"
config={abscissaConfig}
domain={visibleDomains.xVisibleDomain}
canvasSize={canvasSize}
offset={axisOffsets.bottom}
showAxis={showAxes}
/>
<Axis
type="ordinate"
config={ordinateConfig}
domain={visibleDomains.yVisibleDomain}
canvasSize={canvasSize}
offset={axisOffsets.left}
showAxis={showAxes}
flipAxis
/>
</>,
visCanvas,
)}
</Html>
);
}
Expand Down
42 changes: 32 additions & 10 deletions packages/lib/src/vis/shared/VisCanvas.module.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
.canvasWrapper {
.visCanvas {
flex: 1 1 0%;
overflow: hidden; /* prevent overflow when resizing */
position: relative; /* for axis system */
overflow: hidden; /* prevent overflow, notably when resizing */
z-index: 0; /* stacking context for anything rendered above the canvas (axis grid, SVG scene, floating toolbar, tooltip, etc.) */

display: grid;
grid-template-areas:
'. title'
'left-axis canvas'
'. bottom-axis';

/*
* Stacking order, from furthest to closest:
* 1. WebGL canvas
Expand All @@ -20,14 +25,31 @@
--h5w-zi-floatingToolbar: 2000;
}

.r3fRoot {
/* `r3fRoot` is implicitely stacked at `z-index: 0`, so it intercepts events before they reach the canvas.
* We can't stack it explicitly without creating a new stacking context, which breaks the stacking order
* (the axis system ends up either behind or in front of everything else, neither of which is acceptable).
* So we disable pointer events and restore them only on the `canvas` and on specific interactive elements,
* like the floating toolbar. */
pointer-events: none;
.title {
grid-area: title;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
color: var(--h5w-plotTitle--color, inherit);
font-family: var(--h5w-plotTitle--fontFamily, inherit);
font-size: var(--h5w-plotTitle--fontSize, 1.125em);
font-weight: var(--h5w-plotTitle--fontWeight, inherit);
}

.canvasWrapper {
grid-area: canvas;
position: relative; /* for `.r3fRoot`, `.svgOverlay`, `.floatingToolbar`, overflowing annotations, etc. */
background-color: var(--h5w-canvas--bgColor, transparent);

/*
* `.canvasWrapper` and `.r3fRoot` are implicitely stacked at `z-index: 0`, so
* they intercept events before they can reach the canvas. We can't stack them
* explicitly without creating a new stacking context, which would break the
* stacking order. So we disable pointer events instead and restore them only
* on the `canvas` and on specific interactive elements, like the floating toolbar.
*/
pointer-events: none;
}

.r3fRoot > canvas {
Expand Down
74 changes: 36 additions & 38 deletions packages/lib/src/vis/shared/VisCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,48 +54,46 @@ function VisCanvas(props: PropsWithChildren<Props>) {

return (
<div
className={styles.canvasWrapper}
className={styles.visCanvas}
style={{
paddingBottom: axisOffsets.bottom,
paddingLeft: axisOffsets.left,
paddingTop: axisOffsets.top,
paddingRight: axisOffsets.right,
gridTemplateColumns: `${axisOffsets.left}px minmax(0, 1fr) ${axisOffsets.right}px`,
gridTemplateRows: `${axisOffsets.top}px minmax(0, 1fr) ${axisOffsets.bottom}px`,
}}
>
<R3FCanvas className={styles.r3fRoot} orthographic>
<VisCanvasProvider
visRatio={visRatio}
abscissaConfig={abscissaConfig}
ordinateConfig={ordinateConfig}
svgOverlay={svgOverlay}
floatingToolbar={floatingToolbar}
>
<AxisSystem
axisOffsets={axisOffsets}
title={title}
showAxes={showAxes}
/>
<InteractionsProvider>{children}</InteractionsProvider>
<ViewportCenterer />
<RatioEnforcer />
{raycasterThreshold !== undefined && (
<ThresholdAdjuster value={raycasterThreshold} />
)}
</VisCanvasProvider>
{showAxes && title && <p className={styles.title}>{title}</p>}

<Html>
<svg
ref={(elem) => setSvgOverlay(elem || undefined)}
className={styles.svgOverlay}
/>
</Html>
<Html>
<div
ref={(elem) => setFloatingToolbar(elem || undefined)}
className={styles.floatingToolbar}
/>
</Html>
</R3FCanvas>
<div className={styles.canvasWrapper}>
<R3FCanvas className={styles.r3fRoot} orthographic>
<VisCanvasProvider
visRatio={visRatio}
abscissaConfig={abscissaConfig}
ordinateConfig={ordinateConfig}
svgOverlay={svgOverlay}
floatingToolbar={floatingToolbar}
>
<AxisSystem axisOffsets={axisOffsets} showAxes={showAxes} />
<InteractionsProvider>{children}</InteractionsProvider>
<ViewportCenterer />
<RatioEnforcer />
{raycasterThreshold !== undefined && (
<ThresholdAdjuster value={raycasterThreshold} />
)}
</VisCanvasProvider>

<Html>
<svg
ref={(elem) => setSvgOverlay(elem || undefined)}
className={styles.svgOverlay}
/>
</Html>
<Html>
<div
ref={(elem) => setFloatingToolbar(elem || undefined)}
className={styles.floatingToolbar}
/>
</Html>
</R3FCanvas>
</div>
</div>
);
}
Expand Down

0 comments on commit c6bd0a2

Please sign in to comment.