Merge pull request #25 from magicismight/propInPercents

support percentage props for elements
This commit is contained in:
Horcrux
2016-04-27 16:54:17 +08:00
60 changed files with 1128 additions and 753 deletions

4
.gitignore vendored
View File

@@ -43,3 +43,7 @@ ios/marry/main.jsbundle
# unfinished documents # unfinished documents
# #
cn-doc.md cn-doc.md
# experimental code
#
experimental/

View File

@@ -11,12 +11,12 @@ class CircleExample extends Component{
render() { render() {
return <Svg return <Svg
height="100" height="100"
width="100" width="140"
> >
<Circle <Circle
cx="50" cx="50%"
cy="50" cy="50%"
r="50" r="40%"
fill="pink" fill="pink"
/> />
</Svg>; </Svg>;

View File

@@ -164,7 +164,7 @@ class TextClipping extends Component{
fontWeight="bold" fontWeight="bold"
fill="red" fill="red"
stroke="blue" stroke="blue"
textAnchor="center" textAnchor="middle"
clipPath="url(#clip)" clipPath="url(#clip)"
>NOT THE FACE</Text> >NOT THE FACE</Text>
</Svg>; </Svg>;
@@ -213,7 +213,7 @@ const icon = <Svg
</G> </G>
</Svg>; </Svg>;
const samples = [ClipPathAttr, ClipRule, ClipPathElement, TextClipping]; const samples = [ClipPathAttr, ClipRule];//, ClipPathElement, TextClipping
export { export {
icon, icon,

View File

@@ -14,10 +14,10 @@ class EllipseExample extends Component{
width="110" width="110"
> >
<Ellipse <Ellipse
cx="55" cx="50%"
cy="55" cy="50%"
rx="50" rx="45%"
ry="30" ry="30%"
stroke="purple" stroke="purple"
strokeWidth="2" strokeWidth="2"
fill="yellow" fill="yellow"

View File

@@ -30,6 +30,7 @@ class GExample extends Component{
<Circle <Circle
cx="25" cx="25"
cy="75" cy="75"
stroke="red"
/> />
<Circle <Circle
cx="50" cx="50"
@@ -39,6 +40,7 @@ class GExample extends Component{
<Circle <Circle
cx="75" cx="75"
cy="25" cy="25"
stroke="red"
/> />
<Circle <Circle
cx="75" cx="75"
@@ -82,7 +84,7 @@ class GTransform extends Component{
y="75" y="75"
stroke="#600" stroke="#600"
fill="#600" fill="#600"
textAnchor="center" textAnchor="middle"
> >
Text grouped with shapes</Text> Text grouped with shapes</Text>
</G> </G>

View File

@@ -15,10 +15,10 @@ class LineExample extends Component{
width="100" width="100"
> >
<Line <Line
x1="0" x1="10%"
y1="0" y1="10%"
x2="100" x2="90%"
y2="100" y2="90%"
stroke="red" stroke="red"
strokeWidth="2" strokeWidth="2"
/> />

View File

@@ -14,10 +14,10 @@ class RectExample extends Component{
height="60" height="60"
> >
<Rect <Rect
x="25" x="5%"
y="5" y="5%"
width="150" width="90%"
height="50" height="90%"
fill="rgb(0,0,255)" fill="rgb(0,0,255)"
strokeWidth="3" strokeWidth="3"
stroke="rgb(0,0,0)" stroke="rgb(0,0,0)"

View File

@@ -76,7 +76,7 @@ class StrokeDashoffset extends Component{
fontWeight="bold" fontWeight="bold"
x="100" x="100"
y="40" y="40"
textAnchor="center" textAnchor="middle"
strokeDasharray="100" strokeDasharray="100"
strokeDashoffset="60" strokeDashoffset="60"
>STROKE</Text> >STROKE</Text>

View File

@@ -22,7 +22,7 @@ class TextExample extends Component{
x="50" x="50"
y="9" y="9"
fill="red" fill="red"
textAnchor="center" textAnchor="middle"
>I love SVG!</Text> >I love SVG!</Text>
</Svg>; </Svg>;
} }
@@ -60,6 +60,34 @@ class TextRotate extends Component{
} }
} }
// TODO: iOS not support text stroke with pattern
class TextStroke extends Component{
static title = 'Stroke the text';
render() {
return <Svg
height="60"
width="200"
>
<Defs>
<LinearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
<Stop offset="100%" stopColor="red" stopOpacity="1" />
<Stop offset="0%" stopColor="#000" stopOpacity="0.5" />
</LinearGradient>
</Defs>
<Text
stroke="url(#grad)"
strokeWidth="2"
fill="none"
fontSize="30"
fontWeight="bold"
x="100"
y="20"
textAnchor="middle"
>STROKE TEXT</Text>
</Svg>;
}
}
class TextFill extends Component{ class TextFill extends Component{
static title = 'Fill the text with LinearGradient'; static title = 'Fill the text with LinearGradient';
render() { render() {
@@ -68,8 +96,8 @@ class TextFill extends Component{
width="200" width="200"
> >
<Defs> <Defs>
<LinearGradient id="grad" x1="0" y1="0" x2="200" y2="0"> <LinearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
<Stop offset="0%" stopColor="rgb(255,255,0)" stopOpacity="0" /> <Stop offset="0%" stopColor="rgb(255,255,0)" stopOpacity="0.5" />
<Stop offset="100%" stopColor="red" stopOpacity="1" /> <Stop offset="100%" stopColor="red" stopOpacity="1" />
</LinearGradient> </LinearGradient>
</Defs> </Defs>
@@ -81,40 +109,12 @@ class TextFill extends Component{
fontWeight="bold" fontWeight="bold"
x="100" x="100"
y="20" y="20"
textAnchor="center" textAnchor="middle"
>FILL TEXT</Text> >FILL TEXT</Text>
</Svg>; </Svg>;
} }
} }
// TODO: iOS not support text stroke with pattern
class TextStroke extends Component{
static title = 'Stroke the text';
render() {
return <Svg
height="60"
width="200"
>
<Defs>
<LinearGradient id="grad" x1="0" y1="0" x2="100" y2="0">
<Stop offset="100%" stopColor="#fff" stopOpacity="0" />
<Stop offset="0%" stopColor="#000" stopOpacity="1" />
</LinearGradient>
</Defs>
<Text
stroke="url(#grad)"
strokeWidth="2"
fill="none"
fontSize="30"
fontWeight="bold"
x="100"
y="20"
textAnchor="center"
>STROKE TEXT</Text>
</Svg>;
}
}
class TextPath extends Component{ class TextPath extends Component{
static title = 'Transform the text'; static title = 'Transform the text';
@@ -148,13 +148,19 @@ const icon = <Svg
y="2" y="2"
fontSize="14" fontSize="14"
fontWeight="bold" fontWeight="bold"
textAnchor="center" textAnchor="middle"
fill="none" fill="none"
stroke="blue" stroke="blue"
></Text> ></Text>
</Svg>; </Svg>;
const samples = [TextExample, TextRotate, TextStroke, TextFill, TextPath]; const samples = [
TextExample,
TextRotate,
TextStroke,
TextFill,
TextPath
];
export { export {
icon, icon,

View File

@@ -562,15 +562,12 @@ npm install
4. [morph animations](https://github.com/gorangajic/react-svg-morph) 4. [morph animations](https://github.com/gorangajic/react-svg-morph)
5. fix propTypes 5. fix propTypes
6. more Text features support 6. more Text features support
7. support percent props 7. Pattern element
8. Pattern element 8. Image element
9. Image element
#### Thanks: #### Thanks:
* [SVG bounding Algorithm](https://github.com/icons8/svg-path-bounding-box)
* [Circle drawing with svg arc path](http://stackoverflow.com/questions/5737975/circle-drawing-with-svgs-arc-path/10477334#10477334)
* [w3schools.com SVG Tutorial](http://www.w3schools.com/svg/) * [w3schools.com SVG Tutorial](http://www.w3schools.com/svg/)
* [SVG Tutorial](http://tutorials.jenkov.com/svg/index.html) * [SVG Tutorial](http://tutorials.jenkov.com/svg/index.html)
* [MDN](https://developer.mozilla.org/en/docs/Web/SVG) * [MDN](https://developer.mozilla.org/en/docs/Web/SVG)

View File

@@ -9,10 +9,15 @@
package com.horcrux.svg; package com.horcrux.svg;
import android.util.Log;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableArray;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* Contains static helper methods for accessing props. * Contains static helper methods for accessing props.
*/ */
@@ -51,4 +56,20 @@ import com.facebook.react.bridge.ReadableArray;
return value.size(); return value.size();
} }
/**
* Converts percentage string into actual based on a relative number
*
* @param percentage percentage string
* @param relative relative number
* @return actual float based on relative number
*/
/*package*/ static float fromPercentageToFloat(String percentage, float relative, float offset, float scale) {
Pattern pattern = Pattern.compile("^(\\-?\\d+(?:\\.\\d+)?)%$");
Matcher matched = pattern.matcher(percentage);
if (matched.matches()) {
return Float.valueOf(matched.group(1)) / 100 * relative + offset;
} else {
return Float.valueOf(percentage) * scale;
}
}
} }

View File

@@ -17,6 +17,7 @@ import android.graphics.Paint;
import android.graphics.Path; import android.graphics.Path;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.RadialGradient; import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.graphics.Color; import android.graphics.Color;
@@ -48,9 +49,9 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
private static final int FILL_RULE_EVENODD = 0; private static final int FILL_RULE_EVENODD = 0;
private static final int FILL_RULE_NONZERO = 1; private static final int FILL_RULE_NONZERO = 1;
protected @Nullable Path mPath; protected Path mPath;
private @Nullable float[] mStrokeColor; private @Nullable ReadableArray mStrokeColor;
private @Nullable float[] mFillColor; private @Nullable ReadableArray mFillColor;
private @Nullable float[] mStrokeDasharray; private @Nullable float[] mStrokeDasharray;
private float mStrokeWidth = 1; private float mStrokeWidth = 1;
private float mStrokeDashoffset = 0; private float mStrokeDashoffset = 0;
@@ -60,6 +61,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
private boolean mFillRuleSet; private boolean mFillRuleSet;
private boolean mPathSet; private boolean mPathSet;
private float[] mShapePath; private float[] mShapePath;
protected RectF mContentBoundingBox;
private Point mPaint; private Point mPaint;
@ReactProp(name = "d") @ReactProp(name = "d")
@@ -72,7 +74,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
@ReactProp(name = "fill") @ReactProp(name = "fill")
public void setFill(@Nullable ReadableArray fillColors) { public void setFill(@Nullable ReadableArray fillColors) {
mFillColor = PropHelper.toFloatArray(fillColors); mFillColor = fillColors;
markUpdated(); markUpdated();
} }
@@ -87,7 +89,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
@ReactProp(name = "stroke") @ReactProp(name = "stroke")
public void setStroke(@Nullable ReadableArray strokeColors) { public void setStroke(@Nullable ReadableArray strokeColors) {
mStrokeColor = PropHelper.toFloatArray(strokeColors); mStrokeColor = strokeColors;
markUpdated(); markUpdated();
} }
@@ -139,10 +141,10 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
clip(canvas, paint); clip(canvas, paint);
if (setupFillPaint(paint, opacity)) { if (setupFillPaint(paint, opacity, null)) {
canvas.drawPath(mPath, paint); canvas.drawPath(mPath, paint);
} }
if (setupStrokePaint(paint, opacity)) { if (setupStrokePaint(paint, opacity, null)) {
canvas.drawPath(mPath, paint); canvas.drawPath(mPath, paint);
} }
@@ -168,21 +170,24 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
} }
mPath = super.createPath(mShapePath, path); mPath = super.createPath(mShapePath, path);
RectF box = new RectF();
mPath.computeBounds(box, true);
mContentBoundingBox = box;
} }
} }
/* /**
* sorting stops and stopsColors from array * sorting stops and stopsColors from array
*/ */
private static void parseGradientStops(float[] value, int stopsCount, float[] stops, int[] stopsColors, int startColorsPosition) { private static void parseGradientStops(ReadableArray value, int stopsCount, float[] stops, int[] stopsColors, int startColorsPosition) {
int startStops = value.length - stopsCount; int startStops = value.size() - stopsCount;
for (int i = 0; i < stopsCount; i++) { for (int i = 0; i < stopsCount; i++) {
stops[i] = value[startStops + i]; stops[i] = (float)value.getDouble(startStops + i);
stopsColors[i] = Color.argb( stopsColors[i] = Color.argb(
(int) (value[startColorsPosition + i * 4 + 3] * 255), (int) (value.getDouble(startColorsPosition + i * 4 + 3) * 255),
(int) (value[startColorsPosition + i * 4] * 255), (int) (value.getDouble(startColorsPosition + i * 4) * 255),
(int) (value[startColorsPosition + i * 4 + 1] * 255), (int) (value.getDouble(startColorsPosition + i * 4 + 1) * 255),
(int) (value[startColorsPosition + i * 4 + 2] * 255)); (int) (value.getDouble(startColorsPosition + i * 4 + 2) * 255));
} }
} }
@@ -192,12 +197,12 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
* Sets up {@link #mPaint} according to the props set on a shadow view. Returns {@code true} * Sets up {@link #mPaint} according to the props set on a shadow view. Returns {@code true}
* if the fill should be drawn, {@code false} if not. * if the fill should be drawn, {@code false} if not.
*/ */
protected boolean setupFillPaint(Paint paint, float opacity) { protected boolean setupFillPaint(Paint paint, float opacity, @Nullable RectF box) {
if (mFillColor != null && mFillColor.length > 0) { if (mFillColor != null && mFillColor.size() > 0) {
paint.reset(); paint.reset();
paint.setFlags(Paint.ANTI_ALIAS_FLAG); paint.setFlags(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL); paint.setStyle(Paint.Style.FILL);
setupPaint(paint, opacity, mFillColor); setupPaint(paint, opacity, mFillColor, box);
return true; return true;
} }
return false; return false;
@@ -207,8 +212,8 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
* Sets up {@link #mPaint} according to the props set on a shadow view. Returns {@code true} * Sets up {@link #mPaint} according to the props set on a shadow view. Returns {@code true}
* if the stroke should be drawn, {@code false} if not. * if the stroke should be drawn, {@code false} if not.
*/ */
protected boolean setupStrokePaint(Paint paint, float opacity) { protected boolean setupStrokePaint(Paint paint, float opacity, @Nullable RectF box) {
if (mStrokeWidth == 0 || mStrokeColor == null || mStrokeColor.length == 0) { if (mStrokeWidth == 0 || mStrokeColor == null || mStrokeColor.size() == 0) {
return false; return false;
} }
paint.reset(); paint.reset();
@@ -244,8 +249,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
} }
paint.setStrokeWidth(mStrokeWidth * mScale); paint.setStrokeWidth(mStrokeWidth * mScale);
setupPaint(paint, opacity, mStrokeColor, box);
setupPaint(paint, opacity, mStrokeColor);
if (mStrokeDasharray != null && mStrokeDasharray.length > 0) { if (mStrokeDasharray != null && mStrokeDasharray.length > 0) {
paint.setPathEffect(new DashPathEffect(mStrokeDasharray, mStrokeDashoffset)); paint.setPathEffect(new DashPathEffect(mStrokeDasharray, mStrokeDashoffset));
@@ -255,65 +259,71 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
} }
private void setupPaint(Paint paint, float opacity, float[] colors) { private void setupPaint(Paint paint, float opacity, ReadableArray colors, @Nullable RectF box) {
int stopsCount; int colorType = colors.getInt(0);
int [] stopsColors; if (colorType == 0) {
float [] stops; // solid color
int colorType = (int) colors[0];
switch (colorType) {
case 0:
paint.setARGB( paint.setARGB(
(int) (colors.length > 4 ? colors[4] * opacity * 255 : opacity * 255), (int) (colors.size() > 4 ? colors.getDouble(4) * opacity * 255 : opacity * 255),
(int) (colors[1] * 255), (int) (colors.getDouble(1) * 255),
(int) (colors[2] * 255), (int) (colors.getDouble(2) * 255),
(int) (colors[3] * 255)); (int) (colors.getDouble(3) * 255));
break; } else if (colorType == 1 || colorType == 2) {
case 1: if (box == null) {
stopsCount = (colors.length - 5) / 5; box = mContentBoundingBox;
stopsColors = new int [stopsCount]; }
stops = new float[stopsCount];
parseGradientStops(colors, stopsCount, stops, stopsColors, 5); int startColorsPosition = colorType == 1 ? 5 : 7;
int stopsCount = (colors.size() - startColorsPosition) / 5;
int [] stopsColors = new int [stopsCount];
float [] stops = new float[stopsCount];
float height = box.height();
float width = box.width();
float midX = box.centerX();
float midY = box.centerY();
float offsetX = (midX - width / 2);
float offsetY = (midY - height / 2);
parseGradientStops(colors, stopsCount, stops, stopsColors, startColorsPosition);
if (colorType == 1) {
float x1 = PropHelper.fromPercentageToFloat(colors.getString(1), width, offsetX, mScale);
float y1 = PropHelper.fromPercentageToFloat(colors.getString(2), height, offsetY, mScale);
float x2 = PropHelper.fromPercentageToFloat(colors.getString(3), width, offsetX, mScale);
float y2 = PropHelper.fromPercentageToFloat(colors.getString(4), height, offsetY, mScale);
paint.setShader( paint.setShader(
new LinearGradient( new LinearGradient(
colors[1] * mScale, x1,
colors[2] * mScale, y1,
colors[3] * mScale, x2,
colors[4] * mScale, y2,
stopsColors, stopsColors,
stops, stops,
Shader.TileMode.CLAMP)); Shader.TileMode.CLAMP));
break; } else {
case 2: float rx = PropHelper.fromPercentageToFloat(colors.getString(3), width, 0f, mScale);
stopsCount = (colors.length - 7) / 5; float ry = PropHelper.fromPercentageToFloat(colors.getString(4), height, 0f, mScale);
stopsColors = new int [stopsCount]; float cx = PropHelper.fromPercentageToFloat(colors.getString(5), width, offsetX, mScale);
stops = new float[stopsCount]; float cy = PropHelper.fromPercentageToFloat(colors.getString(6), height, offsetY, mScale) / (ry / rx);
parseGradientStops(colors, stopsCount, stops, stopsColors, 7); // TODO: do not support focus point.
float fx = PropHelper.fromPercentageToFloat(colors.getString(1), width, offsetX, mScale);
// TODO: support focus float fy = PropHelper.fromPercentageToFloat(colors.getString(2), height, offsetY, mScale) / (ry / rx);
float focusX = colors[1];
float focusY = colors[2];
float radius = colors[3];
float radiusRatio = colors[4] / radius;
Shader radialGradient = new RadialGradient( Shader radialGradient = new RadialGradient(
colors[5] * mScale, cx,
colors[6] * mScale / radiusRatio, cy,
radius * mScale, rx,
stopsColors, stopsColors,
stops, stops,
Shader.TileMode.CLAMP Shader.TileMode.CLAMP
); );
Matrix radialMatrix = new Matrix(); Matrix radialMatrix = new Matrix();
radialMatrix.preScale(1f, ry / rx);
// seems like a bug here?
radialMatrix.preScale(1f, radiusRatio);
radialGradient.setLocalMatrix(radialMatrix); radialGradient.setLocalMatrix(radialMatrix);
paint.setShader(radialGradient); paint.setShader(radialGradient);
break; }
default: } else {
// TODO: Support pattern. // TODO: Support pattern.
FLog.w(ReactConstants.TAG, "RNSVG: Color type " + colorType + " not supported!"); FLog.w(ReactConstants.TAG, "RNSVG: Color type " + colorType + " not supported!");
} }

View File

@@ -27,6 +27,7 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
/* package */ static final String CLASS_GROUP = "RNSVGGroup"; /* package */ static final String CLASS_GROUP = "RNSVGGroup";
/* package */ static final String CLASS_SVG = "RNSVGPath"; /* package */ static final String CLASS_SVG = "RNSVGPath";
/* package */ static final String CLASS_TEXT = "RNSVGText"; /* package */ static final String CLASS_TEXT = "RNSVGText";
/* package */ static final String CLASS_SHAPE = "RNSVGShape";
private final String mClassName; private final String mClassName;
@@ -42,6 +43,10 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
return new RNSVGRenderableViewManager(CLASS_TEXT); return new RNSVGRenderableViewManager(CLASS_TEXT);
} }
public static RNSVGRenderableViewManager createRNSVGShapeViewManager() {
return new RNSVGRenderableViewManager(CLASS_SHAPE);
}
private RNSVGRenderableViewManager(String className) { private RNSVGRenderableViewManager(String className) {
mClassName = className; mClassName = className;
} }
@@ -57,6 +62,8 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
return new RNSVGGroupShadowNode(); return new RNSVGGroupShadowNode();
} else if (mClassName == CLASS_SVG) { } else if (mClassName == CLASS_SVG) {
return new RNSVGPathShadowNode(); return new RNSVGPathShadowNode();
} else if (mClassName == CLASS_SHAPE) {
return new RNSVGShapeShadowNode();
} else if (mClassName == CLASS_TEXT) { } else if (mClassName == CLASS_TEXT) {
return new RNSVGTextShadowNode(); return new RNSVGTextShadowNode();
} else { } else {
@@ -70,9 +77,11 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
return RNSVGGroupShadowNode.class; return RNSVGGroupShadowNode.class;
} else if (mClassName == CLASS_SVG) { } else if (mClassName == CLASS_SVG) {
return RNSVGPathShadowNode.class; return RNSVGPathShadowNode.class;
} else if (mClassName == CLASS_SHAPE) {
return RNSVGShapeShadowNode.class;
} else if (mClassName == CLASS_TEXT) { } else if (mClassName == CLASS_TEXT) {
return RNSVGTextShadowNode.class; return RNSVGTextShadowNode.class;
} else { }else {
throw new IllegalStateException("Unexpected type " + mClassName); throw new IllegalStateException("Unexpected type " + mClassName);
} }
} }

View File

@@ -0,0 +1,143 @@
/**
* 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.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Log;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.annotations.ReactProp;
import javax.annotation.Nullable;
/**
* Shadow node for virtual RNSVGPath view
*/
public class RNSVGShapeShadowNode extends RNSVGPathShadowNode {
protected ReadableMap mShape;
@ReactProp(name = "shape")
public void setShape(@Nullable ReadableMap shape) {
mShape = shape;
markUpdated();
}
@Override
public void draw(Canvas canvas, Paint paint, float opacity) {
if (mShape != null) {
int type = mShape.getInt("type");
Rect box = canvas.getClipBounds();
float height = box.height();
float width = box.width();
mPath = new Path();
switch (type) {
case 0: {
// draw circle
// TODO:
float cx = getActualProp("cx", width);
float cy = getActualProp("cy", height);
float r;
ReadableMap value = mShape.getMap("r");
if (value.getBoolean("percentage")) {
float percent = (float)value.getDouble("value");
float powX = (float)Math.pow((width * percent), 2);
float powY = (float)Math.pow((height*percent), 2);
r = (float)Math.sqrt(powX + powY) / (float)Math.sqrt(2);
} else {
r = (float)value.getDouble("value") * mScale;
}
mPath.addCircle(cx, cy, r, Path.Direction.CW);
break;
}
case 1: {
// draw ellipse
float cx = getActualProp("cx", width);
float cy = getActualProp("cy", height);
float rx = getActualProp("rx", width);
float ry = getActualProp("ry", height);
RectF oval = new RectF(cx - rx, cy - ry, cx + rx, cy + ry);
mPath.addOval(oval, Path.Direction.CW);
break;
}
case 2: {
// draw line
float x1 = getActualProp("x1", width);
float y1 = getActualProp("y1", height);
float x2 = getActualProp("x2", width);
float y2 = getActualProp("y2", height);
mPath.moveTo(x1, y1);
mPath.lineTo(x2, y2);
break;
}
case 3: {
// draw rect
float x = getActualProp("x", width);
float y = getActualProp("y", height);
float w = getActualProp("width", width);
float h = getActualProp("height", height);
float rx = getActualProp("rx", width);
float ry = getActualProp("ry", height);
if (rx != 0 || ry != 0) {
if (rx == 0) {
rx = ry;
} else if (ry == 0) {
ry = rx;
}
if (rx > w / 2) {
rx = w / 2;
}
if (ry > h / 2) {
ry = h / 2;
}
mPath.addRoundRect(new RectF(x, y, x + w, y + h), rx, ry, Path.Direction.CW);
} else {
mPath.addRect(x, y, x + w, y + h, Path.Direction.CW);
}
break;
}
default:
FLog.e(ReactConstants.TAG, "RNSVG: Invalid Shape type " + type + " at " + mShape);
}
RectF shapeBox = new RectF();
mPath.computeBounds(shapeBox, true);
mContentBoundingBox = shapeBox;
super.draw(canvas, paint, opacity);
}
}
private float getActualProp(String name, float relative) {
if (mShape.hasKey(name)) {
ReadableMap value = mShape.getMap(name);
if (value.getBoolean("percentage")) {
return (float)value.getDouble("value") * relative;
} else {
return (float)value.getDouble("value") * mScale;
}
} else {
return 0f;
}
}
}

View File

@@ -14,8 +14,11 @@ import javax.annotation.Nullable;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Path; import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableArray;
@@ -89,7 +92,11 @@ public class RNSVGTextShadowNode extends RNSVGPathShadowNode {
lines[i] = linesProp.getString(i); lines[i] = linesProp.getString(i);
} }
String text = TextUtils.join("\n", lines); String text = TextUtils.join("\n", lines);
if (setupStrokePaint(paint, opacity)) {
Rect bound = new Rect();
paint.getTextBounds(text, 0, text.length(), bound);
RectF box = new RectF(bound);
if (setupStrokePaint(paint, opacity, box)) {
applyTextPropertiesToPaint(paint); applyTextPropertiesToPaint(paint);
if (mPath == null) { if (mPath == null) {
canvas.drawText(text, 0, -paint.ascent(), paint); canvas.drawText(text, 0, -paint.ascent(), paint);
@@ -97,7 +104,7 @@ public class RNSVGTextShadowNode extends RNSVGPathShadowNode {
canvas.drawTextOnPath(text, mPath, 0, 0, paint); canvas.drawTextOnPath(text, mPath, 0, 0, paint);
} }
} }
if (setupFillPaint(paint, opacity)) { if (setupFillPaint(paint, opacity, box)) {
applyTextPropertiesToPaint(paint); applyTextPropertiesToPaint(paint);
if (mPath == null) { if (mPath == null) {
canvas.drawText(text, 0, -paint.ascent(), paint); canvas.drawText(text, 0, -paint.ascent(), paint);

View File

@@ -30,6 +30,7 @@ public class RNSvgPackage implements ReactPackage {
RNSVGRenderableViewManager.createRNSVGGroupViewManager(), RNSVGRenderableViewManager.createRNSVGGroupViewManager(),
RNSVGRenderableViewManager.createRNSVGPathViewManager(), RNSVGRenderableViewManager.createRNSVGPathViewManager(),
RNSVGRenderableViewManager.createRNSVGTextViewManager(), RNSVGRenderableViewManager.createRNSVGTextViewManager(),
RNSVGRenderableViewManager.createRNSVGShapeViewManager(),
new RNSVGSvgViewManager()); new RNSVGSvgViewManager());
} }

View File

@@ -2,34 +2,28 @@ import React, {
Component, Component,
PropTypes PropTypes
} from 'react-native'; } from 'react-native';
import Ellipse from './Ellipse'; import Shape, {CIRCLE} from './Shape';
let propType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); import {circleProps, pathProps, fillProps, strokeProps} from '../lib/props';
class Circle extends Component{ import _ from 'lodash';
class Circle extends Shape{
static displayName = 'Circle'; static displayName = 'Circle';
static propTypes = { static propTypes = {
cx: propType, ...pathProps,
cy: propType, ...circleProps
r: propType
};
static defaultProps = {
cx: 0,
cy: 0
}; };
static getPath = props => Ellipse.getPath({ static contextTypes = {
cx: props.cx, ...fillProps,
cy: props.cy, ...strokeProps,
rx: props.r, ...circleProps,
ry: props.r isInGroup: PropTypes.bool
}); };
render() { constructor() {
return <Ellipse super(...arguments);
{...this.props} this.type = CIRCLE;
rx={+this.props.r} };
ry={+this.props.r}
/>
}
} }
export default Circle; export default Circle;

View File

@@ -61,11 +61,10 @@ class DefsUse extends Component{
if (matched) { if (matched) {
let template = map[matched[1] + ':' + this.props.svgId]; let template = map[matched[1] + ':' + this.props.svgId];
if (template) { if (template) {
let props = { return cloneElement(template, {
...this.props, ...this.props,
href: null href: null
}; });
return cloneElement(template, props);
} }
} }
} }

View File

@@ -2,35 +2,27 @@ import React, {
Component, Component,
PropTypes PropTypes
} from 'react-native'; } from 'react-native';
import Path from './Path'; import Shape, {ELLIPSE} from './Shape';
let propType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); import {ellipseProps, pathProps, fillProps, strokeProps} from '../lib/props';
class Ellipse extends Component{
class Ellipse extends Shape{
static displayName = 'Ellipse'; static displayName = 'Ellipse';
static propTypes = { static propTypes = {
cx: propType, ...pathProps,
cy: propType, ...ellipseProps
rx: propType,
ry: propType
}; };
static getPath = props => { static contextTypes = {
let {cx, cy, rx, ry} = props; ...fillProps,
return ` ...strokeProps,
M ${cx - rx} ${cy} ...ellipseProps,
a ${rx}, ${ry} 0 1, 0 ${rx * 2}, 0 isInGroup: PropTypes.bool
a ${rx}, ${ry} 0 1, 0 ${-rx * 2}, 0
Z
`;
}; };
render() { constructor() {
let {props} = this; super(...arguments);
let d = Ellipse.getPath(this.props); this.type = ELLIPSE;
return <Path };
{...props}
d={d}
/>;
}
} }
export default Ellipse; export default Ellipse;

View File

@@ -2,42 +2,34 @@ import React, {
Component, Component,
Children, Children,
cloneElement, cloneElement,
PropTypes,
requireNativeComponent requireNativeComponent
} from 'react-native'; } from 'react-native';
import createReactNativeComponentClass from 'react-native/Libraries/ReactNative/createReactNativeComponentClass'; import createReactNativeComponentClass from 'react-native/Libraries/ReactNative/createReactNativeComponentClass';
import Defs from './Defs'; import Defs from './Defs';
import _ from 'lodash';
import {GroupAttributes} from '../lib/attributes'; import {GroupAttributes} from '../lib/attributes';
import {numberProp, contextProps, textProps} from '../lib/props';
const transformProps = {
scale: null,
scaleX: null,
scaleY: null,
rotate: null,
transform: null,
x: null,
y: null,
originX: null,
originY: null
};
const clipProps = {
clipPath: null,
clipRule: null
};
import extractProps from '../lib/extract/extractProps'; import extractProps from '../lib/extract/extractProps';
class G extends Component{ class G extends Component{
static displayName = 'G'; static displayName = 'G';
getChildren = () => { static childContextTypes = {
return Children.map(this.props.children, child => cloneElement(child, { svgId: numberProp,
...this.props, isInGroup: PropTypes.bool,
...transformProps, ...contextProps
...clipProps, };
...child.props,
id: null getChildContext = () => {
})); return _.reduce(contextProps, (props, value, key) => {
props[key] = this.props[key];
return props;
}, {
svgId: this.props.svgId,
isInGroup: true
});
}; };
render() { render() {
@@ -56,7 +48,7 @@ class G extends Component{
return <NativeGroup return <NativeGroup
{...extractProps(this.props, {transform: true})} {...extractProps(this.props, {transform: true})}
> >
{this.getChildren()} {this.props.children}
</NativeGroup>; </NativeGroup>;
} }
} }

View File

@@ -5,7 +5,6 @@ import React, {
} from 'react-native'; } from 'react-native';
import {NativeGroup} from './G'; import {NativeGroup} from './G';
import {set, remove} from '../lib/extract/patterns'; import {set, remove} from '../lib/extract/patterns';
import percentFactory from '../lib/percentFactory';
import percentToFloat from '../lib/percentToFloat'; import percentToFloat from '../lib/percentToFloat';
import Stop from './Stop'; import Stop from './Stop';
import Color from 'color'; import Color from 'color';
@@ -28,7 +27,7 @@ class RadialGradient extends Component{
remove(this.id); remove(this.id);
}; };
render(distanceProps, fromPercent, fromNumber) { render(generator) {
let stops = {}; let stops = {};
Children.forEach(this.props.children, child => { Children.forEach(this.props.children, child => {
if (child.type === Stop && child.props.stopColor && child.props.offset) { if (child.type === Stop && child.props.stopColor && child.props.offset) {
@@ -37,13 +36,7 @@ class RadialGradient extends Component{
// add stop // add stop
stops[offset] = Color(child.props.stopColor).alpha(+child.props.stopOpacity); stops[offset] = Color(child.props.stopColor).alpha(+child.props.stopOpacity);
set(this.id, generator.bind(null, stops));
let factories = percentFactory(...distanceProps);
if (factories) {
set(this.id, fromPercent.bind(null, factories, stops));
} else {
set(this.id, fromNumber.bind(null, stops));
}
} else { } else {
console.warn(`'RadialGradient' can only receive 'Stop' elements as children`); console.warn(`'RadialGradient' can only receive 'Stop' elements as children`);
} }

View File

@@ -2,30 +2,27 @@ import React, {
Component, Component,
PropTypes PropTypes
} from 'react-native'; } from 'react-native';
import Path from './Path'; import Shape, {LINE} from './Shape';
import {lineProps, pathProps, fillProps, strokeProps} from '../lib/props';
let propType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); class Line extends Shape{
class Line extends Component{
static displayName = 'Line'; static displayName = 'Line';
static propTypes = { static propTypes = {
x1: propType, ...pathProps,
x2: propType, ...lineProps
y1: propType,
y2: propType,
strokeLinecap: PropTypes.oneOf(['butt', 'square', 'round'])
}; };
static getPath = (props) => ( static contextTypes = {
`M${props.x1},${props.y1} L${props.x2},${props.y2}` ...fillProps,
); ...strokeProps,
...lineProps,
isInGroup: PropTypes.bool
};
render() { constructor() {
return <Path super(...arguments);
{...this.props} this.type = LINE;
d={Line.getPath(this.props)} };
/>;
}
} }
export default Line; export default Line;

View File

@@ -5,32 +5,13 @@ import React, {
} from 'react-native'; } from 'react-native';
import stopsOpacity from '../lib/stopsOpacity'; import stopsOpacity from '../lib/stopsOpacity';
import numberProp from '../lib/numberProp'; import {numberProp} from '../lib/props';
import Gradient from './Gradient'; import Gradient from './Gradient';
import {LINEAR_GRADIENT} from '../lib/extract/extractBrush'; import {LINEAR_GRADIENT} from '../lib/extract/extractBrush';
import insertColorStopsIntoArray from '../lib/insertProcessor'; import insertColorStopsIntoArray from '../lib/insertProcessor';
function LinearGradientGenerator(stops, x1, y1, x2, y2) { function LinearGradientGenerator(stops, x1, y1, x2, y2) {
var type = LINEAR_GRADIENT; var brushData = [LINEAR_GRADIENT, x1, y1, x2, y2];
if (arguments.length < 5) {
var angle = ((x1 == null) ? 270 : x1) * Math.PI / 180;
var x = Math.cos(angle);
var y = -Math.sin(angle);
var l = (Math.abs(x) + Math.abs(y)) / 2;
x *= l; y *= l;
x1 = 0.5 - x;
x2 = 0.5 + x;
y1 = 0.5 - y;
y2 = 0.5 + y;
this._bb = true;
} else {
this._bb = false;
}
var brushData = [type, +x1, +y1, +x2, +y2];
insertColorStopsIntoArray(stops, brushData, 5); insertColorStopsIntoArray(stops, brushData, 5);
this._brush = brushData; this._brush = brushData;
} }
@@ -53,20 +34,9 @@ class LinearGradient extends Gradient{
x2, x2,
y2 y2
} = this.props; } = this.props;
let gradientProps = [x1, y1, x2, y2];
return super.render( return super.render(
gradientProps, (stops, opacity) => {
function (factories, stops, boundingBox, opacity) { return new LinearGradientGenerator(stopsOpacity(stops, opacity), ...[x1, y1, x2, y2]);
return new LinearGradientGenerator(
stopsOpacity(stops, opacity),
factories[0](boundingBox.width),
factories[1](boundingBox.height),
factories[2](boundingBox.width),
factories[3](boundingBox.height)
);
},
function (stops, opacity) {
return new LinearGradientGenerator(stopsOpacity(stops, opacity), ...gradientProps);
} }
); );
} }

View File

@@ -1,34 +1,31 @@
import React, { import React, {
Component, Component,
PropTypes, PropTypes,
requireNativeComponent, requireNativeComponent
cloneElement
} from 'react-native'; } from 'react-native';
import Defs from './Defs'; import Defs from './Defs';
import createReactNativeComponentClass from 'react-native/Libraries/ReactNative/createReactNativeComponentClass'; import createReactNativeComponentClass from 'react-native/Libraries/ReactNative/createReactNativeComponentClass';
import calculateBoundingBox from '../lib/calculateBoundingBox';
import extractProps from '../lib/extract/extractProps'; import extractProps from '../lib/extract/extractProps';
import SerializablePath from 'react-native/Libraries/ART/ARTSerializablePath'; import SerializablePath from 'react-native/Libraries/ART/ARTSerializablePath';
import {PathAttributes} from '../lib/attributes'; import {PathAttributes} from '../lib/attributes';
import {pathProps} from '../lib/props';
let propType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); let propType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
class Path extends Component{ class Path extends Component{
static displayName = 'Path'; static displayName = 'Path';
static propTypes = { static propTypes = {
visible: PropTypes.bool, visible: PropTypes.bool,
d: PropTypes.string, d: PropTypes.string,
x: propType, ...pathProps
y: propType, };
strokeLinecap: PropTypes.oneOf(['butt', 'square', 'round']),
strokeCap: PropTypes.oneOf(['butt', 'square', 'round']), static contextTypes = {
strokeLinejoin: PropTypes.oneOf(['miter', 'bevel', 'round']), ...pathProps,
strokeJoin: PropTypes.oneOf(['miter', 'bevel', 'round']), isInGroup: PropTypes.bool
strokeDasharray: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.number)])
}; };
static getPath = props => props.d;
_dimensions = null; _dimensions = null;
@@ -38,16 +35,15 @@ class Path extends Component{
} }
}; };
getBoundingBox = () => {
if (!this._dimensions) {
this._dimensions = calculateBoundingBox(this.props.d);
}
return this._dimensions;
};
render() { render() {
let {props} = this; let {props} = this;
if (this.context.isInGroup) {
props = _.defaults(this.context, props, {
isInGroup: null
});
}
if (props.id) { if (props.id) {
return <Defs.Item return <Defs.Item
id={props.id} id={props.id}
@@ -62,7 +58,7 @@ class Path extends Component{
return ( return (
<NativePath <NativePath
{...extractProps.call(this, props)} {...extractProps(props)}
d={d} d={d}
/> />
); );

View File

@@ -4,36 +4,15 @@ import React, {
Children Children
} from 'react-native'; } from 'react-native';
import stopsOpacity from '../lib/stopsOpacity'; import stopsOpacity from '../lib/stopsOpacity';
import numberProp from '../lib/numberProp'; import {numberProp} from '../lib/props';
import Gradient from './Gradient'; import Gradient from './Gradient';
import {RADIAL_GRADIENT} from '../lib/extract/extractBrush'; import {RADIAL_GRADIENT} from '../lib/extract/extractBrush';
import insertColorStopsIntoArray from '../lib/insertProcessor'; import insertColorStopsIntoArray from '../lib/insertProcessor';
function RadialGradientGenerator(stops, fx, fy, rx, ry, cx, cy) { function RadialGradientGenerator(stops, fx, fy, rx, ry, cx, cy) {
if (ry == null) { var brushData = [RADIAL_GRADIENT, fx, fy, rx, ry, cx, cy];
ry = rx; insertColorStopsIntoArray(stops, brushData, 7);
}
if (cx == null) {
cx = fx;
}
if (cy == null) {
cy = fy;
}
if (fx == null) {
// As a convenience we allow the whole radial gradient to cover the
// bounding box. We should consider dropping this API.
fx = fy = rx = ry = cx = cy = 0.5;
this._bb = true;
} else {
this._bb = false;
}
// The ART API expects the radial gradient to be repeated at the edges.
// To simulate this we render the gradient twice as large and add double
// color stops. Ideally this API would become more restrictive so that this
// extra work isn't needed.
var brushData = [RADIAL_GRADIENT, +fx, +fy, +rx * 2, +ry * 2, +cx, +cy];
insertColorStopsIntoArray(stops, brushData, 7, 0.5);
this._brush = brushData; this._brush = brushData;
} }
@@ -60,24 +39,10 @@ class RadialGradient extends Gradient{
cy, cy,
r r
} = this.props; } = this.props;
let gradientProps = [fx, fy, rx || r, ry || r, cx, cy];
return super.render( return super.render(
gradientProps, (stops, opacity) => {
function (factories, stops, boundingBox, opacity) {
let {x1,y1,width, height} = boundingBox; return new RadialGradientGenerator(stopsOpacity(stops, opacity), ...[fx, fy, rx || r, ry || r, cx, cy]);
return new RadialGradientGenerator(
stopsOpacity(stops, opacity),
x1 + factories[0](width),
y1 + factories[1](height),
factories[2](width),
factories[3](height),
x1 + factories[4](width),
y1 + factories[5](height)
);
},
function (stops, opacity) {
return new RadialGradientGenerator(stopsOpacity(stops, opacity), ...gradientProps);
} }
); );
} }

View File

@@ -2,115 +2,29 @@ import React, {
Component, Component,
PropTypes PropTypes
} from 'react-native'; } from 'react-native';
import Shape, {RECT} from './Shape';
import {rectProps, pathProps, fillProps, strokeProps} from '../lib/props';
import Path from './Path'; class Rect extends Shape{
let propType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
function processRadius(radius) {
radius = +radius;
return radius || 0;
}
function getR(props) {
let {
width,
height,
rx,
ry
} = props;
rx = processRadius(rx);
ry = processRadius(ry);
if ((rx && !ry) || (ry && !rx)) {
if (rx) {
ry = rx;
} else {
rx = ry;
}
}
if (rx > width / 2) {
rx = width / 2;
}
if (ry > height / 2) {
ry = height / 2;
}
return {
rx,
ry
};
}
class Rect extends Component{
static displayName = 'Rect'; static displayName = 'Rect';
static propTypes = { static propTypes = {
x: propType, ...pathProps,
y: propType, ...rectProps
width: propType,
height: propType,
rx: propType,
ry: propType,
strokeLinecap: PropTypes.oneOf(['butt', 'square', 'round']),
strokeCap: PropTypes.oneOf(['butt', 'square', 'round']),
strokeLinejoin: PropTypes.oneOf(['miter', 'bevel', 'round']),
strokeJoin: PropTypes.oneOf(['miter', 'bevel', 'round'])
};
static defaultProps = {
x: 0,
y: 0,
rx: 0,
ry: 0
};
static getPath = (props, r) => {
let {
x,
y,
width,
height
} = props;
if (!r) {
r = getR(props);
}
let {rx, ry} = r;
return (rx || ry) ? `
M ${x}, ${y}
h ${width - 2 * rx}
a ${rx},${ry} 0 0 1 ${rx},${ry}
v ${height - 2 * ry}
a ${rx},${ry} 0 0 1 ${-rx},${ry}
h ${2 * rx - width}
a ${rx},${ry} 0 0 1 ${-rx},${-ry}
v ${2 * ry - height}
a ${rx},${ry} 0 0 1 ${rx},${-ry}
Z
` : `
M ${x}, ${y}
h ${width}
v ${height}
h ${-width}
Z
`;
}; };
render() { static contextTypes = {
let r = getR(this.props); ...fillProps,
return <Path ...strokeProps,
{...this.props} ...rectProps,
width={null} isInGroup: PropTypes.bool
height={null} };
x={r.rx || null}
y={null} constructor() {
d={Rect.getPath(this.props, r)} super(...arguments);
/>; this.type = RECT;
} };
} }
export default Rect; export default Rect;

87
elements/Shape.js Normal file
View File

@@ -0,0 +1,87 @@
import React, {
Component,
PropTypes
} from 'react-native';
import Path from './Path';
import _ from 'lodash';
import extractProps from '../lib/extract/extractProps';
import {ShapeAttributes} from '../lib/attributes';
import SerializableShape from '../lib/SerializableShape';
import createReactNativeComponentClass from 'react-native/Libraries/ReactNative/createReactNativeComponentClass';
import {circleProps, ellipseProps, lineProps, rectProps} from '../lib/props';
/**
* Circle shape type
*/
const CIRCLE = 0;
/**
* ELLIPSE shape type
*/
const ELLIPSE = 1;
/**
* LINE shape type
*/
const LINE = 2;
/**
* RECT shape type
*/
const RECT = 3;
/**
* coord props
*/
const COORD_PROPS = {
/**
* algorithm for radius in percentage
* radius = Math.sqrt(Math.pow((width*percent), 2) + Math.pow((height*percent), 2)) / Math.sqrt(2);
*/
[CIRCLE]: Object.keys(circleProps),
[ELLIPSE]: Object.keys(ellipseProps),
[LINE]: Object.keys(lineProps),
[RECT]: Object.keys(rectProps)
};
/**
* virtualNode component for shape elements
*/
class Shape extends Component{
static displayName = 'Shape';
render() {
let props = this.props;
if (this.context.isInGroup) {
props = _.defaults({}, props, this.context, {
isInGroup: null
});
}
let shape = new SerializableShape(props, COORD_PROPS[this.type]).toJSON();
return <NativePath
{...extractProps(this.type === 3 ? {
...props,
x: null,
y: null
} : props)}
shape={{
...shape,
type: this.type
}}
/>;
};
}
let NativePath = createReactNativeComponentClass({
validAttributes: ShapeAttributes,
uiViewClassName: 'RNSVGShape'
});
export default Shape;
export {
CIRCLE,
ELLIPSE,
LINE,
RECT
};

View File

@@ -6,7 +6,6 @@ import React, {
View, View,
requireNativeComponent requireNativeComponent
} from 'react-native'; } from 'react-native';
import extractViewbox from '../lib/extract/extractViewbox';
import ViewBox from './ViewBox'; import ViewBox from './ViewBox';
import _ from 'lodash'; import _ from 'lodash';

