diff --git a/android/src/main/java/com/reactnativecommunity/art/ARTShapeShadowNode.java b/android/src/main/java/com/reactnativecommunity/art/ARTShapeShadowNode.java index 5da3828..cf57bbb 100644 --- a/android/src/main/java/com/reactnativecommunity/art/ARTShapeShadowNode.java +++ b/android/src/main/java/com/reactnativecommunity/art/ARTShapeShadowNode.java @@ -167,6 +167,9 @@ protected boolean setupStrokePaint(Paint paint, float opacity) { if (mStrokeDash != null && mStrokeDash.length > 0) { paint.setPathEffect(new DashPathEffect(mStrokeDash, 0)); } + if (mShadowOpacity > 0) { + paint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mShadowColor); + } return true; } @@ -231,6 +234,9 @@ protected boolean setupFillPaint(Paint paint, float opacity) { default: FLog.w(ReactConstants.TAG, "ART: Color type " + colorType + " not supported!"); } + if (mShadowOpacity > 0) { + paint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mShadowColor); + } return true; } return false; diff --git a/android/src/main/java/com/reactnativecommunity/art/ARTTextShadowNode.java b/android/src/main/java/com/reactnativecommunity/art/ARTTextShadowNode.java index bd79fb6..55a4b20 100644 --- a/android/src/main/java/com/reactnativecommunity/art/ARTTextShadowNode.java +++ b/android/src/main/java/com/reactnativecommunity/art/ARTTextShadowNode.java @@ -92,6 +92,9 @@ public void draw(Canvas canvas, Paint paint, float opacity) { canvas.drawTextOnPath(text, mPath, 0, 0, paint); } } + if (mShadowOpacity > 0) { + paint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mShadowColor); + } restoreCanvas(canvas); markUpdateSeen(); } diff --git a/android/src/main/java/com/reactnativecommunity/art/ARTVirtualNode.java b/android/src/main/java/com/reactnativecommunity/art/ARTVirtualNode.java index f3cf624..d2b4018 100644 --- a/android/src/main/java/com/reactnativecommunity/art/ARTVirtualNode.java +++ b/android/src/main/java/com/reactnativecommunity/art/ARTVirtualNode.java @@ -8,8 +8,11 @@ package com.reactnativecommunity.art; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; +import android.support.v4.graphics.ColorUtils; + import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.uimanager.DisplayMetricsHolder; @@ -30,6 +33,11 @@ public abstract class ARTVirtualNode extends ReactShadowNodeImpl { protected float mOpacity = 1f; private @Nullable Matrix mMatrix = new Matrix(); + protected int mShadowColor = 0; + protected float mShadowOpacity = 1; + protected float mShadowRadius = 0; + protected float mShadowOffsetX = 0; + protected float mShadowOffsetY = 0; protected final float mScale; @@ -91,6 +99,32 @@ public void setTransform(@Nullable ReadableArray transformArray) { markUpdated(); } + @ReactProp(name = "shadow") + public void setShadow(@Nullable ReadableArray shadowArray) { + if (shadowArray != null) { + mShadowOpacity = (float)shadowArray.getDouble(1); + mShadowRadius = (float)shadowArray.getDouble(2); + mShadowOffsetX = (float)shadowArray.getDouble(3); + mShadowOffsetY = (float)shadowArray.getDouble(4); + + int color = shadowArray.getInt(0); + + if (mShadowOpacity < 1) { + color = ColorUtils.setAlphaComponent(color, (int)(mShadowOpacity * 255)); + } + + mShadowColor = color; + + } else { + mShadowColor = 0; + mShadowOpacity = 0; + mShadowRadius = 0; + mShadowOffsetX = 0; + mShadowOffsetY = 0; + } + markUpdated(); + } + protected void setupMatrix() { sRawMatrix[0] = sMatrixData[0]; sRawMatrix[1] = sMatrixData[2]; diff --git a/docs/api.md b/docs/api.md index cd85e3a..85abba0 100644 --- a/docs/api.md +++ b/docs/api.md @@ -31,6 +31,7 @@ Container to combine shapes or other groups into hierarchies that can be transfo | :---------------: | :-----------------------------------: | :-----: | | ...opacityProps | [`OpacityProps`](###OpacityProps) | --- | | ...transformProps | [`TransformProps`](###TransformProps) | --- | +| ...shadowProps | [`ShadowProps`](###ShadowProps) | --- | | children | `React.Node` | --- | ```jsx @@ -53,7 +54,8 @@ Used to draw arbitrary vector shapes from Path. Shape implements Transform as a | :---------------: | :-----------------------------------: | :-------: | | ...opacityProps | [`OpacityProps`](###OpacityProps) | --- | | ...transformProps | [`TransformProps`](###TransformProps) | --- | -| fill | `string \| Brush` | --- | +| ...shadowProps | [`ShadowProps`](###ShadowProps) | --- | +| fill | `string \| Brush` | --- | | stroke | `string` | --- | | strokeCap | `'butt' \| 'square' \| 'round'` | `'round'` | | strokeDash | `Array` | --- | @@ -84,7 +86,8 @@ Text component creates a shape based on text content using native text rendering | :---------------: | :-----------------------------------: | :-------: | | ...opacityProps | [`OpacityProps`](###OpacityProps) | --- | | ...transformProps | [`TransformProps`](###TransformProps) | --- | -| fill | `string \| Brush` | --- | +| ...shadowProps | [`ShadowProps`](###ShadowProps) | --- | +| fill | `string \| Brush` | --- | | stroke | `string` | --- | | strokeCap | `'butt' \| 'square' \| 'round'` | `'round'` | | strokeDash | `Array` | --- | @@ -354,6 +357,22 @@ function Component() { | originY | `number` | --- | | transform | `TransformObject` | --- | +### ShadowProps + +| Prop | Type | Default | +| :-----------: | :------------------: | :------: | +| shadowOpacity | `number` | `1` | +| shadowColor | `string` | `black` | +| shadowRadius | `number` | `0` | +| shadowOffset | `ShadowOffsetObject` | --- | + +### ShadowOffsetObject + +| Prop | Type | Default | +| :--: | :------: | :-----: | +| y | `number` | `0` | +| x | `number` | `0` | + ### Font | Prop | Type | Default | diff --git a/example/components/CustomText.js b/example/components/CustomText.js index 7d1b6de..78cf264 100644 --- a/example/components/CustomText.js +++ b/example/components/CustomText.js @@ -2,6 +2,13 @@ import React from 'react'; import {Dimensions, StyleSheet} from 'react-native'; import {Surface, Text, Group} from '@react-native-community/art'; +const SHADOW = { + shadowOpacity: 0.5, + shadowColor: 'blue', + shadowRadius: 10, + shadowOffset: {x: 4, y: 4}, +}; + export default function CustomText() { const surfaceWidth = Dimensions.get('window').width; const surfaceHeight = surfaceWidth / 3; @@ -9,7 +16,10 @@ export default function CustomText() { return ( - + React Native Community diff --git a/example/components/Heart.js b/example/components/Heart.js index e9321c3..5e4667c 100644 --- a/example/components/Heart.js +++ b/example/components/Heart.js @@ -11,6 +11,13 @@ import { const HEART_SHAPE = 'M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z'; +const SHADOW = { + shadowOpacity: 1, + shadowColor: 'red', + shadowRadius: 8, + shadowOffset: {x: 0, y: 0}, +}; + export default function Heart() { const surfaceDimensions = Dimensions.get('window').width; const gradient = new RadialGradient( @@ -42,6 +49,7 @@ export default function Heart() { stroke={'#00ff00'} fill={gradient} visible={true} + {...SHADOW} /> diff --git a/index.d.ts b/index.d.ts index 7811e18..98ed7e0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -13,6 +13,13 @@ declare module '@react-native-community/art' { x?: number; y?: number; visible?: boolean; + shadowOpacity?: number; + shadowColor?: string | number; + shadowRadius?: number; + shadowOffset?: { + x: number, + y: number, + } } export interface ARTGroupProps extends ARTNodeMixin { diff --git a/ios/ART.xcodeproj/project.pbxproj b/ios/ART.xcodeproj/project.pbxproj index 2aec17b..732f450 100644 --- a/ios/ART.xcodeproj/project.pbxproj +++ b/ios/ART.xcodeproj/project.pbxproj @@ -67,6 +67,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 09966EAF23996E3900E9C452 /* ARTShadow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARTShadow.h; sourceTree = ""; }; 0CF68AC11AF0540F00FF9E5C /* libART.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libART.a; sourceTree = BUILT_PRODUCTS_DIR; }; 0CF68ADB1AF0549300FF9E5C /* ARTCGFloatArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTCGFloatArray.h; sourceTree = ""; }; 0CF68ADC1AF0549300FF9E5C /* ARTContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTContainer.h; sourceTree = ""; }; @@ -131,6 +132,7 @@ 0CF68AB81AF0540F00FF9E5C = { isa = PBXGroup; children = ( + 09966EAF23996E3900E9C452 /* ARTShadow.h */, 0CF68AEA1AF0549300FF9E5C /* Brushes */, 0CF68AF81AF0549300FF9E5C /* ViewManagers */, 0CF68ADB1AF0549300FF9E5C /* ARTCGFloatArray.h */, @@ -261,6 +263,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 0CF68AB81AF0540F00FF9E5C; diff --git a/ios/ARTNode.h b/ios/ARTNode.h index 9f38111..8c6a7a6 100644 --- a/ios/ARTNode.h +++ b/ios/ARTNode.h @@ -6,7 +6,7 @@ */ #import - +#import "ARTShadow.h" /** * ART nodes are implemented as empty UIViews but this is just an implementation detail to fit * into the existing view management. They should also be shadow views and painted on a background @@ -16,6 +16,7 @@ @interface ARTNode : UIView @property (nonatomic, assign) CGFloat opacity; +@property (nonatomic, assign) ARTShadow shadow; - (void)invalidate; - (void)renderTo:(CGContextRef)context; diff --git a/ios/ARTNode.m b/ios/ARTNode.m index ae1a152..b75a4ee 100644 --- a/ios/ARTNode.m +++ b/ios/ARTNode.m @@ -41,6 +41,12 @@ - (void)setTransform:(CGAffineTransform)transform super.transform = transform; } +- (void)setShadow:(ARTShadow)shadow +{ + [self invalidate]; + _shadow = shadow; +} + - (void)invalidate { id container = (id)self.superview; @@ -55,23 +61,27 @@ - (void)renderTo:(CGContextRef)context } if (self.opacity >= 1) { // Just paint at full opacity - CGContextSaveGState(context); - CGContextConcatCTM(context, self.transform); - CGContextSetAlpha(context, 1); + [self renderContentTo:context]; [self renderLayerTo:context]; CGContextRestoreGState(context); return; } + // This needs to be painted on a layer before being composited. - CGContextSaveGState(context); - CGContextConcatCTM(context, self.transform); - CGContextSetAlpha(context, self.opacity); + [self renderContentTo:context]; CGContextBeginTransparencyLayer(context, NULL); [self renderLayerTo:context]; CGContextEndTransparencyLayer(context); CGContextRestoreGState(context); } +- (void)renderContentTo:(CGContextRef)context { + CGContextSaveGState(context); + CGContextConcatCTM(context, self.transform); + CGContextSetAlpha(context, self.opacity); + CGContextSetShadowWithColor(context, self.shadow.offset, self.shadow.blur, self.shadow.color.CGColor); +} + - (void)renderLayerTo:(CGContextRef)context { // abstract diff --git a/ios/ARTShadow.h b/ios/ARTShadow.h new file mode 100644 index 0000000..2aa5b48 --- /dev/null +++ b/ios/ARTShadow.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +typedef struct { + CGSize offset; + CGFloat blur; + UIColor* color; +} ARTShadow; diff --git a/ios/RCTConvert+ART.h b/ios/RCTConvert+ART.h index 5fe3b4e..74b35df 100644 --- a/ios/RCTConvert+ART.h +++ b/ios/RCTConvert+ART.h @@ -10,6 +10,7 @@ #import #import "ARTBrush.h" +#import "ARTShadow.h" #import "ARTCGFloatArray.h" #import "ARTTextFrame.h" @@ -20,6 +21,7 @@ + (ARTTextFrame)ARTTextFrame:(id)json; + (ARTCGFloatArray)ARTCGFloatArray:(id)json; + (ARTBrush *)ARTBrush:(id)json; ++ (ARTShadow)ARTShadow:(id)json; + (CGPoint)CGPoint:(id)json offset:(NSUInteger)offset; + (CGRect)CGRect:(id)json offset:(NSUInteger)offset; diff --git a/ios/RCTConvert+ART.m b/ios/RCTConvert+ART.m index 3312e31..cbcfdcd 100644 --- a/ios/RCTConvert+ART.m +++ b/ios/RCTConvert+ART.m @@ -161,6 +161,23 @@ + (ARTBrush *)ARTBrush:(id)json } } ++ (ARTShadow)ARTShadow:(id)json +{ + NSArray *arr = [self NSArray:json]; + + UIColor *color = [UIColor colorWithCGColor:[self CGColor:[arr objectAtIndex:0]]]; + color = [color colorWithAlphaComponent:[[arr objectAtIndex:1] floatValue]]; + + return (ARTShadow){ + .color = color, + .blur = [[arr objectAtIndex:2] floatValue], + .offset = (CGSize){ + .width = [[arr objectAtIndex:3] floatValue], + .height = [[arr objectAtIndex:4] floatValue] + }, + }; +} + + (CGPoint)CGPoint:(id)json offset:(NSUInteger)offset { NSArray *arr = [self NSArray:json]; diff --git a/ios/ViewManagers/ARTNodeManager.m b/ios/ViewManagers/ARTNodeManager.m index 6268097..c0c745d 100644 --- a/ios/ViewManagers/ARTNodeManager.m +++ b/ios/ViewManagers/ARTNodeManager.m @@ -30,5 +30,6 @@ - (RCTShadowView *)shadowView RCT_EXPORT_VIEW_PROPERTY(opacity, CGFloat) RCT_EXPORT_VIEW_PROPERTY(transform, CGAffineTransform) +RCT_EXPORT_VIEW_PROPERTY(shadow, ARTShadow) @end diff --git a/lib/ClippingRectangle.js b/lib/ClippingRectangle.js index 9f6b090..39757ee 100644 --- a/lib/ClippingRectangle.js +++ b/lib/ClippingRectangle.js @@ -9,17 +9,19 @@ import * as React from 'react'; import merge from 'react-native/Libraries/vendor/core/merge'; -import {extractOpacity, extractTransform} from './helpers'; +import {extractOpacity, extractTransform, extractShadow} from './helpers'; import {NativeGroup} from './nativeComponents'; -import type {OpacityProps} from './types'; +import type {OpacityProps, TransformProps, ShadowProps} from './types'; -type ClippingRectangleProps = OpacityProps & { - x: number, - y: number, - width: number, - height: number, - children?: React.Node, -}; +type ClippingRectangleProps = OpacityProps & + TransformProps & + ShadowProps & { + x: number, + y: number, + width: number, + height: number, + children?: React.Node, + }; export default class ClippingRectangle extends React.Component { static defaultProps = { @@ -44,7 +46,8 @@ export default class ClippingRectangle extends React.Component + transform={extractTransform(propsExcludingXAndY)} + shadow={extractShadow(this.props)}> {this.props.children} ); diff --git a/lib/Group.js b/lib/Group.js index 24d32dc..a4bb485 100644 --- a/lib/Group.js +++ b/lib/Group.js @@ -11,10 +11,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import invariant from 'invariant'; import {NativeGroup} from './nativeComponents'; -import {extractOpacity, extractTransform} from './helpers'; -import type {OpacityProps, TransformProps} from './types'; +import {extractOpacity, extractTransform, extractShadow} from './helpers'; +import type {OpacityProps, TransformProps, ShadowProps} from './types'; type GroupProps = OpacityProps & + ShadowProps & TransformProps & { children: React.Node, }; @@ -33,7 +34,8 @@ export default class Group extends React.Component { return ( + transform={extractTransform(this.props)} + shadow={extractShadow(this.props)}> {this.props.children} ); diff --git a/lib/Shape.js b/lib/Shape.js index fe2acea..a7c3dc1 100644 --- a/lib/Shape.js +++ b/lib/Shape.js @@ -12,6 +12,7 @@ import {NativeShape} from './nativeComponents'; import Path from './ARTSerializablePath'; import { extractTransform, + extractShadow, extractOpacity, childrenAsString, extractColor, @@ -21,6 +22,7 @@ import { } from './helpers'; import type { TransformProps, + ShadowProps, OpacityProps, StrokeJoin, StrokeCap, @@ -28,6 +30,7 @@ import type { } from './types'; export type ShapeProps = TransformProps & + ShadowProps & OpacityProps & { fill?: string | Brush, stroke?: string, @@ -64,6 +67,7 @@ export default class Shape extends React.Component { strokeJoin={extractStrokeJoin(props.strokeJoin)} strokeWidth={props.strokeWidth} transform={extractTransform(props)} + shadow={extractShadow(this.props)} d={d} /> ); diff --git a/lib/Text.js b/lib/Text.js index 55ee318..5615a71 100644 --- a/lib/Text.js +++ b/lib/Text.js @@ -17,12 +17,14 @@ import { extractStrokeCap, extractStrokeJoin, extractTransform, + extractShadow, extractAlignment, childrenAsString, extractFontAndLines, } from './helpers'; import type { TransformProps, + ShadowProps, OpacityProps, Alignment, Brush, @@ -32,6 +34,7 @@ import type { } from './types'; export type TextProps = TransformProps & + ShadowProps & OpacityProps & { fill?: string | Brush, stroke?: string, @@ -75,6 +78,7 @@ export default class Text extends React.Component { strokeWidth={props.strokeWidth} transform={extractTransform(props)} alignment={extractAlignment(props.alignment)} + shadow={extractShadow(this.props)} frame={textFrame} path={textPath} /> diff --git a/lib/helpers.js b/lib/helpers.js index 8514900..4b30788 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -7,6 +7,7 @@ * @flow */ +import processColor from 'react-native/Libraries/StyleSheet/processColor'; import Color from 'art/core/color'; import Transform from 'art/core/transform'; import {Platform} from 'react-native'; @@ -20,6 +21,7 @@ import type { StrokeCap, StrokeJoin, TransformProps, + ShadowProps, } from './types'; export function childrenAsString(children?: string | Array) { @@ -89,6 +91,33 @@ function toHex(color: Color) { return '#' + hexValues.join(''); } +export function extractShadow( + props: ShadowProps, +): Array | void { + if ( + !props.shadowColor && + !props.shadowOpacity && + !props.shadowRadius && + !props.shadowOffset + ) { + return; + } + + let opacity = props.shadowOpacity; + + if (opacity === null || opacity === undefined) { + opacity = 1; + } + + return [ + processColor(props.shadowColor || 'black'), + opacity, + props.shadowRadius || 4, + props.shadowOffset?.x || 0, + props.shadowOffset?.y || 0, + ]; +} + export function extractColor(color?: ColorType) { if (color == null) { return null; diff --git a/lib/types.js b/lib/types.js index 33692a0..856de81 100644 --- a/lib/types.js +++ b/lib/types.js @@ -31,6 +31,13 @@ export type TransformProps = { }, }; +export type ShadowProps = { + shadowOpacity?: number, + shadowColor?: string | number, + shadowRadius?: number, + shadowOffset?: {x: number, y: number}, +}; + export type ARTColor = { isColor: true, red: string,