From bd26c04a1d7bf52b8947dbd77cd9a1157dd5a8fb Mon Sep 17 00:00:00 2001 From: Horcrux Date: Tue, 30 Aug 2016 09:37:38 +0800 Subject: [PATCH] first step of TSpan --- Example/examples/Text.js | 33 +++++++- elements/TSpan.js | 40 +++++++++ elements/Text.js | 21 +---- ios/Text/RNSVGTSpan.h | 19 +++++ ios/Text/RNSVGTSpan.m | 136 ++++++++++++++++++++++++++++++ lib/attributes.js | 47 ++++------- lib/extract/extractFrame.js | 21 +++++ lib/extract/extractSpan.js | 76 +++++++++++++++++ lib/extract/extractText.js | 118 ++++++++------------------ lib/extract/extractTextContent.js | 26 ++++++ 10 files changed, 400 insertions(+), 137 deletions(-) create mode 100644 elements/TSpan.js create mode 100644 ios/Text/RNSVGTSpan.h create mode 100644 ios/Text/RNSVGTSpan.m create mode 100644 lib/extract/extractFrame.js create mode 100644 lib/extract/extractSpan.js create mode 100644 lib/extract/extractTextContent.js diff --git a/Example/examples/Text.js b/Example/examples/Text.js index 68d8004d..f905c844 100644 --- a/Example/examples/Text.js +++ b/Example/examples/Text.js @@ -8,7 +8,8 @@ import Svg, { Stop, Defs, Path, - G + G, + TSpan } from 'react-native-svg'; class TextExample extends Component{ @@ -148,6 +149,33 @@ class TextPath extends Component{ } } +class TSpanExample extends Component{ + static title = 'TSpan nest'; + + render() { + + return + + tspan line 1 + tspan line 2 + tspan line 3 + + + 12345 + + 6 + 7 + + 89a + + delta on text + ; + } +} + const icon = { + this.root.setNativeProps(...args); + }; + + render() { + return {this.root = ele;}} + {...extractFrame(this.props)} + />; + } +} + +const RNSVGTSpan = createReactNativeComponentClass({ + validAttributes: TSpanAttributes, + uiViewClassName: 'RNSVGTSpan' +}); + +export default TSpan; diff --git a/elements/Text.js b/elements/Text.js index a9179c09..744e52d3 100644 --- a/elements/Text.js +++ b/elements/Text.js @@ -13,18 +13,11 @@ class Text extends Shape { dx: numberProp, dy: numberProp, 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 - }; - - static defaultProps = { - dx: 0, - dy: 0 + font: PropTypes.object }; setNativeProps = (...args) => { @@ -33,19 +26,9 @@ class Text extends Shape { render() { let props = this.props; - - let x = 0; - if (props.x) { - x = props.dx ? +props.x + (+props.dx) : +props.x; - } - let y = 0; - if (props.y) { - y = props.dy ? +props.y + (+props.dy) : +props.y; - } - return {this.root = ele;}} - {...this.extractProps({...props, x, y})} + {...this.extractProps({...props})} {...extractText(props)} />; } diff --git a/ios/Text/RNSVGTSpan.h b/ios/Text/RNSVGTSpan.h new file mode 100644 index 00000000..1c894734 --- /dev/null +++ b/ios/Text/RNSVGTSpan.h @@ -0,0 +1,19 @@ +/** + * 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 +#import "RNSVGPath.h" +#import "RNSVGTextFrame.h" + +@interface RNSVGText : RNSVGPath + +@property (nonatomic, assign) CTTextAlignment alignment; +@property (nonatomic, assign) RNSVGTextFrame textFrame; +@property (nonatomic, assign) NSString + +@end diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m new file mode 100644 index 00000000..a2ca287b --- /dev/null +++ b/ios/Text/RNSVGTSpan.m @@ -0,0 +1,136 @@ +/** + * 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 "RNSVGText.h" +#import "RNSVGBezierPath.h" +#import + +@implementation RNSVGText + +static void RNSVGFreeTextFrame(RNSVGTextFrame frame) +{ + if (frame.count) { + // We must release each line before freeing up this struct + for (int i = 0; i < frame.count; i++) { + CFRelease(frame.lines[i]); + } + free(frame.lines); + free(frame.widths); + } +} + +- (void)setAlignment:(CTTextAlignment)alignment +{ + [self invalidate]; + _alignment = alignment; +} + +- (void)setTextFrame:(RNSVGTextFrame)textFrame +{ + RNSVGFreeTextFrame(_textFrame); + [self invalidate]; + _textFrame = textFrame; +} + +- (void)setPath:(NSArray *)path +{ + if (path == _path) { + return; + } + [self invalidate]; + _path = path; +} + +- (void)dealloc +{ + RNSVGFreeTextFrame(_textFrame); +} + +- (CGPathRef)getPath:(CGContextRef)context +{ + CGMutablePathRef path = CGPathCreateMutable(); + RNSVGTextFrame frame = self.textFrame; + for (int i = 0; i < frame.count; i++) { + CGFloat shift; + CGFloat width = frame.widths[i]; + switch (self.alignment) { + case kCTTextAlignmentRight: + shift = width; + break; + case kCTTextAlignmentCenter: + shift = width / 2; + break; + default: + shift = 0; + break; + } + // We should consider snapping this shift to device pixels to improve rendering quality + // when a line has subpixel width. + CGAffineTransform offset = CGAffineTransformMakeTranslation(-shift, frame.baseLine + frame.lineHeight * i + (self.path ? -frame.lineHeight : 0)); + + CGMutablePathRef line = [self setLinePath:frame.lines[i]]; + CGPathAddPath(path, &offset, line); + CGPathRelease(line); + } + + return (CGPathRef)CFAutorelease(path); +} + +- (CGMutablePathRef)setLinePath:(CTLineRef)line +{ + CGAffineTransform upsideDown = CGAffineTransformMakeScale(1.0, -1.0); + CGMutablePathRef path = CGPathCreateMutable(); + + CFArrayRef glyphRuns = CTLineGetGlyphRuns(line); + CTRunRef run = CFArrayGetValueAtIndex(glyphRuns, 0); + + CFIndex runGlyphCount = CTRunGetGlyphCount(run); + CGPoint positions[runGlyphCount]; + CGGlyph glyphs[runGlyphCount]; + + // Grab the glyphs, positions, and font + CTRunGetPositions(run, CFRangeMake(0, 0), positions); + CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs); + CFDictionaryRef attributes = CTRunGetAttributes(run); + + CTFontRef runFont = CFDictionaryGetValue(attributes, kCTFontAttributeName); + + RNSVGBezierPath *bezierPath = [[RNSVGBezierPath alloc] initWithBezierCurves:self.path]; + + for(CFIndex i = 0; i < runGlyphCount; ++i) { + CGPathRef letter = CTFontCreatePathForGlyph(runFont, glyphs[i], nil); + CGPoint point = positions[i]; + + if (letter) { + CGAffineTransform transform; + + // draw glyphs along path + if (self.path) { + transform = [bezierPath transformAtDistance:point.x]; + + // break loop if line reaches the end of the Path. + if (!transform.a || !transform.d) { + CGPathRelease(letter); + break; + } + transform = CGAffineTransformScale(transform, 1.0, -1.0); + } else { + transform = CGAffineTransformTranslate(upsideDown, point.x, point.y); + } + + + CGPathAddPath(path, &transform, letter); + } + + CGPathRelease(letter); + } + + return path; +} + +@end diff --git a/lib/attributes.js b/lib/attributes.js index a74fd36f..35aedfe8 100644 --- a/lib/attributes.js +++ b/lib/attributes.js @@ -17,30 +17,6 @@ function arrayDiffer(a, b) { return false; } -function fontAndLinesDiffer(a, b) { - if (a === b) { - return false; - } - if (a.font !== b.font) { - if (a.font === null) { - return true; - } - if (b.font === null) { - return true; - } - - if ( - a.font.fontFamily !== b.font.fontFamily || - a.font.fontSize !== b.font.fontSize || - a.font.fontWeight !== b.font.fontWeight || - a.font.fontStyle !== b.font.fontStyle - ) { - return true; - } - } - return arrayDiffer(a.lines, b.lines); -} - const ViewBoxAttributes = { minX: true, minY: true, @@ -104,16 +80,22 @@ const PathAttributes = merge({ } }, RenderableAttributes); -const TextAttributes = merge({ - alignment: true, - frame: { - diff: fontAndLinesDiffer - }, - path: { - diff: arrayDiffer - } +const FontAttributes = merge({ + fontFamily: true, + fontSize: true, + fontWeight: true, + fontStyle: true }, RenderableAttributes); +const TSpanAttributes = merge({ + line: true +}, FontAttributes); + + +const TextAttributes = merge({ + alignment: true +}, FontAttributes); + const ClipPathAttributes = { name: true }; @@ -184,6 +166,7 @@ const RectAttributes = merge({ export { PathAttributes, TextAttributes, + TSpanAttributes, GroupAttributes, ClipPathAttributes, CircleAttributes, diff --git a/lib/extract/extractFrame.js b/lib/extract/extractFrame.js new file mode 100644 index 00000000..7f8c2a74 --- /dev/null +++ b/lib/extract/extractFrame.js @@ -0,0 +1,21 @@ +import extractSpan from './extractSpan'; +import extractTextContent from './extractTextContent'; + +export default function (props) { + let {children, line} = props; + let extractedProps = extractSpan(props); + + if (typeof children === 'string') { + line = children; + children = null; + } else { + children = extractTextContent(props.children); + line = null; + } + console.log(extractedProps); + return { + ...extractedProps, + children, + line + }; +}; diff --git a/lib/extract/extractSpan.js b/lib/extract/extractSpan.js new file mode 100644 index 00000000..151885ef --- /dev/null +++ b/lib/extract/extractSpan.js @@ -0,0 +1,76 @@ +import React, { + Children +} from 'react'; + +import TSpan from '../../elements/TSpan'; +import extractTextContent from './extractTextContent'; +import SerializablePath from '../SerializablePath'; +import _ from 'lodash'; +const fontRegExp = /^\s*((?:(?:normal|bold|italic)\s+)*)(?:(\d+(?:\.\d+)?)[ptexm%]*(?:\s*\/.*?)?\s+)?\s*"?([^"]*)/i; +const fontFamilyPrefix = /^[\s"']*/; +const fontFamilySuffix = /[\s"']*$/; +const spaceReg = /\s+/; +const commaReg = /,/; + +let cachedFontObjectsFromString = {}; + +function extractSingleFontFamily(fontFamilyString) { + // SVG on the web allows for multiple font-families to be specified. + // For compatibility, we extract the first font-family, hoping + // we'll get a match. + return fontFamilyString ? fontFamilyString.split(commaReg)[0] + .replace(fontFamilyPrefix, '') + .replace(fontFamilySuffix, '') : null; +} + +function parseFontString(font) { + if (cachedFontObjectsFromString.hasOwnProperty(font)) { + return cachedFontObjectsFromString[font]; + } + let match = fontRegExp.exec(font); + if (!match) { + return null; + } + let fontFamily = extractSingleFontFamily(match[3]); + let fontSize = +match[2] || 12; + let isBold = /bold/.exec(match[1]); + let isItalic = /italic/.exec(match[1]); + cachedFontObjectsFromString[font] = { + fontFamily: fontFamily, + fontSize: fontSize, + fontWeight: isBold ? 'bold' : 'normal', + fontStyle: isItalic ? 'italic' : 'normal' + }; + return cachedFontObjectsFromString[font]; +} + +function extractFont(props) { + let font = props.font; + let fontSize = +props.fontSize; + + let ownedFont = { + fontFamily: extractSingleFontFamily(props.fontFamily), + fontSize: isNaN(fontSize) ? null : fontSize, + fontWeight: props.fontWeight, + fontStyle: props.fontStyle + }; + + if (typeof props.font === 'string') { + font = parseFontString(props.font); + } + ownedFont = _.pickBy(ownedFont, prop => !_.isNil(prop)); + + return _.defaults(ownedFont, font); +} + +function parseDelta(delta) { + return delta.toString().split(spaceReg); +} + +export default function(props) { + return { + dx: parseDelta(props.dx || ''), + dy: parseDelta(props.dy || ''), + ...extractFont(props) + }; +} diff --git a/lib/extract/extractText.js b/lib/extract/extractText.js index 7a44f2ed..fa9aaa78 100644 --- a/lib/extract/extractText.js +++ b/lib/extract/extractText.js @@ -1,93 +1,43 @@ -import SerializablePath from '../SerializablePath'; -import _ from 'lodash'; -const newLine = /\n/g; -const defaultFontFamily = '"Helvetica Neue", "Helvetica", Arial'; -const fontRegExp = /^\s*((?:(?:normal|bold|italic)\s+)*)(?:(\d+(?:\.\d+)?)[ptexm%]*(?:\s*\/.*?)?\s+)?\s*"?([^"]*)/i; -const fontFamilyPrefix = /^[\s"']*/; -const fontFamilySuffix = /[\s"']*$/; -let cachedFontObjectsFromString = {}; +import extractTextContent from './extractTextContent'; +import extractSpan from './extractSpan'; -function childrenAsString(children) { - if (!children) { - return ''; - } - if (typeof children === 'string') { - return children; - } - if (children.length) { - return children.join('\n'); - } - return ''; -} - -function extractFontAndLines(font, text) { - return { - font: extractFont(font), - lines: text.split(newLine) - }; -} - -function extractSingleFontFamily(fontFamilyString = defaultFontFamily) { - // SVG on the web allows for multiple font-families to be specified. - // For compatibility, we extract the first font-family, hoping - // we'll get a match. - return fontFamilyString.split(',')[0] - .replace(fontFamilyPrefix, '') - .replace(fontFamilySuffix, ''); -} - -function parseFontString(font) { - if (cachedFontObjectsFromString.hasOwnProperty(font)) { - return cachedFontObjectsFromString[font]; - } - let match = fontRegExp.exec(font); - if (!match) { - return null; - } - let fontFamily = extractSingleFontFamily(match[3]); - let fontSize = +match[2] || 12; - let isBold = /bold/.exec(match[1]); - let isItalic = /italic/.exec(match[1]); - cachedFontObjectsFromString[font] = { - fontFamily: fontFamily, - fontSize: fontSize, - fontWeight: isBold ? 'bold' : 'normal', - fontStyle: isItalic ? 'italic' : 'normal' - }; - return cachedFontObjectsFromString[font]; -} - -function extractFont(font) { - if (_.isNil(font)) { - return null; - } - if (typeof font === 'string') { - return parseFontString(font); - } - let fontFamily = extractSingleFontFamily(font.fontFamily); - let fontSize = +font.fontSize || 12; - - return { - fontFamily: fontFamily, - fontSize: fontSize, - fontWeight: font.fontWeight, - fontStyle: font.fontStyle - }; -} - -const anchord = { +const anchors = { end: 1, middle: 2, start: 0 }; export default function(props) { + let children = extractTextContent(props.children); + let extractedProps = extractSpan(props); + let firstSpan = children[0]; + let alignment; + let {dx, dy} = extractedProps; + let maxDeltaLength = Math.max(dx.length, dy.length); + + if (firstSpan && firstSpan.props.hasOwnProperty('textAnchor')) { + alignment = anchors[firstSpan.props.textAnchor]; + } else if (anchors[props.textAnchor]) { + alignment = anchors[props.textAnchor]; + } + + if (!alignment) { + alignment = 0; + } + + for (let i = 0; i < maxDeltaLength; i++) { + console.log(i); + } + + console.log(extractedProps); + return { - alignment: anchord[props.textAnchor] || 0, - frame: extractFontAndLines( - props, - childrenAsString(props.children) - ), - path: props.path ? new SerializablePath(props.path).toJSON() : undefined - }; + alignment, + children, + fontFamily: 'Helvetica Neue', + fontSize: 12, + fontStyle: 'normal', + fontWeight: 'normal', + ...extractedProps + } } diff --git a/lib/extract/extractTextContent.js b/lib/extract/extractTextContent.js new file mode 100644 index 00000000..d0a2a537 --- /dev/null +++ b/lib/extract/extractTextContent.js @@ -0,0 +1,26 @@ +import React, { + Children +} from 'react'; + +import TSpan from '../../elements/TSpan'; +const newLine = /\n/g; + +export default function(children) { + let spans = []; + + Children.forEach(children, function (child = '') { + let span; + if (typeof child === 'string') { + span = {child.replace(newLine, ' ')}; + } else if (child.type === TSpan) { + span = child; + } else { + // give warning about the illegal child type + return; + } + + spans.push(span); + }); + + return spans; +}