diff --git a/Example/examples.js b/Example/examples.js index 77ccf860..a90ced1b 100644 --- a/Example/examples.js +++ b/Example/examples.js @@ -10,6 +10,7 @@ import * as Text from './examples/Text'; import * as G from './examples/G'; import * as Stroking from './examples/Stroking'; import * as Use from './examples/Use'; +import * as Gradients from './examples/Gradients'; export { Svg, @@ -23,5 +24,6 @@ export { Text, Stroking, G, - Use + Use, + Gradients }; diff --git a/Example/examples/Gradients.js b/Example/examples/Gradients.js new file mode 100644 index 00000000..4b591a72 --- /dev/null +++ b/Example/examples/Gradients.js @@ -0,0 +1,121 @@ +import React, { + Component +} from 'react-native'; + +import Svg, { + Defs, + LinearGradient, + RadialGradient, + Stop, + Ellipse, + Circle +} from 'react-native-art-svg'; + +class LinearGradientHorizontal extends Component{ + static title = 'Define an ellipse with a horizontal linear gradient from yellow to red'; + render() { + return + + + + + + + + ; + } +} + +class LinearGradientVertical extends Component{ + static title = 'Define an ellipse with a horizontal linear gradient from yellow to red'; + render() { + return + + + + + + + + ; + } +} + +class RadialGradientExample extends Component{ + static title = 'Define an ellipse with a radial gradient from yellow to purple'; + render() { + return + + + + + + + + ; + } +} + +class RadialGradientExample2 extends Component{ + static title = 'Define another ellipse with a radial gradient from white to blue'; + render() { + return + + + + + + + + ; + } +} + +const icon = + + + + + + + +; + + +const samples = [LinearGradientHorizontal, LinearGradientVertical, RadialGradientExample, RadialGradientExample2]; + +export { + icon, + samples +}; diff --git a/Example/main.js b/Example/main.js index 70a44d11..4ebf7373 100644 --- a/Example/main.js +++ b/Example/main.js @@ -104,7 +104,7 @@ const styles = StyleSheet.create({ } }); -const names = ['Svg', 'Stroking', 'Path', 'Line', 'Rect', 'Polygon', 'Polyline', 'Circle', 'Ellipse', 'G', 'Text', 'Use']; +const names = ['Svg', 'Stroking', 'Path', 'Line', 'Rect', 'Polygon', 'Polyline', 'Circle', 'Ellipse', 'G', 'Text', 'Use', 'Gradients']; class ArtSvgExample extends Component { constructor() { diff --git a/Example/package.json b/Example/package.json index 373ca2d7..dbc0b6ff 100644 --- a/Example/package.json +++ b/Example/package.json @@ -6,8 +6,8 @@ "start": "react-native start" }, "dependencies": { - "react-native": "^0 .18.1", - "react-native-art-svg": "^1.0.1", + "react-native": "^0.18.1", + "react-native-art-svg": "^1.0.3", "react-native-root-modal": "^1.0.2" } } diff --git a/README.md b/README.md index 80906afa..c9b435e1 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,7 @@ TODO: 5. animations https://github.com/maxwellito/vivus elements: -1.defs -2.tref ! -3.tspan ! -4.clipPath (wait for official todo) -5.glyph ? missing-glyph? -6.linearGradient -7.radialGradient -8.marker? -9.pattern -10.textPath -11.stop -12.symbol +1.clipPath (wait for official complete) +2.textPath (wait for official complete) +3.symbol +4.pattern diff --git a/elements/Defs.js b/elements/Defs.js index 9fd4b121..290d80f3 100644 --- a/elements/Defs.js +++ b/elements/Defs.js @@ -8,6 +8,8 @@ import React, { let {Group} = ART; let map = {}; +import LinearGradient from './LinearGradient'; +import RadialGradient from './RadialGradient'; let onlyChild = Children.only; class DefsItem extends Component{ @@ -81,6 +83,11 @@ class Defs extends Component{ getChildren = () => { return Children.map(this.props.children, child => { + if (child.type === LinearGradient || child.type === RadialGradient) { + return cloneElement(child, { + svgId: this.props.svgId + }); + } if (child.props.id) { return {child}; } diff --git a/elements/Ellipse.js b/elements/Ellipse.js index 25f0f051..af794433 100644 --- a/elements/Ellipse.js +++ b/elements/Ellipse.js @@ -3,8 +3,6 @@ import React, { PropTypes, ART } from 'react-native'; -import fillFilter from '../lib/fillFilter'; -import strokeFilter from '../lib/strokeFilter'; import Path from './Path'; let propType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); diff --git a/elements/LinearGradient.js b/elements/LinearGradient.js new file mode 100644 index 00000000..fe215de7 --- /dev/null +++ b/elements/LinearGradient.js @@ -0,0 +1,71 @@ +import React, { + Component, + PropTypes, + ART, + Children +} from 'react-native'; +let { + Group, + LinearGradient: ARTLinearGradient +} = ART; +import {set, remove} from '../lib/fillFilter'; +import Stop from './Stop'; +import Color from 'color'; +let percentReg = /^(\-?\d+(?:\.\d+)?)(%?)$/; +let propType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); +class LinearGradient extends Component{ + static displayName = 'LinearGradient'; + static propTypes = { + x1: propType, + x2: propType, + y1: propType, + y2: propType, + id: PropTypes.string + }; + + constructor() { + super(...arguments); + this.id = this.props.id + ':' + this.props.svgId; + } + + componentWillReceiveProps = nextProps => { + let id = nextProps.id + ':' + nextProps.svgId; + if (id !== this.id) { + remove(this.id); + } + }; + + componentWillUnmount = () => { + remove(this.id); + }; + + render() { + let { + x1, + y1, + x2, + y2 + } = this.props; + let stops = {}; + Children.forEach(this.props.children, child => { + if (child.type === Stop && child.props.stopColor && child.props.offset) { + + // convert percent to float. + let matched = child.props.offset.match(percentReg); + let offset = matched[2] ? matched[1] / 100 : matched[1]; + + // add stop + stops[offset] = Color(child.props.stopColor).alpha(+child.props.stopOpacity).rgbaString(); + + // TODO: convert percent to float. + set(this.id, new ARTLinearGradient(stops, x1, y1, x2, y2)); + } else { + console.warn(`'LinearGradient' can only receive 'Stop' elements as children`); + } + }); + return ; + } +} + +export default LinearGradient; + diff --git a/elements/Polygon.js b/elements/Polygon.js index 5984e75d..e4d8323e 100644 --- a/elements/Polygon.js +++ b/elements/Polygon.js @@ -3,8 +3,6 @@ import React, { PropTypes, ART } from 'react-native'; -import fillFilter from '../lib/fillFilter'; -import strokeFilter from '../lib/strokeFilter'; import Path from './Path'; class Polygon extends Component{ diff --git a/elements/Polyline.js b/elements/Polyline.js index bab6b9d9..b99bbc0b 100644 --- a/elements/Polyline.js +++ b/elements/Polyline.js @@ -5,10 +5,6 @@ import React, { } from 'react-native'; import Path from './Path'; - -import strokeFilter from '../lib/strokeFilter'; -import fillFilter from '../lib/fillFilter'; -import transformFilter from '../lib/transformFilter'; class Polyline extends Component{ static displayName = 'Polyline'; static propTypes = { diff --git a/elements/RadialGradient.js b/elements/RadialGradient.js new file mode 100644 index 00000000..0c20d447 --- /dev/null +++ b/elements/RadialGradient.js @@ -0,0 +1,83 @@ +import React, { + Component, + PropTypes, + ART, + Children +} from 'react-native'; +let { + Group, + RadialGradient: ARTRadialGradient +} = ART; +import {set, remove} from '../lib/fillFilter'; +import Stop from './Stop'; +import Color from 'color'; +let percentReg = /^(\-?\d+(?:\.\d+)?)(%?)$/; +let propType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); +class RadialGradient extends Component{ + static displayName = 'RadialGradient'; + static propTypes = { + fx: propType, + fy: propType, + rx: propType, + ry: propType, + cx: propType, + cy: propType, + r: propType, + id: PropTypes.string + }; + + constructor() { + super(...arguments); + this.id = this.props.id + ':' + this.props.svgId; + } + + componentWillReceiveProps = nextProps => { + let id = nextProps.id + ':' + nextProps.svgId; + if (id !== this.id) { + remove(this.id); + } + }; + + componentWillUnmount = () => { + remove(this.id); + }; + + render() { + let { + fx, + fy, + rx, + ry, + cx, + cy, + r + } = this.props; + + // TODO: render differently from svg in html + if (r) { + rx = ry = +r; + } + let stops = {}; + Children.forEach(this.props.children, child => { + if (child.type === Stop && child.props.stopColor && child.props.offset) { + + // convert percent to float. + let matched = child.props.offset.match(percentReg); + let offset = matched[2] ? matched[1] / 100 : matched[1]; + + // add stop + stops[offset] = Color(child.props.stopColor).alpha(+child.props.stopOpacity).rgbaString(); + + // TODO: convert percent to float. + set(this.id, new ARTRadialGradient(stops, fx, fy, rx, ry, cx, cy)); + + } else { + console.warn(`'RadialGradient' can only receive 'Stop' elements as children`); + } + }); + return ; + } +} + +export default RadialGradient; + diff --git a/elements/Rect.js b/elements/Rect.js index 237c1565..19abd30e 100644 --- a/elements/Rect.js +++ b/elements/Rect.js @@ -4,9 +4,6 @@ import React, { ART } from 'react-native'; -import fillFilter from '../lib/fillFilter'; -import strokeFilter from '../lib/strokeFilter'; -import transformFilter from '../lib/transformFilter'; import Path from './Path'; let propType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); diff --git a/elements/Stop.js b/elements/Stop.js new file mode 100644 index 00000000..0903d179 --- /dev/null +++ b/elements/Stop.js @@ -0,0 +1,21 @@ +import React, { + Component, + PropTypes, + ART +} from 'react-native'; +let { + Group +} = ART; +class Stop extends Component{ + static displayName = 'Stop'; + static propTypes = { + stopColor: PropTypes.string, + stopOpacity: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + }; + render() { + return null; + } +} + +export default Stop; + diff --git a/elements/Text.js b/elements/Text.js index eab26c8c..5e77f4a0 100644 --- a/elements/Text.js +++ b/elements/Text.js @@ -3,6 +3,7 @@ import React, { Component, PropTypes } from 'react-native'; + let { Text:ARTText } = ART; @@ -38,6 +39,7 @@ class Text extends Component{ id={this.props.id} svgId={this.props.svgId} visible={true} + text={true} > ; diff --git a/elements/Use.js b/elements/Use.js index 261fe9a5..e06227eb 100644 --- a/elements/Use.js +++ b/elements/Use.js @@ -1,9 +1,9 @@ import React, { Component, PropTypes, - ART + ART, + cloneElement } from 'react-native'; - import Defs from './Defs'; class Use extends Component{ static displayName = 'Use'; diff --git a/index.js b/index.js index 3346cbc9..fa60fbca 100644 --- a/index.js +++ b/index.js @@ -14,10 +14,13 @@ import G from './elements/G'; import Text from './elements/Text'; import Use from './elements/Use'; import Defs from './elements/Defs'; +import LinearGradient from './elements/LinearGradient'; +import RadialGradient from './elements/RadialGradient'; +import Stop from './elements/Stop'; let { Group - } = ART; +} = ART; export { Svg, @@ -32,7 +35,10 @@ export { Line, Rect, Use, - Defs + Defs, + LinearGradient, + RadialGradient, + Stop }; export default Svg; diff --git a/lib/fillFilter.js b/lib/fillFilter.js index d0e78117..7a684bea 100644 --- a/lib/fillFilter.js +++ b/lib/fillFilter.js @@ -1,4 +1,47 @@ import rgba from './rgba'; +import { + ART +} from 'react-native'; + +let { + LinearGradient, + RadialGradient +} = ART; + +let fillPatterns = {}; +let fillReg = /^url\(#(\w+?)\)$/; export default function (props) { + let {fill} = props; + + if (fill) { + if (fill instanceof LinearGradient || fill instanceof RadialGradient) { + return fill; + } + + // 尝试匹配 fill="url(#pattern)" + let matched = fill.match(fillReg); + if (matched) { + let patternName = `${matched[1]}:${props.svgId}`; + if (fillPatterns[patternName]) { + return fillPatterns[patternName]; + } else { + return null; + } + } + } + return rgba(props.fill === undefined ? '#000' : props.fill, props.fillOpacity); } + +function set(id, pattern) { + fillPatterns[id] = pattern; +} + +function remove(id) { + delete fillPatterns[id]; +} + +export { + set, + remove +} diff --git a/lib/transformFilter.js b/lib/transformFilter.js index 00193802..37f95c0d 100644 --- a/lib/transformFilter.js +++ b/lib/transformFilter.js @@ -1,14 +1,14 @@ export default function (props) { - let coords = props.origin ? props.origin.split(',') : []; + let coords = props.origin ? props.origin.split(/\s*,\s*/) : []; let originX = coords.length === 2 ? coords[0] : props.originX; - let originY = coords.length === 2 ? coords[1] :props.originY; - + let originY = coords.length === 2 ? coords[1] : props.originY; + let scale = props.scale == 0 ? 0 : props.scale; return { - rotation: props.rotation || props.rotate || 0, - scale: props.scale || 1, - originX, - originY, + rotation: +props.rotation / 2 || +props.rotate / 2 || 0, + scale: isNaN(scale) ? 1 : scale, + originX: +originX || 0, + originY: +originY || 0, x: +props.x || 0, y: +props.y || 0 }