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
#
cn-doc.md
# experimental code
#
experimental/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ class TextExample extends Component{
x="50"
y="9"
fill="red"
textAnchor="center"
textAnchor="middle"
>I love SVG!</Text>
</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{
static title = 'Fill the text with LinearGradient';
render() {
@@ -68,8 +96,8 @@ class TextFill extends Component{
width="200"
>
<Defs>
<LinearGradient id="grad" x1="0" y1="0" x2="200" y2="0">
<Stop offset="0%" stopColor="rgb(255,255,0)" stopOpacity="0" />
<LinearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
<Stop offset="0%" stopColor="rgb(255,255,0)" stopOpacity="0.5" />
<Stop offset="100%" stopColor="red" stopOpacity="1" />
</LinearGradient>
</Defs>
@@ -81,40 +109,12 @@ class TextFill extends Component{
fontWeight="bold"
x="100"
y="20"
textAnchor="center"
textAnchor="middle"
>FILL TEXT</Text>
</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{
static title = 'Transform the text';
@@ -148,13 +148,19 @@ const icon = <Svg
y="2"
fontSize="14"
fontWeight="bold"
textAnchor="center"
textAnchor="middle"
fill="none"
stroke="blue"
></Text>
</Svg>;
const samples = [TextExample, TextRotate, TextStroke, TextFill, TextPath];
const samples = [
TextExample,
TextRotate,
TextStroke,
TextFill,
TextPath
];
export {
icon,

View File

@@ -562,15 +562,12 @@ npm install
4. [morph animations](https://github.com/gorangajic/react-svg-morph)
5. fix propTypes
6. more Text features support
7. support percent props
8. Pattern element
9. Image element
7. Pattern element
8. Image element
#### 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/)
* [SVG Tutorial](http://tutorials.jenkov.com/svg/index.html)
* [MDN](https://developer.mozilla.org/en/docs/Web/SVG)

View File

@@ -9,10 +9,15 @@
package com.horcrux.svg;
import android.util.Log;
import javax.annotation.Nullable;
import com.facebook.react.bridge.ReadableArray;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Contains static helper methods for accessing props.
*/
@@ -51,4 +56,20 @@ import com.facebook.react.bridge.ReadableArray;
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.Point;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
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_NONZERO = 1;
protected @Nullable Path mPath;
private @Nullable float[] mStrokeColor;
private @Nullable float[] mFillColor;
protected Path mPath;
private @Nullable ReadableArray mStrokeColor;
private @Nullable ReadableArray mFillColor;
private @Nullable float[] mStrokeDasharray;
private float mStrokeWidth = 1;
private float mStrokeDashoffset = 0;
@@ -60,6 +61,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
private boolean mFillRuleSet;
private boolean mPathSet;
private float[] mShapePath;
protected RectF mContentBoundingBox;
private Point mPaint;
@ReactProp(name = "d")
@@ -72,7 +74,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
@ReactProp(name = "fill")
public void setFill(@Nullable ReadableArray fillColors) {
mFillColor = PropHelper.toFloatArray(fillColors);
mFillColor = fillColors;
markUpdated();
}
@@ -87,7 +89,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
@ReactProp(name = "stroke")
public void setStroke(@Nullable ReadableArray strokeColors) {
mStrokeColor = PropHelper.toFloatArray(strokeColors);
mStrokeColor = strokeColors;
markUpdated();
}
@@ -139,10 +141,10 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
clip(canvas, paint);
if (setupFillPaint(paint, opacity)) {
if (setupFillPaint(paint, opacity, null)) {
canvas.drawPath(mPath, paint);
}
if (setupStrokePaint(paint, opacity)) {
if (setupStrokePaint(paint, opacity, null)) {
canvas.drawPath(mPath, paint);
}
@@ -168,21 +170,24 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
}
mPath = super.createPath(mShapePath, path);
RectF box = new RectF();
mPath.computeBounds(box, true);
mContentBoundingBox = box;
}
}
/*
/**
* sorting stops and stopsColors from array
*/
private static void parseGradientStops(float[] value, int stopsCount, float[] stops, int[] stopsColors, int startColorsPosition) {
int startStops = value.length - stopsCount;
private static void parseGradientStops(ReadableArray value, int stopsCount, float[] stops, int[] stopsColors, int startColorsPosition) {
int startStops = value.size() - stopsCount;
for (int i = 0; i < stopsCount; i++) {
stops[i] = value[startStops + i];
stops[i] = (float)value.getDouble(startStops + i);
stopsColors[i] = Color.argb(
(int) (value[startColorsPosition + i * 4 + 3] * 255),
(int) (value[startColorsPosition + i * 4] * 255),
(int) (value[startColorsPosition + i * 4 + 1] * 255),
(int) (value[startColorsPosition + i * 4 + 2] * 255));
(int) (value.getDouble(startColorsPosition + i * 4 + 3) * 255),
(int) (value.getDouble(startColorsPosition + i * 4) * 255),
(int) (value.getDouble(startColorsPosition + i * 4 + 1) * 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}
* if the fill should be drawn, {@code false} if not.
*/
protected boolean setupFillPaint(Paint paint, float opacity) {
if (mFillColor != null && mFillColor.length > 0) {
protected boolean setupFillPaint(Paint paint, float opacity, @Nullable RectF box) {
if (mFillColor != null && mFillColor.size() > 0) {
paint.reset();
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL);
setupPaint(paint, opacity, mFillColor);
setupPaint(paint, opacity, mFillColor, box);
return true;
}
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}
* if the stroke should be drawn, {@code false} if not.
*/
protected boolean setupStrokePaint(Paint paint, float opacity) {
if (mStrokeWidth == 0 || mStrokeColor == null || mStrokeColor.length == 0) {
protected boolean setupStrokePaint(Paint paint, float opacity, @Nullable RectF box) {
if (mStrokeWidth == 0 || mStrokeColor == null || mStrokeColor.size() == 0) {
return false;
}
paint.reset();
@@ -244,8 +249,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
}
paint.setStrokeWidth(mStrokeWidth * mScale);
setupPaint(paint, opacity, mStrokeColor);
setupPaint(paint, opacity, mStrokeColor, box);
if (mStrokeDasharray != null && mStrokeDasharray.length > 0) {
paint.setPathEffect(new DashPathEffect(mStrokeDasharray, mStrokeDashoffset));
@@ -255,67 +259,73 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
}
private void setupPaint(Paint paint, float opacity, float[] colors) {
int stopsCount;
int [] stopsColors;
float [] stops;
private void setupPaint(Paint paint, float opacity, ReadableArray colors, @Nullable RectF box) {
int colorType = colors.getInt(0);
if (colorType == 0) {
// solid color
paint.setARGB(
(int) (colors.size() > 4 ? colors.getDouble(4) * opacity * 255 : opacity * 255),
(int) (colors.getDouble(1) * 255),
(int) (colors.getDouble(2) * 255),
(int) (colors.getDouble(3) * 255));
} else if (colorType == 1 || colorType == 2) {
if (box == null) {
box = mContentBoundingBox;
}
int colorType = (int) colors[0];
switch (colorType) {
case 0:
paint.setARGB(
(int) (colors.length > 4 ? colors[4] * opacity * 255 : opacity * 255),
(int) (colors[1] * 255),
(int) (colors[2] * 255),
(int) (colors[3] * 255));
break;
case 1:
stopsCount = (colors.length - 5) / 5;
stopsColors = new int [stopsCount];
stops = new float[stopsCount];
int startColorsPosition = colorType == 1 ? 5 : 7;
parseGradientStops(colors, stopsCount, stops, stopsColors, 5);
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(
new LinearGradient(
colors[1] * mScale,
colors[2] * mScale,
colors[3] * mScale,
colors[4] * mScale,
x1,
y1,
x2,
y2,
stopsColors,
stops,
Shader.TileMode.CLAMP));
break;
case 2:
stopsCount = (colors.length - 7) / 5;
stopsColors = new int [stopsCount];
stops = new float[stopsCount];
parseGradientStops(colors, stopsCount, stops, stopsColors, 7);
// TODO: support focus
float focusX = colors[1];
float focusY = colors[2];
float radius = colors[3];
float radiusRatio = colors[4] / radius;
} else {
float rx = PropHelper.fromPercentageToFloat(colors.getString(3), width, 0f, mScale);
float ry = PropHelper.fromPercentageToFloat(colors.getString(4), height, 0f, mScale);
float cx = PropHelper.fromPercentageToFloat(colors.getString(5), width, offsetX, mScale);
float cy = PropHelper.fromPercentageToFloat(colors.getString(6), height, offsetY, mScale) / (ry / rx);
// TODO: do not support focus point.
float fx = PropHelper.fromPercentageToFloat(colors.getString(1), width, offsetX, mScale);
float fy = PropHelper.fromPercentageToFloat(colors.getString(2), height, offsetY, mScale) / (ry / rx);
Shader radialGradient = new RadialGradient(
colors[5] * mScale,
colors[6] * mScale / radiusRatio,
radius * mScale,
cx,
cy,
rx,
stopsColors,
stops,
Shader.TileMode.CLAMP
);
Matrix radialMatrix = new Matrix();
// seems like a bug here?
radialMatrix.preScale(1f, radiusRatio);
radialMatrix.preScale(1f, ry / rx);
radialGradient.setLocalMatrix(radialMatrix);
paint.setShader(radialGradient);
break;
default:
// TODO: Support pattern.
FLog.w(ReactConstants.TAG, "RNSVG: Color type " + colorType + " not supported!");
}
} else {
// TODO: Support pattern.
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_SVG = "RNSVGPath";
/* package */ static final String CLASS_TEXT = "RNSVGText";
/* package */ static final String CLASS_SHAPE = "RNSVGShape";
private final String mClassName;
@@ -42,6 +43,10 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
return new RNSVGRenderableViewManager(CLASS_TEXT);
}
public static RNSVGRenderableViewManager createRNSVGShapeViewManager() {
return new RNSVGRenderableViewManager(CLASS_SHAPE);
}
private RNSVGRenderableViewManager(String className) {
mClassName = className;
}
@@ -57,6 +62,8 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
return new RNSVGGroupShadowNode();
} else if (mClassName == CLASS_SVG) {
return new RNSVGPathShadowNode();
} else if (mClassName == CLASS_SHAPE) {
return new RNSVGShapeShadowNode();
} else if (mClassName == CLASS_TEXT) {
return new RNSVGTextShadowNode();
} else {
@@ -70,9 +77,11 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
return RNSVGGroupShadowNode.class;
} else if (mClassName == CLASS_SVG) {
return RNSVGPathShadowNode.class;
} else if (mClassName == CLASS_SHAPE) {
return RNSVGShapeShadowNode.class;
} else if (mClassName == CLASS_TEXT) {
return RNSVGTextShadowNode.class;
} else {
}else {
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.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.util.Log;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableArray;
@@ -89,7 +92,11 @@ public class RNSVGTextShadowNode extends RNSVGPathShadowNode {
lines[i] = linesProp.getString(i);
}
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);
if (mPath == null) {
canvas.drawText(text, 0, -paint.ascent(), paint);
@@ -97,7 +104,7 @@ public class RNSVGTextShadowNode extends RNSVGPathShadowNode {
canvas.drawTextOnPath(text, mPath, 0, 0, paint);
}
}
if (setupFillPaint(paint, opacity)) {
if (setupFillPaint(paint, opacity, box)) {
applyTextPropertiesToPaint(paint);
if (mPath == null) {
canvas.drawText(text, 0, -paint.ascent(), paint);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,32 +5,13 @@ import React, {
} from 'react-native';
import stopsOpacity from '../lib/stopsOpacity';
import numberProp from '../lib/numberProp';
import {numberProp} from '../lib/props';
import Gradient from './Gradient';
import {LINEAR_GRADIENT} from '../lib/extract/extractBrush';
import insertColorStopsIntoArray from '../lib/insertProcessor';
function LinearGradientGenerator(stops, x1, y1, x2, y2) {
var type = LINEAR_GRADIENT;
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];
var brushData = [LINEAR_GRADIENT, x1, y1, x2, y2];
insertColorStopsIntoArray(stops, brushData, 5);
this._brush = brushData;
}
@@ -53,20 +34,9 @@ class LinearGradient extends Gradient{
x2,
y2
} = this.props;
let gradientProps = [x1, y1, x2, y2];
return super.render(
gradientProps,
function (factories, stops, boundingBox, opacity) {
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);
(stops, opacity) => {
return new LinearGradientGenerator(stopsOpacity(stops, opacity), ...[x1, y1, x2, y2]);
}
);
}

View File

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

View File

@@ -4,36 +4,15 @@ import React, {
Children
} from 'react-native';
import stopsOpacity from '../lib/stopsOpacity';
import numberProp from '../lib/numberProp';
import {numberProp} from '../lib/props';
import Gradient from './Gradient';
import {RADIAL_GRADIENT} from '../lib/extract/extractBrush';
import insertColorStopsIntoArray from '../lib/insertProcessor';
function RadialGradientGenerator(stops, fx, fy, rx, ry, cx, cy) {
if (ry == null) {
ry = rx;
}
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);
var brushData = [RADIAL_GRADIENT, fx, fy, rx, ry, cx, cy];
insertColorStopsIntoArray(stops, brushData, 7);
this._brush = brushData;
}
@@ -60,24 +39,10 @@ class RadialGradient extends Gradient{
cy,
r
} = this.props;
let gradientProps = [fx, fy, rx || r, ry || r, cx, cy];
return super.render(
gradientProps,
function (factories, stops, boundingBox, opacity) {
let {x1,y1,width, height} = boundingBox;
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);
(stops, opacity) => {
return new RadialGradientGenerator(stopsOpacity(stops, opacity), ...[fx, fy, rx || r, ry || r, cx, cy]);
}
);
}

View File

@@ -2,115 +2,29 @@ import React, {
Component,
PropTypes
} from 'react-native';
import Shape, {RECT} from './Shape';
import {rectProps, pathProps, fillProps, strokeProps} from '../lib/props';
import Path from './Path';
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{
class Rect extends Shape{
static displayName = 'Rect';
static propTypes = {
x: propType,
y: propType,
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
`;
...pathProps,
...rectProps
};
render() {
let r = getR(this.props);
return <Path
{...this.props}
width={null}
height={null}
x={r.rx || null}
y={null}
d={Rect.getPath(this.props, r)}
/>;
}
static contextTypes = {
...fillProps,
...strokeProps,
...rectProps,
isInGroup: PropTypes.bool
};
constructor() {
super(...arguments);
this.type = 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,
requireNativeComponent
} from 'react-native';
import extractViewbox from '../lib/extract/extractViewbox';
import ViewBox from './ViewBox';
import _ from 'lodash';

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,9 @@
#import <Foundation/Foundation.h>
@interface RNSVGBrush : NSObject
{
NSArray *_points;
}
/* @abstract */
- (instancetype)initWithArray:(NSArray *)data NS_DESIGNATED_INITIALIZER;
@@ -26,6 +29,8 @@
- (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
* be clipped.

View File

@@ -29,6 +29,30 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
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
{
// abstract

View File

@@ -13,36 +13,47 @@
@implementation RNSVGLinearGradient
{
CGGradientRef _gradient;
CGPoint _startPoint;
CGPoint _endPoint;
CGGradientRef _gradient;
}
- (instancetype)initWithArray:(NSArray<NSNumber *> *)array
- (instancetype)initWithArray:(NSArray *)array
{
if ((self = [super initWithArray:array])) {
if (array.count < 5) {
RCTLogError(@"-[%@ %@] expects 5 elements, received %@",
self.class, NSStringFromSelector(_cmd), array);
return nil;
if ((self = [super initWithArray:array])) {
if (array.count < 5) {
RCTLogError(@"-[%@ %@] expects 5 elements, received %@",
self.class, NSStringFromSelector(_cmd), array);
return nil;
}
_points = [array subarrayWithRange:NSMakeRange(1, 4)];
_gradient = CGGradientRetain([RCTConvert CGGradient:array offset:5]);
}
_startPoint = [RCTConvert CGPoint:array offset:1];
_endPoint = [RCTConvert CGPoint:array offset:3];
_gradient = CGGradientRetain([RCTConvert CGGradient:array offset:5]);
}
return self;
return self;
}
- (void)dealloc
{
CGGradientRelease(_gradient);
CGGradientRelease(_gradient);
}
- (void)paint:(CGContextRef)context
{
CGGradientDrawingOptions extendOptions =
CGGradientDrawingOptions extendOptions =
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

View File

@@ -13,43 +13,65 @@
@implementation RNSVGRadialGradient
{
CGGradientRef _gradient;
CGPoint _focusPoint;
CGPoint _centerPoint;
CGFloat _radius;
CGFloat _radiusRatio;
CGGradientRef _gradient;
}
- (instancetype)initWithArray:(NSArray<NSNumber *> *)array
- (instancetype)initWithArray:(NSArray *)array
{
if ((self = [super initWithArray:array])) {
if (array.count < 7) {
RCTLogError(@"-[%@ %@] expects 7 elements, received %@",
self.class, NSStringFromSelector(_cmd), array);
return nil;
if ((self = [super initWithArray:array])) {
if (array.count < 7) {
RCTLogError(@"-[%@ %@] expects 7 elements, received %@",
self.class, NSStringFromSelector(_cmd), array);
return nil;
}
_points = [array subarrayWithRange:NSMakeRange(1, 6)];
NSMutableArray *gradient = [[NSMutableArray alloc] init];
int count = ([array count] - 7) / 5;
int startStops = [array count] - count;
for (int i = 0; i < count; i++) {
[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]);
}
_radius = [RCTConvert CGFloat:array[3]];
_radiusRatio = [RCTConvert CGFloat:array[4]] / _radius;
_focusPoint.x = [RCTConvert CGFloat:array[1]];
_focusPoint.y = [RCTConvert CGFloat:array[2]] / _radiusRatio;
_centerPoint.x = [RCTConvert CGFloat:array[5]];
_centerPoint.y = [RCTConvert CGFloat:array[6]] / _radiusRatio;
_gradient = CGGradientRetain([RCTConvert CGGradient:array offset:7]);
}
return self;
return self;
}
- (void)dealloc
{
CGGradientRelease(_gradient);
CGGradientRelease(_gradient);
}
- (void)paint:(CGContextRef)context
{
CGAffineTransform transform = CGAffineTransformMakeScale(1, _radiusRatio);
CGContextConcatCTM(context, transform);
CGGradientDrawingOptions extendOptions = kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation;
CGContextDrawRadialGradient(context, _gradient, _focusPoint, 0, _centerPoint, _radius, 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 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);
CGGradientDrawingOptions extendOptions = kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation;
CGContextDrawRadialGradient(context, _gradient, CGPointMake(fx, fy), 0, CGPointMake(cx, cy), rx, extendOptions);
}
@end

View File

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

View File

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

View File

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

View File

@@ -80,7 +80,6 @@
_clipPath = CGPathRetain(clipPath);
}
- (void)dealloc
{
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

@@ -22,7 +22,7 @@
{
CGContextRef context = UIGraphicsGetCurrentContext();
for (RNSVGNode *node in self.subviews) {
[node renderTo:context];
[node renderTo:context];
}
}

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) {
return true;
}
for (var i = 0; i < a.length; i++) {
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return true;
}
@@ -14,10 +14,27 @@ function arrayDiffer(a, b) {
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) {
if (a === b) {
return false;
}
if (a.font !== b.font) {
if (a.font === null) {
return true;
@@ -38,7 +55,7 @@ function fontAndLinesDiffer(a, b) {
return arrayDiffer(a.lines, b.lines);
}
var NodeAttributes = {
const NodeAttributes = {
transform: {
diff: arrayDiffer
},
@@ -49,45 +66,57 @@ var NodeAttributes = {
clipRule: true
};
var GroupAttributes = Object.assign({
}, NodeAttributes);
const GroupAttributes = {
...NodeAttributes
};
var RenderableAttributes = Object.assign({
const RenderableAttributes = {
fill: {
diff: arrayDiffer
},
fillRule: true,
stroke: {
diff: arrayDiffer
},
strokeWidth: true,
strokeLinecap: true,
strokeLinejoin: true,
fillRule: true,
strokeDasharray: {
diff: arrayDiffer
},
strokeDashoffset: true
}, NodeAttributes);
strokeDashoffset: true,
...NodeAttributes
};
var PathAttributes = Object.assign({
const PathAttributes ={
d: {
diff: arrayDiffer
}
}, RenderableAttributes);
},
...RenderableAttributes
};
var TextAttributes = Object.assign({
const TextAttributes = {
alignment: true,
frame: {
diff: fontAndLinesDiffer
},
path: {
diff: arrayDiffer
}
}, RenderableAttributes);
},
...RenderableAttributes
};
const ShapeAttributes = {
shape: {
diff: shapeDiffer
},
...RenderableAttributes
};
export {
GroupAttributes,
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 PATTERN = 3;
function applyBoundingBoxToBrushData(brushData, props) {
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) {
export default function (colorOrBrush) {
if (!colorOrBrush) {
return null;
}
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;
}
@@ -48,5 +18,6 @@ export default function (colorOrBrush, props) {
export {
LINEAR_GRADIENT,
RADIAL_GRADIENT
RADIAL_GRADIENT,
PATTERN
}

View File

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

View File

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

View File

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

View File

@@ -75,15 +75,15 @@ function extractFont(font) {
};
}
const alignments = {
right: 1,
center: 2,
left: 0
const anchord = {
end: 1,
middle: 2,
start: 0
};
export default function(props) {
return {
alignment: alignments[props.textAnchor] || 0,
alignment: anchord[props.textAnchor] || 0,
frame: extractFontAndLines(
props,
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) {
return false;
}
@@ -47,18 +47,18 @@ export default function({viewbox, width, height, preserveAspectRatio, x: dx, y:
y = -vy * sy;
}
if (shouldTransform) {
x += (+dx || 0);
y += (+dy || 0);
//if (shouldTransform) {
x += (+dx || 0);
y += (+dy || 0);
if (dScale) {
scaleX *= (+dScale || 1);
scaleY *= (+dScale || 1);
} else {
scaleX *= (+dScaleX || 1);
scaleY *= (+dScaleY || 1);
}
if (dScale) {
scaleX *= (+dScale || 1);
scaleY *= (+dScale || 1);
} else {
scaleX *= (+dScaleX || 1);
scaleY *= (+dScaleY || 1);
}
//}
return {
x,

View File

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

View File

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

View File

@@ -1,5 +1,10 @@
let percentReg = /^(\-?\d+(?:\.\d+)?)(%?)$/;
export default function (percent) {
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];
}

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';
export default function (stops, opacity) {
return _.reduce(stops, (ret, color, key) => {
ret[key] = color.alpha(opacity).rgbaString();
ret[key] = color.alpha(color.alpha() * opacity).rgbaString();
return ret;
}, {});
}