Skip to content

Commit

Permalink
feature: Toggle backdrop visibility (#483)
Browse files Browse the repository at this point in the history
- Added a backdrop visible flag to the `ViewerConfig` type.
- Synced the new backdrop visibility flag to the URL.
- Changed object opacity so it is only applied when backdrops are visible.
- Updated `SettingsTab` with the new vector config.
  - This change moves object opacity to live under the backdrop settings area.
- Adds a new onscreen toggle for the background image in `CanvasWrapper`.
  • Loading branch information
ShrimpCryptid authored Dec 5, 2024
1 parent 76a36a9 commit 85751a9
Show file tree
Hide file tree
Showing 13 changed files with 217 additions and 52 deletions.
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>
);
}

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) {
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

0 comments on commit 85751a9

Please sign in to comment.