First cut of SVG rendering components

These still need work, but they're functional enough to render a diagram
This commit is contained in:
Jeff Avallone 2018-02-16 19:04:23 -05:00
parent 3fdc74bdf2
commit 82b780e9c3
18 changed files with 1169 additions and 23 deletions

View File

@ -36,6 +36,7 @@
"plugins": [ "plugins": [
"transform-runtime", "transform-runtime",
"transform-class-properties", "transform-class-properties",
"transform-object-rest-spread",
"syntax-dynamic-import" "syntax-dynamic-import"
] ]
}, },
@ -120,6 +121,7 @@
"babel-loader": "^7.1.2", "babel-loader": "^7.1.2",
"babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
@ -142,6 +144,7 @@
"i18next": "^10.3.0", "i18next": "^10.3.0",
"i18next-browser-languagedetector": "^2.1.0", "i18next-browser-languagedetector": "^2.1.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"immutable": "^3.8.2",
"jest": "^22.2.2", "jest": "^22.2.2",
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",
"postcss-cssnext": "^3.1.0", "postcss-cssnext": "^3.1.0",

View File

@ -44,5 +44,10 @@ exports[`App rendering 1`] = `
Sample warning message Sample warning message
</p> </p>
</Message> </Message>
<div
className="render"
>
Testing image
</div>
</React.Fragment> </React.Fragment>
`; `;

View File

@ -1,32 +1,46 @@
import React from 'react'; import React from 'react';
import style from './style.css';
import Form from 'components/Form'; import Form from 'components/Form';
import Message from 'components/Message'; import Message from 'components/Message';
import renderImage from 'components/SVG';
import { syntaxes, demoImage } from 'devel';
const syntaxes = { class App extends React.PureComponent {
js: 'JavaScript', state = {
pcre: 'PCRE' syntaxes,
}; image: demoImage,
downloadUrls: [
const downloadUrls = [
{ url: '#svg', filename: 'image.svg', type: 'image/svg+xml', label: 'Download SVG' }, { url: '#svg', filename: 'image.svg', type: 'image/svg+xml', label: 'Download SVG' },
{ url: '#png', filename: 'image.png', type: 'image/png', label: 'Download PNG' } { url: '#png', filename: 'image.png', type: 'image/png', label: 'Download PNG' }
]; ]
}
const handleSubmit = ({ expr, syntax}) => console.log(syntax, expr); // eslint-disable-line no-console handleSubmit = ({expr, syntax}) => {
console.log(syntax, expr); // eslint-disable-line no-console
}
const App = () => <React.Fragment> render() {
const { downloadUrls, syntaxes, image } = this.state;
return <React.Fragment>
<Form <Form
syntaxes={ syntaxes } syntaxes={ syntaxes }
downloadUrls={ downloadUrls } downloadUrls={ downloadUrls }
permalinkUrl="#permalink" permalinkUrl="#permalink"
onSubmit={ handleSubmit }/> onSubmit={ this.handleSubmit }/>
<Message type="error" heading="Sample Error"> <Message type="error" heading="Sample Error">
<p>Sample error message</p> <p>Sample error message</p>
</Message> </Message>
<Message type="warning" heading="Sample Warning"> <Message type="warning" heading="Sample Warning">
<p>Sample warning message</p> <p>Sample warning message</p>
</Message> </Message>
</React.Fragment>; <div className={ style.render }>
{ renderImage(image) }
</div>
</React.Fragment>;
}
}
export default App; export default App;

View File

@ -0,0 +1,17 @@
@import url('../../globals.css');
.render {
display: flex;
justify-content: center;
width: 100%;
background: var(--color-white);
box-sizing: border-box;
overflow: auto;
margin: var(--spacing-margin) 0;
& svg {
display: block;
transform: scaleZ(1); /* Move to separate render layer in Chrome */
flex-shrink: 0;
}
}

View File

@ -1,10 +1,14 @@
jest.mock('components/SVG');
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import App from 'components/App'; import App from 'components/App';
import renderImage from 'components/SVG';
describe('App', () => { describe('App', () => {
test('rendering', () => { test('rendering', () => {
renderImage.mockReturnValue('Testing image');
const component = shallow( const component = shallow(
<App/> <App/>
); );

View File

@ -0,0 +1,83 @@
import React from 'react';
import { Map } from 'immutable';
class Base extends React.PureComponent {
_currentBBox() {
return this.tempBBox ? this.tempBBox : (this.state || {}).bbox;
}
setBBox(box, recalculate = {}) {
let bbox = this._currentBBox() || Map({ width: 0, height: 0});
bbox = bbox.merge(box);
if (!bbox.has('axisY') || recalculate.axisY) {
bbox = bbox.set('axisY', bbox.get('height') / 2);
}
if (!bbox.has('axisX1') || recalculate.axisX1) {
bbox = bbox.set('axisX1', 0);
}
if (!bbox.has('axisX2') || recalculate.axisX2) {
bbox = bbox.set('axisX2', bbox.get('width'));
}
this.tempBBox = bbox; // Want to get the updated bbox while setState is pending
this.setState({ bbox }, () => {
this.tempBBox = null;
});
}
getBBox() {
const bbox = this._currentBBox() || Map();
return {
width: 0,
height: 0,
axisY: 0,
axisX1: 0,
axisX2: 0,
...bbox.toJS()
};
}
async doPreReflow() {
const components = this.preReflow();
// No child components
if (components === undefined) {
return true;
}
// List of child components
if (components.map) {
const componentsReflowed = await Promise.all(components.map(c => c.doReflow()));
return componentsReflowed.reduce((memo, value) => memo || value, false);
}
// One child component
return components.doReflow();
}
async doReflow() {
const oldBBox = this._currentBBox();
const shouldReflow = await this.doPreReflow();
if (shouldReflow) {
this.reflow();
}
return this._currentBBox() !== oldBBox;
}
preReflow() {
// Implemented in subclass, return array of components to reflow before this
}
reflow() {
// Implemented in subclasses
}
}
export default Base;

84
src/components/SVG/Box.js Normal file
View File

@ -0,0 +1,84 @@
import React from 'react';
import PropTypes from 'prop-types';
import Base from './Base';
import style from './style';
/** @extends React.PureComponent */
class Box extends Base {
static defaultProps = {
padding: 5
}
preReflow() {
return this.contained;
}
reflow() {
return new Promise(resolve => {
const { padding, useAnchors } = this.props;
const box = this.contained.getBBox();
const labelBox = this.label ? this.label.getBBox() : { width: 0, height: 0};
this.setBBox({
width: Math.max(box.width + 2 * padding, labelBox.width),
height: box.height + 2 * padding + labelBox.height,
axisY: (useAnchors ? box.axisY : box.height / 2) + padding + labelBox.height,
axisX1: useAnchors ? box.axisX1 + padding : 0,
axisX2: useAnchors ? box.axisX2 + padding : box.width + 2 * padding
});
this.setState({
width: this.getBBox().width,
height: box.height + 2 * padding,
contentTransform: `translate(${ padding } ${ padding + labelBox.height })`,
rectTransform: `translate(0 ${ labelBox.height })`,
labelTransform: `translate(0 ${ labelBox.height })`
}, resolve);
});
}
containedRef = contained => this.contained = contained
labelRef = label => this.label = label
render() {
const { style: propStyle, radius, label, children } = this.props;
const { width, height, labelTransform, rectTransform, contentTransform } = this.state || {};
const rectProps = {
style: propStyle,
width,
height,
rx: radius,
ry: radius,
transform: rectTransform
};
const textProps = {
transform: labelTransform,
style: style.infoText,
ref: this.labelRef
};
return <React.Fragment>
<rect { ...rectProps } ></rect>
{ label && <text { ...textProps }>{ label }</text> }
<g transform={ contentTransform }>
{ React.cloneElement(React.Children.only(children), {
ref: this.containedRef
}) }
</g>
</React.Fragment>;
}
}
Box.propTypes = {
padding: PropTypes.number,
useAnchors: PropTypes.bool,
style: PropTypes.object,
radius: PropTypes.number,
label: PropTypes.string,
children: PropTypes.node
};
export default Box;

View File

@ -0,0 +1,107 @@
import React from 'react';
import PropTypes from 'prop-types';
import { List } from 'immutable';
import Base from './Base';
import style from './style';
import Path from './path';
/** @extends React.PureComponent */
class HorizontalLayout extends Base {
static defaultProps = {
withConnectors: false,
spacing: 10
}
constructor(props) {
super(props);
this.state = {
childTransforms: List()
};
}
updateChildTransforms(childBoxes) {
return childBoxes.reduce((transforms, box, i) => (
transforms.set(i, `translate(${ box.offsetX } ${ box.offsetY })`)
), this.state.childTransforms);
}
updateConnectorPaths(childBoxes) {
let last = childBoxes[0];
return childBoxes.slice(1).reduce((path, box) => {
try {
return path
.moveTo({ x: last.offsetX + last.axisX2, y: this.getBBox().axisY })
.lineTo({ x: box.offsetX + box.axisX1 });
}
finally {
last = box;
}
}, new Path()).toString();
}
preReflow() {
return this.children;
}
reflow() {
if (this.children.length === 0) {
return;
}
return new Promise(resolve => {
const { spacing, withConnectors } = this.props;
const childBoxes = this.children.map(child => child.getBBox());
const verticalCenter = childBoxes.reduce((center, box) => Math.max(center, box.axisY), 0);
const width = childBoxes.reduce((width, box) => width + box.width, 0) + (childBoxes.length - 1) * spacing;
const height = childBoxes.reduce((ascHeight, box) => Math.max(ascHeight, box.axisY), 0) +
childBoxes.reduce((decHeight, box) => Math.max(decHeight, box.height - box.axisY), 0);
this.setBBox({ width, height, axisY: verticalCenter }, { axisX1: true, axisX2: true });
let offset = 0;
childBoxes.forEach(box => {
box.offsetX = offset;
box.offsetY = this.getBBox().axisY - box.axisY;
offset += box.width + spacing;
});
this.setState({
childTransforms: this.updateChildTransforms(childBoxes),
connectorPaths: withConnectors ? this.updateConnectorPaths(childBoxes) : ''
}, resolve);
});
}
childRef = i => child => this.children[i] = child
render() {
const { children } = this.props;
const { childTransforms, connectorPaths } = this.state;
this.children = [];
return <React.Fragment>
<path d={ connectorPaths } style={ style.connectors }></path>
{ React.Children.map(children, (child, i) => (
<g transform={ childTransforms.get(i) }>
{ React.cloneElement(child, {
ref: this.childRef(i)
}) }
</g>
))}
</React.Fragment>;
}
}
HorizontalLayout.propTypes = {
spacing: PropTypes.number,
withConnectors: PropTypes.bool,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
export default HorizontalLayout;

103
src/components/SVG/Image.js Normal file
View File

@ -0,0 +1,103 @@
import React from 'react';
import PropTypes from 'prop-types';
import Base from './Base';
import style from './style';
const namespaceProps = {
'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:cc': 'http://creativecommons.org/ns#',
'xmlns:rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
};
const metadata = `<rdf:rdf>
<cc:license rdf:about="http://creativecommons.org/licenses/by/3.0/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"></cc:permits>
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"></cc:permits>
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice"></cc:requires>
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"></cc:requires>
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"></cc:permits>
</cc:license>
</rdf:rdf>`;
/** @extends React.PureComponent */
class Image extends Base {
static defaultProps = {
padding: 10
}
publishedMarkup = ''
state = {
width: 0,
height: 0
}
componentDidMount() {
this.doReflow().then(() => this.publishMarkup());
}
componentDidUpdate() {
this.doReflow().then(() => this.publishMarkup());
}
publishMarkup() {
const { onRender } = this.props;
const markup = this.svg.outerHTML;
if (onRender && this.publishedMarkup !== markup) {
this.publishedMarkup = markup;
onRender(this.svg.outerHTML);
}
}
preReflow() {
return this.contained;
}
reflow() {
return new Promise(resolve => {
const { padding } = this.props;
const box = this.contained.getBBox();
this.setState({
width: Math.round(box.width + 2 * padding),
height: Math.round(box.height + 2 * padding)
}, resolve);
});
}
containedRef = contained => this.contained = contained
svgRef = svg => this.svg = svg
render() {
const { width, height } = this.state;
const { padding, children } = this.props;
const svgProps = {
width,
height,
viewBox: [0, 0, width, height].join(' '),
style: style.image,
ref: this.svgRef,
...namespaceProps
};
return <svg { ...svgProps }>
<metadata dangerouslySetInnerHTML={{ __html: metadata }}></metadata>
<g transform={ `translate(${ padding } ${ padding })` }>
{ React.cloneElement(React.Children.only(children), {
ref: this.containedRef
}) }
</g>
</svg>;
}
}
Image.propTypes = {
onRender: PropTypes.func,
padding: PropTypes.number,
children: PropTypes.node
};
export default Image;

144
src/components/SVG/Loop.js Normal file
View File

@ -0,0 +1,144 @@
import React from 'react';
import PropTypes from 'prop-types';
import Base from './Base';
import style from './style';
import Path from './path';
const skipPath = (box, greedy) => {
const vert = Math.max(0, box.axisY - 10);
const horiz = box.width - 10;
let path = new Path({ relative: true });
if (!greedy) {
path
.moveTo({ x: 10, y: box.axisY + box.offsetY - 15, relative: false })
.lineTo({ x: 5, y: 5 })
.moveTo({ x: -5, y: -5 })
.lineTo({ x: -5, y: 5 });
}
return path
.moveTo({ x: 0, y: box.axisY + box.offsetY, relative: false })
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: -10 })
.lineTo({ y: -vert })
.quadraticCurveTo({ cx: 0, cy: -10, x: 10, y: -10 })
.lineTo({ x: horiz })
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: 10 })
.lineTo({ y: vert })
.quadraticCurveTo({ cx: 0, cy: 10, x: 10, y: 10 });
};
const repeatPath = (box, greedy) => {
const vert = box.height - box.axisY - 10;
let path = new Path({ relative: true });
if (greedy) {
path
.moveTo({ x: box.offsetX + box.width + 10, y: box.axisY + box.offsetY + 15, relative: false })
.lineTo({ x: 5, y: -5 })
.moveTo({ x: -5, y: 5 })
.lineTo({ x: -5, y: -5 });
}
return path
.moveTo({ x: box.offsetX, y: box.axisY + box.offsetY, relative: false })
.quadraticCurveTo({ cx: -10, cy: 0, x: -10, y: 10 })
.lineTo({ y: vert })
.quadraticCurveTo({ cx: 0, cy: 10, x: 10, y: 10 })
.lineTo({ x: box.width })
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: -10 })
.lineTo({ y: -vert })
.quadraticCurveTo({ cx: 0, cy: -10, x: -10, y: -10 });
};
/** @extends React.PureComponent */
class Loop extends Base {
get contentOffset() {
const { skip, repeat } = this.props;
if (skip) {
return { x: 15, y: 10 };
} else if (repeat) {
return { x: 10, y: 0 };
} else {
return { x: 0, y: 0 };
}
}
preReflow() {
return this.contained;
}
reflow() {
return new Promise(resolve => {
const { skip, repeat, greedy } = this.props;
const box = this.contained.getBBox();
const labelBox = this.label ? this.label.getBBox() : { width: 0, height: 0 };
let height = box.height + labelBox.height;
if (skip) {
height += 10;
}
if (repeat) {
height += 10;
}
this.setBBox({
width: box.width + this.contentOffset.x * 2,
height,
axisY: box.axisY + this.contentOffset.y,
axisX1: box.axisX1 + this.contentOffset.x,
axisX2: box.axisX2 + this.contentOffset.x
});
box.offsetX = this.contentOffset.x;
box.offsetY = this.contentOffset.y;
this.setState({
labelTransform: `translate(${ this.getBBox().width - labelBox.width - 10 } ${ this.getBBox().height + 2 })`,
loopPaths: [
skip && skipPath(box, greedy),
repeat && repeatPath(box, greedy)
].filter(Boolean).join('')
}, resolve);
});
}
containedRef = contained => this.contained = contained
labelRef = label => this.label = label
render() {
const { label, children } = this.props;
const { loopPaths, labelTransform } = this.state || {};
const textProps = {
transform: labelTransform,
style: style.infoText,
ref: this.labelRef
};
return <React.Fragment>
<path d={ loopPaths } style={ style.connectors }></path>
{ label && <text { ...textProps }>{ label }</text> }
<g transform={ `translate(${ this.contentOffset.x } ${ this.contentOffset.y })` }>
{ React.cloneElement(React.Children.only(children), {
ref: this.containedRef
}) }
</g>
</React.Fragment>;
}
}
Loop.propTypes = {
skip: PropTypes.bool,
repeat: PropTypes.bool,
greedy: PropTypes.bool,
label: PropTypes.string,
children: PropTypes.node.isRequired
};
export default Loop;

46
src/components/SVG/Pin.js Normal file
View File

@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import Base from './Base';
import style from './style';
/** @extends React.PureComponent */
class Pin extends Base {
static defaultProps = {
radius: 5
}
reflow() {
return new Promise(resolve => {
const { radius } = this.props;
this.setBBox({
width: radius * 2,
height: radius * 2
});
this.setState({
transform: `translate(${ radius } ${ radius })`
}, resolve);
});
}
render() {
const { radius } = this.props;
const { transform } = this.state || {};
const circleProps = {
r: radius,
style: style.pin,
transform
};
return <circle { ...circleProps }></circle>;
}
}
Pin.propTypes = {
radius: PropTypes.number
};
export default Pin;

View File

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import Base from './Base';
import style from './style';
/** @extends React.PureComponent */
class Text extends Base {
reflow() {
return new Promise(resolve => {
const box = this.text.getBBox();
this.setBBox({
width: box.width,
height: box.height
});
this.setState({
transform: `translate(${-box.x} ${-box.y})`
}, resolve);
});
}
textRef = text => this.text = text
render() {
const { transform } = this.state || {};
const { style: styleProp, children } = this.props;
const textProps = {
style: { ...style.text, ...styleProp },
transform,
ref: this.textRef
};
return <text { ...textProps }>
{ children }
</text>;
}
}
Text.propTypes = {
style: PropTypes.object,
transform: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
export default Text;

View File

@ -0,0 +1,149 @@
import React from 'react';
import PropTypes from 'prop-types';
import { List } from 'immutable';
import Base from './Base';
import style from './style';
import Path from './path';
const connectorMargin = 20;
/** @extends React.PureComponent */
class VerticalLayout extends Base {
static defaultProps = {
withConnectors: false,
spacing: 10
}
constructor(props) {
super(props);
this.state = {
childTransforms: List()
};
}
updateChildTransforms(childBoxes) {
return childBoxes.reduce((transforms, box, i) => (
transforms.set(i, `translate(${ box.offsetX } ${ box.offsetY })`)
), this.state.childTransforms);
}
makeCurve(box) {
const thisBox = this.getBBox();
const distance = Math.abs(box.offsetY + box.axisY - thisBox.axisY);
if (distance >= 15) {
const curve = (box.axisY + box.offsetY > thisBox.axisY) ? 10 : -10;
return new Path()
// Left
.moveTo({ x: 10, y: box.axisY + box.offsetY - curve })
.quadraticCurveTo({ cx: 0, cy: curve, x: 10, y: curve, relative: true })
.lineTo({ x: box.offsetX + box.axisX1 })
// Right
.moveTo({ x: thisBox.width - 10, y: box.axisY + box.offsetY - curve })
.quadraticCurveTo({ cx: 0, cy: curve, x: -10, y: curve, relative: true })
.lineTo({ x: box.offsetX + box.axisX2 });
} else {
const anchor = box.offsetY + box.axisY - thisBox.axisY;
return new Path()
// Left
.moveTo({ x: 0, y: thisBox.axisY })
.cubicCurveTo({ cx1: 15, cy1: 0, cx2: 10, cy2: anchor, x: 20, y: anchor, relative: true })
.lineTo({ x: box.offsetX + box.axisX1 })
// Right
.moveTo({ x: thisBox.width, y: thisBox.axisY })
.cubicCurveTo({ cx1: -15, cy1: 0, cx2: -10, cy2: anchor, x: -20, y: anchor, relative: true })
.lineTo({ x: box.offsetX + box.axisX2 });
}
}
makeSide(box) {
const thisBox = this.getBBox();
const distance = Math.abs(box.offsetY + box.axisY - thisBox.axisY);
if (distance >= 15) {
const shift = (box.offsetY + box.axisY > thisBox.axisY) ? 10 : -10;
const edge = box.offsetY + box.axisY - shift;
return new Path()
// Left
.moveTo({ x: 0, y: thisBox.axisY })
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: shift, relative: true })
.lineTo({ y: edge })
// Right
.moveTo({ x: thisBox.width, y: thisBox.axisY })
.quadraticCurveTo({ cx: -10, cy: 0, x: -10, y: shift, relative: true })
.lineTo({ y: edge });
}
}
preReflow() {
return this.children;
}
reflow() {
if (this.children.length === 0) {
return Promise.resolve();
}
return new Promise(resolve => {
const { spacing, withConnectors } = this.props;
const childBoxes = this.children.map(child => child.getBBox());
const horizontalCenter = childBoxes.reduce((center, box) => Math.max(center, box.width / 2), 0);
const margin = withConnectors ? connectorMargin : 0;
const width = childBoxes.reduce((width, box) => Math.max(width, box.width), 0) + 2 * margin;
const height = childBoxes.reduce((height, box) => height + box.height, 0) + (childBoxes.length - 1) * spacing;
this.setBBox({ width, height }, { axisY: true, axisX1: true, axisX2: true });
let offset = 0;
childBoxes.forEach(box => {
box.offsetX = horizontalCenter - box.width / 2 + margin;
box.offsetY = offset;
offset += spacing + box.height;
});
this.setState({
childTransforms: this.updateChildTransforms(childBoxes),
connectorPaths: withConnectors ? [
...childBoxes.map(box => this.makeCurve(box)),
this.makeSide(childBoxes[0]),
this.makeSide(childBoxes[childBoxes.length - 1])
].join('') : ''
}, resolve);
});
}
childRef = i => child => this.children[i] = child
render() {
const { children } = this.props;
const { childTransforms, connectorPaths } = this.state;
this.children = [];
return <React.Fragment>
<path d={ connectorPaths } style={ style.connectors }></path>
{ React.Children.map(children, (child, i) => (
<g transform={ childTransforms.get(i) }>
{ React.cloneElement(child, {
ref: this.childRef(i)
}) }
</g>
)) }
</React.Fragment>;
}
}
VerticalLayout.propTypes = {
spacing: PropTypes.number,
withConnectors: PropTypes.bool,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
export default VerticalLayout;

View File

@ -0,0 +1,41 @@
import React from 'react';
import Box from './Box';
import HorizontalLayout from './HorizontalLayout';
import Image from './Image';
import Loop from './Loop';
import Pin from './Pin';
import Text from './Text';
import VerticalLayout from './VerticalLayout';
const nodeTypes = {
Box,
HorizontalLayout,
Image,
Loop,
Pin,
Text,
VerticalLayout
};
const renderChildren = children => {
if (!children || children.length === 0) {
return;
}
return children.length === 1 ?
renderImage(children[0]) :
children.map(renderImage);
};
const renderImage = (node, key) => {
if (typeof node === 'string') {
return node;
}
const { type, props, children } = node;
return React.createElement(nodeTypes[type], { key, ...props }, renderChildren(children));
};
export default renderImage;

103
src/components/SVG/path.js Normal file
View File

@ -0,0 +1,103 @@
class Path {
constructor({ relative } = {}) {
this.relative = relative || false;
this.currentPosition = { x: 0, y: 0 };
this.lastStartPosition = { x: 0, y: 0 };
this.pathParts = [];
}
moveTo({ x, y, relative }) {
relative = relative === undefined ? this.relative : relative;
const command = relative ? 'm' : 'M';
if (relative) {
this.currentPosition = {
x: x + this.currentPosition.x,
y: y + this.currentPosition.y
};
} else {
this.currentPosition = { x, y };
}
this.pathParts.push(`${command}${x},${y}`);
this.lastStartPosition = { ...this.currentPosition };
return this;
}
lineTo({ x, y, relative }) {
relative = relative === undefined ? this.relative : relative;
if (x !== undefined && (y === undefined || y === this.currentPosition.y)) {
const command = relative ? 'h' : 'H';
this.pathParts.push(`${command}${x}`);
this.currentPosition.x = relative ? this.currentPosition.x + x : x;
} else if (y !== undefined && (x === undefined || x === this.currentPosition.x)) {
const command = relative ? 'v' : 'V';
this.pathParts.push(`${command}${y}`);
this.currentPosition.y = relative ? this.currentPosition.y + y : y;
} else {
const command = relative ? 'l' : 'L';
this.pathParts.push(`${command}${x},${y}`);
this.currentPosition.x = relative ? this.currentPosition.x + x : x;
this.currentPosition.y = relative ? this.currentPosition.y + y : y;
}
return this;
}
closePath() {
this.pathParts.push('Z');
this.currentPosition = { ...this.lastStartPosition };
return this;
}
cubicCurveTo({ cx1, cy1, cx2, cy2, x, y, relative }) {
relative = relative === undefined ? this.relative : relative;
if (cx1 === undefined || cy1 === undefined) {
const command = relative ? 's' : 'S';
this.pathParts.push(`${command}${cx2},${cy2} ${x},${y}`);
} else {
const command = relative ? 'c' : 'C';
this.pathParts.push(`${command}${cx1},${cy1} ${cx2},${cy2} ${x},${y}`);
}
this.currentPosition.x = relative ? this.currentPosition.x + x : x;
this.currentPosition.y = relative ? this.currentPosition.y + y : y;
return this;
}
quadraticCurveTo({ cx, cy, x, y, relative }) {
relative = relative === undefined ? this.relative : relative;
if (cx === undefined || cy === undefined) {
const command = relative ? 't' : 'T';
this.pathParts.push(`${command} ${x},${y}`);
} else {
const command = relative ? 'q' : 'Q';
this.pathParts.push(`${command}${cx},${cy} ${x},${y}`);
}
this.currentPosition.x = relative ? this.currentPosition.x + x : x;
this.currentPosition.y = relative ? this.currentPosition.y + y : y;
return this;
}
arcTo({ rx, ry, rotation, arc, sweep, x, y, relative }) {
relative = relative === undefined ? this.relative : relative;
const command = relative ? 'a' : 'A';
this.pathParts.push(`${command}${rx},${ry} ${rotation} ${arc} ${sweep},${x},${y}`);
this.currentPosition.x = relative ? this.currentPosition.x + x : x;
this.currentPosition.y = relative ? this.currentPosition.y + y : y;
return this;
}
toString() {
return this.pathParts.join('');
}
}
export default Path;

View File

@ -0,0 +1,43 @@
// Styles are in JS instead of CSS so they will be inlined as attributes
// instead of served as a CSS file. This is so styles are included in
// downloaded SVG files.
//const green = '#bada55';
const brown = '#6b6659';
//const tan = '#cbcbba';
const black = '#000';
const white = '#fff';
//const red = '#b3151a';
//const orange = '#fa0';
const fontFamily = 'Arial';
const fontSize = '16px';
const fontSizeSmall = '12px';
const strokeBase = {
strokeWidth: '2px',
stroke: black
};
export default {
image: {
backgroundColor: white
},
connectors: {
fillOpacity: 0,
...strokeBase
},
text: {
fontSize: fontSize,
fontFamily: fontFamily
},
infoText: {
fontSize: fontSizeSmall,
fontFamily: fontFamily,
dominantBaseline: 'text-after-edge'
},
pin: {
fill: brown,
...strokeBase
}
};

138
src/devel.js Normal file
View File

@ -0,0 +1,138 @@
// Data used during development.
// Once everything is built, this file will go away
const syntaxes = {
js: 'JavaScript',
pcre: 'PCRE'
};
const demoImage = {
type: 'Image',
children: [
{
type: 'HorizontalLayout',
props: {
withConnectors: true
},
children: [
{
type: 'Pin'
},
{
type: 'VerticalLayout',
props: {
withConnectors: true
},
children: [
{
type: 'Loop',
props: {
skip: false,
repeat: true
},
children: [
{
type: 'Box',
props: {
style: { fill: '#bada55' },
radius: 3
},
children: [
{
type: 'Text',
children: [
'Demo Text'
]
}
]
}
]
},
{
type: 'Loop',
props: {
skip: true,
repeat: false
},
children: [
{
type: 'Box',
props: {
style: { fill: '#bada55' },
radius: 3
},
children: [
{
type: 'Text',
children: [
'Demo Text'
]
}
]
}
]
},
{
type: 'Loop',
props: {
skip: true,
repeat: true,
greedy: true
},
children: [
{
type: 'Box',
props: {
style: { fill: '#bada55' },
radius: 3
},
children: [
{
type: 'Text',
children: [
'Demo Text'
]
}
]
}
]
},
{
type: 'Loop',
props: {
skip: true,
repeat: true,
label: 'Loop label'
},
children: [
{
type: 'Box',
props: {
style: { fill: '#bada55' },
radius: 3
},
children: [
{
type: 'Text',
children: [
'Demo Text'
]
}
]
}
]
}
]
},
{
type: 'Pin'
}
]
}
]
};
export {
syntaxes,
demoImage
};

View File

@ -650,7 +650,7 @@ babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0:
version "6.18.0" version "6.18.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
babel-plugin-syntax-object-rest-spread@^6.13.0: babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0:
version "6.13.0" version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
@ -858,6 +858,13 @@ babel-plugin-transform-flow-strip-types@^6.22.0:
babel-plugin-syntax-flow "^6.18.0" babel-plugin-syntax-flow "^6.18.0"
babel-runtime "^6.22.0" babel-runtime "^6.22.0"
babel-plugin-transform-object-rest-spread@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
dependencies:
babel-plugin-syntax-object-rest-spread "^6.8.0"
babel-runtime "^6.26.0"
babel-plugin-transform-react-display-name@^6.23.0: babel-plugin-transform-react-display-name@^6.23.0:
version "6.25.0" version "6.25.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1"
@ -3761,6 +3768,10 @@ image-size@^0.5.0:
version "0.5.5" version "0.5.5"
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
immutable@^3.8.2:
version "3.8.2"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
import-local@^1.0.0: import-local@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc"