support stroke in pattern.

support stroke="url(#pattern)"
This commit is contained in:
Horcrux
2016-04-22 19:04:07 +08:00
parent bfcc39f73e
commit 74b5e82781
20 changed files with 456 additions and 369 deletions

View File

@@ -185,7 +185,7 @@ class FillGradientWithOpacity extends Component{
} }
class FillGradientInRect extends Component{ class FillGradientInRect extends Component{
static title = 'Fill a radial gradient inside a rect'; static title = 'Fill a radial gradient inside a rect and stroke it';
render() { render() {
return <Svg return <Svg
height="150" height="150"
@@ -205,7 +205,7 @@ class FillGradientInRect extends Component{
/> />
</RadialGradient> </RadialGradient>
</Defs> </Defs>
<Rect x="0" y="0" width="300" height="150" fill="url(#grad)" /> <Rect x="5" y="5" width="290" height="130" fill="url(#grad)" stroke="pink" strokeWidth="5" />
</Svg>; </Svg>;
} }
} }

View File

@@ -21,6 +21,7 @@ class RectExample extends Component{
fill="rgb(0,0,255)" fill="rgb(0,0,255)"
strokeWidth="3" strokeWidth="3"
stroke="rgb(0,0,0)" stroke="rgb(0,0,0)"
strokeDasharray="5,10"
/> />
</Svg>; </Svg>;
} }
@@ -39,10 +40,10 @@ class RectStrokeFill extends Component{
width="75" width="75"
height="75" height="75"
fill="blue" fill="blue"
fillOpacity="0.1" fillOpacity="0.5"
stroke="pink" stroke="red"
strokeWidth="5" strokeWidth="5"
strokeOpacity="0.9" strokeOpacity="0.5"
/> />
</Svg>; </Svg>;
} }

View File

