diff --git a/android/src/main/java/com/horcrux/svg/GroupShadowNode.java b/android/src/main/java/com/horcrux/svg/GroupShadowNode.java index 9dbcbb67..a29221ba 100644 --- a/android/src/main/java/com/horcrux/svg/GroupShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/GroupShadowNode.java @@ -42,6 +42,7 @@ class GroupShadowNode extends RenderableShadowNode { void setupGlyphContext(Canvas canvas) { RectF clipBounds = new RectF(canvas.getClipBounds()); mMatrix.mapRect(clipBounds); + mTransform.mapRect(clipBounds); mGlyphContext = new GlyphContext(mScale, clipBounds.width(), clipBounds.height()); } diff --git a/android/src/main/java/com/horcrux/svg/ImageShadowNode.java b/android/src/main/java/com/horcrux/svg/ImageShadowNode.java index e1b48dec..1028d5a0 100644 --- a/android/src/main/java/com/horcrux/svg/ImageShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/ImageShadowNode.java @@ -28,19 +28,19 @@ import com.facebook.imagepipeline.image.CloseableBitmap; import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; +import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableType; import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.annotations.ReactProp; -import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper; import com.facebook.react.views.imagehelper.ImageSource; +import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import com.facebook.react.bridge.ReadableArray; - /** * Shadow node for virtual Image view */ @@ -127,9 +127,10 @@ class ImageShadowNode extends RenderableShadowNode { } @ReactProp(name = "matrix") - public void setMatrix(@Nullable ReadableArray matrixArray) { - if (matrixArray != null) { - int matrixSize = PropHelper.toMatrixData(matrixArray, sRawMatrix, mScale); + public void setMatrix(Dynamic matrixArray) { + ReadableType type = matrixArray.getType(); + if (!matrixArray.isNull() && type.equals(ReadableType.Array)) { + int matrixSize = PropHelper.toMatrixData(matrixArray.asArray(), sRawMatrix, mScale); if (matrixSize == 6) { if (mMatrix == null) { mMatrix = new Matrix(); diff --git a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java index 8e06f6b5..91607a15 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java @@ -9,18 +9,35 @@ package com.horcrux.svg; +import android.graphics.Matrix; import android.util.SparseArray; +import android.view.View; +import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.LayoutShadowNode; +import com.facebook.react.uimanager.MatrixMathHelper; +import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.TransformHelper; import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.annotations.ReactProp; import javax.annotation.Nullable; +import static com.facebook.react.uimanager.MatrixMathHelper.determinant; +import static com.facebook.react.uimanager.MatrixMathHelper.inverse; +import static com.facebook.react.uimanager.MatrixMathHelper.multiplyVectorByMatrix; +import static com.facebook.react.uimanager.MatrixMathHelper.roundTo3Places; +import static com.facebook.react.uimanager.MatrixMathHelper.transpose; +import static com.facebook.react.uimanager.MatrixMathHelper.v3Combine; +import static com.facebook.react.uimanager.MatrixMathHelper.v3Cross; +import static com.facebook.react.uimanager.MatrixMathHelper.v3Dot; +import static com.facebook.react.uimanager.MatrixMathHelper.v3Length; +import static com.facebook.react.uimanager.MatrixMathHelper.v3Normalize; import static com.horcrux.svg.RenderableShadowNode.CAP_ROUND; import static com.horcrux.svg.RenderableShadowNode.FILL_RULE_NONZERO; import static com.horcrux.svg.RenderableShadowNode.JOIN_ROUND; @@ -53,9 +70,207 @@ class RenderableViewManager extends ViewGroupManager PERSPECTIVE_ARRAY_INVERTED_CAMERA_DISTANCE_INDEX) { + float invertedCameraDistance = (float) perspectiveArray[PERSPECTIVE_ARRAY_INVERTED_CAMERA_DISTANCE_INDEX]; + if (invertedCameraDistance == 0) { + // Default camera distance, before scale multiplier (1280) + invertedCameraDistance = 0.00078125f; + } + float cameraDistance = -1 / invertedCameraDistance; + float scale = DisplayMetricsHolder.getScreenDisplayMetrics().density; + + // The following converts the matrix's perspective to a camera distance + // such that the camera perspective looks the same on Android and iOS + float normalizedCameraDistance = scale * cameraDistance * CAMERA_DISTANCE_NORMALIZATION_MULTIPLIER; + view.setCameraDistance(normalizedCameraDistance); + + } + } + + private static void resetTransformProperty(View view) { + view.setTranslationX(PixelUtil.toPixelFromDIP(0)); + view.setTranslationY(PixelUtil.toPixelFromDIP(0)); + view.setRotation(0); + view.setRotationX(0); + view.setRotationY(0); + view.setScaleX(1); + view.setScaleY(1); + view.setCameraDistance(0); + } static RenderableViewManager createGroupViewManager() { - return new RenderableViewManager<>(CLASS_GROUP); + return new RenderableViewManager(CLASS_GROUP){ + + @ReactProp(name = "transform") + public void setTransform(RenderableView node, ReadableArray matrix) { + + if (matrix == null) { + resetTransformProperty(node); + } else { + setTransformProperty(node, matrix); + Matrix m = node.getMatrix(); + node.shadowNode.mTransform = m; + } + } + }; } static RenderableViewManager createPathViewManager() { @@ -221,11 +436,6 @@ class RenderableViewManager extends ViewGroupManager node, int meetOrSlice) { node.shadowNode.setMeetOrSlice(meetOrSlice); } - - @ReactProp(name = "matrix") - public void setMatrix(RenderableView node, @Nullable ReadableArray matrixArray) { - node.shadowNode.setMatrix(matrixArray); - } }; } @@ -768,6 +978,11 @@ class RenderableViewManager extends ViewGroupManager node, Dynamic matrixArray) { + node.shadowNode.setMatrix(matrixArray); + } + @ReactProp(name = "propList") public void setPropList(RenderableView node, @Nullable ReadableArray propList) { node.shadowNode.setPropList(propList); diff --git a/android/src/main/java/com/horcrux/svg/VirtualNode.java b/android/src/main/java/com/horcrux/svg/VirtualNode.java index f0f22cc9..2e64164d 100644 --- a/android/src/main/java/com/horcrux/svg/VirtualNode.java +++ b/android/src/main/java/com/horcrux/svg/VirtualNode.java @@ -17,7 +17,9 @@ import android.graphics.RectF; import android.graphics.Region; import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableType; import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.LayoutShadowNode; @@ -54,6 +56,7 @@ abstract class VirtualNode extends LayoutShadowNode { }; float mOpacity = 1f; Matrix mMatrix = new Matrix(); + Matrix mTransform = new Matrix(); Matrix mInvMatrix = new Matrix(); boolean mInvertible = true; private RectF mClientRect; @@ -188,6 +191,7 @@ abstract class VirtualNode extends LayoutShadowNode { int saveAndSetupCanvas(Canvas canvas) { int count = canvas.save(); canvas.concat(mMatrix); + canvas.concat(mTransform); return count; } @@ -234,9 +238,10 @@ abstract class VirtualNode extends LayoutShadowNode { } @ReactProp(name = "matrix") - public void setMatrix(@Nullable ReadableArray matrixArray) { - if (matrixArray != null) { - int matrixSize = PropHelper.toMatrixData(matrixArray, sRawMatrix, mScale); + public void setMatrix(Dynamic matrixArray) { + ReadableType type = matrixArray.getType(); + if (!matrixArray.isNull() && type.equals(ReadableType.Array)) { + int matrixSize = PropHelper.toMatrixData(matrixArray.asArray(), sRawMatrix, mScale); if (matrixSize == 6) { if (mMatrix == null) { mMatrix = new Matrix(); @@ -250,6 +255,7 @@ abstract class VirtualNode extends LayoutShadowNode { } else { mMatrix = null; mInvMatrix = null; + mInvertible = false; } super.markUpdated(); diff --git a/ios/Elements/RNSVGGroup.m b/ios/Elements/RNSVGGroup.m index d152e0e2..4df5f549 100644 --- a/ios/Elements/RNSVGGroup.m +++ b/ios/Elements/RNSVGGroup.m @@ -81,6 +81,8 @@ { CGRect clipBounds = CGContextGetClipBoundingBox(context); clipBounds = CGRectApplyAffineTransform(clipBounds, self.matrix); + CGAffineTransform transform = CATransform3DGetAffineTransform(self.layer.transform); + clipBounds = CGRectApplyAffineTransform(clipBounds, transform); CGFloat width = CGRectGetWidth(clipBounds); CGFloat height = CGRectGetHeight(clipBounds); diff --git a/ios/RNSVGRenderable.m b/ios/RNSVGRenderable.m index 74161f5e..6d390c71 100644 --- a/ios/RNSVGRenderable.m +++ b/ios/RNSVGRenderable.m @@ -171,6 +171,8 @@ UInt32 saturate(double value) { // This needs to be painted on a layer before being composited. CGContextSaveGState(context); CGContextConcatCTM(context, self.matrix); + CGAffineTransform transform = CATransform3DGetAffineTransform(self.layer.transform); + CGContextConcatCTM(context, transform); CGContextSetAlpha(context, self.opacity); [self beginTransparencyLayer:context]; diff --git a/ios/ViewManagers/RNSVGNodeManager.m b/ios/ViewManagers/RNSVGNodeManager.m index 556f4cc0..fde71717 100644 --- a/ios/ViewManagers/RNSVGNodeManager.m +++ b/ios/ViewManagers/RNSVGNodeManager.m @@ -10,8 +10,134 @@ #import "RNSVGNode.h" +static const NSUInteger kMatrixArrayLength = 4 * 4; + @implementation RNSVGNodeManager ++ (CGFloat)convertToRadians:(id)json +{ + if ([json isKindOfClass:[NSString class]]) { + NSString *stringValue = (NSString *)json; + if ([stringValue hasSuffix:@"deg"]) { + CGFloat degrees = [[stringValue substringToIndex:stringValue.length - 3] floatValue]; + return degrees * M_PI / 180; + } + if ([stringValue hasSuffix:@"rad"]) { + return [[stringValue substringToIndex:stringValue.length - 3] floatValue]; + } + } + return [json floatValue]; +} + ++ (CATransform3D)CATransform3DFromMatrix:(id)json +{ + CATransform3D transform = CATransform3DIdentity; + if (!json) { + return transform; + } + if (![json isKindOfClass:[NSArray class]]) { + RCTLogConvertError(json, @"a CATransform3D. Expected array for transform matrix."); + return transform; + } + if ([json count] != kMatrixArrayLength) { + RCTLogConvertError(json, @"a CATransform3D. Expected 4x4 matrix array."); + return transform; + } + for (NSUInteger i = 0; i < kMatrixArrayLength; i++) { + ((CGFloat *)&transform)[i] = [RCTConvert CGFloat:json[i]]; + } + return transform; +} + ++ (CATransform3D)CATransform3D:(id)json +{ + CATransform3D transform = CATransform3DIdentity; + if (!json) { + return transform; + } + if (![json isKindOfClass:[NSArray class]]) { + RCTLogConvertError(json, @"a CATransform3D. Did you pass something other than an array?"); + return transform; + } + // legacy matrix support + if ([(NSArray *)json count] == kMatrixArrayLength && [json[0] isKindOfClass:[NSNumber class]]) { + RCTLogWarn(@"[RCTConvert CATransform3D:] has deprecated a matrix as input. Pass an array of configs (which can contain a matrix key) instead."); + return [self CATransform3DFromMatrix:json]; + } + + CGFloat zeroScaleThreshold = FLT_EPSILON; + + for (NSDictionary *transformConfig in (NSArray *)json) { + if (transformConfig.count != 1) { + RCTLogConvertError(json, @"a CATransform3D. You must specify exactly one property per transform object."); + return transform; + } + NSString *property = transformConfig.allKeys[0]; + id value = transformConfig[property]; + + if ([property isEqualToString:@"matrix"]) { + transform = [self CATransform3DFromMatrix:value]; + + } else if ([property isEqualToString:@"perspective"]) { + transform.m34 = -1 / [value floatValue]; + + } else if ([property isEqualToString:@"rotateX"]) { + CGFloat rotate = [self convertToRadians:value]; + transform = CATransform3DRotate(transform, rotate, 1, 0, 0); + + } else if ([property isEqualToString:@"rotateY"]) { + CGFloat rotate = [self convertToRadians:value]; + transform = CATransform3DRotate(transform, rotate, 0, 1, 0); + + } else if ([property isEqualToString:@"rotate"] || [property isEqualToString:@"rotateZ"]) { + CGFloat rotate = [self convertToRadians:value]; + transform = CATransform3DRotate(transform, rotate, 0, 0, 1); + + } else if ([property isEqualToString:@"scale"]) { + CGFloat scale = [value floatValue]; + scale = ABS(scale) < zeroScaleThreshold ? zeroScaleThreshold : scale; + transform = CATransform3DScale(transform, scale, scale, 1); + + } else if ([property isEqualToString:@"scaleX"]) { + CGFloat scale = [value floatValue]; + scale = ABS(scale) < zeroScaleThreshold ? zeroScaleThreshold : scale; + transform = CATransform3DScale(transform, scale, 1, 1); + + } else if ([property isEqualToString:@"scaleY"]) { + CGFloat scale = [value floatValue]; + scale = ABS(scale) < zeroScaleThreshold ? zeroScaleThreshold : scale; + transform = CATransform3DScale(transform, 1, scale, 1); + + } else if ([property isEqualToString:@"translate"]) { + NSArray *array = (NSArray *)value; + CGFloat translateX = [array[0] floatValue]; + CGFloat translateY = [array[1] floatValue]; + CGFloat translateZ = array.count > 2 ? [array[2] floatValue] : 0; + transform = CATransform3DTranslate(transform, translateX, translateY, translateZ); + + } else if ([property isEqualToString:@"translateX"]) { + CGFloat translate = [value floatValue]; + transform = CATransform3DTranslate(transform, translate, 0, 0); + + } else if ([property isEqualToString:@"translateY"]) { + CGFloat translate = [value floatValue]; + transform = CATransform3DTranslate(transform, 0, translate, 0); + + } else if ([property isEqualToString:@"skewX"]) { + CGFloat skew = [self convertToRadians:value]; + transform.m21 = tanf(skew); + + } else if ([property isEqualToString:@"skewY"]) { + CGFloat skew = [self convertToRadians:value]; + transform.m12 = tanf(skew); + + } else { + RCTLogError(@"Unsupported transform type for a CATransform3D: %@.", property); + } + } + return transform; +} + RCT_EXPORT_MODULE() - (RNSVGNode *)node @@ -27,6 +153,13 @@ RCT_EXPORT_MODULE() RCT_EXPORT_VIEW_PROPERTY(name, NSString) RCT_EXPORT_VIEW_PROPERTY(opacity, CGFloat) RCT_EXPORT_VIEW_PROPERTY(matrix, CGAffineTransform) +RCT_CUSTOM_VIEW_PROPERTY(transform, CATransform3D, RNSVGNode) +{ + view.layer.transform = json ? [RNSVGNodeManager CATransform3D:json] : defaultView.layer.transform; + // TODO: Improve this by enabling edge antialiasing only for transforms with rotation or skewing + view.layer.allowsEdgeAntialiasing = !CATransform3DIsIdentity(view.layer.transform); + [view invalidate]; +} RCT_EXPORT_VIEW_PROPERTY(mask, NSString) RCT_EXPORT_VIEW_PROPERTY(clipPath, NSString) RCT_EXPORT_VIEW_PROPERTY(clipRule, RNSVGCGFCRule)