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(ImageViewer): Add FilterControls component to ImageViewer #402

Merged
merged 13 commits into from
Nov 24, 2020
Merged
Show file tree
Hide file tree
Changes from 11 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
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