Skip to content

Commit

Permalink
feature(ImageViewer): Add FilterControls component to ImageViewer (#402)
Browse files Browse the repository at this point in the history
* Update filters

* Revert files

* Rm images

* Rm files

* Add hideLabel param

* Add hideLabel param

* Lint

* Build

* Build

* Update comments

* Rebuild

* Rebuild

Co-authored-by: mheitman <mae_heitmann@brown.edu>
  • Loading branch information
mheitman and mheitman authored Nov 24, 2020
1 parent 527e23e commit 98f26c4
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 17 deletions.
127 changes: 127 additions & 0 deletions packages/core/src/components/ImageViewer/FilterControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React, { useCallback, useState } from 'react';
import IconTune from '@airbnb/lunar-icons/lib/interface/IconTune';
import ButtonGroup from '../ButtonGroup';
import Card, { Content } from '../Card';
import Dropdown from '../Dropdown';
import IconButton from '../IconButton';
import Range from '../Range';
import Text from '../Text';
import T from '../Translate';
import useStyles, { StyleSheet } from '../../hooks/useStyles';

export type FilterControlsProps = {
/** The current brightness. 1 by default. Valid range: 0 -> ∞. */
brightness?: number;
/** Callback when brightness changes. */
onBrightnessChange: (brightness: number) => void;
/** The current contrast. 1 by default. Valid range: 0 -> ∞. */
contrast?: number;
/** Callback when contrast changes. */
onContrastChange: (contrast: number) => void;
/** Size of the icons. */
iconSize?: number | string;
/** Place dropdown menu above. */
dropdownAbove?: boolean;
};

const styleSheet: StyleSheet = () => ({
controls: {
position: 'relative',
},
filterLabel: {
width: 100,
},
filterRow: {
alignItems: 'baseline',
display: 'flex',
},
});

/** Filter controls that can be used with an image viewer component */
export default function FilterControls(props: FilterControlsProps) {
const [styles, cx] = useStyles(styleSheet);
const [visible, setVisible] = useState(false);

const {
onBrightnessChange,
brightness = 1,
onContrastChange,
contrast = 1,
iconSize = '2em',
dropdownAbove,
} = props;

const handleBrightnessChange = useCallback(
(v) => {
onBrightnessChange(10 ** v);
},
[onBrightnessChange],
);

const handleContrastChange = useCallback(
(v) => {
onContrastChange(10 ** v);
},
[onContrastChange],
);

const toggleContrastPicker = useCallback(() => setVisible(!visible), [visible]);

return (
<ButtonGroup>
<div className={cx(styles.controls)}>
<IconButton onClick={toggleContrastPicker}>
<IconTune
accessibilityLabel={T.phrase('lunar.image.adjustContrast', 'Adjust contrast')}
size={iconSize}
/>
</IconButton>

{visible && (
<Dropdown
visible={visible}
bottom={dropdownAbove ? '100%' : undefined}
left={0}
zIndex={5}
onClickOutside={toggleContrastPicker}
>
<Card>
<Content>
<div className={cx(styles.filterRow)}>
<div className={cx(styles.filterLabel)}>
<Text>{T.phrase('lunar.image.brightness', 'Brightness')}</Text>
</div>
<Range
hideLabel
label="brightness"
width={200}
min={-0.5}
max={0.5}
step={0.05}
value={Math.log10(brightness)}
onChange={handleBrightnessChange}
/>
</div>
<div className={cx(styles.filterRow)}>
<div className={cx(styles.filterLabel)}>
<Text>{T.phrase('lunar.image.contrast', 'Contrast')}</Text>
</div>
<Range
hideLabel
label="contrast"
width={200}
min={-0.5}
max={0.5}
step={0.05}
value={Math.log10(contrast)}
onChange={handleContrastChange}
/>
</div>
</Content>
</Card>
</Dropdown>
)}
</div>
</ButtonGroup>
);
}
12 changes: 10 additions & 2 deletions packages/core/src/components/ImageViewer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import useStyles, { StyleSheet } from '../../hooks/useStyles';
import FilterControls from './FilterControls';
import ZoomControls from './ZoomControls';
import RotateControls from './RotateControls';
import ResponsiveImage from '../ResponsiveImage';
Expand All @@ -18,6 +19,10 @@ export type ImageViewerProps = {
src: string;
/** The current scale / zoom level. 1 by default. */
scale?: number;
/** The current brightness. 1 by default. */
brightness?: number;
/** The current contrast. 1 by default. */
contrast?: number;
/** Render width. Unconstrained (css value 'none') by default. */
width?: number | string;
/** Custom style sheet. */
Expand All @@ -36,6 +41,8 @@ export default function ImageViewer({
height = 'none',
rotation = 0,
scale = 1,
brightness = 1,
contrast = 1,
src,
width,
styleSheet,
Expand Down Expand Up @@ -101,6 +108,7 @@ export default function ImageViewer({
const translateX = (y * sinRotation + x * cosRotation) / scale;
const translateY = (y * cosRotation - x * sinRotation) / scale;
const transform = `scale(${scale}) rotate(${rotation}deg) translateY(${translateY}px) translateX(${translateX}px)`;
const filter = `brightness(${brightness}) contrast(${contrast})`;

return (
<div
Expand All @@ -110,7 +118,7 @@ export default function ImageViewer({
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
>
<div className={cx(styles.image)} style={{ transform }}>
<div className={cx(styles.image)} style={{ transform, filter }}>
<ResponsiveImage
contain
noShadow
Expand All @@ -125,4 +133,4 @@ export default function ImageViewer({
);
}

export { ZoomControls, RotateControls };
export { FilterControls, ZoomControls, RotateControls };
50 changes: 35 additions & 15 deletions packages/core/src/components/ImageViewer/story.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import React, { useState } from 'react';
import space from ':storybook/images/space.jpg';
import ImageViewer, { ZoomControls, RotateControls } from '.';
import Row from '../Row';
import ImageViewer, { FilterControls, ZoomControls, RotateControls } from '.';
import useStyles, { StyleSheet } from '../../hooks/useStyles';

type ImageViewerDemoProps = {
width?: string;
height?: string;
controlsBottom?: boolean;
};

const styleSheet: StyleSheet = () => ({
controls: {
display: 'flex',
},
});

function ImageViewerDemo({ width, height, controlsBottom }: ImageViewerDemoProps) {
const [styles, cx] = useStyles(styleSheet);
const [brightness, setBrightness] = useState(1);
const [contrast, setContrast] = useState(1);
const [scale, setScale] = useState(1);
const [rotation, setRotation] = useState(0);

Expand All @@ -20,31 +29,42 @@ function ImageViewerDemo({ width, height, controlsBottom }: ImageViewerDemoProps
scale={scale}
src={space}
rotation={rotation}
brightness={brightness}
contrast={contrast}
height={height}
width={width}
/>
<Row
before={
<RotateControls rotation={rotation} onRotation={(value: number) => setRotation(value)} />
}
>
<div className={cx(styles.controls)}>
<FilterControls
dropdownAbove
brightness={brightness}
contrast={contrast}
onBrightnessChange={(value: number) => setBrightness(value)}
onContrastChange={(value: number) => setContrast(value)}
/>
<RotateControls rotation={rotation} onRotation={(value: number) => setRotation(value)} />
<ZoomControls dropdownAbove scale={scale} onScale={(value: number) => setScale(value)} />
</Row>
</div>
</>
) : (
<>
<Row
before={
<RotateControls rotation={rotation} onRotation={(value: number) => setRotation(value)} />
}
>
<div className={cx(styles.controls)}>
<FilterControls
brightness={brightness}
contrast={contrast}
onBrightnessChange={(value: number) => setBrightness(value)}
onContrastChange={(value: number) => setContrast(value)}
/>
<RotateControls rotation={rotation} onRotation={(value: number) => setRotation(value)} />
<ZoomControls scale={scale} onScale={(value: number) => setScale(value)} />
</Row>
</div>
<ImageViewer
alt="Testing"
scale={scale}
src={space}
rotation={rotation}
brightness={brightness}
contrast={contrast}
height={height}
width={width}
/>
Expand All @@ -55,7 +75,7 @@ function ImageViewerDemo({ width, height, controlsBottom }: ImageViewerDemoProps
export default {
title: 'Core/ImageViewer',
parameters: {
inspectComponents: [ImageViewer, ZoomControls, RotateControls],
inspectComponents: [ImageViewer, FilterControls, ZoomControls, RotateControls],
},
};

Expand Down

0 comments on commit 98f26c4

Please sign in to comment.