Skip to content

Commit

Permalink
[add] Image support for blurRadius, tintColor, and shadows
Browse files Browse the repository at this point in the history
Use CSS filters to implement React Native image styles.

Ref #362
Ref #548
  • Loading branch information
necolas committed Jun 3, 2018
1 parent d74b2d0 commit 829fb8e
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ const ImageStylePropTypes = {
backgroundColor: ColorPropType,
opacity: number,
resizeMode: oneOf(Object.keys(ImageResizeMode)),
tintColor: ColorPropType,
/**
* @platform unsupported
*/
overlayColor: string,
tintColor: ColorPropType,
/**
* @platform web
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`components/Image prop "blurRadius" 1`] = `"blur(5px)"`;

exports[`components/Image prop "defaultSource" sets background image when value is a string 1`] = `"url(\\"https://google.com/favicon.ico\\")"`;

exports[`components/Image prop "defaultSource" sets background image when value is an object 1`] = `"url(\\"https://google.com/favicon.ico\\")"`;
Expand All @@ -14,4 +16,6 @@ exports[`components/Image prop "resizeMode" value "stretch" 1`] = `"100% 100%"`;

exports[`components/Image prop "resizeMode" value "undefined" 1`] = `"cover"`;

exports[`components/Image prop "style" correctly supports "resizeMode" property 1`] = `"contain"`;
exports[`components/Image prop "style" supports "resizeMode" property 1`] = `"contain"`;

exports[`components/Image prop "style" supports "shadow" properties (convert to filter) 1`] = `"drop-shadow(1px 1px 0px rgba(255,0,0,1.00))"`;
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ describe('components/Image', () => {
expect(component.prop('accessible')).toBe(false);
});

test('prop "blurRadius"', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' };
const component = shallow(<Image blurRadius={5} defaultSource={defaultSource} />);
expect(findImageSurfaceStyle(component).filter).toMatchSnapshot();
});

describe('prop "defaultSource"', () => {
test('sets background image when value is an object', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' };
Expand Down Expand Up @@ -207,11 +213,29 @@ describe('components/Image', () => {
});

describe('prop "style"', () => {
test('correctly supports "resizeMode" property', () => {
test('supports "resizeMode" property', () => {
const component = shallow(<Image style={{ resizeMode: Image.resizeMode.contain }} />);
expect(findImageSurfaceStyle(component).backgroundSize).toMatchSnapshot();
});

test('supports "shadow" properties (convert to filter)', () => {
const component = shallow(
<Image style={{ shadowColor: 'red', shadowOffset: { width: 1, height: 1 } }} />
);
expect(findImageSurfaceStyle(component).filter).toMatchSnapshot();
});

test('supports "tintcolor" property (convert to filter)', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' };
const component = shallow(
<Image defaultSource={defaultSource} style={{ tintColor: 'red' }} />
);
// filter
expect(findImageSurfaceStyle(component).filter).toContain('url(#tint-');
// svg
expect(component.childAt(2).type()).toBe('svg');
});

test('removes other unsupported View styles', () => {
const component = shallow(<Image style={{ overlayColor: 'red', tintColor: 'blue' }} />);
expect(component.props().style.overlayColor).toBeUndefined();
Expand Down
55 changes: 50 additions & 5 deletions packages/react-native-web/src/exports/Image/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import applyNativeMethods from '../../modules/applyNativeMethods';
import createElement from '../createElement';
import { getAssetByID } from '../../modules/AssetRegistry';
import resolveShadowValue from '../StyleSheet/resolveShadowValue';
import ImageLoader from '../../modules/ImageLoader';
import ImageResizeMode from './ImageResizeMode';
import ImageSourcePropType from './ImageSourcePropType';
Expand Down Expand Up @@ -73,6 +74,20 @@ const resolveAssetUri = source => {
return uri;
};

let filterId = 0;

const createTintColorSVG = (tintColor, id) =>
tintColor && id != null ? (
<svg style={{ position: 'absolute', height: 0, visibility: 'hidden', width: 0 }}>
<defs>
<filter id={`tint-${id}`}>
<feFlood floodColor={`${tintColor}`} />
<feComposite in2="SourceAlpha" operator="atop" />
</filter>
</defs>
</svg>
) : null;

type State = {
shouldDisplaySource: boolean
};
Expand All @@ -86,6 +101,7 @@ class Image extends Component<*, State> {

static propTypes = {
...ViewPropTypes,
blurRadius: number,
defaultSource: ImageSourcePropType,
draggable: bool,
onError: func,
Expand All @@ -98,7 +114,6 @@ class Image extends Component<*, State> {
style: StyleSheetPropType(ImageStylePropTypes),
// compatibility with React Native
/* eslint-disable react/sort-prop-types */
blurRadius: number,
capInsets: shape({ top: number, left: number, bottom: number, right: number }),
resizeMethod: oneOf(['auto', 'resize', 'scale'])
/* eslint-enable react/sort-prop-types */
Expand All @@ -118,6 +133,7 @@ class Image extends Component<*, State> {

static resizeMode = ImageResizeMode;

_filterId = 0;
_imageRef = null;
_imageRequestId = null;
_imageState = null;
Expand All @@ -130,6 +146,8 @@ class Image extends Component<*, State> {
const shouldDisplaySource = ImageUriCache.has(uri);
this.state = { shouldDisplaySource };
this._imageState = getImageState(uri, shouldDisplaySource);
this._filterId = filterId;
filterId++;
}

componentDidMount() {
Expand Down Expand Up @@ -170,13 +188,13 @@ class Image extends Component<*, State> {
const {
accessibilityLabel,
accessible,
blurRadius,
defaultSource,
draggable,
onLayout,
source,
testID,
/* eslint-disable */
blurRadius,
capInsets,
onError,
onLoad,
Expand Down Expand Up @@ -206,10 +224,35 @@ class Image extends Component<*, State> {
const backgroundImage = displayImageUri ? `url("${displayImageUri}")` : null;
const flatStyle = { ...StyleSheet.flatten(this.props.style) };
const finalResizeMode = resizeMode || flatStyle.resizeMode || ImageResizeMode.cover;
// View doesn't support these styles

// CSS filters
const filters = [];
const tintColor = flatStyle.tintColor;
if (flatStyle.filter) {
filters.push(flatStyle.filter);
}
if (blurRadius) {
filters.push(`blur(${blurRadius}px)`);
}
if (flatStyle.shadowOffset) {
const shadowString = resolveShadowValue(flatStyle);
if (shadowString) {
filters.push(`drop-shadow(${shadowString})`);
}
}
if (flatStyle.tintColor) {
filters.push(`url(#tint-${this._filterId})`);
}

// these styles were converted to filters
delete flatStyle.shadowColor;
delete flatStyle.shadowOpacity;
delete flatStyle.shadowOffset;
delete flatStyle.shadowRadius;
delete flatStyle.tintColor;
// these styles are not supported on View
delete flatStyle.overlayColor;
delete flatStyle.resizeMode;
delete flatStyle.tintColor;

// Accessibility image allows users to trigger the browser's image context menu
const hiddenImage = displayImageUri
Expand Down Expand Up @@ -240,10 +283,12 @@ class Image extends Component<*, State> {
style={[
styles.image,
resizeModeStyles[finalResizeMode],
backgroundImage && { backgroundImage }
backgroundImage && { backgroundImage },
filters.length > 0 && { filter: filters.join(' ') }
]}
/>
{hiddenImage}
{createTintColorSVG(tintColor, this._filterId)}
</View>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import normalizeColor from '../../modules/normalizeColor';
import normalizeValue from './normalizeValue';
import resolveShadowValue from './resolveShadowValue';

/**
* The browser implements the CSS cascade, where the order of properties is a
Expand Down Expand Up @@ -82,16 +83,9 @@ const defaultOffset = { height: 0, width: 0 };
* Shadow
*/

// TODO: add inset and spread support
const resolveShadow = (resolvedStyle, style) => {
const { boxShadow, shadowColor, shadowOffset, shadowOpacity, shadowRadius } = style;
const { height, width } = shadowOffset || defaultOffset;
const offsetX = normalizeValue(null, width);
const offsetY = normalizeValue(null, height);
const blurRadius = normalizeValue(null, shadowRadius || 0);
const color = normalizeColor(shadowColor || 'black', shadowOpacity);

const shadow = `${offsetX} ${offsetY} ${blurRadius} ${color}`;
const { boxShadow } = style;
const shadow = resolveShadowValue(style);
resolvedStyle.boxShadow = boxShadow ? `${boxShadow}, ${shadow}` : shadow;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2018-present, Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import normalizeColor from '../../modules/normalizeColor';
import normalizeValue from './normalizeValue';

const defaultOffset = { height: 0, width: 0 };

const resolveShadowValue = (style: Object) => {
const { shadowColor, shadowOffset, shadowOpacity, shadowRadius } = style;
const { height, width } = shadowOffset || defaultOffset;
const offsetX = normalizeValue(null, width);
const offsetY = normalizeValue(null, height);
const blurRadius = normalizeValue(null, shadowRadius || 0);
const color = normalizeColor(shadowColor || 'black', shadowOpacity);
if (color) {
return `${offsetX} ${offsetY} ${blurRadius} ${color}`;
}
};

export default resolveShadowValue;
26 changes: 25 additions & 1 deletion packages/website/storybook/1-components/Image/ImageScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import UIExplorer, {
Description,
DocItem,
Section,
StyleList,
storiesOf
} from '../../ui-explorer';

Expand All @@ -34,6 +35,12 @@ const ImageScreen = () => (
<Section title="Props">
<DocItem name="...View props" />

<DocItem
name="blurRadius"
typeInfo="?number"
description="The blur radius of the blur filter added to the image"
/>

<DocItem
name="defaultSource"
typeInfo="?object"
Expand Down Expand Up @@ -122,7 +129,11 @@ const ImageScreen = () => (
}}
/>

<DocItem name="style" typeInfo="?style" />
<DocItem
name="style"
typeInfo="?style"
description={<StyleList stylePropTypes={stylePropTypes} />}
/>
</Section>

<Section title="Properties">
Expand Down Expand Up @@ -168,4 +179,17 @@ const ImageScreen = () => (
</UIExplorer>
);

const stylePropTypes = [
{
name: '...View#style'
},
{
name: 'resizeMode'
},
{
name: 'tintColor',
typeInfo: 'color'
}
];

storiesOf('Components', module).add('Image', ImageScreen);

0 comments on commit 829fb8e

Please sign in to comment.