diff --git a/android/src/main/java/com/horcrux/svg/GroupShadowNode.java b/android/src/main/java/com/horcrux/svg/GroupShadowNode.java index 8c230d84..d2e120cf 100644 --- a/android/src/main/java/com/horcrux/svg/GroupShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/GroupShadowNode.java @@ -84,7 +84,7 @@ class GroupShadowNode extends RenderableShadowNode { } int count = node.saveAndSetupCanvas(canvas); - node.draw(canvas, paint, opacity * mOpacity); + node.render(canvas, paint, opacity * mOpacity); RectF r = node.getClientRect(); if (r != null) { groupRect.union(r); diff --git a/android/src/main/java/com/horcrux/svg/MaskShadowNode.java b/android/src/main/java/com/horcrux/svg/MaskShadowNode.java new file mode 100644 index 00000000..ad1eb8ff --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/MaskShadowNode.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2015-present, Horcrux. + * All rights reserved. + * + * This source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + + +package com.horcrux.svg; + +import android.graphics.Matrix; +import android.graphics.RectF; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.uimanager.annotations.ReactProp; + +import javax.annotation.Nullable; + +/** + * Shadow node for virtual Mask definition view + */ +class MaskShadowNode extends GroupShadowNode { + + String mX; + String mY; + String mWidth; + String mHeight; + Brush.BrushUnits mMaskUnits; + Brush.BrushUnits mMaskContentUnits; + + private float mMinX; + private float mMinY; + private float mVbWidth; + private float mVbHeight; + String mAlign; + int mMeetOrSlice; + + private static final float[] sRawMatrix = new float[]{ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + }; + private Matrix mMatrix = null; + + @ReactProp(name = "x") + public void setX(String x) { + mX = x; + markUpdated(); + } + + @ReactProp(name = "y") + public void setY(String y) { + mY = y; + markUpdated(); + } + + @ReactProp(name = "maskwidth") + public void setWidth(String width) { + mWidth = width; + markUpdated(); + } + + @ReactProp(name = "maskheight") + public void setHeight(String height) { + mHeight = height; + markUpdated(); + } + + @ReactProp(name = "maskUnits") + public void setMaskUnits(int maskUnits) { + switch (maskUnits) { + case 0: + mMaskUnits = Brush.BrushUnits.OBJECT_BOUNDING_BOX; + break; + case 1: + mMaskUnits = Brush.BrushUnits.USER_SPACE_ON_USE; + break; + } + markUpdated(); + } + + @ReactProp(name = "maskContentUnits") + public void setMaskContentUnits(int maskContentUnits) { + switch (maskContentUnits) { + case 0: + mMaskContentUnits = Brush.BrushUnits.OBJECT_BOUNDING_BOX; + break; + case 1: + mMaskContentUnits = Brush.BrushUnits.USER_SPACE_ON_USE; + break; + } + markUpdated(); + } + + @ReactProp(name = "maskTransform") + public void setMaskTransform(@Nullable ReadableArray matrixArray) { + if (matrixArray != null) { + int matrixSize = PropHelper.toMatrixData(matrixArray, sRawMatrix, mScale); + if (matrixSize == 6) { + if (mMatrix == null) { + mMatrix = new Matrix(); + } + mMatrix.setValues(sRawMatrix); + } else if (matrixSize != -1) { + FLog.w(ReactConstants.TAG, "RNSVG: Transform matrices must be of size 6"); + } + } else { + mMatrix = null; + } + + markUpdated(); + } + + @ReactProp(name = "minX") + public void setMinX(float minX) { + mMinX = minX; + markUpdated(); + } + + @ReactProp(name = "minY") + public void setMinY(float minY) { + mMinY = minY; + markUpdated(); + } + + @ReactProp(name = "vbWidth") + public void setVbWidth(float vbWidth) { + mVbWidth = vbWidth; + markUpdated(); + } + + @ReactProp(name = "vbHeight") + public void setVbHeight(float vbHeight) { + mVbHeight = vbHeight; + markUpdated(); + } + + @ReactProp(name = "align") + public void setAlign(String align) { + mAlign = align; + markUpdated(); + } + + @ReactProp(name = "meetOrSlice") + public void setMeetOrSlice(int meetOrSlice) { + mMeetOrSlice = meetOrSlice; + markUpdated(); + } + + RectF getViewBox() { + return new RectF(mMinX * mScale, mMinY * mScale, (mMinX + mVbWidth) * mScale, (mMinY + mVbHeight) * mScale); + } + + @Override + protected void saveDefinition() { + if (mName != null) { + SvgViewShadowNode svg = getSvgShadowNode(); + svg.defineMask(this, mName); + } + } +} diff --git a/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java b/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java index 5cba5b4a..2a1c1748 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java @@ -9,25 +9,24 @@ package com.horcrux.svg; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.DashPathEffect; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; -import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.JavaOnlyArray; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableType; -import com.facebook.react.bridge.WritableArray; -import com.facebook.react.uimanager.OnLayoutEvent; -import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.annotations.ReactProp; -import com.facebook.react.uimanager.events.EventDispatcher; import java.lang.reflect.Field; import java.util.ArrayList; @@ -236,6 +235,73 @@ abstract public class RenderableShadowNode extends VirtualNode { markUpdated(); } + static double saturate(double v) { + return v <= 0 ? 0 : (v >= 1 ? 1 : v); + } + + public void render(Canvas canvas, Paint paint, float opacity) { + if (mMask != null) { + SvgViewShadowNode root = getSvgShadowNode(); + MaskShadowNode mask = (MaskShadowNode) root.getDefinedMask(mMask); + + Rect clipBounds = canvas.getClipBounds(); + int height = clipBounds.height(); + int width = clipBounds.width(); + + Bitmap maskBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Bitmap original = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + + Canvas originalCanvas = new Canvas(original); + Canvas maskCanvas = new Canvas(maskBitmap); + Canvas resultCanvas = new Canvas(result); + + // Clip to mask bounds and render the mask + float maskX = (float) relativeOnWidth(mask.mX); + float maskY = (float) relativeOnWidth(mask.mY); + float maskWidth = (float) relativeOnWidth(mask.mWidth); + float maskHeight = (float) relativeOnWidth(mask.mHeight); + maskCanvas.clipRect(maskX, maskY, maskWidth, maskHeight); + + Paint maskpaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mask.draw(maskCanvas, maskpaint, 1); + + // Apply luminanceToAlpha filter primitive https://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement + int nPixels = width * height; + int[] pixels = new int[nPixels]; + maskBitmap.getPixels(pixels, 0, width, 0, 0, width, height); + + for (int i = 0; i < nPixels; i++) { + int color = pixels[i]; + + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = color & 0xFF; + int a = color >>> 24; + + double luminance = saturate(((0.299 * r) + (0.587 * g) + (0.144 * b)) / 255); + int alpha = (int) (a * luminance); + int pixel = (alpha << 24); + pixels[i] = pixel; + } + + maskBitmap.setPixels(pixels, 0, width, 0, 0, width, height); + + // Render content of current SVG Renderable to image + draw(originalCanvas, paint, opacity); + + // Blend current element and mask + maskpaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); + resultCanvas.drawBitmap(original, 0, 0, null); + resultCanvas.drawBitmap(maskBitmap, 0, 0, maskpaint); + + // Render blended result into current render context + canvas.drawBitmap(result, 0, 0, paint); + } else { + draw(canvas, paint, opacity); + } + } + @Override public void draw(Canvas canvas, Paint paint, float opacity) { opacity *= mOpacity; diff --git a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java index 1069457e..96f5cb11 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java @@ -50,6 +50,7 @@ class RenderableViewManager extends ViewGroupManager extends ViewGroupManager createMaskManager() { + return new RenderableViewManager(CLASS_MASK) { + + @ReactProp(name = "x") + public void setX(RenderableView node, String x) { + node.shadowNode.setX(x); + } + + @ReactProp(name = "y") + public void setY(RenderableView node, String y) { + node.shadowNode.setY(y); + } + + @ReactProp(name = "maskwidth") + public void setWidth(RenderableView node, String width) { + node.shadowNode.setWidth(width); + } + + @ReactProp(name = "maskheight") + public void setHeight(RenderableView node, String height) { + node.shadowNode.setHeight(height); + } + + @ReactProp(name = "maskUnits") + public void setMaskUnits(RenderableView node, int maskUnits) { + node.shadowNode.setMaskUnits(maskUnits); + } + + @ReactProp(name = "maskContentUnits") + public void setMaskContentUnits(RenderableView node, int maskContentUnits) { + node.shadowNode.setMaskContentUnits(maskContentUnits); + } + + @ReactProp(name = "maskTransform") + public void setMaskTransform(RenderableView node, @Nullable ReadableArray matrixArray) { + node.shadowNode.setMaskTransform(matrixArray); + } + + @ReactProp(name = "minX") + public void setMinX(RenderableView node, float minX) { + node.shadowNode.setMinX(minX); + } + + @ReactProp(name = "minY") + public void setMinY(RenderableView node, float minY) { + node.shadowNode.setMinY(minY); + } + + @ReactProp(name = "vbWidth") + public void setVbWidth(RenderableView node, float vbWidth) { + node.shadowNode.setVbWidth(vbWidth); + } + + @ReactProp(name = "vbHeight") + public void setVbHeight(RenderableView node, float vbHeight) { + node.shadowNode.setVbHeight(vbHeight); + } + + @ReactProp(name = "align") + public void setAlign(RenderableView node, String align) { + node.shadowNode.setAlign(align); + } + + @ReactProp(name = "meetOrSlice") + public void setMeetOrSlice(RenderableView node, int meetOrSlice) { + node.shadowNode.setMeetOrSlice(meetOrSlice); + } + }; + } + static RenderableViewManager createLinearGradientManager() { return new RenderableViewManager(CLASS_LINEAR_GRADIENT) { @@ -596,6 +667,8 @@ class RenderableViewManager extends ViewGroupManager extends ViewGroupManager node, String mask) { + node.shadowNode.setMask(mask); + } + + @ReactProp(name = "clipPath") + public void setClipPath(RenderableView node, String mask) { + node.shadowNode.setClipPath(mask); + } + + @ReactProp(name = "clipRule") + public void setClipRule(RenderableView node, int clipRule) { + node.shadowNode.setClipRule(clipRule); + } + @ReactProp(name = "fill") public void setFill(RenderableView node, @Nullable Dynamic fill) { node.shadowNode.setFill(fill); diff --git a/android/src/main/java/com/horcrux/svg/SvgPackage.java b/android/src/main/java/com/horcrux/svg/SvgPackage.java index 4644b2e2..499a3770 100644 --- a/android/src/main/java/com/horcrux/svg/SvgPackage.java +++ b/android/src/main/java/com/horcrux/svg/SvgPackage.java @@ -42,6 +42,7 @@ public class SvgPackage implements ReactPackage { RenderableViewManager.createLinearGradientManager(), RenderableViewManager.createRadialGradientManager(), RenderableViewManager.createPatternManager(), + RenderableViewManager.createMaskManager(), new SvgViewManager()); } diff --git a/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java b/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java index cceaf198..e4f132bd 100644 --- a/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/SvgViewShadowNode.java @@ -38,6 +38,7 @@ public class SvgViewShadowNode extends LayoutShadowNode { private final Map mDefinedClipPaths = new HashMap<>(); private final Map mDefinedTemplates = new HashMap<>(); + private final Map mDefinedMasks = new HashMap<>(); private final Map mDefinedBrushes = new HashMap<>(); private Canvas mCanvas; private final float mScale; @@ -192,7 +193,7 @@ public class SvgViewShadowNode extends LayoutShadowNode { if (lNode instanceof VirtualNode) { VirtualNode node = (VirtualNode)lNode; int count = node.saveAndSetupCanvas(canvas); - node.draw(canvas, paint, 1f); + node.render(canvas, paint, 1f); node.restoreCanvas(canvas, count); node.markUpdateSeen(); @@ -279,6 +280,14 @@ public class SvgViewShadowNode extends LayoutShadowNode { return mDefinedBrushes.get(brushRef); } + void defineMask(VirtualNode mask, String maskRef) { + mDefinedMasks.put(maskRef, mask); + } + + VirtualNode getDefinedMask(String maskRef) { + return mDefinedMasks.get(maskRef); + } + void traverseChildren(VirtualNode.NodeRunnable runner) { for (int i = 0; i < getChildCount(); i++) { ReactShadowNode child = getChildAt(i); diff --git a/android/src/main/java/com/horcrux/svg/VirtualNode.java b/android/src/main/java/com/horcrux/svg/VirtualNode.java index 464664bb..23e43e33 100644 --- a/android/src/main/java/com/horcrux/svg/VirtualNode.java +++ b/android/src/main/java/com/horcrux/svg/VirtualNode.java @@ -60,6 +60,7 @@ abstract class VirtualNode extends LayoutShadowNode { private int mClipRule; private @Nullable String mClipPath; + @Nullable String mMask; private static final int CLIP_RULE_EVENODD = 0; private static final int CLIP_RULE_NONZERO = 1; @@ -153,6 +154,9 @@ abstract class VirtualNode extends LayoutShadowNode { } public abstract void draw(Canvas canvas, Paint paint, float opacity); + public void render(Canvas canvas, Paint paint, float opacity) { + draw(canvas, paint, opacity); + }; /** * Sets up the transform matrix on the canvas before an element is drawn. @@ -186,6 +190,12 @@ abstract class VirtualNode extends LayoutShadowNode { } + @ReactProp(name = "mask") + public void setMask(String mask) { + mMask = mask; + markUpdated(); + } + @ReactProp(name = "clipPath") public void setClipPath(String clipPath) { mCachedClipPath = null; @@ -194,7 +204,7 @@ abstract class VirtualNode extends LayoutShadowNode { } @ReactProp(name = "clipRule", defaultInt = CLIP_RULE_NONZERO) - public void clipRule(int clipRule) { + public void setClipRule(int clipRule) { mClipRule = clipRule; markUpdated(); } diff --git a/elements/Mask.js b/elements/Mask.js new file mode 100644 index 00000000..3e105eea --- /dev/null +++ b/elements/Mask.js @@ -0,0 +1,75 @@ +import React, { Component } from "react"; +import PropTypes from 'prop-types'; +import { requireNativeComponent } from "react-native"; +import { numberProp } from '../lib/props'; +import PATTERN_UNITS from '../lib/PATTERN_UNITS'; +import { MaskAttributes } from '../lib/attributes'; +import extractTransform from '../lib/extract/extractTransform'; +import extractViewBox from "react-native-svg/lib/extract/extractViewBox"; + +export default class extends Component { + static displayName = 'Mask'; + static propTypes = { + id: PropTypes.string.isRequired, + x: numberProp, + y: numberProp, + width: numberProp, + height: numberProp, + maskTransform: PropTypes.string, + maskUnits: PropTypes.oneOf(['userSpaceOnUse', 'objectBoundingBox']), + maskContentUnits: PropTypes.oneOf([ + 'userSpaceOnUse', + 'objectBoundingBox', + ]), + viewBox: PropTypes.string, + preserveAspectRatio: PropTypes.string + }; + + render() { + const { props } = this; + const { + maskTransform, + transform, + id, + x, + y, + width, + height, + maskUnits, + maskContentUnits, + children, + viewBox, + preserveAspectRatio, + } = props; + + let extractedTransform; + if (maskTransform) { + extractedTransform = extractTransform(maskTransform); + } else if (transform) { + extractedTransform = extractTransform(transform); + } else { + extractedTransform = extractTransform(props); + } + + return ( + + {children} + + ); + } +} + +const RNSVGMask = requireNativeComponent('RNSVGMask', null, { + nativeOnly: MaskAttributes, +}); diff --git a/index.js b/index.js index f1848316..7a2fc66b 100644 --- a/index.js +++ b/index.js @@ -19,6 +19,7 @@ import RadialGradient from "./elements/RadialGradient"; import Stop from "./elements/Stop"; import ClipPath from "./elements/ClipPath"; import Pattern from "./elements/Pattern"; +import Mask from "./elements/Mask"; export { Svg, @@ -41,7 +42,8 @@ export { RadialGradient, Stop, ClipPath, - Pattern + Pattern, + Mask, }; //noinspection JSUnusedGlobalSymbols diff --git a/ios/Elements/RNSVGMask.h b/ios/Elements/RNSVGMask.h new file mode 100644 index 00000000..8831da12 --- /dev/null +++ b/ios/Elements/RNSVGMask.h @@ -0,0 +1,21 @@ + +#import "RNSVGGroup.h" + +@interface RNSVGMask : RNSVGGroup + +@property (nonatomic, strong) NSString *x; +@property (nonatomic, strong) NSString *y; +@property (nonatomic, strong) NSString *maskwidth; +@property (nonatomic, strong) NSString *maskheight; +@property (nonatomic, assign) RNSVGUnits maskUnits; +@property (nonatomic, assign) RNSVGUnits maskContentUnits; +@property (nonatomic, assign) CGAffineTransform maskTransform; + +@property (nonatomic, assign) CGFloat minX; +@property (nonatomic, assign) CGFloat minY; +@property (nonatomic, assign) CGFloat vbWidth; +@property (nonatomic, assign) CGFloat vbHeight; +@property (nonatomic, strong) NSString *align; +@property (nonatomic, assign) RNSVGVBMOS meetOrSlice; + +@end diff --git a/ios/Elements/RNSVGMask.m b/ios/Elements/RNSVGMask.m new file mode 100644 index 00000000..b612ba62 --- /dev/null +++ b/ios/Elements/RNSVGMask.m @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Horcrux. + * All rights reserved. + * + * This source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ +#import "RNSVGMask.h" +#import "RNSVGPainter.h" +#import "RNSVGBrushType.h" +#import "RNSVGNode.h" + +@implementation RNSVGMask + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + return nil; +} + +- (void)parseReference +{ + [self.svgView defineMask:self maskName:self.name]; +} + +@end + diff --git a/ios/Elements/RNSVGSvgView.h b/ios/Elements/RNSVGSvgView.h index 3a76e0c2..83201b44 100644 --- a/ios/Elements/RNSVGSvgView.h +++ b/ios/Elements/RNSVGSvgView.h @@ -46,6 +46,10 @@ - (RNSVGPainter *)getDefinedPainter:(NSString *)painterName; +- (void)defineMask:(RNSVGNode *)mask maskName:(NSString *)maskName; + +- (RNSVGNode *)getDefinedMask:(NSString *)maskName; + - (NSString *)getDataURL; - (CGRect)getContextBounds; diff --git a/ios/Elements/RNSVGSvgView.m b/ios/Elements/RNSVGSvgView.m index a1db83c8..77814fa7 100644 --- a/ios/Elements/RNSVGSvgView.m +++ b/ios/Elements/RNSVGSvgView.m @@ -16,6 +16,7 @@ NSMutableDictionary *_clipPaths; NSMutableDictionary *_templates; NSMutableDictionary *_painters; + NSMutableDictionary *_masks; CGAffineTransform _viewBoxTransform; CGAffineTransform _invviewBoxTransform; } @@ -263,6 +264,19 @@ return _painters ? [_painters objectForKey:painterName] : nil; } +- (void)defineMask:(RNSVGNode *)mask maskName:(NSString *)maskName +{ + if (!_masks) { + _masks = [[NSMutableDictionary alloc] init]; + } + [_masks setObject:mask forKey:maskName]; +} + +- (RNSVGNode *)getDefinedMask:(NSString *)maskName; +{ + return _masks ? [_masks objectForKey:maskName] : nil; +} + - (CGRect)getContextBounds { return CGContextGetClipBoundingBox(UIGraphicsGetCurrentContext()); diff --git a/ios/RNSVG.xcodeproj/project.pbxproj b/ios/RNSVG.xcodeproj/project.pbxproj index e8b9d9bb..273d1ca0 100644 --- a/ios/RNSVG.xcodeproj/project.pbxproj +++ b/ios/RNSVG.xcodeproj/project.pbxproj @@ -62,6 +62,10 @@ 94241667213B0D4500088E93 /* RNSVGPattern.m in Sources */ = {isa = PBXBuildFile; fileRef = 94241665213B0D4500088E93 /* RNSVGPattern.m */; }; 9424166D213B302600088E93 /* RNSVGPatternManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9424166C213B302600088E93 /* RNSVGPatternManager.m */; }; 9424166E213B302600088E93 /* RNSVGPatternManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9424166C213B302600088E93 /* RNSVGPatternManager.m */; }; + 947F380B214810DC00677F2A /* RNSVGMask.m in Sources */ = {isa = PBXBuildFile; fileRef = 947F380A214810DC00677F2A /* RNSVGMask.m */; }; + 947F380C214810DC00677F2A /* RNSVGMask.m in Sources */ = {isa = PBXBuildFile; fileRef = 947F380A214810DC00677F2A /* RNSVGMask.m */; }; + 947F380F2148119A00677F2A /* RNSVGMaskManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 947F380E2148119A00677F2A /* RNSVGMaskManager.m */; }; + 947F38102148119A00677F2A /* RNSVGMaskManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 947F380E2148119A00677F2A /* RNSVGMaskManager.m */; }; 9494C4D81F473BA700D5BCFD /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9494C4D71F473BA700D5BCFD /* QuartzCore.framework */; }; 9494C4DA1F473BCB00D5BCFD /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9494C4D91F473BCB00D5BCFD /* CoreText.framework */; }; 9494C4DC1F473BD900D5BCFD /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9494C4DB1F473BD900D5BCFD /* CoreGraphics.framework */; }; @@ -246,6 +250,10 @@ 94241669213B0DB800088E93 /* RNSVGPattern.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSVGPattern.h; sourceTree = ""; }; 9424166A213B2FF100088E93 /* RNSVGPatternManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSVGPatternManager.h; sourceTree = ""; }; 9424166C213B302600088E93 /* RNSVGPatternManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSVGPatternManager.m; sourceTree = ""; }; + 947F3809214810B800677F2A /* RNSVGMask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGMask.h; path = Elements/RNSVGMask.h; sourceTree = ""; }; + 947F380A214810DC00677F2A /* RNSVGMask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNSVGMask.m; path = Elements/RNSVGMask.m; sourceTree = ""; }; + 947F380D2148118300677F2A /* RNSVGMaskManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSVGMaskManager.h; sourceTree = ""; }; + 947F380E2148119A00677F2A /* RNSVGMaskManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSVGMaskManager.m; sourceTree = ""; }; 9494C4D71F473BA700D5BCFD /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 9494C4D91F473BCB00D5BCFD /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; 9494C4DB1F473BD900D5BCFD /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -344,6 +352,8 @@ 7F08CE991E23476900650F83 /* RNSVGTSpanManager.m */, 9424166A213B2FF100088E93 /* RNSVGPatternManager.h */, 9424166C213B302600088E93 /* RNSVGPatternManager.m */, + 947F380D2148118300677F2A /* RNSVGMaskManager.h */, + 947F380E2148119A00677F2A /* RNSVGMaskManager.m */, 10BEC1BE1D3F680F00FDCB19 /* RNSVGLinearGradientManager.h */, 10BEC1BF1D3F680F00FDCB19 /* RNSVGLinearGradientManager.m */, 10BEC1C01D3F680F00FDCB19 /* RNSVGRadialGradientManager.h */, @@ -425,6 +435,8 @@ 10BEC1B91D3F66F500FDCB19 /* RNSVGLinearGradient.m */, 94241669213B0DB800088E93 /* RNSVGPattern.h */, 94241665213B0D4500088E93 /* RNSVGPattern.m */, + 947F3809214810B800677F2A /* RNSVGMask.h */, + 947F380A214810DC00677F2A /* RNSVGMask.m */, 10BEC1BA1D3F66F500FDCB19 /* RNSVGRadialGradient.h */, 10BEC1BB1D3F66F500FDCB19 /* RNSVGRadialGradient.m */, 1023B4911D3DF5060051496D /* RNSVGUse.h */, @@ -558,6 +570,7 @@ 94241666213B0D4500088E93 /* RNSVGPattern.m in Sources */, 10BA0D4B1CE74E3D00887C2B /* RNSVGRect.m in Sources */, 10BA0D341CE74E3100887C2B /* RNSVGCircleManager.m in Sources */, + 947F380B214810DC00677F2A /* RNSVGMask.m in Sources */, 10BEC1BC1D3F66F500FDCB19 /* RNSVGLinearGradient.m in Sources */, 1039D2B01CE72F27001E90A8 /* RNSVGPercentageConverter.m in Sources */, 10BA0D491CE74E3D00887C2B /* RNSVGEllipse.m in Sources */, @@ -599,6 +612,7 @@ B56895B120352B9C004DBF1E /* RNSVGPropHelper.m in Sources */, 7FC260CE1E3499BC00A39833 /* RNSVGViewBox.m in Sources */, 7F08CEA11E23479700650F83 /* RNSVGTSpan.m in Sources */, + 947F380F2148119A00677F2A /* RNSVGMaskManager.m in Sources */, 10BA0D4A1CE74E3D00887C2B /* RNSVGLine.m in Sources */, 10FDEEB21D3FB60500A5C46C /* RNSVGPainterBrush.m in Sources */, 1039D28C1CE71EB7001E90A8 /* RNSVGSvgView.m in Sources */, @@ -618,6 +632,7 @@ 94241667213B0D4500088E93 /* RNSVGPattern.m in Sources */, A361E7711EB0C33D00646005 /* RNSVGCircleManager.m in Sources */, A361E7721EB0C33D00646005 /* RNSVGLinearGradient.m in Sources */, + 947F380C214810DC00677F2A /* RNSVGMask.m in Sources */, A361E7731EB0C33D00646005 /* RNSVGPercentageConverter.m in Sources */, A361E7751EB0C33D00646005 /* RNSVGEllipse.m in Sources */, A361E7761EB0C33D00646005 /* RNSVGPath.m in Sources */, @@ -659,6 +674,7 @@ A361E7961EB0C33D00646005 /* RNSVGTSpanManager.m in Sources */, A361E7971EB0C33D00646005 /* RNSVGViewBox.m in Sources */, A361E7981EB0C33D00646005 /* RNSVGTSpan.m in Sources */, + 947F38102148119A00677F2A /* RNSVGMaskManager.m in Sources */, A361E7991EB0C33D00646005 /* RNSVGLine.m in Sources */, 167AF4582087C2910035AA75 /* RNSVGFontData.m in Sources */, A361E79A1EB0C33D00646005 /* RNSVGPainterBrush.m in Sources */, diff --git a/ios/RNSVGNode.h b/ios/RNSVGNode.h index d90e6a33..ec8304ea 100644 --- a/ios/RNSVGNode.h +++ b/ios/RNSVGNode.h @@ -30,6 +30,7 @@ extern CGFloat const RNSVG_DEFAULT_FONT_SIZE; @property (nonatomic, assign) CGFloat opacity; @property (nonatomic, assign) RNSVGCGFCRule clipRule; @property (nonatomic, strong) NSString *clipPath; +@property (nonatomic, strong) NSString *mask; @property (nonatomic, assign) BOOL responsible; @property (nonatomic, assign) CGAffineTransform matrix; @property (nonatomic, assign) CGAffineTransform invmatrix; diff --git a/ios/RNSVGNode.m b/ios/RNSVGNode.m index f157a399..c0687cdf 100644 --- a/ios/RNSVGNode.m +++ b/ios/RNSVGNode.m @@ -226,7 +226,7 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12; } } - return [self getClipPath]; + return _cachedClipPath; } - (void)clip:(CGContextRef)context diff --git a/ios/RNSVGRenderable.m b/ios/RNSVGRenderable.m index 3fb427b9..8ad85597 100644 --- a/ios/RNSVGRenderable.m +++ b/ios/RNSVGRenderable.m @@ -8,6 +8,8 @@ #import "RNSVGRenderable.h" #import "RNSVGClipPath.h" +#import "RNSVGMask.h" +#import "RNSVGViewBox.h" @implementation RNSVGRenderable { @@ -159,6 +161,11 @@ } } + +UInt32 saturate(double value) { + return value <= 0 ? 0 : value >= 255 ? 255 : value; +} + - (void)renderTo:(CGContextRef)context rect:(CGRect)rect { // This needs to be painted on a layer before being composited. @@ -167,7 +174,103 @@ CGContextSetAlpha(context, self.opacity); [self beginTransparencyLayer:context]; - [self renderLayerTo:context rect:rect]; + + if (self.mask) { + // https://www.w3.org/TR/SVG11/masking.html#MaskElement + RNSVGMask *_maskNode = (RNSVGMask*)[self.svgView getDefinedMask:self.mask]; + CGRect bounds = CGContextGetClipBoundingBox(context); + CGSize boundsSize = bounds.size; + float height = boundsSize.height; + float width = boundsSize.width; + NSUInteger iheight = height; + NSUInteger iwidth = width; + NSUInteger npixels = iheight * iwidth; + + // Allocate pixel buffer and bitmap context for mask + NSUInteger bytesPerPixel = 4; + NSUInteger bitsPerComponent = 8; + NSUInteger bytesPerRow = bytesPerPixel * iwidth; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + UInt32 * pixels = (UInt32 *) calloc(npixels, sizeof(UInt32)); + CGContextRef bcontext = CGBitmapContextCreate(pixels, iwidth, iheight, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); + + // Clip to mask bounds, adjust to viewbox and render the mask + CGFloat x = [RNSVGPercentageConverter stringToFloat:[_maskNode x] + relative:width + offset:0]; + CGFloat y = [RNSVGPercentageConverter stringToFloat:[_maskNode y] + relative:height + offset:0]; + CGFloat w = [RNSVGPercentageConverter stringToFloat:[_maskNode maskwidth] + relative:width + offset:0]; + CGFloat h = [RNSVGPercentageConverter stringToFloat:[_maskNode maskheight] + relative:height + offset:0]; + CGRect maskBounds = CGRectMake(x, y, w, h); + CGContextClipToRect(bcontext, maskBounds); + + CGRect drawBounds = CGRectMake(0, 0, width, height); + CGAffineTransform _viewBoxTransform = [RNSVGViewBox getTransform:rect + eRect:drawBounds + align:_maskNode.align + meetOrSlice:_maskNode.meetOrSlice]; + CGContextConcatCTM(bcontext, _viewBoxTransform); + [_maskNode renderLayerTo:bcontext rect:rect]; + + // Apply luminanceToAlpha filter primitive + // https://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement + UInt32 * currentPixel = pixels; + for (NSUInteger i = 0; i < npixels; i++) { + UInt32 color = *currentPixel; + + UInt32 r = color & 0xFF; + UInt32 g = (color >> 8) & 0xFF; + UInt32 b = (color >> 16) & 0xFF; + + double luma = 0.299 * r + 0.587 * g + 0.144 * b; + *currentPixel = saturate(luma) << 24; + currentPixel++; + } + + // Create mask image and release memory + CGImageRef maskImage = CGBitmapContextCreateImage(bcontext); + CGColorSpaceRelease(colorSpace); + CGContextRelease(bcontext); + free(pixels); + + // Render content of current SVG Renderable to image + UIGraphicsBeginImageContextWithOptions(boundsSize, NO, 0.0); + CGContextRef newContext = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(newContext, 0.0, height); + CGContextScaleCTM(newContext, 1.0, -1.0); + [self renderLayerTo:newContext rect:rect]; + CGImageRef contentImage = CGBitmapContextCreateImage(newContext); + UIGraphicsEndImageContext(); + + // Blend current element and mask + UIGraphicsBeginImageContextWithOptions(boundsSize, NO, 0.0); + newContext = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(newContext, 0.0, height); + CGContextScaleCTM(newContext, 1.0, -1.0); + + CGContextSetBlendMode(newContext, kCGBlendModeCopy); + CGContextDrawImage(newContext, drawBounds, maskImage); + CGImageRelease(maskImage); + + CGContextSetBlendMode(newContext, kCGBlendModeSourceIn); + CGContextDrawImage(newContext, drawBounds, contentImage); + CGImageRelease(contentImage); + + CGImageRef blendedImage = CGBitmapContextCreateImage(newContext); + UIGraphicsEndImageContext(); + + // Render blended result into current render context + CGContextDrawImage(context, drawBounds, blendedImage); + CGImageRelease(blendedImage); + } else { + [self renderLayerTo:context rect:rect]; + } [self endTransparencyLayer:context]; CGContextRestoreGState(context); diff --git a/ios/ViewManagers/RNSVGMaskManager.h b/ios/ViewManagers/RNSVGMaskManager.h new file mode 100644 index 00000000..bb5fa532 --- /dev/null +++ b/ios/ViewManagers/RNSVGMaskManager.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2015-present, Horcrux. + * All rights reserved. + * + * This source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RNSVGGroupManager.h" + +@interface RNSVGMaskManager : RNSVGGroupManager + +@end diff --git a/ios/ViewManagers/RNSVGMaskManager.m b/ios/ViewManagers/RNSVGMaskManager.m new file mode 100644 index 00000000..cf6629d7 --- /dev/null +++ b/ios/ViewManagers/RNSVGMaskManager.m @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Horcrux. + * All rights reserved. + * + * This source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RNSVGMaskManager.h" +#import "RNSVGMask.h" + +@implementation RNSVGMaskManager + +RCT_EXPORT_MODULE() + +- (RNSVGMask *)node +{ + return [RNSVGMask new]; +} + +RCT_EXPORT_VIEW_PROPERTY(x, NSString) +RCT_EXPORT_VIEW_PROPERTY(y, NSString) +RCT_EXPORT_VIEW_PROPERTY(maskwidth, NSString) +RCT_EXPORT_VIEW_PROPERTY(maskheight, NSString) +RCT_EXPORT_VIEW_PROPERTY(maskUnits, RNSVGUnits) +RCT_EXPORT_VIEW_PROPERTY(maskContentUnits, RNSVGUnits) +RCT_EXPORT_VIEW_PROPERTY(maskTransform, CGAffineTransform) + +RCT_EXPORT_VIEW_PROPERTY(minX, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(minY, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(vbWidth, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(vbHeight, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(align, NSString) +RCT_EXPORT_VIEW_PROPERTY(meetOrSlice, RNSVGVBMOS) + +@end diff --git a/ios/ViewManagers/RNSVGNodeManager.m b/ios/ViewManagers/RNSVGNodeManager.m index 8e9837e7..556f4cc0 100644 --- a/ios/ViewManagers/RNSVGNodeManager.m +++ b/ios/ViewManagers/RNSVGNodeManager.m @@ -27,6 +27,7 @@ RCT_EXPORT_MODULE() RCT_EXPORT_VIEW_PROPERTY(name, NSString) RCT_EXPORT_VIEW_PROPERTY(opacity, CGFloat) RCT_EXPORT_VIEW_PROPERTY(matrix, CGAffineTransform) +RCT_EXPORT_VIEW_PROPERTY(mask, NSString) RCT_EXPORT_VIEW_PROPERTY(clipPath, NSString) RCT_EXPORT_VIEW_PROPERTY(clipRule, RNSVGCGFCRule) RCT_EXPORT_VIEW_PROPERTY(responsible, BOOL) diff --git a/lib/attributes.js b/lib/attributes.js index 316174f2..06a84b73 100644 --- a/lib/attributes.js +++ b/lib/attributes.js @@ -55,6 +55,7 @@ const NodeAttributes = { opacity: true, clipRule: true, clipPath: true, + mask: true, propList: { diff: arrayDiffer }, @@ -175,6 +176,20 @@ const PatternAttributes = { } }; +const MaskAttributes = { + ...ViewBoxAttributes, + name: true, + x: true, + y: true, + maskwidth: true, + maskheight: true, + maskUnits: true, + maskContentUnits: true, + maskTransform: { + diff: arrayDiffer + } +}; + const LinearGradientAttributes = { ...GradientAttributes, x1: true, @@ -255,5 +270,6 @@ export { LinearGradientAttributes, RadialGradientAttributes, ViewBoxAttributes, - PatternAttributes + PatternAttributes, + MaskAttributes, }; diff --git a/lib/extract/extractProps.js b/lib/extract/extractProps.js index 6f5def49..4c26a78d 100644 --- a/lib/extract/extractProps.js +++ b/lib/extract/extractProps.js @@ -4,24 +4,40 @@ import extractTransform, { props2transform } from "./extractTransform"; import extractClipPath from "./extractClipPath"; import extractResponder from "./extractResponder"; import extractOpacity from "./extractOpacity"; +import urlRegex from "./patternReg"; export default function(props, ref) { + const { opacity, onLayout, id, clipPath, mask } = props; const styleProperties = []; const extractedProps = { - opacity: extractOpacity(props.opacity), + opacity: extractOpacity(opacity), propList: styleProperties, - onLayout: props.onLayout + onLayout, }; - if (props.id) { - extractedProps.name = props.id; + if (id) { + extractedProps.name = id; } - if (props.clipPath) { + if (clipPath) { Object.assign(extractedProps, extractClipPath(props)); } + if (mask) { + let matched = mask.match(urlRegex); + + if (matched) { + extractedProps.mask = matched[1]; + } else { + console.warn( + 'Invalid `mask` prop, expected a mask like `"#id"`, but got: "' + + clipPath + + '"' + ); + } + } + Object.assign(extractedProps, extractStroke(props, styleProperties)); Object.assign(extractedProps, extractFill(props, styleProperties)); diff --git a/lib/props.js b/lib/props.js index 93b24956..f8cff4d5 100644 --- a/lib/props.js +++ b/lib/props.js @@ -41,6 +41,10 @@ const clipProps = { clipPath: PropTypes.string }; +const maskProps = { + mask: PropTypes.string +}; + const definationProps = { name: PropTypes.string }; @@ -80,6 +84,7 @@ const pathProps = { ...fillProps, ...strokeProps, ...clipProps, + ...maskProps, ...transformProps, ...responderProps, ...touchableProps,