@@ -4,7 +4,12 @@ import React, {
import Svg, { import Svg, {
Path, Path,
G Rect,
G,
Defs,
Stop,
RadialGradient,
Polyline
} from 'react-native-svg'; } from 'react-native-svg';
class StrokeExample extends Component{ class StrokeExample extends Component{
@@ -59,6 +64,48 @@ class StrokeDasharray extends Component{
} }
} }
class StrokePattern extends Component{
static title = 'Stroke with gradient.';
render() {
return <Svg height="80" width="200">
<Defs>
<RadialGradient id="grad" cx="50%" cy="50%" rx="80%" ry="80%" fx="50%" fy="50%">
<Stop
offset="50%"
stopColor="#fff"
stopOpacity="1"
/>
<Stop
offset="100%"
stopColor="#f00"
stopOpacity="1"
/>
</RadialGradient>
</Defs>
<Rect
x="5"
y="5"
height="70"
width="190"
fill="blue"
stroke="url(#grad)"
strokeWidth="5"
strokeDasharray="10"
/>
<Polyline
strokeDasharray="20,10,5,5,5,10"
points="10,10 20,12 30,20 40,60 60,70 90,55"
fill="none"
stroke="url(#grad)"
strokeLinecap="round"
strokeWidth="5"
/>
</Svg>;
}
}
const icon = <Svg const icon = <Svg
height="20" height="20"
width="20" width="20"
@@ -70,7 +117,7 @@ const icon = <Svg
</G> </G>
</Svg>; </Svg>;
const samples = [StrokeExample, StrokeWidth, StrokeLinecap, StrokeDasharray]; const samples = [StrokeExample, StrokeWidth, StrokeLinecap, StrokeDasharray, StrokePattern];
export { export {
icon, icon,

View File

@@ -4,7 +4,7 @@ import React, {
Children Children
} from 'react-native'; } from 'react-native';
import {NativeGroup} from './G'; import {NativeGroup} from './G';
import {set, remove} from '../lib/extract/extractFill'; import {set, remove} from '../lib/extract/patterns';
import percentFactory from '../lib/percentFactory'; import percentFactory from '../lib/percentFactory';
import percentToFloat from '../lib/percentToFloat'; import percentToFloat from '../lib/percentToFloat';
import Stop from './Stop'; import Stop from './Stop';

View File

@@ -24,6 +24,8 @@
*/ */
- (BOOL)applyFillColor:(CGContextRef)context; - (BOOL)applyFillColor:(CGContextRef)context;
- (BOOL)applyStrokeColor:(CGContextRef)context;
/** /**
* 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

@@ -24,6 +24,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
return NO; return NO;
} }
- (BOOL)applyStrokeColor:(CGContextRef)context
{
return NO;
}
- (void)paint:(CGContextRef)context - (void)paint:(CGContextRef)context
{ {
// abstract // abstract

View File

@@ -13,26 +13,32 @@
@implementation RNSVGSolidColor @implementation RNSVGSolidColor
{ {
CGColorRef _color; CGColorRef _color;
} }
- (instancetype)initWithArray:(NSArray<NSNumber *> *)array - (instancetype)initWithArray:(NSArray<NSNumber *> *)array
{ {
if ((self = [super initWithArray:array])) { if ((self = [super initWithArray:array])) {
_color = CGColorRetain([RCTConvert CGColor:array offset:1]); _color = CGColorRetain([RCTConvert CGColor:array offset:1]);
} }
return self; return self;
} }
- (void)dealloc - (void)dealloc
{ {
CGColorRelease(_color); CGColorRelease(_color);
} }
- (BOOL)applyFillColor:(CGContextRef)context - (BOOL)applyFillColor:(CGContextRef)context
{ {
CGContextSetFillColorWithColor(context, _color); CGContextSetFillColorWithColor(context, _color);
return YES; return YES;
}
- (BOOL)applyStrokeColor:(CGContextRef)context
{
CGContextSetStrokeColorWithColor(context, _color);
return YES;
} }
@end @end

View File

@@ -19,210 +19,211 @@
+ (CGPathRef)CGPath:(id)json + (CGPathRef)CGPath:(id)json
{ {
NSArray *arr = [self NSNumberArray:json]; NSArray *arr = [self NSNumberArray:json];
NSUInteger count = [arr count]; NSUInteger count = [arr count];
#define NEXT_VALUE [self double:arr[i++]] #define NEXT_VALUE [self double:arr[i++]]
CGMutablePathRef path = CGPathCreateMutable(); CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 0, 0); CGPathMoveToPoint(path, NULL, 0, 0);
@try { @try {
NSUInteger i = 0; NSUInteger i = 0;
while (i < count) { while (i < count) {
NSUInteger type = [arr[i++] unsignedIntegerValue]; NSUInteger type = [arr[i++] unsignedIntegerValue];
switch (type) { switch (type) {
case 0: case 0:
CGPathMoveToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE); CGPathMoveToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE);
break; break;
case 1: case 1:
CGPathCloseSubpath(path); CGPathCloseSubpath(path);
break; break;
case 2: case 2:
CGPathAddLineToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE); CGPathAddLineToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE);
break; break;
case 3: case 3:
CGPathAddCurveToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE); CGPathAddCurveToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE);
break; break;
case 4: case 4:
CGPathAddArc(path, NULL, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE == 0); CGPathAddArc(path, NULL, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE == 0);
break; break;
default: default:
RCTLogError(@"Invalid CGPath type %zd at element %zd of %@", type, i, arr); RCTLogError(@"Invalid CGPath type %zd at element %zd of %@", type, i, arr);
CGPathRelease(path); CGPathRelease(path);
return NULL; return NULL;
} }
}
} }
} @catch (NSException *exception) {
@catch (NSException *exception) { RCTLogError(@"Invalid CGPath format: %@", arr);
RCTLogError(@"Invalid CGPath format: %@", arr); CGPathRelease(path);
CGPathRelease(path); return NULL;
return NULL; }
}
return (CGPathRef)CFAutorelease(path);
return (CGPathRef)CFAutorelease(path);
} }
RCT_ENUM_CONVERTER(CTTextAlignment, (@{ RCT_ENUM_CONVERTER(CTTextAlignment, (@{
@"auto": @(kCTTextAlignmentNatural), @"auto": @(kCTTextAlignmentNatural),
@"left": @(kCTTextAlignmentLeft), @"left": @(kCTTextAlignmentLeft),
@"center": @(kCTTextAlignmentCenter), @"center": @(kCTTextAlignmentCenter),
@"right": @(kCTTextAlignmentRight), @"right": @(kCTTextAlignmentRight),
@"justify": @(kCTTextAlignmentJustified), @"justify": @(kCTTextAlignmentJustified),
}), kCTTextAlignmentNatural, integerValue) }), kCTTextAlignmentNatural, integerValue)
RCT_ENUM_CONVERTER(RNSVGCGFCRule, (@{ RCT_ENUM_CONVERTER(RNSVGCGFCRule, (@{
@"evenodd": @(kRNSVGCGFCRuleEvenodd), @"evenodd": @(kRNSVGCGFCRuleEvenodd),
@"nonzero": @(kRNSVGCGFCRuleNonzero), @"nonzero": @(kRNSVGCGFCRuleNonzero),
}), kRNSVGCGFCRuleNonzero, intValue) }), kRNSVGCGFCRuleNonzero, intValue)
// This takes a tuple of text lines and a font to generate a CTLine for each text line. // This takes a tuple of text lines and a font to generate a CTLine for each text line.
// This prepares everything for rendering a frame of text in RNSVGText. // This prepares everything for rendering a frame of text in RNSVGText.
+ (RNSVGTextFrame)RNSVGTextFrame:(id)json + (RNSVGTextFrame)RNSVGTextFrame:(id)json
{ {
NSDictionary *dict = [self NSDictionary:json]; NSDictionary *dict = [self NSDictionary:json];
RNSVGTextFrame frame; RNSVGTextFrame frame;
frame.count = 0; frame.count = 0;
NSArray *lines = [self NSArray:dict[@"lines"]]; NSArray *lines = [self NSArray:dict[@"lines"]];
NSUInteger lineCount = [lines count]; NSUInteger lineCount = [lines count];
if (lineCount == 0) { if (lineCount == 0) {
return frame;
}
NSDictionary *fontDict = dict[@"font"];
CTFontRef font = (__bridge CTFontRef)[self UIFont:nil withFamily:fontDict[@"fontFamily"] size:fontDict[@"fontSize"] weight:fontDict[@"fontWeight"] style:fontDict[@"fontStyle"] scaleMultiplier:1.0];
if (!font) {
return frame;
}
// Create a dictionary for this font
CFDictionaryRef attributes = (__bridge CFDictionaryRef)@{
(NSString *)kCTFontAttributeName: (__bridge id)font,
(NSString *)kCTForegroundColorFromContextAttributeName: @YES
};
// Set up text frame with font metrics
CGFloat size = CTFontGetSize(font);
frame.count = lineCount;
frame.baseLine = size; // estimate base line
frame.lineHeight = size * 1.1; // Base on RNSVG canvas line height estimate
frame.lines = malloc(sizeof(CTLineRef) * lineCount);
frame.widths = malloc(sizeof(CGFloat) * lineCount);
[lines enumerateObjectsUsingBlock:^(NSString *text, NSUInteger i, BOOL *stop) {
CFStringRef string = (__bridge CFStringRef)text;
CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes);
CTLineRef line = CTLineCreateWithAttributedString(attrString);
CFRelease(attrString);
frame.lines[i] = line;
frame.widths[i] = CTLineGetTypographicBounds(line, NULL, NULL, NULL);
}];
return frame; return frame;
}
NSDictionary *fontDict = dict[@"font"];
CTFontRef font = (__bridge CTFontRef)[self UIFont:nil withFamily:fontDict[@"fontFamily"] size:fontDict[@"fontSize"] weight:fontDict[@"fontWeight"] style:fontDict[@"fontStyle"] scaleMultiplier:1.0];
if (!font) {
return frame;
}
// Create a dictionary for this font
CFDictionaryRef attributes = (__bridge CFDictionaryRef)@{
(NSString *)kCTFontAttributeName: (__bridge id)font,
(NSString *)kCTForegroundColorFromContextAttributeName: @YES
};
// Set up text frame with font metrics
CGFloat size = CTFontGetSize(font);
frame.count = lineCount;
frame.baseLine = size; // estimate base line
frame.lineHeight = size * 1.1; // Base on RNSVG canvas line height estimate
frame.lines = malloc(sizeof(CTLineRef) * lineCount);
frame.widths = malloc(sizeof(CGFloat) * lineCount);
[lines enumerateObjectsUsingBlock:^(NSString *text, NSUInteger i, BOOL *stop) {
CFStringRef string = (__bridge CFStringRef)text;
CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes);
CTLineRef line = CTLineCreateWithAttributedString(attrString);
CFRelease(attrString);
frame.lines[i] = line;
frame.widths[i] = CTLineGetTypographicBounds(line, NULL, NULL, NULL);
}];
return frame;
} }
+ (RNSVGCGFloatArray)RNSVGCGFloatArray:(id)json + (RNSVGCGFloatArray)RNSVGCGFloatArray:(id)json
{ {
NSArray *arr = [self NSNumberArray:json]; NSArray *arr = [self NSNumberArray:json];
NSUInteger count = arr.count; NSUInteger count = arr.count;
RNSVGCGFloatArray array; RNSVGCGFloatArray array;
array.count = count; array.count = count;
array.array = NULL; array.array = NULL;
if (count) { if (count) {
// Ideally, these arrays should already use the same memory layout. // Ideally, these arrays should already use the same memory layout.
// In that case we shouldn't need this new malloc. // In that case we shouldn't need this new malloc.
array.array = malloc(sizeof(CGFloat) * count); array.array = malloc(sizeof(CGFloat) * count);
for (NSUInteger i = 0; i < count; i++) { for (NSUInteger i = 0; i < count; i++) {
array.array[i] = [arr[i] doubleValue]; array.array[i] = [arr[i] doubleValue];
}
} }
}
return array;
return array;
} }
+ (RNSVGBrush *)RNSVGBrush:(id)json + (RNSVGBrush *)RNSVGBrush:(id)json
{ {
NSArray *arr = [self NSArray:json]; NSArray *arr = [self NSArray:json];
NSUInteger type = [self NSUInteger:arr.firstObject]; NSUInteger type = [self NSUInteger:arr.firstObject];
switch (type) {
case 0: // solid color switch (type) {
// These are probably expensive allocations since it's often the same value. case 0: // solid color
// We should memoize colors but look ups may be just as expensive. // These are probably expensive allocations since it's often the same value.
return [[RNSVGSolidColor alloc] initWithArray:arr]; // We should memoize colors but look ups may be just as expensive.
case 1: // linear gradient return [[RNSVGSolidColor alloc] initWithArray:arr];
return [[RNSVGLinearGradient alloc] initWithArray:arr]; case 1: // linear gradient
case 2: // radial gradient return [[RNSVGLinearGradient alloc] initWithArray:arr];
return [[RNSVGRadialGradient alloc] initWithArray:arr]; case 2: // radial gradient
case 3: // pattern return [[RNSVGRadialGradient alloc] initWithArray:arr];
return [[RNSVGPattern alloc] initWithArray:arr]; case 3: // pattern
default: return [[RNSVGPattern alloc] initWithArray:arr];
RCTLogError(@"Unknown brush type: %zd", type); default:
return nil; RCTLogError(@"Unknown brush type: %zd", type);
} return nil;
}
} }
+ (CGPoint)CGPoint:(id)json offset:(NSUInteger)offset + (CGPoint)CGPoint:(id)json offset:(NSUInteger)offset
{ {
NSArray *arr = [self NSArray:json]; NSArray *arr = [self NSArray:json];
if (arr.count < offset + 2) { if (arr.count < offset + 2) {
RCTLogError(@"Too few elements in array (expected at least %zd): %@", 2 + offset, arr); RCTLogError(@"Too few elements in array (expected at least %zd): %@", 2 + offset, arr);
return CGPointZero; return CGPointZero;
} }
return (CGPoint){ return (CGPoint){
[self CGFloat:arr[offset]], [self CGFloat:arr[offset]],
[self CGFloat:arr[offset + 1]], [self CGFloat:arr[offset + 1]],
}; };
} }
+ (CGRect)CGRect:(id)json offset:(NSUInteger)offset + (CGRect)CGRect:(id)json offset:(NSUInteger)offset
{ {
NSArray *arr = [self NSArray:json]; NSArray *arr = [self NSArray:json];
if (arr.count < offset + 4) { if (arr.count < offset + 4) {
RCTLogError(@"Too few elements in array (expected at least %zd): %@", 4 + offset, arr); RCTLogError(@"Too few elements in array (expected at least %zd): %@", 4 + offset, arr);
return CGRectZero; return CGRectZero;
} }
return (CGRect){ return (CGRect){
{[self CGFloat:arr[offset]], [self CGFloat:arr[offset + 1]]}, {[self CGFloat:arr[offset]], [self CGFloat:arr[offset + 1]]},
{[self CGFloat:arr[offset + 2]], [self CGFloat:arr[offset + 3]]}, {[self CGFloat:arr[offset + 2]], [self CGFloat:arr[offset + 3]]},
}; };
} }
+ (CGColorRef)CGColor:(id)json offset:(NSUInteger)offset + (CGColorRef)CGColor:(id)json offset:(NSUInteger)offset
{ {
NSArray *arr = [self NSArray:json]; NSArray *arr = [self NSArray:json];
if (arr.count < offset + 4) { if (arr.count < offset + 4) {
RCTLogError(@"Too few elements in array (expected at least %zd): %@", 4 + offset, arr); RCTLogError(@"Too few elements in array (expected at least %zd): %@", 4 + offset, arr);
return NULL; return NULL;
} }
return [self CGColor:[arr subarrayWithRange:(NSRange){offset, 4}]]; return [self CGColor:[arr subarrayWithRange:(NSRange){offset, 4}]];
} }
+ (CGGradientRef)CGGradient:(id)json offset:(NSUInteger)offset + (CGGradientRef)CGGradient:(id)json offset:(NSUInteger)offset
{ {
NSArray *arr = [self NSArray:json]; NSArray *arr = [self NSArray:json];
if (arr.count < offset) { if (arr.count < offset) {
RCTLogError(@"Too few elements in array (expected at least %zd): %@", offset, arr); RCTLogError(@"Too few elements in array (expected at least %zd): %@", offset, arr);
return NULL; return NULL;
} }
arr = [arr subarrayWithRange:(NSRange){offset, arr.count - offset}]; arr = [arr subarrayWithRange:(NSRange){offset, arr.count - offset}];
RNSVGCGFloatArray colorsAndOffsets = [self RNSVGCGFloatArray:arr]; RNSVGCGFloatArray colorsAndOffsets = [self RNSVGCGFloatArray:arr];
size_t stops = colorsAndOffsets.count / 5; size_t stops = colorsAndOffsets.count / 5;
CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents( CGGradientRef gradient = CGGradientCreateWithColorComponents(
rgb, rgb,
colorsAndOffsets.array, colorsAndOffsets.array,
colorsAndOffsets.array + stops * 4, colorsAndOffsets.array + stops * 4,
stops stops
); );
CGColorSpaceRelease(rgb); CGColorSpaceRelease(rgb);
free(colorsAndOffsets.array); free(colorsAndOffsets.array);
return (CGGradientRef)CFAutorelease(gradient); return (CGGradientRef)CFAutorelease(gradient);
} }
@end @end

View File

@@ -32,10 +32,13 @@
} }
CGPathDrawingMode mode = kCGPathStroke; CGPathDrawingMode mode = kCGPathStroke;
BOOL fillColor = YES;
if (self.fill) { if (self.fill) {
if ([self.fill applyFillColor:context]) { mode = self.fillRule == kRNSVGCGFCRuleEvenodd ? kCGPathEOFill : kCGPathFill;
mode = self.fillRule == kRNSVGCGFCRuleEvenodd ? kCGPathEOFill : kCGPathFill; fillColor = [self.fill applyFillColor:context];
} else {
if (!fillColor) {
if (self.clipPath) { if (self.clipPath) {
[self clip:context]; [self clip:context];
} }
@@ -51,23 +54,46 @@
} }
} }
if (self.stroke) { if (self.stroke) {
CGContextSetStrokeColorWithColor(context, self.stroke);
CGContextSetLineWidth(context, self.strokeWidth); CGContextSetLineWidth(context, self.strokeWidth);
CGContextSetLineCap(context, self.strokeLinecap); CGContextSetLineCap(context, self.strokeLinecap);
CGContextSetLineJoin(context, self.strokeLinejoin); CGContextSetLineJoin(context, self.strokeLinejoin);
RNSVGCGFloatArray dash = self.strokeDash; RNSVGCGFloatArray dash = self.strokeDash;
// TODO: render as web svgs do
if (dash.count) { if (dash.count) {
CGContextSetLineDash(context, 0, dash.array, dash.count); CGContextSetLineDash(context, 0, dash.array, dash.count);
} }
if (mode == kCGPathFill) {
mode = kCGPathFillStroke; if (!fillColor) {
} else if (mode == kCGPathEOFill) { CGContextAddPath(context, self.d);
mode = kCGPathEOFillStroke; CGContextReplacePathWithStrokedPath(context);
CGContextClip(context);
}
if ([self.stroke applyStrokeColor:context]) {
if (mode == kCGPathFill) {
mode = kCGPathFillStroke;
} else if (mode == kCGPathEOFill) {
mode = kCGPathEOFillStroke;
}
} else {
// draw fill
[self clip:context];
CGContextAddPath(context, self.d);
CGContextDrawPath(context, mode);
// draw stroke
CGContextAddPath(context, self.d);
CGContextReplacePathWithStrokedPath(context);
CGContextClip(context);
[self.stroke paint:context];
return;
} }
} }
[self clip:context]; [self clip:context];
CGContextAddPath(context, self.d); CGContextAddPath(context, self.d);
CGContextDrawPath(context, mode); CGContextDrawPath(context, mode);
} }

View File

@@ -17,7 +17,7 @@
@property (nonatomic, strong) RNSVGBrush *fill; @property (nonatomic, strong) RNSVGBrush *fill;
@property (nonatomic, assign) RNSVGCGFCRule fillRule; @property (nonatomic, assign) RNSVGCGFCRule fillRule;
@property (nonatomic, assign) CGColorRef stroke; @property (nonatomic, strong) RNSVGBrush *stroke;
@property (nonatomic, assign) CGFloat strokeWidth; @property (nonatomic, assign) CGFloat strokeWidth;
@property (nonatomic, assign) CGLineCap strokeLinecap; @property (nonatomic, assign) CGLineCap strokeLinecap;
@property (nonatomic, assign) CGLineJoin strokeLinejoin; @property (nonatomic, assign) CGLineJoin strokeLinejoin;

View File

@@ -16,14 +16,10 @@
_fill = fill; _fill = fill;
} }
- (void)setStroke:(CGColorRef)stroke - (void)setStroke:(RNSVGBrush *)stroke
{ {
if (stroke == _stroke) {
return;
}
[self invalidate]; [self invalidate];
CGColorRelease(_stroke); _stroke = stroke;
_stroke = CGColorRetain(stroke);
} }
- (void)setStrokeWidth:(CGFloat)strokeWidth - (void)setStrokeWidth:(CGFloat)strokeWidth
@@ -58,7 +54,6 @@
- (void)dealloc - (void)dealloc
{ {
CGColorRelease(_stroke);
if (_strokeDash.array) { if (_strokeDash.array) {
free(_strokeDash.array); free(_strokeDash.array);
} }

View File

@@ -14,114 +14,117 @@
- (void)setAlignment:(CTTextAlignment)alignment - (void)setAlignment:(CTTextAlignment)alignment
{ {
[self invalidate]; [self invalidate];
_alignment = alignment; _alignment = alignment;
} }
static void RNSVGFreeTextFrame(RNSVGTextFrame frame) static void RNSVGFreeTextFrame(RNSVGTextFrame frame)
{ {
if (frame.count) { if (frame.count) {
// We must release each line before freeing up this struct // We must release each line before freeing up this struct
for (int i = 0; i < frame.count; i++) { for (int i = 0; i < frame.count; i++) {
CFRelease(frame.lines[i]); CFRelease(frame.lines[i]);
}
free(frame.lines);
free(frame.widths);
} }
free(frame.lines);
free(frame.widths);
}
} }
- (void)setTextFrame:(RNSVGTextFrame)frame - (void)setTextFrame:(RNSVGTextFrame)frame
{ {
if (frame.lines != _textFrame.lines) { if (frame.lines != _textFrame.lines) {
RNSVGFreeTextFrame(_textFrame); RNSVGFreeTextFrame(_textFrame);
} }
[self invalidate]; [self invalidate];
_textFrame = frame; _textFrame = frame;
} }
- (void)dealloc - (void)dealloc
{ {
RNSVGFreeTextFrame(_textFrame); RNSVGFreeTextFrame(_textFrame);
} }
- (void)renderLayerTo:(CGContextRef)context - (void)renderLayerTo:(CGContextRef)context
{ {
RNSVGTextFrame frame = self.textFrame; RNSVGTextFrame frame = self.textFrame;
if ((!self.fill && !self.stroke) || !frame.count) { if ((!self.fill && !self.stroke) || !frame.count) {
return;
}
// to-do: draw along a path
// to-do: fill-rule
// to-do: clip
CGTextDrawingMode mode = kCGTextStroke;
if (self.fill) {
if ([self.fill applyFillColor:context]) {
mode = kCGTextFill;
} else {
for (int i = 0; i < frame.count; i++) {
CGContextSaveGState(context);
// Inverse the coordinate space since CoreText assumes a bottom-up coordinate space
CGContextScaleCTM(context, 1.0, -1.0);
CGContextSetTextDrawingMode(context, kCGTextClip);
[self renderLineTo:context atIndex:i];
// Inverse the coordinate space back to the original before filling
CGContextScaleCTM(context, 1.0, -1.0);
[self.fill paint:context];
// Restore the state so that the next line can be clipped separately
CGContextRestoreGState(context);
}
if (!self.stroke) {
return; return;
}
} }
}
if (self.stroke) { // to-do: draw along a path
CGContextSetStrokeColorWithColor(context, self.stroke); // to-do: fill-rule
CGContextSetLineWidth(context, self.strokeWidth); // to-do: clip
CGContextSetLineCap(context, self.strokeLinecap); CGTextDrawingMode mode = kCGTextStroke;
CGContextSetLineJoin(context, self.strokeLinejoin); if (self.fill) {
RNSVGCGFloatArray dash = self.strokeDash; if ([self.fill applyFillColor:context]) {
if (dash.count) { mode = kCGTextFill;
CGContextSetLineDash(context, 0, dash.array, dash.count); } else {
for (int i = 0; i < frame.count; i++) {
CGContextSaveGState(context);
// Inverse the coordinate space since CoreText assumes a bottom-up coordinate space
CGContextScaleCTM(context, 1.0, -1.0);
CGContextSetTextDrawingMode(context, kCGTextClip);
[self renderLineTo:context atIndex:i];
// Inverse the coordinate space back to the original before filling
CGContextScaleCTM(context, 1.0, -1.0);
[self.fill paint:context];
// Restore the state so that the next line can be clipped separately
CGContextRestoreGState(context);
}
if (!self.stroke) {
return;
}
}
} }
if (mode == kCGTextFill) { if (self.stroke) {
mode = kCGTextFillStroke; if ([self.stroke applyStrokeColor:context] == NO) {
[self.stroke paint:context];
}
CGContextSetLineWidth(context, self.strokeWidth);
CGContextSetLineCap(context, self.strokeLinecap);
CGContextSetLineJoin(context, self.strokeLinejoin);
RNSVGCGFloatArray dash = self.strokeDash;
if (dash.count) {
CGContextSetLineDash(context, 0, dash.array, dash.count);
}
if (mode == kCGTextFill) {
mode = kCGTextFillStroke;
}
}
CGContextSetTextDrawingMode(context, mode);
// Inverse the coordinate space since CoreText assumes a bottom-up coordinate space
CGContextScaleCTM(context, 1.0, -1.0);
for (int i = 0; i < frame.count; i++) {
[self renderLineTo:context atIndex:i];
} }
}
CGContextSetTextDrawingMode(context, mode);
// Inverse the coordinate space since CoreText assumes a bottom-up coordinate space
CGContextScaleCTM(context, 1.0, -1.0);
for (int i = 0; i < frame.count; i++) {
[self renderLineTo:context atIndex:i];
}
} }
- (void)renderLineTo:(CGContextRef)context atIndex:(int)index - (void)renderLineTo:(CGContextRef)context atIndex:(int)index
{ {
RNSVGTextFrame frame = self.textFrame; RNSVGTextFrame frame = self.textFrame;
CGFloat shift; CGFloat shift;
switch (self.alignment) { switch (self.alignment) {
case kCTTextAlignmentRight: case kCTTextAlignmentRight:
shift = frame.widths[index]; shift = frame.widths[index];
break; break;
case kCTTextAlignmentCenter: case kCTTextAlignmentCenter:
shift = (frame.widths[index] / 2); shift = (frame.widths[index] / 2);
break; break;
default: default:
shift = 0; shift = 0;
break; break;
} }
// We should consider snapping this shift to device pixels to improve rendering quality // We should consider snapping this shift to device pixels to improve rendering quality
// when a line has subpixel width. // when a line has subpixel width.
CGContextSetTextPosition(context, -shift, -frame.baseLine - frame.lineHeight * index); CGContextSetTextPosition(context, -shift, -frame.baseLine - frame.lineHeight * index);
CTLineRef line = frame.lines[index]; CTLineRef line = frame.lines[index];
CTLineDraw(line, context); CTLineDraw(line, context);
} }
@end @end

View File

@@ -20,12 +20,12 @@ RCT_EXPORT_MODULE()
return [RNSVGRenderable new]; return [RNSVGRenderable new];
} }
RCT_EXPORT_VIEW_PROPERTY(fill, RNSVGBrush)
RCT_EXPORT_VIEW_PROPERTY(fillRule, RNSVGCGFCRule)
RCT_EXPORT_VIEW_PROPERTY(stroke, RNSVGBrush)
RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat) RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(strokeLinecap, CGLineCap) RCT_EXPORT_VIEW_PROPERTY(strokeLinecap, CGLineCap)
RCT_EXPORT_VIEW_PROPERTY(strokeLinejoin, CGLineJoin) RCT_EXPORT_VIEW_PROPERTY(strokeLinejoin, CGLineJoin)
RCT_EXPORT_VIEW_PROPERTY(fill, RNSVGBrush)
RCT_EXPORT_VIEW_PROPERTY(fillRule, RNSVGCGFCRule)
RCT_EXPORT_VIEW_PROPERTY(stroke, CGColor)
RCT_EXPORT_VIEW_PROPERTY(strokeDash, RNSVGCGFloatArray) RCT_EXPORT_VIEW_PROPERTY(strokeDash, RNSVGCGFloatArray)
RCT_EXPORT_VIEW_PROPERTY(clipPath, CGPath) RCT_EXPORT_VIEW_PROPERTY(clipPath, CGPath)
RCT_EXPORT_VIEW_PROPERTY(clipRule, RNSVGCGFCRule) RCT_EXPORT_VIEW_PROPERTY(clipRule, RNSVGCGFCRule)

View File

@@ -26,7 +26,7 @@ function applyBoundingBoxToBrushData(brushData, props) {
} }
export default function (colorOrBrush, props) { export default function (colorOrBrush, props) {
if (colorOrBrush == null) { if (!colorOrBrush) {
return null; return null;
} }
if (colorOrBrush._brush) { if (colorOrBrush._brush) {

View File

@@ -1,63 +1,19 @@
import rgba from '../rgba'; import rgba from '../rgba';
import {LinearGradientGenerator} from '../../elements/LinearGradient';
import {RadialGradientGenerator} from '../../elements/RadialGradient';
import extractBrush from './extractBrush'; import extractBrush from './extractBrush';
import fillReg from './patternReg'; import patterns from './patterns';
let fillPatterns = {};
function isGradient(obj) {
return obj instanceof LinearGradientGenerator || obj instanceof RadialGradientGenerator;
}
function set(id, pattern) {
fillPatterns[id] = pattern;
}
function remove(id) {
delete fillPatterns[id];
}
const fillRules = { const fillRules = {
evenodd: 0, evenodd: 0,
nonzero: 1 nonzero: 1
}; };
function fillFilter(props) { function fillFilter(props, dimensions) {
let {fill} = props; let {fill} = props;
if (fill === 'none') { if (fill === 'none') {
return null; return null;
} if (fill) { } else if (fill) {
return patterns(fill, +props.fillOpacity, dimensions, props.svgId);
if (isGradient(fill)) {
return fill;
}
let fillOpacity = +props.fillOpacity;
if (isNaN(fillOpacity)) {
fillOpacity = 1;
}
// 尝试匹配 fill="url(#pattern)"
let matched = fill.match(fillReg);
if (matched) {
let patternName = `${matched[1]}:${props.svgId}`;
let pattern = fillPatterns[patternName];
if (pattern) {
if (pattern.length === 2) {
let dimensions = this.getBoundingBox();
return pattern(dimensions, fillOpacity);
} else {
return pattern(fillOpacity);
}
}
return null;
}
return rgba(props.fill, fillOpacity).rgbaString();
} else if (props.fill === undefined) { } else if (props.fill === undefined) {
let fillOpacity = +props.fillOpacity; let fillOpacity = +props.fillOpacity;
if (isNaN(fillOpacity)) { if (isNaN(fillOpacity)) {
@@ -67,20 +23,14 @@ function fillFilter(props) {
} else { } else {
return null; return null;
} }
} }
export default function(props) { export default function(props, dimensions) {
let fill = fillFilter.call(this, props); let fill = extractBrush(fillFilter(props, dimensions), props);
let fillRule = fillRules[props.fillRule] === 0 ? 0 : 1; let fillRule = fillRules[props.fillRule] === 0 ? 0 : 1;
return { return {
fill: extractBrush(fill, props), fill,
fillRule fillRule
}; };
} }
export {
set,
remove
}

View File

@@ -14,16 +14,18 @@ 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)); _.assign(extractedProps, extractStroke(props, dimensions));
} }
if (options.fill) { if (options.fill) {
_.assign(extractedProps, extractFill.call(this, props)); _.assign(extractedProps, extractFill(props, dimensions));
} }
if (options.transform) { if (options.transform) {

View File

@@ -1,5 +1,6 @@
import rgba from '../rgba';
import extractBrush from './extractBrush'; import extractBrush from './extractBrush';
import patterns from './patterns';
let separator = /\s*,\s*/; let separator = /\s*,\s*/;
const caps = { const caps = {
@@ -14,9 +15,10 @@ const joins = {
round: 1 round: 1
}; };
function strokeFilter(props) { function strokeFilter(props, dimensions) {
if (!props.stroke && !props.strokeLinecap && !props.strokeOpacity && let strokeWidth = +props.strokeWidth;
!props.strokeLinejoin && !props.strokeDasharray && !props.strokeWidth) { let {stroke} = props;
if (!strokeWidth && !stroke) {
return null; return null;
} }
@@ -26,30 +28,25 @@ function strokeFilter(props) {
strokeDasharray = strokeDasharray.split(separator).map(dash => +dash); strokeDasharray = strokeDasharray.split(separator).map(dash => +dash);
} }
if (!stroke) {
stroke = '#000';
}
// TODO: propTypes check
return { return {
stroke: rgba(props.stroke, props.strokeOpacity).rgbaArray(), stroke: patterns(stroke, +props.strokeOpacity, dimensions, props.svgId),
strokeLinecap: caps[props.strokeLinecap] || 2, strokeLinecap: caps[props.strokeLinecap] || 2,
strokeLinejoin: joins[props.strokeLinejoin] || 0, strokeLinejoin: joins[props.strokeLinejoin] || 0,
strokeDash: strokeDasharray || null, strokeDash: strokeDasharray || null,
strokeWidth: +props.strokeWidth || 1 strokeWidth: strokeWidth || 1
}; };
} }
export default function(props) { export default function(props, dimensions) {
let strokeProps = strokeFilter(props); let strokeProps = strokeFilter(props, dimensions);
if (!strokeProps) { return strokeProps ? {
return {};
}
let {stroke} = strokeProps;
return {
...strokeProps, ...strokeProps,
stroke: stroke ? [stroke[0] / 255, stroke[1] / 255, stroke[2] / 255, stroke[3]] : null stroke: extractBrush(strokeProps.stroke, props)
}; } : null;
// TODO: implement brush on stroke prop
//return {
// ...strokeProps,
// stroke: extractBrush(strokeProps.stroke, props)
//};
} }

View File

@@ -1 +0,0 @@
export default /^url\(#(\w+?)\)$/;

53
lib/extract/patterns.js Normal file
View File

@@ -0,0 +1,53 @@
import rgba from '../rgba';
let patterns = {};
let patternReg = /^url\(#(\w+?)\)$/;
import {LinearGradientGenerator} from '../../elements/LinearGradient';
import {RadialGradientGenerator} from '../../elements/RadialGradient';
function isGradient(obj) {
return obj instanceof LinearGradientGenerator || obj instanceof RadialGradientGenerator;
}
function set(id, pattern) {
patterns[id] = pattern;
}
function remove(id) {
delete patterns[id];
}
export {
set,
remove
}
export default function(patternSting, opacity, dimensions, svgId) {
if (isGradient(patternSting)) {
return patternSting;
}
if (isNaN(opacity)) {
opacity = 1;
}
// 尝试匹配 "url(#pattern)"
let matched = patternSting.match(patternReg);
if (matched) {
let patternName = `${matched[1]}:${svgId}`;
let pattern = patterns[patternName];
if (pattern) {
if (pattern.length === 2) {
return pattern(dimensions, opacity);
} else {
return pattern(opacity);
}
}
return null;
}
return rgba(patternSting, opacity).rgbaString();
}

View File

@@ -1,5 +1,5 @@
{ {
"version": "1.0.0", "version": "1.0.1",
"name": "react-native-svg", "name": "react-native-svg",
"description": "react native svg library", "description": "react native svg library",
"repository": { "repository": {