Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Toggle backdrop visibility #483

Merged
merged 12 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/Viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,21 @@ function Viewer(): ReactElement {
const [featureKey, setFeatureKey] = useState("");
const [selectedTrack, setSelectedTrack] = useState<Track | null>(null);
const [currentFrame, setCurrentFrame] = useState<number>(0);
/** Backdrop key is null if the dataset has no backdrops, or during initialization. */
const [selectedBackdropKey, setSelectedBackdropKey] = useState<string | null>(null);

useEffect(() => {
// Switch to default backdrop if the dataset has one and none is currently selected.
// If the dataset has no backdrops, hide the backdrop.
if (dataset && (selectedBackdropKey === null || !dataset.hasBackdrop(selectedBackdropKey))) {
const defaultBackdropKey = dataset.getDefaultBackdropKey();
setSelectedBackdropKey(defaultBackdropKey);
if (!defaultBackdropKey) {
updateConfig({ backdropVisible: false });
}
}
}, [dataset, selectedBackdropKey]);

// TODO: Save these settings in local storage
// Use reducer here in case multiple updates happen simultaneously
const [config, updateConfig] = useReducer(
Expand Down Expand Up @@ -462,9 +475,16 @@ function Viewer(): ReactElement {
await setFrame(newFrame);

setFindTrackInput("");
if (selectedBackdropKey && !newDataset.hasBackdrop(selectedBackdropKey)) {
setSelectedBackdropKey(null);

// Switch to the new dataset's default backdrop if the current one is not in the
// new dataset. `selectedBackdropKey` is null only if the current dataset has no backdrops.
if (
selectedBackdropKey === null ||
(selectedBackdropKey !== null && !newDataset.hasBackdrop(selectedBackdropKey))
) {
setSelectedBackdropKey(newDataset.getDefaultBackdropKey());
}

setSelectedTrack(null);
setDatasetOpen(true);
setFeatureThresholds(validateThresholds(newDataset, featureThresholds));
Expand Down Expand Up @@ -1061,6 +1081,7 @@ function Viewer(): ReactElement {
categoricalColors={categoricalPalette}
selectedTrack={selectedTrack}
config={config}
updateConfig={updateConfig}
onTrackClicked={(track) => {
setFindTrackInput(track?.trackId.toString() || "");
setSelectedTrack(track);
Expand Down
5 changes: 5 additions & 0 deletions src/assets/images/icon-images-slash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/images/icon-images.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/images/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import AicsLogoSVGAsset from "./AICS-logo.svg?react";
import AicsLogoAndNameSVGAsset from "./AICS-logo-and-name.svg?react";
import DropdownSVGAsset from "./dropdown-arrow.svg?react";
import ExportIconSVGAsset from "./icon-export.svg?react";
import ImagesIconSVGAsset from "./icon-images.svg?react";
import ImagesSlashIconSVGAsset from "./icon-images-slash.svg?react";
import SwitchIconSVGAsset from "./icon-switch-arrows.svg?react";
import NoImageSVGAsset from "./no-image.svg?react";
import SpinBoxHandleDownSVGAsset from "./spin_box-handle-down.svg?react";
Expand All @@ -12,6 +14,8 @@ export const AicsLogoSVG = AicsLogoSVGAsset;
export const DropdownSVG = DropdownSVGAsset;
export const SwitchIconSVG = SwitchIconSVGAsset;
export const NoImageSVG = NoImageSVGAsset;
export const ImagesIconSVG = ImagesIconSVGAsset;
export const ImagesSlashIconSVG = ImagesSlashIconSVGAsset;
export const ExportIconSVG = ExportIconSVGAsset;
export const SpinBoxHandleDownSVG = SpinBoxHandleDownSVGAsset;
export const SpinBoxHandleUpSVG = SpinBoxHandleUpSVGAsset;
4 changes: 4 additions & 0 deletions src/colorizer/Dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,10 @@ export default class Dataset {
return loadedFrame;
}

public getDefaultBackdropKey(): string | null {
return this.backdropData.keys().next().value ?? null;
}

public hasBackdrop(key: string): boolean {
return this.backdropData.has(key);
}
Expand Down
5 changes: 3 additions & 2 deletions src/colorizer/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ export const getDefaultViewerConfig = (): ViewerConfig => ({
showLegendDuringExport: true,
showHeaderDuringExport: true,
keepRangeBetweenDatasets: false,
backdropVisible: false,
/** Brightness, as an integer percentage. */
backdropBrightness: 100,
/** Saturation, as an integer percentage. */
backdropSaturation: 100,
/** Opacity, as an integer percentage. */
objectOpacity: 100,
/** Opacity when backdrops are visible, as an integer percentage. */
objectOpacity: 50,
outOfRangeDrawSettings: { mode: DrawMode.USE_COLOR, color: new Color(OUT_OF_RANGE_COLOR_DEFAULT) },
outlierDrawSettings: { mode: DrawMode.USE_COLOR, color: new Color(OUTLIER_COLOR_DEFAULT) },
outlineColor: new Color(OUTLINE_COLOR_DEFAULT),
Expand Down
3 changes: 2 additions & 1 deletion src/colorizer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,12 @@ export type ViewerConfig = {
showLegendDuringExport: boolean;
showHeaderDuringExport: boolean;
keepRangeBetweenDatasets: boolean;
backdropVisible: boolean;
/** Brightness, as an integer percentage. */
backdropBrightness: number;
/** Saturation, as an integer percentage. */
backdropSaturation: number;
/** Opacity, as an integer percentage. */
/** Object opacity when backdrop is visible, as an integer percentage. */
objectOpacity: number;
outOfRangeDrawSettings: DrawSettings;
outlierDrawSettings: DrawSettings;
Expand Down
3 changes: 3 additions & 0 deletions src/colorizer/utils/url_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ enum UrlParam {
COLOR_RAMP_REVERSED_SUFFIX = "!",
PALETTE = "palette",
PALETTE_KEY = "palette-key",
SHOW_BACKDROP = "bg",
BACKDROP_KEY = "bg-key",
BACKDROP_BRIGHTNESS = "bg-brightness",
BACKDROP_SATURATION = "bg-sat",
Expand Down Expand Up @@ -301,6 +302,7 @@ function serializeViewerConfig(config: Partial<ViewerConfig>): string[] {
parameters.push(`${UrlParam.OPEN_TAB}=${config.openTab}`);
}

tryAddBooleanParam(parameters, config.backdropVisible, UrlParam.SHOW_BACKDROP);
tryAddBooleanParam(parameters, config.showScaleBar, UrlParam.SHOW_SCALEBAR);
tryAddBooleanParam(parameters, config.showTimestamp, UrlParam.SHOW_TIMESTAMP);
tryAddBooleanParam(parameters, config.showTrackPath, UrlParam.SHOW_PATH);
Expand Down Expand Up @@ -366,6 +368,7 @@ function deserializeViewerConfig(params: URLSearchParams): Partial<ViewerConfig>
newConfig.openTab = openTab;
}

newConfig.backdropVisible = getBooleanParam(params.get(UrlParam.SHOW_BACKDROP));
newConfig.showScaleBar = getBooleanParam(params.get(UrlParam.SHOW_SCALEBAR));
newConfig.showTimestamp = getBooleanParam(params.get(UrlParam.SHOW_TIMESTAMP));
newConfig.showTrackPath = getBooleanParam(params.get(UrlParam.SHOW_PATH));
Expand Down
2 changes: 2 additions & 0 deletions src/components/AppStyle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ const theme = {
theme: palette.theme,
link: palette.link,
linkHover: palette.linkDark,
darkLink: palette.gray20,
darkLinkHover: palette.gray10,
},
layout: {
background: palette.gray0,
Expand Down
36 changes: 36 additions & 0 deletions src/components/Buttons/LinkStyleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import styled, { css } from "styled-components";

/**
* A button styled to remove default styles and visually resemble a link.
* This ensures the button still follows accessibility semantics.
*/
export const LinkStyleButton = styled.button<{
$color?: string;
$hoverColor?: string;
}>`
margin: 0;
padding: 0;
width: auto;
overflow: visible;
background: transparent;
line-height: normal;
font: inherit;
font-size: inherit;
border: none;

text-decoration: underline;

${(props) => {
return css`
color: ${props.$color || "var(--color-text-link)"};

&:hover {
color: ${props.$hoverColor || "var(--color-text-link-hover)"};
}

&:focus-visible {
box-shadow: 0 0 0 3px ${props.$color || "var(--color-text-link)"};
}
`;
}}
`;
114 changes: 90 additions & 24 deletions src/components/CanvasWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import styled from "styled-components";
import { Color, ColorRepresentation, Vector2 } from "three";
import { clamp } from "three/src/math/MathUtils";

import { NoImageSVG } from "../assets";
import { ImagesIconSVG, ImagesSlashIconSVG, NoImageSVG } from "../assets";
import { ColorRamp, Dataset, Track } from "../colorizer";
import { LoadTroubleshooting, ViewerConfig } from "../colorizer/types";
import { LoadTroubleshooting, TabType, ViewerConfig } from "../colorizer/types";
import * as mathUtils from "../colorizer/utils/math_utils";
import { FlexColumn, FlexColumnAlignCenter, VisuallyHidden } from "../styles/utils";

import CanvasUIOverlay from "../colorizer/CanvasWithOverlay";
import Collection from "../colorizer/Collection";
import { AppThemeContext } from "./AppStyle";
import { AlertBannerProps } from "./Banner";
import { LinkStyleButton } from "./Buttons/LinkStyleButton";
import IconButton from "./IconButton";
import LoadingSpinner from "./LoadingSpinner";

Expand All @@ -30,19 +31,32 @@ const RIGHT_CLICK_BUTTON = 2;
const MAX_INVERSE_ZOOM = 2; // 0.5x zoom
const MIN_INVERSE_ZOOM = 0.1; // 10x zoom

function TooltipWithSubtext(props: TooltipProps & { title: ReactNode; subtext: ReactNode }): ReactElement {
function TooltipWithSubtext(
props: TooltipProps & { title: ReactNode; subtitle?: ReactNode; subtitleList?: ReactNode[] }
): ReactElement {
const divRef = useRef<HTMLDivElement>(null);
return (
<Tooltip
{...props}
title={
<>
<p style={{ margin: 0 }}>{props.title}</p>
<p style={{ margin: 0, fontSize: "12px" }}>{props.subtext}</p>
</>
}
>
{props.children}
</Tooltip>
<div ref={divRef}>
<Tooltip
{...props}
trigger={["hover", "focus"]}
title={
<>
<p style={{ margin: 0 }}>{props.title}</p>
{props.subtitle && <p style={{ margin: 0, fontSize: "12px" }}>{props.subtitle}</p>}
{props.subtitleList &&
props.subtitleList.map((text, i) => (
<p key={i} style={{ margin: 0, fontSize: "12px" }}>
{text}
</p>
))}
</>
}
getPopupContainer={() => divRef.current ?? document.body}
>
{props.children}
</Tooltip>
</div>
Comment on lines +34 to +59
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend reviewing this with whitespace off!

);
}

Expand Down Expand Up @@ -84,6 +98,7 @@ type CanvasWrapperProps = {
/** Pan and zoom will be reset on collection change. */
collection: Collection | null;
config: ViewerConfig;
updateConfig: (settings: Partial<ViewerConfig>) => void;
vectorData: Float32Array | null;

loading: boolean;
Expand Down Expand Up @@ -225,10 +240,22 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem

// Update backdrops
useMemo(() => {
canv.setBackdropKey(props.selectedBackdropKey);
canv.setBackdropBrightness(props.config.backdropBrightness);
canv.setBackdropSaturation(props.config.backdropSaturation);
}, [props.selectedBackdropKey, props.config.backdropBrightness, props.config.backdropSaturation]);
if (props.selectedBackdropKey !== null && props.config.backdropVisible) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice and important check to have!

canv.setBackdropKey(props.selectedBackdropKey);
canv.setBackdropBrightness(props.config.backdropBrightness);
canv.setBackdropSaturation(props.config.backdropSaturation);
canv.setObjectOpacity(props.config.objectOpacity);
} else {
canv.setBackdropKey(null);
canv.setObjectOpacity(100);
}
}, [
props.selectedBackdropKey,
props.config.backdropVisible,
props.config.backdropBrightness,
props.config.backdropSaturation,
props.config.objectOpacity,
]);

// Update categorical colors
useMemo(() => {
Expand All @@ -246,10 +273,6 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem
canv.setOutlierDrawMode(settings.mode, settings.color);
}, [props.config.outlierDrawSettings]);

useMemo(() => {
canv.setObjectOpacity(props.config.objectOpacity);
}, [props.config.objectOpacity]);

useMemo(() => {
canv.setInRangeLUT(props.inRangeLUT);
}, [props.inRangeLUT]);
Expand Down Expand Up @@ -574,7 +597,33 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem
}, [props.dataset, canv]);

// RENDERING /////////////////////////////////////////////////

canv.render();

const onViewerSettingsLinkClicked = (): void => {
props.updateConfig({ openTab: TabType.SETTINGS });
};

const backdropTooltipContents: ReactNode[] = [];
backdropTooltipContents.push(
<span key="backdrop-name">
{props.selectedBackdropKey === null
? "(No backdrops available)"
: props.dataset?.getBackdropData().get(props.selectedBackdropKey)?.name}
</span>
);
// Link to viewer settings
backdropTooltipContents.push(
<LinkStyleButton
key="backdrop-viewer-settings-link"
$color={theme.color.text.darkLink}
$hoverColor={theme.color.text.darkLinkHover}
onClick={onViewerSettingsLinkClicked}
>
{"Viewer settings > Backdrop"} <VisuallyHidden>(opens settings tab)</VisuallyHidden>
</LinkStyleButton>
);

return (
<FlexColumnAlignCenter
style={{
Expand Down Expand Up @@ -607,7 +656,7 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem
<VisuallyHidden>Reset view</VisuallyHidden>
</IconButton>
</Tooltip>
<TooltipWithSubtext title={"Zoom in"} subtext="Ctrl + Scroll" placement="right" trigger={["hover", "focus"]}>
<TooltipWithSubtext title={"Zoom in"} subtitle="Ctrl + Scroll" placement="right" trigger={["hover", "focus"]}>
<IconButton
type="link"
onClick={() => {
Expand All @@ -618,7 +667,7 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem
<VisuallyHidden>Zoom in</VisuallyHidden>
</IconButton>
</TooltipWithSubtext>
<TooltipWithSubtext title={"Zoom out"} subtext="Ctrl + Scroll" placement="right" trigger={["hover", "focus"]}>
<TooltipWithSubtext title={"Zoom out"} subtitle="Ctrl + Scroll" placement="right" trigger={["hover", "focus"]}>
<IconButton
type="link"
onClick={() => {
Expand All @@ -632,6 +681,23 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem
<VisuallyHidden>Zoom out</VisuallyHidden>
</IconButton>
</TooltipWithSubtext>
<TooltipWithSubtext
title={props.config.backdropVisible ? "Hide backdrop" : "Show backdrop"}
placement="right"
subtitleList={backdropTooltipContents}
trigger={["hover", "focus"]}
>
<IconButton
type={props.config.backdropVisible ? "primary" : "link"}
onClick={() => {
props.updateConfig({ backdropVisible: !props.config.backdropVisible });
}}
disabled={props.selectedBackdropKey === null}
>
{props.config.backdropVisible ? <ImagesSlashIconSVG /> : <ImagesIconSVG />}
<VisuallyHidden>{props.config.backdropVisible ? "Hide backdrop" : "Show backdrop"}</VisuallyHidden>
</IconButton>
</TooltipWithSubtext>
</CanvasControlsContainer>
</FlexColumnAlignCenter>
);
Expand Down
Loading
Loading