View File

@@ -5,7 +5,6 @@ import React, {
import ViewBox from './ViewBox'; import ViewBox from './ViewBox';
import Defs from './Defs'; import Defs from './Defs';
import extractViewbox from '../lib/extract/extractViewbox';
class SymbolElement extends Component{ class SymbolElement extends Component{
static displayName = 'Symbol'; static displayName = 'Symbol';
static propType = { static propType = {

View File

@@ -1,24 +1,41 @@
import React, { import React, {
Component Component,
PropTypes
} from 'react-native'; } from 'react-native';
import Defs from './Defs'; import Defs from './Defs';
import createReactNativeComponentClass from 'react-native/Libraries/ReactNative/createReactNativeComponentClass'; import createReactNativeComponentClass from 'react-native/Libraries/ReactNative/createReactNativeComponentClass';
import extractProps from '../lib/extract/extractProps'; import extractProps from '../lib/extract/extractProps';
import extractText from '../lib/extract/extractText'; import extractText from '../lib/extract/extractText';
import {TextAttributes} from '../lib/attributes'; import {TextAttributes} from '../lib/attributes';
import numberProp from '../lib/numberProp'; import {numberProp, textProps, fillProps, strokeProps, pathProps} from '../lib/props';
class Text extends Component{ class Text extends Component{
static displayName = 'Text'; static displayName = 'Text';
static propTypes = { static propTypes = {
x: numberProp,
y: numberProp,
dx: numberProp, dx: numberProp,
dy: numberProp dy: numberProp,
...textProps,
...fillProps,
...strokeProps,
...pathProps
}; };
static contextTypes = {
...textProps,
...fillProps,
...strokeProps,
isInGroup: PropTypes.bool
};
render() { render() {
let {props} = this; let {props} = this;
if (this.context.isInGroup) {
props = _.defaults(this.context, props, {
isInGroup: null
});
}
let x = 0; let x = 0;
if (props.x) { if (props.x) {
x = props.dx ? +props.x + (+props.dx) : +props.x; x = props.dx ? +props.x + (+props.dx) : +props.x;

View File

@@ -1,7 +1,6 @@
import React, { import React, {
Component, Component,
PropTypes, PropTypes
cloneElement
} from 'react-native'; } from 'react-native';
import Defs from './Defs'; import Defs from './Defs';
class Use extends Component{ class Use extends Component{

View File

@@ -25,6 +25,7 @@ class ViewBox extends Component{
x = viewbox.x; x = viewbox.x;
y = viewbox.y; y = viewbox.y;
} }
return <G return <G
{...this.props} {...this.props}
x={x} x={x}

View File

@@ -10,6 +10,9 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
@interface RNSVGBrush : NSObject @interface RNSVGBrush : NSObject
{
NSArray *_points;
}
/* @abstract */ /* @abstract */
- (instancetype)initWithArray:(NSArray *)data NS_DESIGNATED_INITIALIZER; - (instancetype)initWithArray:(NSArray *)data NS_DESIGNATED_INITIALIZER;
@@ -26,6 +29,8 @@
- (BOOL)applyStrokeColor:(CGContextRef)context; - (BOOL)applyStrokeColor:(CGContextRef)context;
- (CGFloat)getActualProp:(int)percent relative:(CGFloat)relative offset:(CGFloat)offset;
/** /**
* paint fills the context with a brush. The context is assumed to * paint fills the context with a brush. The context is assumed to
* be clipped. * be clipped.

View File

@@ -29,6 +29,30 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
return NO; return NO;
} }
- (CGFloat)getActualProp:(int)index relative:(CGFloat)relative offset:(CGFloat)offset
{
id coord = [_points objectAtIndex:index];
if ([coord isKindOfClass:[NSString class]]) {
__block float matched = [coord floatValue];
NSString *percentage = (NSString *)coord;
NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:@"^(\\-?\\d+(?:\\.\\d+)?)%$" options:0 error:NULL];
[regex enumerateMatchesInString:percentage
options:0
range:NSMakeRange(0, percentage.length)
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop)
{
matched = [[percentage substringWithRange:NSMakeRange(result.range.location, result.range.length)] floatValue];
matched = matched / 100 * relative + offset;
}];
return matched;
} else {
return [coord floatValue];
}
}
- (void)paint:(CGContextRef)context - (void)paint:(CGContextRef)context
{ {
// abstract // abstract

View File

@@ -14,11 +14,9 @@
@implementation RNSVGLinearGradient @implementation RNSVGLinearGradient
{ {
CGGradientRef _gradient; CGGradientRef _gradient;
CGPoint _startPoint;
CGPoint _endPoint;
} }
- (instancetype)initWithArray:(NSArray<NSNumber *> *)array - (instancetype)initWithArray:(NSArray *)array
{ {
if ((self = [super initWithArray:array])) { if ((self = [super initWithArray:array])) {
if (array.count < 5) { if (array.count < 5) {
@@ -26,8 +24,8 @@
self.class, NSStringFromSelector(_cmd), array); self.class, NSStringFromSelector(_cmd), array);
return nil; return nil;
} }
_startPoint = [RCTConvert CGPoint:array offset:1];
_endPoint = [RCTConvert CGPoint:array offset:3]; _points = [array subarrayWithRange:NSMakeRange(1, 4)];
_gradient = CGGradientRetain([RCTConvert CGGradient:array offset:5]); _gradient = CGGradientRetain([RCTConvert CGGradient:array offset:5]);
} }
return self; return self;
@@ -42,7 +40,20 @@
{ {
CGGradientDrawingOptions extendOptions = CGGradientDrawingOptions extendOptions =
kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation;
CGContextDrawLinearGradient(context, _gradient, _startPoint, _endPoint, extendOptions);
CGRect box = CGContextGetClipBoundingBox(context);
float height = CGRectGetHeight(box);
float width = CGRectGetWidth(box);
float midX = CGRectGetMidX(box);
float midY = CGRectGetMidY(box);
float offsetX = (midX - width / 2);
float offsetY = (midY - height / 2);
CGFloat x1 = [self getActualProp:0 relative:width offset:offsetX];
CGFloat y1 = [self getActualProp:1 relative:height offset:offsetY];
CGFloat x2 = [self getActualProp:2 relative:width offset:offsetX];
CGFloat y2 = [self getActualProp:3 relative:height offset:offsetY];
CGContextDrawLinearGradient(context, _gradient, CGPointMake(x1, y1), CGPointMake(x2, y2), extendOptions);
} }
@end @end

View File

@@ -14,13 +14,9 @@
@implementation RNSVGRadialGradient @implementation RNSVGRadialGradient
{ {
CGGradientRef _gradient; CGGradientRef _gradient;
CGPoint _focusPoint;
CGPoint _centerPoint;
CGFloat _radius;
CGFloat _radiusRatio;
} }
- (instancetype)initWithArray:(NSArray<NSNumber *> *)array - (instancetype)initWithArray:(NSArray *)array
{ {
if ((self = [super initWithArray:array])) { if ((self = [super initWithArray:array])) {
if (array.count < 7) { if (array.count < 7) {
@@ -28,13 +24,23 @@
self.class, NSStringFromSelector(_cmd), array); self.class, NSStringFromSelector(_cmd), array);
return nil; return nil;
} }
_radius = [RCTConvert CGFloat:array[3]]; _points = [array subarrayWithRange:NSMakeRange(1, 6)];
_radiusRatio = [RCTConvert CGFloat:array[4]] / _radius;
_focusPoint.x = [RCTConvert CGFloat:array[1]]; NSMutableArray *gradient = [[NSMutableArray alloc] init];
_focusPoint.y = [RCTConvert CGFloat:array[2]] / _radiusRatio; int count = ([array count] - 7) / 5;
_centerPoint.x = [RCTConvert CGFloat:array[5]]; int startStops = [array count] - count;
_centerPoint.y = [RCTConvert CGFloat:array[6]] / _radiusRatio; for (int i = 0; i < count; i++) {
_gradient = CGGradientRetain([RCTConvert CGGradient:array offset:7]); [gradient insertObject:[array objectAtIndex:(i * 4 + 7)] atIndex:(i * 4)];
[gradient insertObject:[array objectAtIndex:(i * 4 + 8)] atIndex:(i * 4 + 1)];
[gradient insertObject:[array objectAtIndex:(i * 4 + 9)] atIndex:(i * 4 + 2)];
[gradient insertObject:[array objectAtIndex:(i * 4 + 10)] atIndex:(i * 4 + 3)];
NSNumber *stop = [NSNumber numberWithFloat:[[array objectAtIndex:(startStops + (count - i - 1))] floatValue]];
[gradient insertObject:stop atIndex:i * 4 + 4];
}
_gradient = CGGradientRetain([RCTConvert CGGradient:gradient offset:0]);
} }
return self; return self;
} }
@@ -46,10 +52,26 @@
- (void)paint:(CGContextRef)context - (void)paint:(CGContextRef)context
{ {
CGAffineTransform transform = CGAffineTransformMakeScale(1, _radiusRatio);
CGRect box = CGContextGetClipBoundingBox(context);
float height = CGRectGetHeight(box);
float width = CGRectGetWidth(box);
float midX = CGRectGetMidX(box);
float midY = CGRectGetMidY(box);
float offsetX = (midX - width / 2);
float offsetY = (midY - height / 2);
CGFloat rx = [self getActualProp:2 relative:width offset:0];
CGFloat ry = [self getActualProp:3 relative:height offset:0];
CGFloat fx = [self getActualProp:0 relative:width offset:offsetX];
CGFloat fy = [self getActualProp:1 relative:height offset:offsetY] / (ry / rx);
CGFloat cx = [self getActualProp:4 relative:width offset:offsetX];
CGFloat cy = [self getActualProp:5 relative:height offset:offsetY] / (ry / rx);
CGAffineTransform transform = CGAffineTransformMakeScale(1, ry / rx);
CGContextConcatCTM(context, transform); CGContextConcatCTM(context, transform);
CGGradientDrawingOptions extendOptions = kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; CGGradientDrawingOptions extendOptions = kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation;
CGContextDrawRadialGradient(context, _gradient, _focusPoint, 0, _centerPoint, _radius, extendOptions);
CGContextDrawRadialGradient(context, _gradient, CGPointMake(fx, fy), 0, CGPointMake(cx, cy), rx, extendOptions);
} }
@end @end

View File

@@ -161,6 +161,8 @@ RCT_ENUM_CONVERTER(RNSVGCGFCRule, (@{
case 2: // radial gradient case 2: // radial gradient
return [[RNSVGRadialGradient alloc] initWithArray:arr]; return [[RNSVGRadialGradient alloc] initWithArray:arr];
case 3: // pattern case 3: // pattern
// TODO:
return nil;
return [[RNSVGPattern alloc] initWithArray:arr]; return [[RNSVGPattern alloc] initWithArray:arr];
default: default:
RCTLogError(@"Unknown brush type: %zd", type); RCTLogError(@"Unknown brush type: %zd", type);

View File

@@ -25,6 +25,8 @@
10A062FF1CC732020000CEEF /* RNSVGSvgViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 10A062FD1CC732020000CEEF /* RNSVGSvgViewManager.m */; }; 10A062FF1CC732020000CEEF /* RNSVGSvgViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 10A062FD1CC732020000CEEF /* RNSVGSvgViewManager.m */; };
10A063041CC7320C0000CEEF /* RNSVGPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 10A063011CC7320C0000CEEF /* RNSVGPath.m */; }; 10A063041CC7320C0000CEEF /* RNSVGPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 10A063011CC7320C0000CEEF /* RNSVGPath.m */; };
10A063051CC7320C0000CEEF /* RNSVGSvgView.m in Sources */ = {isa = PBXBuildFile; fileRef = 10A063031CC7320C0000CEEF /* RNSVGSvgView.m */; }; 10A063051CC7320C0000CEEF /* RNSVGSvgView.m in Sources */ = {isa = PBXBuildFile; fileRef = 10A063031CC7320C0000CEEF /* RNSVGSvgView.m */; };
10C068671CCF0F87007C6982 /* RNSVGShape.m in Sources */ = {isa = PBXBuildFile; fileRef = 10C068651CCF0F87007C6982 /* RNSVGShape.m */; };
10C0686A1CCF1061007C6982 /* RNSVGShapeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 10C068691CCF1061007C6982 /* RNSVGShapeManager.m */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
@@ -80,6 +82,10 @@
10A063011CC7320C0000CEEF /* RNSVGPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSVGPath.m; sourceTree = "<group>"; }; 10A063011CC7320C0000CEEF /* RNSVGPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSVGPath.m; sourceTree = "<group>"; };
10A063021CC7320C0000CEEF /* RNSVGSvgView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSVGSvgView.h; sourceTree = "<group>"; }; 10A063021CC7320C0000CEEF /* RNSVGSvgView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSVGSvgView.h; sourceTree = "<group>"; };
10A063031CC7320C0000CEEF /* RNSVGSvgView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSVGSvgView.m; sourceTree = "<group>"; }; 10A063031CC7320C0000CEEF /* RNSVGSvgView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSVGSvgView.m; sourceTree = "<group>"; };
10C068641CCF0F87007C6982 /* RNSVGShape.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSVGShape.h; sourceTree = SOURCE_ROOT; };
10C068651CCF0F87007C6982 /* RNSVGShape.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSVGShape.m; sourceTree = SOURCE_ROOT; };
10C068681CCF1061007C6982 /* RNSVGShapeManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSVGShapeManager.h; sourceTree = "<group>"; };
10C068691CCF1061007C6982 /* RNSVGShapeManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSVGShapeManager.m; sourceTree = "<group>"; };
10FEAC6A1CC7D05200F1C23C /* RNSVGCGFCRule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSVGCGFCRule.h; sourceTree = "<group>"; }; 10FEAC6A1CC7D05200F1C23C /* RNSVGCGFCRule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSVGCGFCRule.h; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -104,10 +110,12 @@
0CF68ADC1AF0549300FF9E5C /* RNSVGContainer.h */, 0CF68ADC1AF0549300FF9E5C /* RNSVGContainer.h */,
0CF68ADD1AF0549300FF9E5C /* RNSVGGroup.h */, 0CF68ADD1AF0549300FF9E5C /* RNSVGGroup.h */,
0CF68ADE1AF0549300FF9E5C /* RNSVGGroup.m */, 0CF68ADE1AF0549300FF9E5C /* RNSVGGroup.m */,
0CF68ADF1AF0549300FF9E5C /* RNSVGNode.h */, 10C068641CCF0F87007C6982 /* RNSVGShape.h */,
0CF68AE01AF0549300FF9E5C /* RNSVGNode.m */, 10C068651CCF0F87007C6982 /* RNSVGShape.m */,
10A063001CC7320C0000CEEF /* RNSVGPath.h */, 10A063001CC7320C0000CEEF /* RNSVGPath.h */,
10A063011CC7320C0000CEEF /* RNSVGPath.m */, 10A063011CC7320C0000CEEF /* RNSVGPath.m */,
0CF68ADF1AF0549300FF9E5C /* RNSVGNode.h */,
0CF68AE01AF0549300FF9E5C /* RNSVGNode.m */,
10A063021CC7320C0000CEEF /* RNSVGSvgView.h */, 10A063021CC7320C0000CEEF /* RNSVGSvgView.h */,
10A063031CC7320C0000CEEF /* RNSVGSvgView.m */, 10A063031CC7320C0000CEEF /* RNSVGSvgView.m */,
0CF68AE11AF0549300FF9E5C /* RNSVGRenderable.h */, 0CF68AE11AF0549300FF9E5C /* RNSVGRenderable.h */,
@@ -149,6 +157,8 @@
0CF68AF81AF0549300FF9E5C /* ViewManagers */ = { 0CF68AF81AF0549300FF9E5C /* ViewManagers */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
10C068681CCF1061007C6982 /* RNSVGShapeManager.h */,
10C068691CCF1061007C6982 /* RNSVGShapeManager.m */,
10A062FA1CC732020000CEEF /* RNSVGPathManager.h */, 10A062FA1CC732020000CEEF /* RNSVGPathManager.h */,
10A062FB1CC732020000CEEF /* RNSVGPathManager.m */, 10A062FB1CC732020000CEEF /* RNSVGPathManager.m */,
10A062FC1CC732020000CEEF /* RNSVGSvgViewManager.h */, 10A062FC1CC732020000CEEF /* RNSVGSvgViewManager.h */,
@@ -220,6 +230,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
10C068671CCF0F87007C6982 /* RNSVGShape.m in Sources */,
0CF68B161AF0549300FF9E5C /* RNSVGTextManager.m in Sources */, 0CF68B161AF0549300FF9E5C /* RNSVGTextManager.m in Sources */,
10A063041CC7320C0000CEEF /* RNSVGPath.m in Sources */, 10A063041CC7320C0000CEEF /* RNSVGPath.m in Sources */,
0CF68B111AF0549300FF9E5C /* RNSVGGroupManager.m in Sources */, 0CF68B111AF0549300FF9E5C /* RNSVGGroupManager.m in Sources */,
@@ -233,6 +244,7 @@
10A063051CC7320C0000CEEF /* RNSVGSvgView.m in Sources */, 10A063051CC7320C0000CEEF /* RNSVGSvgView.m in Sources */,
0CF68B071AF0549300FF9E5C /* RNSVGRenderable.m in Sources */, 0CF68B071AF0549300FF9E5C /* RNSVGRenderable.m in Sources */,
0CF68B101AF0549300FF9E5C /* RCTConvert+RNSVG.m in Sources */, 0CF68B101AF0549300FF9E5C /* RCTConvert+RNSVG.m in Sources */,
10C0686A1CCF1061007C6982 /* RNSVGShapeManager.m in Sources */,
10A062FE1CC732020000CEEF /* RNSVGPathManager.m in Sources */, 10A062FE1CC732020000CEEF /* RNSVGPathManager.m in Sources */,
0CF68B061AF0549300FF9E5C /* RNSVGNode.m in Sources */, 0CF68B061AF0549300FF9E5C /* RNSVGNode.m in Sources */,
0CF68B0F1AF0549300FF9E5C /* RNSVGSolidColor.m in Sources */, 0CF68B0F1AF0549300FF9E5C /* RNSVGSolidColor.m in Sources */,

View File

@@ -18,8 +18,8 @@
@interface RNSVGNode : UIView @interface RNSVGNode : UIView
@property (nonatomic, assign) CGRect rect;
@property (nonatomic, assign) CGFloat opacity; @property (nonatomic, assign) CGFloat opacity;
@property (nonatomic, assign) CGPathRef clipPath; @property (nonatomic, assign) CGPathRef clipPath;
@property (nonatomic, assign) RNSVGCGFCRule clipRule; @property (nonatomic, assign) RNSVGCGFCRule clipRule;

View File

@@ -80,7 +80,6 @@
_clipPath = CGPathRetain(clipPath); _clipPath = CGPathRetain(clipPath);
} }
- (void)dealloc - (void)dealloc
{ {
CGPathRelease(_clipPath); CGPathRelease(_clipPath);

17
ios/RNSVGShape.h Normal file
View File

@@ -0,0 +1,17 @@
/**
* 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 <Foundation/Foundation.h>
#import "RNSVGRenderable.h"
#import "RNSVGPath.h"
@interface RNSVGShape : RNSVGPath
@property (nonatomic, strong) NSDictionary* shape;
@end

127
ios/RNSVGShape.m Normal file
View File

@@ -0,0 +1,127 @@
/**
* 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 "RNSVGShape.h"
#import "RCTLog.h"
@implementation RNSVGShape
- (void)dealloc
{
}
- (void)renderLayerTo:(CGContextRef)context
{
int type = [[self.shape objectForKey:@"type"] intValue];
CGRect box = CGContextGetClipBoundingBox(context);
CGMutablePathRef path = CGPathCreateMutable();
float height = CGRectGetHeight(box);
float width = CGRectGetWidth(box);
switch (type) {
case 0:
{
// draw circle
CGFloat cx = [self getActualProp:@"cx" relative:width];
CGFloat cy = [self getActualProp:@"cy" relative:height];
CGFloat r;
// radius in percentage calculate formula:
// radius = sqrt(pow((width*percent), 2) + pow((height*percent), 2)) / sqrt(2)
NSDictionary *prop = [self.shape objectForKey:@"r"];
CGFloat value = [[prop objectForKey:@"value"] floatValue];
if ([[prop objectForKey:@"percentage"] integerValue] == 1) {
r = sqrt(pow((width * value), 2) + pow((height * value), 2)) / sqrt(2);
} else {
r = value;
}
CGPathAddArc(path, nil, cx, cy, r, -M_PI, M_PI, YES);
break;
}
case 1:
{
// draw ellipse
CGFloat cx = [self getActualProp:@"cx" relative:width];
CGFloat cy = [self getActualProp:@"cy" relative:height];
CGFloat rx = [self getActualProp:@"rx" relative:height];
CGFloat ry = [self getActualProp:@"ry" relative:height];
CGPathAddEllipseInRect(path, nil, CGRectMake(cx - rx, cy - ry, rx * 2, ry * 2));
break;
}
case 2:
{
// draw line
CGFloat x1 = [self getActualProp:@"x1" relative:height];
CGFloat y1 = [self getActualProp:@"y1" relative:height];
CGFloat x2 = [self getActualProp:@"x2" relative:height];
CGFloat y2 = [self getActualProp:@"y2" relative:height];
CGPathMoveToPoint(path, nil, x1, y1);
CGPathAddLineToPoint(path, nil, x2, y2);
break;
}
case 3:
{
// draw rect
CGFloat x = [self getActualProp:@"x" relative:width];
CGFloat y = [self getActualProp:@"y" relative:height];
CGFloat w = [self getActualProp:@"width" relative:width];
CGFloat h = [self getActualProp:@"height" relative:height];
CGFloat rx = [self getActualProp:@"rx" relative:width];
CGFloat ry = [self getActualProp:@"ry" relative:height];
if (rx != 0 || ry != 0) {
if (rx == 0) {
rx = ry;
} else if (ry == 0) {
ry = rx;
}
if (rx > w / 2) {
rx = w / 2;
}
if (ry > h / 2) {
ry = h / 2;
}
CGPathAddRoundedRect(path, nil, CGRectMake(x, y, w, h), rx, ry);
} else {
CGPathAddRect(path, nil, CGRectMake(x, y, w, h));
}
break;
}
default:
RCTLogError(@"Invalid Shape type %d at %@", type, self.shape);
//CGPathRelease(path);
}
self.d = path;
[super renderLayerTo:context];
//NSLog(@"%@", NSStringFromCGRect(box));
//NSLog(@"%@", self.shape);
}
- (CGFloat)getActualProp:(NSString *)name relative:(float)relative
{
NSDictionary *prop = [self.shape objectForKey:name];
CGFloat value = [[prop objectForKey:@"value"] floatValue];
if ([[prop objectForKey:@"percentage"] integerValue] == 1) {
return relative * value;
} else {
return value;
}
}
@end

View File

@@ -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 "RNSVGRenderableManager.h"
@interface RNSVGShapeManager : RNSVGRenderableManager
@end

View File

@@ -0,0 +1,25 @@
/**
* 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 "RNSVGShapeManager.h"
#import "RNSVGShape.h"
#import "RCTConvert+RNSVG.h"
@implementation RNSVGShapeManager
RCT_EXPORT_MODULE()
- (RNSVGRenderable *)node
{
return [RNSVGShape new];
}
RCT_EXPORT_VIEW_PROPERTY(shape, NSDictionary)
@end

View File

@@ -1,101 +0,0 @@
// from https://github.com/gabelerner/canvg/blob/860e418aca67b9a41e858a223d74d375793ec364/canvg.js#L449
class BoundingBox{
constructor(x1, y1, x2, y2) {
this.x1 = NaN;
this.y1 = NaN;
this.x2 = NaN;
this.y2 = NaN;
this.addPoint(x1, y1);
this.addPoint(x2, y2);
}
width = () => this.x2 - this.x1;
height = () => this.y2 - this.y1;
addPoint = (x, y) => {
if (x != null) {
if (isNaN(this.x1) || isNaN(this.x2)) {
this.x1 = x;
this.x2 = x;
}
if (x < this.x1) this.x1 = x;
if (x > this.x2) this.x2 = x;
}
if (y != null) {
if (isNaN(this.y1) || isNaN(this.y2)) {
this.y1 = y;
this.y2 = y;
}
if (y < this.y1) this.y1 = y;
if (y > this.y2) this.y2 = y;
}
};
addX = x => this.addPoint(x, null);
addY = y => this.addPoint(null, y);
addQuadraticCurve = (p0x, p0y, p1x, p1y, p2x, p2y) => {
let cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0)
let cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0)
let cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0)
let cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0)
this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y);
};
addBezierCurve = (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) => {
// from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
var p0 = [p0x, p0y],
p1 = [p1x, p1y],
p2 = [p2x, p2y],
p3 = [p3x, p3y];
this.addPoint(p0[0], p0[1]);
this.addPoint(p3[0], p3[1]);
[0, 1].forEach(i => {
let f = t => {
return Math.pow(1 - t, 3) * p0[i]
+ 3 * Math.pow(1 - t, 2) * t * p1[i]
+ 3 * (1 - t) * Math.pow(t, 2) * p2[i]
+ Math.pow(t, 3) * p3[i];
};
let b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i];
let a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i];
let c = 3 * p1[i] - 3 * p0[i];
if (a == 0) {
if (b != 0) {
let t = -c / b;
if (0 < t && t < 1) {
if (i == 0) this.addX(f(t));
if (i == 1) this.addY(f(t));
}
}
return;
}
let b2ac = Math.pow(b, 2) - 4 * c * a;
if (b2ac < 0) {
return;
}
let t1 = (-b + Math.sqrt(b2ac)) / (2 * a);
if (0 < t1 && t1 < 1) {
i == 0 && this.addX(f(t1));
i == 1 && this.addY(f(t1));
}
let t2 = (-b - Math.sqrt(b2ac)) / (2 * a);
if (0 < t2 && t2 < 1) {
i == 0 && this.addX(f(t2));
i == 1 && this.addY(f(t2));
}
});
};
}
export default BoundingBox;

55
lib/SerializableShape.js Normal file
View File

@@ -0,0 +1,55 @@
/**
* Format potential percentage props
*
* convert somet props like those
* width="50%"
* height="500"
*
* to
* {
* width: {
* percentage: true,
* value: 0.5
* },
* height: {
* percentage: false,
* value: 500
* }
* }
*
*
*/
import percentToFloat from './percentToFloat';
function percentageTransform(value) {
if (typeof value === 'number') {
return {
percentage: false,
value
};
}
let float = percentToFloat(value);
return {
percentage: float !== +value,
value: float
}
}
export default class {
constructor(props, list) {
this.shape = {};
list.forEach(name => {
if (props[name] != null) {
this.shape[name] = percentageTransform(props[name]);
}
});
}
toJSON = () => {
return this.shape;
};
};

View File

@@ -6,7 +6,7 @@ function arrayDiffer(a, b) {
if (a.length !== b.length) { if (a.length !== b.length) {
return true; return true;
} }
for (var i = 0; i < a.length; i++) { for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) { if (a[i] !== b[i]) {
return true; return true;
} }
@@ -14,10 +14,27 @@ function arrayDiffer(a, b) {
return false; return false;
} }
function shapeDiffer(a, b) {
if (a === b) {
return false;
}
for (let key in a) {
if (a.hasOwnProperty(key)) {
if (key === 'type' && a.type !== b.type) {
return true;
} else if (a[key].percentage !== b[key].percentage || a[key].value !== b[key].value) {
return true;
}
}
}
return false;
}
function fontAndLinesDiffer(a, b) { function fontAndLinesDiffer(a, b) {
if (a === b) { if (a === b) {
return false; return false;
} }
if (a.font !== b.font) { if (a.font !== b.font) {
if (a.font === null) { if (a.font === null) {
return true; return true;
@@ -38,7 +55,7 @@ function fontAndLinesDiffer(a, b) {
return arrayDiffer(a.lines, b.lines); return arrayDiffer(a.lines, b.lines);
} }
var NodeAttributes = { const NodeAttributes = {
transform: { transform: {
diff: arrayDiffer diff: arrayDiffer
}, },
@@ -49,45 +66,57 @@ var NodeAttributes = {
clipRule: true clipRule: true
}; };
var GroupAttributes = Object.assign({ const GroupAttributes = {
}, NodeAttributes); ...NodeAttributes
};
var RenderableAttributes = Object.assign({ const RenderableAttributes = {
fill: { fill: {
diff: arrayDiffer diff: arrayDiffer
}, },
fillRule: true,
stroke: { stroke: {
diff: arrayDiffer diff: arrayDiffer
}, },
strokeWidth: true, strokeWidth: true,
strokeLinecap: true, strokeLinecap: true,
strokeLinejoin: true, strokeLinejoin: true,
fillRule: true,
strokeDasharray: { strokeDasharray: {
diff: arrayDiffer diff: arrayDiffer
}, },
strokeDashoffset: true strokeDashoffset: true,
}, NodeAttributes); ...NodeAttributes
};
var PathAttributes = Object.assign({ const PathAttributes ={
d: { d: {
diff: arrayDiffer diff: arrayDiffer
} },
}, RenderableAttributes); ...RenderableAttributes
};
var TextAttributes = Object.assign({ const TextAttributes = {
alignment: true, alignment: true,
frame: { frame: {
diff: fontAndLinesDiffer diff: fontAndLinesDiffer
}, },
path: { path: {
diff: arrayDiffer diff: arrayDiffer
} },
}, RenderableAttributes); ...RenderableAttributes
};
const ShapeAttributes = {
shape: {
diff: shapeDiffer
},
...RenderableAttributes
};
export { export {
GroupAttributes, GroupAttributes,
PathAttributes, PathAttributes,
TextAttributes TextAttributes,
ShapeAttributes
} }

View File

@@ -1,59 +0,0 @@
import SvgPath from 'svgpath';
import BoundingBox from './BoundingBox';
export default function(d) {
let pathDriver = new SvgPath(d);
let boundingBox = new BoundingBox();
pathDriver
.abs()
.unarc()
.unshort()
.iterate((seg, index, x, y) => {
switch(seg[0]) {
case 'M':
case 'L':
boundingBox.addPoint(
seg[1],
seg[2]
);
break;
case 'H':
boundingBox.addX(seg[1]);
break;
case 'V':
boundingBox.addY(seg[1]);
break;
case 'Q':
boundingBox.addQuadraticCurve(
x,
y,
seg[1],
seg[2],
seg[3],
seg[4]
);
break;
case 'C':
boundingBox.addBezierCurve(
x,
y,
seg[1],
seg[2],
seg[3],
seg[4],
seg[5],
seg[6]
);
break;
}
});
return {
width: boundingBox.width(),
height: boundingBox.height(),
x1: boundingBox.x1,
y1: boundingBox.y1,
x2: boundingBox.x2,
y2: boundingBox.y2
};
}

View File

@@ -4,41 +4,11 @@ const LINEAR_GRADIENT = 1;
const RADIAL_GRADIENT = 2; const RADIAL_GRADIENT = 2;
const PATTERN = 3; const PATTERN = 3;
function applyBoundingBoxToBrushData(brushData, props) { export default function (colorOrBrush) {
let type = brushData[0];
let width = +props.width;
let height = +props.height;
if (type === LINEAR_GRADIENT) {
brushData[1] *= width;
brushData[2] *= height;
brushData[3] *= width;
brushData[4] *= height;
} else if (type === RADIAL_GRADIENT) {
brushData[1] *= width;
brushData[2] *= height;
brushData[3] *= width;
brushData[4] *= height;
brushData[5] *= width;
brushData[6] *= height;
} else if (type === PATTERN) {
// todo
}
}
export default function (colorOrBrush, props) {
if (!colorOrBrush) { if (!colorOrBrush) {
return null; return null;
} }
if (colorOrBrush._brush) { if (colorOrBrush._brush) {
if (colorOrBrush._bb) {
// The legacy API for Gradients allow for the bounding box to be used
// as a convenience for specifying gradient positions. This should be
// deprecated. It's not properly implemented in canvas mode. ReactART
// doesn't handle update to the bounding box correctly. That's why we
// mutate this so that if it's reused, we reuse the same resolved box.
applyBoundingBoxToBrushData(colorOrBrush._brush, props);
colorOrBrush._bb = false;
}
return colorOrBrush._brush; return colorOrBrush._brush;
} }
@@ -48,5 +18,6 @@ export default function (colorOrBrush, props) {
export { export {
LINEAR_GRADIENT, LINEAR_GRADIENT,
RADIAL_GRADIENT RADIAL_GRADIENT,
PATTERN
} }

View File

@@ -7,13 +7,13 @@ const fillRules = {
nonzero: 1 nonzero: 1
}; };
function fillFilter(props, dimensions) { function fillFilter(props) {
let {fill} = props; let {fill} = props;
if (fill === 'none') { if (fill === 'none') {
return null; return null;
} else if (fill) { } else if (fill) {
return patterns(fill, +props.fillOpacity, dimensions, props.svgId); return patterns(fill, +props.fillOpacity, props.svgId);
} else if (props.fill === undefined) { } else if (props.fill === undefined) {
let fillOpacity = +props.fillOpacity; let fillOpacity = +props.fillOpacity;
if (isNaN(fillOpacity)) { if (isNaN(fillOpacity)) {
@@ -25,8 +25,8 @@ function fillFilter(props, dimensions) {
} }
} }
export default function(props, dimensions) { export default function(props) {
let fill = extractBrush(fillFilter(props, dimensions), props); let fill = extractBrush(fillFilter(props), props);
let fillRule = fillRules[props.fillRule] === 0 ? 0 : 1; let fillRule = fillRules[props.fillRule] === 0 ? 0 : 1;
return { return {

View File

@@ -14,18 +14,16 @@ export default function(props, options = {stroke: true, join: true, transform: t
opacity: +props.opacity || 1 opacity: +props.opacity || 1
}; };
let dimensions = this.getBoundingBox ? this.getBoundingBox() : null;
if (props.clipPath) { if (props.clipPath) {
_.assign(extractedProps, extractClipping(props)); _.assign(extractedProps, extractClipping(props));
} }
if (options.stroke) { if (options.stroke) {
_.assign(extractedProps, extractStroke(props, dimensions)); _.assign(extractedProps, extractStroke(props));
} }
if (options.fill) { if (options.fill) {
_.assign(extractedProps, extractFill(props, dimensions)); _.assign(extractedProps, extractFill(props));
} }
if (options.transform) { if (options.transform) {

View File

@@ -15,7 +15,7 @@ const joins = {
round: 1 round: 1
}; };
function strokeFilter(props, dimensions) { function strokeFilter(props) {
let strokeWidth = +props.strokeWidth; let strokeWidth = +props.strokeWidth;
let {stroke} = props; let {stroke} = props;
if (!strokeWidth && !stroke) { if (!strokeWidth && !stroke) {
@@ -39,7 +39,7 @@ function strokeFilter(props, dimensions) {
// TODO: propTypes check // TODO: propTypes check
return { return {
stroke: patterns(stroke, +props.strokeOpacity, dimensions, props.svgId), stroke: patterns(stroke, +props.strokeOpacity, props.svgId),
strokeLinecap: caps[props.strokeLinecap] || 0, strokeLinecap: caps[props.strokeLinecap] || 0,
strokeLinejoin: joins[props.strokeLinejoin] || 0, strokeLinejoin: joins[props.strokeLinejoin] || 0,
strokeDasharray: strokeDasharray || null, strokeDasharray: strokeDasharray || null,
@@ -48,8 +48,8 @@ function strokeFilter(props, dimensions) {
}; };
} }
export default function(props, dimensions) { export default function(props) {
let strokeProps = strokeFilter(props, dimensions); let strokeProps = strokeFilter(props);
return strokeProps ? { return strokeProps ? {
...strokeProps, ...strokeProps,

View File

@@ -75,15 +75,15 @@ function extractFont(font) {
}; };
} }
const alignments = { const anchord = {
right: 1, end: 1,
center: 2, middle: 2,
left: 0 start: 0
}; };
export default function(props) { export default function(props) {
return { return {
alignment: alignments[props.textAnchor] || 0, alignment: anchord[props.textAnchor] || 0,
frame: extractFontAndLines( frame: extractFontAndLines(
props, props,
childrenAsString(props.children) childrenAsString(props.children)

View File

@@ -1,4 +1,4 @@
export default function({viewbox, width, height, preserveAspectRatio, x: dx, y: dy, dScale, dScaleX, dScaleY, shouldTransform}) { export default function({viewbox, width, height, preserveAspectRatio, x: dx, y: dy, dScale, dScaleX, dScaleY}) {
if (!viewbox || !width || !height) { if (!viewbox || !width || !height) {
return false; return false;
} }
@@ -47,7 +47,7 @@ export default function({viewbox, width, height, preserveAspectRatio, x: dx, y:
y = -vy * sy; y = -vy * sy;
} }
if (shouldTransform) { //if (shouldTransform) {
x += (+dx || 0); x += (+dx || 0);
y += (+dy || 0); y += (+dy || 0);
@@ -58,7 +58,7 @@ export default function({viewbox, width, height, preserveAspectRatio, x: dx, y:
scaleX *= (+dScaleX || 1); scaleX *= (+dScaleX || 1);
scaleY *= (+dScaleY || 1); scaleY *= (+dScaleY || 1);
} }
} //}
return { return {
x, x,

View File

@@ -23,7 +23,7 @@ export {
remove remove
} }
export default function(patternSting, opacity, dimensions, svgId) { export default function(patternSting, opacity, svgId) {
if (isGradient(patternSting)) { if (isGradient(patternSting)) {
return patternSting; return patternSting;
} }
@@ -40,12 +40,8 @@ export default function(patternSting, opacity, dimensions, svgId) {
let pattern = patterns[patternName]; let pattern = patterns[patternName];
if (pattern) { if (pattern) {
if (pattern.length === 2) {
return pattern(dimensions, opacity);
} else {
return pattern(opacity); return pattern(opacity);
} }
}
return null; return null;
} }

View File

@@ -11,7 +11,6 @@ function insertColorIntoArray(color, targetArray, atIndex) {
function insertColorsIntoArray(stops, targetArray, atIndex) { function insertColorsIntoArray(stops, targetArray, atIndex) {
let i = 0; let i = 0;
if ('length' in stops) { if ('length' in stops) {
while (i < stops.length) { while (i < stops.length) {
insertColorIntoArray(stops[i], targetArray, atIndex + i * 4); insertColorIntoArray(stops[i], targetArray, atIndex + i * 4);
@@ -28,27 +27,28 @@ function insertColorsIntoArray(stops, targetArray, atIndex) {
return atIndex + i * 4; return atIndex + i * 4;
} }
function insertColorStopsIntoArray(stops, targetArray, atIndex, multi = 1) { function insertColorStopsIntoArray(stops, targetArray, atIndex) {
let lastIndex = insertColorsIntoArray(stops, targetArray, atIndex, false); let lastIndex = insertColorsIntoArray(stops, targetArray, atIndex);
insertOffsetsIntoArray(stops, targetArray, lastIndex, multi, false); insertOffsetsIntoArray(stops, targetArray, lastIndex);
} }
function insertOffsetsIntoArray(stops, targetArray, atIndex, multi) { function insertOffsetsIntoArray(stops, targetArray, atIndex) {
let offsetNumber; let offsetNumber;
let i = 0; let i = 0;
let arr = []; let arr = [];
if ('length' in stops) { if ('length' in stops) {
while (i < stops.length) { while (i < stops.length) {
offsetNumber = i / (stops.length - 1) * multi; offsetNumber = i / (stops.length - 1);
targetArray[atIndex + i] = offsetNumber; targetArray[atIndex + i] = offsetNumber;
i++; i++;
} }
} else { } else {
_.forEach(stops, (stop, offsetString) => { _.forEach(stops, (stop, offsetString) => {
offsetNumber = (+offsetString) * multi; offsetNumber = (+offsetString);
arr.push(offsetNumber); arr.push(offsetNumber);
i++; i++;
}); });
arr.sort(); arr.sort();
targetArray.splice(atIndex, 0, ...arr); targetArray.splice(atIndex, 0, ...arr);
} }

View File

@@ -1,5 +1,10 @@
let percentReg = /^(\-?\d+(?:\.\d+)?)(%?)$/; let percentReg = /^(\-?\d+(?:\.\d+)?)(%?)$/;
export default function (percent) { export default function (percent) {
let matched = percent.match(percentReg); let matched = percent.match(percentReg);
if (!matched) {
console.warn(`\`${percent}\` is not a valid number or percentage string.`);
return 0;
}
return matched[2] ? matched[1] / 100 : +matched[1]; return matched[2] ? matched[1] / 100 : +matched[1];
} }

110
lib/props.js Normal file
View File

@@ -0,0 +1,110 @@
import {
PropTypes
} from 'react-native';
const numberProp = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
const fillProps = {
fill: PropTypes.string,
fillOpacity: numberProp,
fillRule: PropTypes.oneOf(['evenodd', 'nonzero'])
};
const clipProps = {
clipRule: PropTypes.oneOf(['evenodd', 'nonzero']),
clipPath: PropTypes.string
};
const strokeProps = {
stroke: PropTypes.string,
strokeWidth: numberProp,
strokeOpacity: numberProp,
strokeDasharray: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.string]),
strokeDashoffset: numberProp,
strokeLinecap: PropTypes.oneOf(['butt', 'square', 'round']),
strokeLinejoin: PropTypes.oneOf(['miter', 'bevel', 'round'])
};
const textProps = {
textAnchor: PropTypes.oneOf(['start', 'middle', 'end']),
path: PropTypes.string,
fontFamily: PropTypes.string,
fontSize: numberProp,
fontWeight: PropTypes.string,
fontStyle: PropTypes.string,
font: PropTypes.object,
lines: numberProp
};
const transformProps = {
scale: numberProp,
scaleX: numberProp,
scaleY: numberProp,
rotate: numberProp,
x: numberProp,
y: numberProp,
originX: numberProp,
originY: numberProp,
transform: PropTypes.string
};
const pathProps = {
...fillProps,
...strokeProps,
...clipProps,
...transformProps
};
const circleProps = {
cx: numberProp,
cy: numberProp,
r: numberProp
};
const ellipseProps = {
cx: numberProp,
cy: numberProp,
rx: numberProp,
ry: numberProp
};
const lineProps = {
x1: numberProp,
x2: numberProp,
y1: numberProp,
y2: numberProp
};
const rectProps = {
x: numberProp,
y: numberProp,
width: numberProp,
height: numberProp,
rx: numberProp,
ry: numberProp
};
const contextProps = {
...circleProps,
...ellipseProps,
...lineProps,
...rectProps,
...fillProps,
...strokeProps,
...textProps
};
export {
numberProp,
fillProps,
clipProps,
strokeProps,
transformProps,
textProps,
circleProps,
ellipseProps,
lineProps,
rectProps,
contextProps,
pathProps
}

View File

@@ -1,7 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
export default function (stops, opacity) { export default function (stops, opacity) {
return _.reduce(stops, (ret, color, key) => { return _.reduce(stops, (ret, color, key) => {
ret[key] = color.alpha(opacity).rgbaString(); ret[key] = color.alpha(color.alpha() * opacity).rgbaString();
return ret; return ret;
}, {}); }, {});
} }