Skip to content

Commit 98f26c4

Browse files
mheitmanmheitman
andauthored
feature(ImageViewer): Add FilterControls component to ImageViewer (#402)
* 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>
1 parent 527e23e commit 98f26c4

File tree

3 files changed

+172
-17
lines changed

3 files changed

+172
-17
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import React, { useCallback, useState } from 'react';
2+
import IconTune from '@airbnb/lunar-icons/lib/interface/IconTune';
3+
import ButtonGroup from '../ButtonGroup';
4+
import Card, { Content } from '../Card';
5+
import Dropdown from '../Dropdown';
6+
import IconButton from '../IconButton';
7+
import Range from '../Range';
8+
import Text from '../Text';
9+
import T from '../Translate';
10+
import useStyles, { StyleSheet } from '../../hooks/useStyles';
11+
12+
export type FilterControlsProps = {
13+
/** The current brightness. 1 by default. Valid range: 0 -> ∞. */
14+
brightness?: number;
15+
/** Callback when brightness changes. */
16+
onBrightnessChange: (brightness: number) => void;
17+
/** The current contrast. 1 by default. Valid range: 0 -> ∞. */
18+
contrast?: number;
19+
/** Callback when contrast changes. */
20+
onContrastChange: (contrast: number) => void;
21+
/** Size of the icons. */
22+
iconSize?: number | string;
23+
/** Place dropdown menu above. */
24+
dropdownAbove?: boolean;
25+
};
26+
27+
const styleSheet: StyleSheet = () => ({
28+
controls: {
29+
position: 'relative',
30+
},
31+
filterLabel: {
32+
width: 100,
33+
},
34+
filterRow: {
35+
alignItems: 'baseline',
36+
display: 'flex',
37+
},
38+
});
39+
40+
/** Filter controls that can be used with an image viewer component */
41+
export default function FilterControls(props: FilterControlsProps) {
42+
const [styles, cx] = useStyles(styleSheet);
43+
const [visible, setVisible] = useState(false);
44+
45+
const {
46+
onBrightnessChange,
47+
brightness = 1,
48+
onContrastChange,
49+
contrast = 1,
50+
iconSize = '2em',
51+
dropdownAbove,
52+
} = props;
53+
54+
const handleBrightnessChange = useCallback(
55+
(v) => {
56+
onBrightnessChange(10 ** v);
57+
},
58+
[onBrightnessChange],
59+
);
60+
61+
const handleContrastChange = useCallback(
62+
(v) => {
63+
onContrastChange(10 ** v);
64+
},
65+
[onContrastChange],
66+
);
67+
68+
const toggleContrastPicker = useCallback(() => setVisible(!visible), [visible]);
69+
70+
return (
71+
<ButtonGroup>
72+
<div className={cx(styles.controls)}>
73+
<IconButton onClick={toggleContrastPicker}>
74+
<IconTune
75+
accessibilityLabel={T.phrase('lunar.image.adjustContrast', 'Adjust contrast')}
76+
size={iconSize}
77+
/>
78+
</IconButton>
79+
80+
{visible && (
81+
<Dropdown
82+
visible={visible}
83+
bottom={dropdownAbove ? '100%' : undefined}
84+
left={0}
85+
zIndex={5}
86+
onClickOutside={toggleContrastPicker}
87+
>
88+
<Card>
89+
<Content>
90+
<div className={cx(styles.filterRow)}>
91+
<div className={cx(styles.filterLabel)}>
92+
<Text>{T.phrase('lunar.image.brightness', 'Brightness')}</Text>
93+
</div>
94+
<Range
95+
hideLabel
96+
label="brightness"
97+
width={200}
98+
min={-0.5}
99+
max={0.5}
100+
step={0.05}
101+
value={Math.log10(brightness)}
102+
onChange={handleBrightnessChange}
103+
/>
104+
</div>
105+
<div className={cx(styles.filterRow)}>
106+
<div className={cx(styles.filterLabel)}>
107+
<Text>{T.phrase('lunar.image.contrast', 'Contrast')}</Text>
108+
</div>
109+
<Range
110+
hideLabel
111+
label="contrast"
112+
width={200}
113+
min={-0.5}
114+
max={0.5}
115+
step={0.05}
116+
value={Math.log10(contrast)}
117+
onChange={handleContrastChange}
118+
/>
119+
</div>
120+
</Content>
121+
</Card>
122+
</Dropdown>
123+
)}
124+
</div>
125+
</ButtonGroup>
126+
);
127+
}

packages/core/src/components/ImageViewer/index.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useState, useEffect } from 'react';
22
import useStyles, { StyleSheet } from '../../hooks/useStyles';
3+
import FilterControls from './FilterControls';
34
import ZoomControls from './ZoomControls';
45
import RotateControls from './RotateControls';
56
import ResponsiveImage from '../ResponsiveImage';
@@ -18,6 +19,10 @@ export type ImageViewerProps = {
1819
src: string;
1920
/** The current scale / zoom level. 1 by default. */
2021
scale?: number;
22+
/** The current brightness. 1 by default. */
23+
brightness?: number;
24+
/** The current contrast. 1 by default. */
25+
contrast?: number;
2126
/** Render width. Unconstrained (css value 'none') by default. */
2227
width?: number | string;
2328
/** Custom style sheet. */
@@ -36,6 +41,8 @@ export default function ImageViewer({
3641
height = 'none',
3742
rotation = 0,
3843
scale = 1,
44+
brightness = 1,
45+
contrast = 1,
3946
src,
4047
width,
4148
styleSheet,
@@ -101,6 +108,7 @@ export default function ImageViewer({
101108
const translateX = (y * sinRotation + x * cosRotation) / scale;
102109
const translateY = (y * cosRotation - x * sinRotation) / scale;
103110
const transform = `scale(${scale}) rotate(${rotation}deg) translateY(${translateY}px) translateX(${translateX}px)`;
111+
const filter = `brightness(${brightness}) contrast(${contrast})`;
104112

105113
return (
106114
<div
@@ -110,7 +118,7 @@ export default function ImageViewer({
110118
onMouseDown={handleMouseDown}
111119
onMouseUp={handleMouseUp}
112120
>
113-
<div className={cx(styles.image)} style={{ transform }}>
121+
<div className={cx(styles.image)} style={{ transform, filter }}>
114122
<ResponsiveImage
115123
contain
116124
noShadow
@@ -125,4 +133,4 @@ export default function ImageViewer({
125133
);
126134
}
127135

128-
export { ZoomControls, RotateControls };
136+
export { FilterControls, ZoomControls, RotateControls };

packages/core/src/components/ImageViewer/story.tsx

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import React, { useState } from 'react';
22
import space from ':storybook/images/space.jpg';
3-
import ImageViewer, { ZoomControls, RotateControls } from '.';
4-
import Row from '../Row';
3+
import ImageViewer, { FilterControls, ZoomControls, RotateControls } from '.';
4+
import useStyles, { StyleSheet } from '../../hooks/useStyles';
55

66
type ImageViewerDemoProps = {
77
width?: string;
88
height?: string;
99
controlsBottom?: boolean;
1010
};
1111

12+
const styleSheet: StyleSheet = () => ({
13+
controls: {
14+
display: 'flex',
15+
},
16+
});
17+
1218
function ImageViewerDemo({ width, height, controlsBottom }: ImageViewerDemoProps) {
19+
const [styles, cx] = useStyles(styleSheet);
20+
const [brightness, setBrightness] = useState(1);
21+
const [contrast, setContrast] = useState(1);
1322
const [scale, setScale] = useState(1);
1423
const [rotation, setRotation] = useState(0);
1524

@@ -20,31 +29,42 @@ function ImageViewerDemo({ width, height, controlsBottom }: ImageViewerDemoProps
2029
scale={scale}
2130
src={space}
2231
rotation={rotation}
32+
brightness={brightness}
33+
contrast={contrast}
2334
height={height}
2435
width={width}
2536
/>
26-
<Row
27-
before={
28-
<RotateControls rotation={rotation} onRotation={(value: number) => setRotation(value)} />
29-
}
30-
>
37+
<div className={cx(styles.controls)}>
38+
<FilterControls
39+
dropdownAbove
40+
brightness={brightness}
41+
contrast={contrast}
42+
onBrightnessChange={(value: number) => setBrightness(value)}
43+
onContrastChange={(value: number) => setContrast(value)}
44+
/>
45+
<RotateControls rotation={rotation} onRotation={(value: number) => setRotation(value)} />
3146
<ZoomControls dropdownAbove scale={scale} onScale={(value: number) => setScale(value)} />
32-
</Row>
47+
</div>
3348
</>
3449
) : (
3550
<>
36-
<Row
37-
before={
38-
<RotateControls rotation={rotation} onRotation={(value: number) => setRotation(value)} />
39-
}
40-
>
51+
<div className={cx(styles.controls)}>
52+
<FilterControls
53+
brightness={brightness}
54+
contrast={contrast}
55+
onBrightnessChange={(value: number) => setBrightness(value)}
56+
onContrastChange={(value: number) => setContrast(value)}
57+
/>
58+
<RotateControls rotation={rotation} onRotation={(value: number) => setRotation(value)} />
4159
<ZoomControls scale={scale} onScale={(value: number) => setScale(value)} />
42-
</Row>
60+
</div>
4361
<ImageViewer
4462
alt="Testing"
4563
scale={scale}
4664
src={space}
4765
rotation={rotation}
66+
brightness={brightness}
67+
contrast={contrast}
4868
height={height}
4969
width={width}
5070
/>
@@ -55,7 +75,7 @@ function ImageViewerDemo({ width, height, controlsBottom }: ImageViewerDemoProps
5575
export default {
5676
title: 'Core/ImageViewer',
5777
parameters: {
58-
inspectComponents: [ImageViewer, ZoomControls, RotateControls],
78+
inspectComponents: [ImageViewer, FilterControls, ZoomControls, RotateControls],
5979
},
6080
};
6181

0 commit comments

Comments
 (0)