setAnchorHovered(true)}
19 | onMouseOut={() => setAnchorHovered(false)}
20 | onMouseDown={props.onMouseDown} />
21 | );
22 | };
23 |
24 | function RotateAnchor(props) {
25 | let style = {
26 | marginLeft: props.boundingBox.width + 5
27 | };
28 | let [anchorHovered, setAnchorHovered] = useState(false);
29 | return (
30 |
setAnchorHovered(true)}
36 | onMouseOut={() => setAnchorHovered(false)}
37 | onMouseDown={props.onMouseDown} />
38 | )
39 | };
40 |
41 | class Handler extends Component {
42 | onMouseDown(event) {
43 | // event.preventDefault();
44 |
45 | if (event.target.classList.contains('handler')) {
46 | this.props.onDrag(event);
47 | }
48 | }
49 |
50 | render() {
51 | let {props} = this;
52 | let {boundingBox} = props;
53 |
54 | let handlerStyle = {
55 | ...styles.handler,
56 | ...boundingBox,
57 | width: boundingBox.width + 10,
58 | height: boundingBox.height + 10,
59 | left: boundingBox.left - 5,
60 | top: boundingBox.top - 5,
61 | transform: `rotate(${boundingBox.rotate}deg)`
62 | };
63 |
64 | return (
65 |
70 | {props.canRotate &&
71 | }
73 | {props.canResize &&
74 | }
76 |
77 | );
78 | }
79 | }
80 |
81 | const styles = {
82 | handler: {
83 | 'position': 'absolute',
84 | 'border': '2px solid #dedede',
85 | 'zIndex': 999999
86 | },
87 | anchor: {
88 | 'width': 10,
89 | 'height': 10
90 | },
91 | anchorHovered: {
92 | 'borderColor': 'gray'
93 | },
94 | scaleAnchor: {
95 | 'marginTop': -3,
96 | 'borderRight': '2px solid #dedede',
97 | 'borderBottom': '2px solid #dedede',
98 | 'position': 'absolute',
99 | 'zIndex': -1
100 | },
101 | rotateAnchor: {
102 | 'marginTop': -8,
103 | 'borderRight': '2px solid #dedede',
104 | 'borderTop': '2px solid #dedede',
105 | 'position': 'absolute',
106 | 'borderTopRightRadius': 3,
107 | 'zIndex': -1
108 | }
109 | };
110 |
111 | export default Handler;
112 |
--------------------------------------------------------------------------------
/src/Icon.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | export default class Icon extends Component {
4 | static defaultProps = {
5 | size: 16
6 | };
7 |
8 | renderGraphic() {
9 | switch (this.props.icon) {
10 | case 'image':
11 | return (
12 |
13 | );
14 | case 'my-icon':
15 | return (
16 |
17 | );
18 | case 'another-icon':
19 | return (
20 |
21 | );
22 | case 'format-bold':
23 | return (
24 |
27 | );
28 | case 'format-italic':
29 | return (
30 |
31 | );
32 | case 'format-underline':
33 | return (
34 |
36 | );
37 | case 'format-align-left':
38 | return (
39 |
41 | );
42 | case 'format-align-center':
43 | return (
44 |
45 | );
46 | case 'format-align-right':
47 | return (
48 |
49 | );
50 | case 'add-box':
51 | return (
52 |
54 | );
55 | case 'add':
56 | return (
57 |
58 | );
59 | case 'text-format':
60 | return (
61 |
63 | );
64 | case 'text':
65 | return (
66 |
68 | );
69 | case 'rectangle':
70 | return (
71 |
72 | );
73 | case 'circle':
74 | return (
75 |
76 | );
77 | case 'polygon':
78 | return (
79 |
80 |
84 |
85 | );
86 | case 'rotate':
87 | return (
88 |
91 | );
92 | case 'send-to-back':
93 | return (
94 |
95 |
96 |
97 |
98 | );
99 | case 'bring-to-front':
100 | return (
101 |
102 |
103 |
104 |
105 | );
106 | }
107 | }
108 | render() {
109 | let styles = {
110 | fill: this.props.active? "black": "#b5b5b5",
111 | verticalAlign: "middle",
112 | width: this.props.size,
113 | height: this.props.size
114 | };
115 | return (
116 |
120 | {this.renderGraphic()}
121 |
122 | );
123 | }
124 | };
125 |
--------------------------------------------------------------------------------
/src/Preview.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import SVGRenderer from './SVGRenderer';
3 |
4 | import {Text, Path, Rect, Circle, Image} from './objects';
5 |
6 | class Preview extends Component {
7 | static defaultProps = {
8 | objectTypes: {
9 | 'text': Text,
10 | 'rectangle': Rect,
11 | 'circle': Circle,
12 | 'polygon': Path,
13 | 'image': Image
14 | }
15 | };
16 |
17 | componentWillMount() {
18 | this.objectRefs = {};
19 | }
20 |
21 | render() {
22 | let {width, height, objects, objectTypes, responsive = false} = this.props;
23 |
24 | let style = {
25 | ...styles.container,
26 | ...this.props.style,
27 | width: responsive ? '100%' : width,
28 | height: responsive ? '100%' : height,
29 | padding: 0
30 | };
31 |
32 | let canvas = {
33 | width: responsive ? '100%' : width,
34 | height: responsive ? '100%' : height,
35 | canvasWidth: responsive ? '100%' : width,
36 | canvasHeight: responsive ? '100%' : height
37 | };
38 |
39 | if (responsive) {
40 | objects = objects.map(object => ({
41 | ...object,
42 | width: (object.width / width) * 100 + '%',
43 | height: (object.height / height) * 100 + '%',
44 | x: (object.x / width)*100 + '%',
45 | y: (object.y / height)*100 + '%',
46 | }))
47 | }
48 |
49 | return (
50 |
51 | this.svgElement = ref}
58 | canvas={canvas} />
59 |
60 | );
61 | }
62 | }
63 |
64 | const styles = {
65 | container: {
66 | position: "relative"
67 | }
68 | };
69 |
70 | export default Preview;
71 |
--------------------------------------------------------------------------------
/src/SVGRenderer.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | class SVGRenderer extends Component {
4 | static defaultProps = {
5 | onMouseOver() {}
6 | };
7 |
8 | getObjectComponent(type) {
9 | let {objectTypes} = this.props;
10 | return objectTypes[type];
11 | }
12 |
13 | renderObject(object, index) {
14 | let {objectRefs, onMouseOver} = this.props;
15 | let Renderer = this.getObjectComponent(object.type);
16 | return (
17 |
objectRefs[index] = ref}
18 | onMouseOver={onMouseOver.bind(this, index)}
19 | object={object} key={index} index={index} />
20 | );
21 | }
22 |
23 | render() {
24 | let {background, objects, svgStyle, canvas,
25 | onMouseDown, onRender} = this.props;
26 | let {width, height, canvasOffsetX, canvasOffsetY} = canvas;
27 |
28 | let style = {
29 | ...styles.canvas,
30 | ...background ? {
31 | backgroundColor: background
32 | }: styles.grid,
33 | ...{
34 | ...svgStyle,
35 | marginTop: canvasOffsetY,
36 | marginLeft: canvasOffsetX
37 | }
38 | };
39 |
40 | return (
41 |
48 | {objects.map(this.renderObject.bind(this))}
49 |
50 | );
51 | }
52 | }
53 |
54 | export const styles = {
55 | canvas: {
56 | backgroundSize: 400
57 | },
58 | grid: {
59 | backgroundImage: 'url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5'
60 | + 'vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCI+CjxyZWN0IHdpZHRoPSIyMCIgaGVpZ2h0'
61 | + 'PSIyMCIgZmlsbD0iI2ZmZiI+PC9yZWN0Pgo8cmVjdCB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIGZpbGw9I'
62 | + 'iNGN0Y3RjciPjwvcmVjdD4KPHJlY3QgeD0iMTAiIHk9IjEwIiB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIG'
63 | + 'ZpbGw9IiNGN0Y3RjciPjwvcmVjdD4KPC9zdmc+)',
64 | backgroundSize: "auto"
65 | }
66 | };
67 |
68 | export default SVGRenderer;
69 |
--------------------------------------------------------------------------------
/src/actions/Dragger.js:
--------------------------------------------------------------------------------
1 | export default ({object, startPoint, mouse}) => {
2 | return {
3 | ...object,
4 | x: mouse.x - (startPoint.clientX - startPoint.objectX),
5 | y: mouse.y - (startPoint.clientY - startPoint.objectY)
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/src/actions/Rotator.js:
--------------------------------------------------------------------------------
1 | export default ({object, startPoint, mouse}) => {
2 | let angle = Math.atan2(
3 | startPoint.objectX + (object.width || 0) / 2 - mouse.x,
4 | startPoint.objectY + (object.height || 0) / 2 - mouse.y
5 | );
6 |
7 | let asDegree = angle * 180 / Math.PI;
8 | let rotation = (asDegree + 45) * -1;
9 |
10 | return {
11 | ...object,
12 | rotate: rotation
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/src/actions/Scaler.js:
--------------------------------------------------------------------------------
1 | export default ({object, startPoint, mouse}) => {
2 | let {objectX, objectY, clientX, clientY} = startPoint;
3 | let width = startPoint.width + mouse.x - clientX;
4 | let height = startPoint.height + mouse.y - clientY;
5 |
6 | return {
7 | ...object,
8 | x: width > 0 ? objectX: objectX + width,
9 | y: height > 0 ? objectY: objectY + height,
10 | width: Math.abs(width),
11 | height: Math.abs(height)
12 | };
13 | };
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export scale from './Scaler';
2 | export drag from './Dragger';
3 | export rotate from './Rotator';
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | const FREE = 0;
2 | const DRAG = 1;
3 | const SCALE = 2;
4 | const ROTATE = 3;
5 | const DRAW = 4;
6 | const TYPE = 5;
7 | const EDIT_OBJECT = 6;
8 |
9 | export const modes = {
10 | FREE,
11 | DRAG,
12 | SCALE,
13 | ROTATE,
14 | DRAW,
15 | TYPE,
16 | EDIT_OBJECT
17 | };
18 |
19 |
--------------------------------------------------------------------------------
/src/editors/BezierEditor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 |
4 | class BezierEditor extends Component {
5 | state = {
6 | mode: 'source'
7 | };
8 |
9 | getMouseCoords(event) {
10 | let {object, offset} = this.props;
11 | return {
12 | x: event.clientX - offset.x - (object.x - object.moveX),
13 | y: event.clientY - offset.y - (object.y - object.moveY)
14 | };
15 | }
16 |
17 | componentWillMount(props) {
18 | let {object} = this.props;
19 | if (!object.path.length) {
20 | this.props.onUpdate({
21 | path: [
22 | {x1: object.x, y1: object.y}
23 | ],
24 | moveX: object.x,
25 | moveY: object.y
26 | });
27 | } else {
28 | this.setState({
29 | mode: 'edit'
30 | });
31 | }
32 | }
33 |
34 | getCurrentPath() {
35 | let {path} = this.props.object;
36 | return path[path.length - 1];
37 | }
38 |
39 | updatePath(updates, index) {
40 | let {path} = this.props.object;
41 | let current = path[index];
42 |
43 | this.props.onUpdate({
44 | path: [
45 | ...path.slice(0, index),
46 | {
47 | ...current,
48 | ...updates
49 | },
50 | ...path.slice(index + 1)
51 | ]
52 | });
53 | }
54 |
55 | updateCurrentPath(updates, close=false) {
56 | let {path} = this.props.object;
57 | let current = this.getCurrentPath();
58 |
59 | this.props.onUpdate({
60 | closed: close,
61 | path: [
62 | ...path.slice(0, path.length - 1),
63 | {
64 | ...current,
65 | ...updates
66 | }
67 | ]
68 | });
69 | }
70 |
71 | onMouseMove(event) {
72 | let {mode} = this.state;
73 | let currentPath = this.getCurrentPath();
74 | let mouse = this.getMouseCoords(event);
75 | let {object} = this.props;
76 | let {moveX, moveY} = object;
77 | let {x, y} = mouse;
78 |
79 | let snapToInitialVertex = (
80 | this.isCollides(moveX, moveY, x, y)
81 | );
82 |
83 | if (snapToInitialVertex) {
84 | x = moveX;
85 | y = moveY;
86 | }
87 |
88 | if (mode === 'source') {
89 | this.updateCurrentPath({
90 | x1: mouse.x,
91 | y1: mouse.y
92 | });
93 | }
94 |
95 | if (mode === 'target') {
96 | this.updateCurrentPath({
97 | x2: x,
98 | y2: y,
99 | x: x,
100 | y: y
101 | })
102 | }
103 |
104 | if (mode === 'connect') {
105 | this.updateCurrentPath({x, y})
106 | }
107 |
108 | if (mode === 'target' || mode === 'connect') {
109 | this.setState({
110 | closePath: snapToInitialVertex
111 | });
112 | }
113 |
114 | if (mode === 'move') {
115 | let {movedPathIndex,
116 | movedTargetX,
117 | movedTargetY} = this.state;
118 | this.updatePath({
119 | [movedTargetX]: x,
120 | [movedTargetY]: y
121 | }, movedPathIndex);
122 | }
123 |
124 | if (mode === 'moveInitial') {
125 | this.props.onUpdate({
126 | moveX: x,
127 | moveY: y
128 | });
129 | }
130 | }
131 |
132 | isCollides(x1, y1, x2, y2, radius=5) {
133 | let xd = x1 - x2;
134 | let yd = y1 - y2;
135 | let wt = radius * 2;
136 | return (xd * xd + yd * yd <= wt * wt);
137 | }
138 |
139 | onMouseDown(event) {
140 | if (this.state.closePath) {
141 | return this.closePath();
142 | }
143 |
144 | if (event.target.tagName === 'svg') {
145 | return this.props.onClose();
146 | }
147 |
148 | let {mode} = this.state;
149 |
150 | if (mode === 'target') {
151 | this.setState({
152 | mode: 'connect'
153 | });
154 | }
155 |
156 | }
157 |
158 | onMouseUp(event) {
159 | let {mode} = this.state;
160 | let {path} = this.props.object;
161 | let mouse = this.getMouseCoords(event);
162 | let currentPath = this.getCurrentPath();
163 |
164 | if (this.state.closePath) {
165 | return this.closePath();
166 | }
167 |
168 | if (mode === 'source') {
169 | this.setState({
170 | mode: 'target'
171 | });
172 | }
173 |
174 | if (mode === 'connect') {
175 | this.setState({
176 | mode: 'target'
177 | });
178 | this.props.onUpdate({
179 | path: [
180 | ...path,
181 | {
182 | x1: currentPath.x + (currentPath.x - currentPath.x2),
183 | y1: currentPath.y + (currentPath.y - currentPath.y2),
184 | x2: mouse.x,
185 | y2: mouse.y,
186 | x: mouse.x,
187 | y: mouse.y
188 | }
189 | ]
190 | });
191 | }
192 |
193 | if (mode === 'move' || mode === 'moveInitial') {
194 | this.setState({
195 | mode: 'edit'
196 | });
197 | }
198 | }
199 |
200 | getCurrentPoint(pathIndex) {
201 | let {state} = this;
202 | let {object} = this.props;
203 | if (pathIndex === 0) {
204 | return {x: object.moveX, y: object.moveY}
205 | } else {
206 | let path = state.path[pathIndex - 1];
207 | return {x: path.x, y: path.y};
208 | }
209 | }
210 |
211 | closePath() {
212 | this.setState({
213 | mode: null
214 | });
215 |
216 | this.props.onClose();
217 |
218 | this.updateCurrentPath({
219 | x: this.props.object.moveX,
220 | y: this.props.object.moveY
221 | }, true);
222 | }
223 |
224 | moveVertex(pathIndex, targetX, targetY, event) {
225 | event.preventDefault();
226 |
227 | if (this.state.mode !== 'edit') {
228 | return;
229 | }
230 |
231 | let mouse = this.getMouseCoords(event);
232 |
233 | this.setState({
234 | mode: 'move',
235 | movedPathIndex: pathIndex,
236 | movedTargetX: targetX,
237 | movedTargetY: targetY
238 | });
239 | }
240 |
241 | moveInitialVertex(event) {
242 | this.setState({
243 | mode: 'moveInitial'
244 | });
245 | }
246 |
247 | render() {
248 | let {object, width, height} = this.props;
249 | let {path} = object;
250 | let {state} = this;
251 |
252 | let {moveX, moveY, x, y} = object;
253 |
254 | let offsetX = x - moveX,
255 | offsetY = y - moveY;
256 |
257 | return (
258 |
262 |
263 |
265 | {object.path.map(({x1, y1, x2, y2, x, y}, i) => (
266 |
267 | {x2 && y2 && (
268 |
269 |
273 |
274 |
277 |
278 |
281 |
282 | )}
283 | {i === 0 && (
284 |
285 |
289 |
290 |
292 |
293 |
295 |
296 | )}
297 |
298 | ))}
299 |
300 |
301 |
302 | );
303 | }
304 | }
305 |
306 | const styles = {
307 | vertex: {
308 | fill: "#3381ff",
309 | strokeWidth: 0
310 | },
311 | initialVertex: {
312 | fill: "#ffd760"
313 | },
314 | edge: {
315 | stroke: "#b9b9b9"
316 | },
317 | canvas: {
318 | position: "absolute"
319 | }
320 | };
321 |
322 | export default BezierEditor;
323 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export Preview from './Preview';
2 | export {Vector, Path, Rect, Circle, Text, Image} from './objects';
3 | export {TextPanel, SizePanel, StylePanel, ArrangePanel, ImagePanel} from './panels';
4 | export default from './Designer';
5 |
--------------------------------------------------------------------------------
/src/objects/Circle.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {modes} from '../constants';
3 | import Icon from '../Icon';
4 | import _ from 'lodash';
5 |
6 | import Vector from './Vector';
7 |
8 | export default class Circle extends Vector {
9 | static meta = {
10 | icon: ,
11 | initial: {
12 | width: 5,
13 | height: 5,
14 | rotate: 0,
15 | fill: "yellow",
16 | strokeWidth: 0,
17 | blendMode: "normal"
18 | }
19 | };
20 |
21 | render() {
22 | let {object, index} = this.props;
23 | return (
24 |
30 | );
31 | }
32 | }
--------------------------------------------------------------------------------
/src/objects/Image.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {modes} from '../constants';
3 | import Icon from '../Icon';
4 | import _ from 'lodash';
5 |
6 | import Vector from './Vector';
7 |
8 | export default class Image extends Vector {
9 | static meta = {
10 | icon: ,
11 | initial: {
12 | width: 100,
13 | height: 100,
14 | // Just a simple base64-encoded outline
15 | xlinkHref: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAhSURBVHgBtYmxDQAADII8lv9faBNH4yoJLAi4ppxgMZoPoxQrXYyeEfoAAAAASUVORK5CYII="
16 | }
17 | };
18 |
19 | render() {
20 | let {object, index} = this.props;
21 | return (
22 |
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/objects/Path.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {modes} from '../constants';
3 | import Icon from '../Icon';
4 | import _ from 'lodash';
5 |
6 | import Vector from './Vector';
7 | import BezierEditor from '../editors/BezierEditor';
8 |
9 | export default class Path extends Vector {
10 | static meta = {
11 | initial: {
12 | fill: "#e3e3e3",
13 | closed: false,
14 | rotate: 0,
15 | moveX: 0,
16 | moveY: 0,
17 | path: [],
18 | stroke: "gray",
19 | strokeWidth: 1
20 | },
21 | mode: modes.DRAW_PATH,
22 | icon: ,
23 | editor: BezierEditor
24 | };
25 |
26 | buildPath(object) {
27 | let {path} = object;
28 |
29 | let curves = path.map(({x1, y1, x2, y2, x, y}, i) => (
30 | `C ${x1} ${y1}, ${x2} ${y2}, ${x} ${y}`
31 | ));
32 |
33 | let instructions = [
34 | `M ${object.moveX} ${object.moveY}`,
35 | ...curves
36 | ];
37 |
38 | if (object.closed) {
39 | instructions = [
40 | ...instructions, 'Z'
41 | ];
42 | }
43 |
44 | return instructions.join('\n');
45 | }
46 |
47 | getTransformMatrix({rotate, x, y, moveX, moveY}) {
48 | return `
49 | translate(${x - moveX} ${y - moveY})
50 | rotate(${rotate} ${x} ${y})
51 | `;
52 | }
53 |
54 | render() {
55 | let {object} = this.props;
56 | let fill = (object.closed ? object.fill
57 | : "transparent");
58 | return (
59 |
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/objects/Rect.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {modes} from '../constants';
3 | import Icon from '../Icon';
4 | import _ from 'lodash';
5 |
6 | import Vector from './Vector';
7 |
8 | export default class Rect extends Vector {
9 | static meta = {
10 | icon: ,
11 | initial: {
12 | width: 5,
13 | height: 5,
14 | strokeWidth: 0,
15 | fill: "blue",
16 | radius: 0,
17 | blendMode: "normal",
18 | rotate: 0
19 | }
20 | };
21 |
22 | render() {
23 | let {object, index} = this.props;
24 | return (
25 |
30 | );
31 | }
32 | }
--------------------------------------------------------------------------------
/src/objects/Text.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {modes} from '../constants';
3 | import Icon from '../Icon';
4 | import _ from 'lodash';
5 |
6 | import Vector from './Vector';
7 | import WebFont from 'webfontloader';
8 |
9 | export default class Text extends Vector {
10 | static meta = {
11 | icon: ,
12 | initial: {
13 | text: "Type some text...",
14 | rotate: 0,
15 | fontWeight: "normal",
16 | fontStyle: "normal",
17 | textDecoration: "none",
18 | fill: "black",
19 | fontSize: 20,
20 | fontFamily: "Open Sans"
21 | }
22 | };
23 |
24 | getStyle() {
25 | let {object} = this.props;
26 | return {
27 | ...super.getStyle(),
28 | dominantBaseline: "central",
29 | fontWeight: object.fontWeight,
30 | fontStyle: object.fontStyle,
31 | textDecoration: object.textDecoration,
32 | mixBlendMode: object.blendMode,
33 | WebkitUserSelect: "none"
34 | };
35 | }
36 |
37 | getTransformMatrix({rotate, x, y}) {
38 | return `rotate(${rotate} ${x} ${y})`;
39 | }
40 |
41 | render() {
42 | let {object, index} = this.props;
43 | WebFont.load({
44 | google: {
45 | families: [object.fontFamily]
46 | }
47 | });
48 | const {rotate, ... restOfAttributes} = this.getObjectAttributes()
49 | return (
50 |
55 | {object.text}
56 |
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/objects/Vector.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {modes} from '../constants';
3 | import Icon from '../Icon';
4 | import _ from 'lodash';
5 |
6 | import {SizePanel, TextPanel,
7 | StylePanel, ArrangePanel, ImagePanel} from '../panels';
8 |
9 |
10 | export default class Vector extends Component {
11 | static panels = [
12 | SizePanel,
13 | TextPanel,
14 | StylePanel,
15 | ImagePanel,
16 | ArrangePanel
17 | ];
18 |
19 | getStyle() {
20 | let {object} = this.props;
21 | return {
22 | mixBlendMode: object.blendMode
23 | }
24 | }
25 |
26 | getTransformMatrix({rotate, x, y, width, height}) {
27 | if (rotate) {
28 | let centerX = width / 2 + x;
29 | let centerY = height / 2 + y;
30 | return `rotate(${rotate} ${centerX} ${centerY})`;
31 | }
32 | }
33 |
34 | getObjectAttributes() {
35 | let {object, onRender, ...rest} = this.props;
36 | return {
37 | ...object,
38 | transform: this.getTransformMatrix(object),
39 | ref: onRender,
40 | ...rest
41 | };
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/objects/index.js:
--------------------------------------------------------------------------------
1 | export Vector from './Vector';
2 | export Path from './Path';
3 | export Rect from './Rect';
4 | export Circle from './Circle';
5 | export Text from './Text';
6 | export Image from './Image';
7 |
--------------------------------------------------------------------------------
/src/panels/ArrangePanel.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import _ from 'lodash';
3 |
4 | import styles from './styles';
5 | import Icon from '../Icon';
6 | import PropertyGroup from './PropertyGroup';
7 | import Button from './Button';
8 | import SwitchState from './SwitchState';
9 | import Columns from './Columns';
10 | import Column from './Column';
11 |
12 | export default class ArrangePanel extends Component {
13 | render() {
14 | let {object} = this.props;
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | send to back
22 |
23 |
24 |
25 | bring to front
26 |
27 |
28 |
29 |
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/panels/Button.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import _ from 'lodash';
3 | import Icon from '../Icon';
4 |
5 | import styles from './styles';
6 |
7 | const Button = ({onClick, ...props}) => {
8 | let _onClick = (e, ...args) => {
9 | e.preventDefault();
10 | onClick(...args);
11 | }
12 | return (
13 |
14 | {props.children}
15 |
16 | );
17 | }
18 |
19 | export default Button;
20 |
--------------------------------------------------------------------------------
/src/panels/ColorInput.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { SketchPicker } from 'react-color';
3 | import _ from 'lodash';
4 | import Icon from '../Icon';
5 |
6 | import styles from './styles';
7 |
8 | class ColorInput extends Component {
9 | state = {
10 | show: false
11 | };
12 |
13 | toggleVisibility = (event) => {
14 | if (event.preventDefault) {
15 | event.preventDefault();
16 | }
17 |
18 | let {show} = this.state;
19 | this.setState({
20 | show: !show
21 | })
22 | }
23 |
24 | handleChange = (color) => {
25 | let {r, g, b, a} = color.rgb;
26 | this.props.onChange(`rgba(${r}, ${g}, ${b}, ${a})`);
27 | }
28 |
29 | handleClose = (event) => {
30 | if (event.preventDefault) {
31 | event.preventDefault();
32 | }
33 |
34 | this.setState({
35 | show: false
36 | })
37 | }
38 |
39 | render() {
40 | let {show} = this.state;
41 | let {value} = this.props;
42 |
43 | return (
44 |
58 | );
59 | }
60 | }
61 |
62 | export default ColorInput;
63 |
--------------------------------------------------------------------------------
/src/panels/Column.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import _ from 'lodash';
3 | import Icon from '../Icon';
4 |
5 | import styles from './styles';
6 |
7 | const Column = ({showIf=true, ...props}) => {
8 | if (!showIf) {
9 | return
;
10 | }
11 |
12 | return (
13 |
14 | {props.children ||
15 |
props.onChange(e.target.value)} />
17 | }
18 | {props.label &&
19 |
{props.label}
}
20 |
21 | );
22 | };
23 |
24 | export default Column;
25 |
--------------------------------------------------------------------------------
/src/panels/Columns.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import _ from 'lodash';
3 | import Icon from '../Icon';
4 |
5 | import styles from './styles';
6 |
7 | const Columns = ({showIf=true, ...props}) => {
8 | if (!showIf) {
9 | return
;
10 | }
11 | return (
12 |
13 |
{props.label}
14 | {props.children}
15 |
16 | )
17 | };
18 |
19 | export default Columns;
20 |
--------------------------------------------------------------------------------
/src/panels/ImagePanel.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import _ from 'lodash';
3 |
4 | import Icon from '../Icon';
5 |
6 | import styles from './styles';
7 | import PropertyGroup from './PropertyGroup';
8 | import Button from './Button';
9 | import SwitchState from './SwitchState';
10 | import Columns from './Columns';
11 | import Column from './Column';
12 | import Dropzone from 'react-dropzone';
13 | import request from 'superagent';
14 |
15 | export default class ImagePanel extends Component {
16 | onDrop (acceptedFiles) {
17 | if (acceptedFiles.length == 0) {
18 | return;
19 | }
20 |
21 | const file = acceptedFiles[0];
22 | const fr = new FileReader();
23 |
24 | const setImage = function(e) {
25 | this.props.onChange('xlinkHref', e.target.result);
26 | }.bind(this);
27 | fr.onload = setImage;
28 | fr.readAsDataURL(file);
29 | }
30 |
31 | render() {
32 | const {object} = this.props;
33 | return (
34 |
35 |
36 |
37 |
57 | {({getRootProps, getInputProps}) => (
58 |
59 |
60 |
Drop new file
61 |
62 | )}
63 |
64 |
65 |
66 |
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/panels/InsertMenu.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import classNames from 'classnames';
4 | import Icon from '../Icon';
5 |
6 | class InsertMenu extends Component {
7 |
8 | constructor(props) {
9 | super(props)
10 | this.state = {
11 | menuOpened: false,
12 | hoveredTool: null
13 | }
14 | }
15 |
16 | openMenu = () => {
17 | this.setState({menuOpened: true})
18 | }
19 |
20 | closeMenu = () => {
21 | this.setState({menuOpened: false})
22 | }
23 |
24 | hoverTool = type => {
25 | this.setState({hoveredTool: type})
26 | }
27 |
28 | unhoverTool = type => {
29 | if (this.state.hoveredTool == type) {
30 | this.setState({hoveredTool: null})
31 | }
32 | }
33 |
34 | render() {
35 | let {currentTool, tools} = this.props;
36 | let {menuOpened, hoveredTool} = this.state;
37 | let keys = Object.keys(tools);
38 |
39 | return (
40 |
47 |
48 | {currentTool
49 | ? tools[currentTool].meta.icon
50 | : }
51 |
52 |
53 | {keys.map((type, i) => (
54 | this.hoverTool(type)}
60 | onMouseOut={() => this.unhoverTool(type)}
61 | onMouseDown={this.props.onSelect.bind(this, type)}
62 | key={i}>
63 | {tools[type].meta.icon}
64 |
65 | ))}
66 |
67 |
68 | );
69 | }
70 | }
71 |
72 | const styles = {
73 | insertMenu: {
74 | height: 40,
75 | width: 40,
76 | overflow: 'hidden',
77 | },
78 | insertMenuHover: {
79 | background: '#eeeff5',
80 | height: 'auto',
81 | },
82 | toolBox: {
83 | margin: 0,
84 | padding: 0,
85 | },
86 | toolBoxItem: {
87 | listStyle: "none",
88 | padding: "5px 5px"
89 | },
90 | currentToolboxItem: {
91 | background: "#ebebeb"
92 | },
93 | mainIcon: {
94 | padding: "10px 5px",
95 | borderBottom: "1px solid #e0e0e0"
96 | }
97 |
98 | };
99 |
100 | export default InsertMenu;
101 |
--------------------------------------------------------------------------------
/src/panels/PanelList.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import _ from 'lodash';
3 | import { Portal } from 'react-portal';
4 |
5 | import Icon from '../Icon';
6 |
7 | import styles from './styles';
8 | import PropertyGroup from './PropertyGroup';
9 | import Button from './Button';
10 | import SwitchState from './SwitchState';
11 | import Columns from './Columns';
12 | import Column from './Column';
13 |
14 | class PanelList extends Component {
15 | render() {
16 | let {object, objectComponent, id} = this.props;
17 |
18 | return (
19 |
20 | {objectComponent.panels.map((Panel, i) =>
)}
21 |
22 | );
23 | }
24 | };
25 |
26 | export default PanelList;
27 |
--------------------------------------------------------------------------------
/src/panels/PropertyGroup.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import _ from 'lodash';
3 | import Icon from '../Icon';
4 |
5 | import styles from './styles';
6 |
7 | const PropertyGroup = ({showIf=true, ...props}) => {
8 | if (!showIf) {
9 | return
;
10 | }
11 | return (
12 |
13 | {props.children}
14 |
15 | );
16 | };
17 |
18 | export default PropertyGroup;
19 |
--------------------------------------------------------------------------------
/src/panels/SizePanel.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import _ from 'lodash';
3 |
4 | import Icon from '../Icon';
5 |
6 | import styles from './styles';
7 | import PropertyGroup from './PropertyGroup';
8 | import Button from './Button';
9 | import SwitchState from './SwitchState';
10 | import Columns from './Columns';
11 | import Column from './Column';
12 |
13 | export default class SizePanel extends Component {
14 | render() {
15 | let {object} = this.props;
16 | return (
17 |
18 | {_.has(object, 'width', 'height') &&
19 |
22 |
25 | }
26 |
27 |
30 |
32 |
33 | {_.has(object, 'rotate') &&
34 |
36 | }
37 |
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/panels/StylePanel.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import _ from 'lodash';
3 |
4 | import Icon from '../Icon';
5 |
6 | import styles from './styles';
7 | import PropertyGroup from './PropertyGroup';
8 | import Button from './Button';
9 | import SwitchState from './SwitchState';
10 | import Columns from './Columns';
11 | import Column from './Column';
12 | import ColorInput from './ColorInput';
13 |
14 | export default class StylePanel extends Component {
15 | modes = [
16 | 'normal',
17 | 'multiply',
18 | 'screen',
19 | 'overlay',
20 | 'darken',
21 | 'lighten',
22 | 'color-dodge',
23 | 'color-burn',
24 | 'hard-light',
25 | 'soft-light',
26 | 'difference',
27 | 'exclusion',
28 | 'hue',
29 | 'saturation',
30 | 'color',
31 | 'luminosity'
32 | ];
33 |
34 | render() {
35 | let {object} = this.props;
36 | return (
37 |
38 |
39 |
40 |
42 |
43 |
44 |
45 |
46 |
48 |
49 |
50 | this.props.onChange('strokeWidth', e.target.value)}
52 | value={object.strokeWidth} />
53 |
54 |
55 | this.props.onChange('radius', e.target.value)}
57 | value={object.radius} />
58 |
59 |
60 |
61 |
62 | this.props.onChange('blendMode', e.target.value)}>
65 | {this.modes.map(mode => {mode} )}
66 |
67 |
68 |
69 |
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/panels/SwitchState.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import _ from 'lodash';
3 | import Icon from '../Icon';
4 |
5 | const SwitchState = (props) => {
6 | let selected = props.value !== props.defaultValue;
7 | let newValue = selected? props.defaultValue: props.nextState;
8 | return (
9 | props.onChange(newValue)} />
11 | );
12 | }
13 |
14 | export default SwitchState;
15 |
--------------------------------------------------------------------------------
/src/panels/TextPanel.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import _ from 'lodash';
3 |
4 | import Icon from '../Icon';
5 |
6 | import styles from './styles';
7 | import PropertyGroup from './PropertyGroup';
8 | import Button from './Button';
9 | import SwitchState from './SwitchState';
10 | import Columns from './Columns';
11 | import Column from './Column';
12 | import WebFont from 'webfontloader';
13 |
14 | export default class TextPanel extends Component {
15 | fontFamilies = [
16 | {name: 'Alegreya Sans', family: 'Alegreya Sans'},
17 | {name: 'Alegreya', family: 'Alegreya'},
18 | {name: 'American Typewriter', family:'AmericanTypewriter, Georgia, serif'},
19 | {name: 'Anonymous Pro', family: 'Anonymous Pro'},
20 | {name: 'Archivo Narrow', family: 'Archivo Narrow'},
21 | {name: 'Arvo', family: 'Arvo'},
22 | {name: 'Bitter', family: 'Bitter'},
23 | {name: 'Cardo', family: 'Cardo'},
24 | {name: 'Chivo', family: 'Chivo'},
25 | {name: 'Crimson Text', family: 'Crimson Text'},
26 | {name: 'Domine', family: 'Domine'},
27 | {name: 'Fira Sans', family: 'Fira Sans'},
28 | {name: 'Georgia', family:'Georgia, serif'},
29 | {name: 'Helvetica Neue', family:'"Helvetica Neue", Arial, sans-serif'},
30 | {name: 'Helvetica', family:'Helvetica, Arial, sans-serif'},
31 | {name: 'Inconsolata', family: 'Inconsolata'},
32 | {name: 'Karla', family: 'Karla'},
33 | {name: 'Lato', family: 'Lato'},
34 | {name: 'Libre Baskerville', family: 'Libre Baskerville'},
35 | {name: 'Lora', family: 'Lora'},
36 | {name: 'Merriweather', family: 'Merriweather'},
37 | {name: 'Monaco', family:'Monaco, consolas, monospace'},
38 | {name: 'Montserrat', family:'Montserrat'},
39 | {name: 'Neuton', family:'Neuton'},
40 | {name: 'Old Standard TT', family: 'Old Standard TT'},
41 | {name: 'Open Sans', family: 'Open Sans'},
42 | {name: 'PT Serif', family: 'PT Serif'},
43 | {name: 'Playfair Display', family: 'Playfair Display'},
44 | {name: 'Poppins', family: 'Poppins'},
45 | {name: 'Roboto Slab', family: 'Roboto Slab'},
46 | {name: 'Roboto', family: 'Roboto'},
47 | {name: 'Source Pro', family: 'Source Pro'},
48 | {name: 'Source Sans Pro', family: 'Source Sans Pro'},
49 | {name: 'Varela Round', family:'Varela Round'},
50 | {name: 'Work Sans', family: 'Work Sans'},
51 | ];
52 |
53 | handleFontFamilyChange = e => {
54 | const value = e.target.value
55 | WebFont.load({
56 | google: {
57 | families: [value]
58 | }
59 | });
60 | this.props.onChange('fontFamily', value)
61 | }
62 |
63 | sortFonts = (f1, f2) => f1.name.toLowerCase() > f2.name.toLowerCase() ? 1 : f1.name.toLowerCase() < f2.name.toLowerCase() ? -1 : 0
64 |
65 | render() {
66 | let {object} = this.props;
67 | return (
68 |
69 |
70 |
71 | {_.has(object, 'fontWeight') &&
72 | }
77 | {_.has(object, 'fontStyle') &&
78 | }
83 | {_.has(object, 'textDecoration') &&
84 | }
89 |
90 |
91 | {_.has(object, 'fontSize') &&
92 | this.props.onChange('fontSize', e.target.value)} />}
95 |
96 |
97 |
100 | {this.fontFamilies.sort(this.sortFonts).map(({name, family}) => (
101 | {name}
102 | ))}
103 |
104 |
105 |
106 | this.props.onChange('text', e.target.value)}
108 | value={object.text} />
109 |
110 |
111 |
112 | );
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/panels/index.js:
--------------------------------------------------------------------------------
1 | export TextPanel from './TextPanel';
2 | export ArrangePanel from './ArrangePanel';
3 | export StylePanel from './StylePanel';
4 | export SizePanel from './SizePanel';
5 | export InsertMenu from './InsertMenu';
6 | export ImagePanel from './ImagePanel';
--------------------------------------------------------------------------------
/src/panels/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | propertyPanel: {
3 | position: 'relative',
4 | width: 240,
5 | padding: '0 5px 6px 5px',
6 | fontFamily: '"Lucida Grande", sans-serif',
7 | fontSize: 11
8 | },
9 | propertyGroup: {
10 | backgroundColor: '#f1f1f1',
11 | overflow: 'hidden',
12 | paddingBottom: 12,
13 | paddingTop: 2,
14 | paddingLeft: 10,
15 | border: '0px solid #d3d3d3',
16 | marginBottom: 5
17 | },
18 | inputHelper: {
19 | fontSize: 9,
20 | color: '#d2d2d2',
21 | paddingTop: 2,
22 | paddingLeft: 5
23 | },
24 | inlineInputHelper: {
25 | fontSize: 9,
26 | display: 'inline-block',
27 | marginLeft: 10
28 | },
29 | panelTitle: {
30 | float: 'left',
31 | width: 60,
32 | padding: 3,
33 | color: '#b8b8b8'
34 | },
35 | columns: {
36 | overflow: 'hidden',
37 | marginTop: 10
38 | },
39 | column: {
40 | float: 'left',
41 | marginRight: 5
42 | },
43 | input: {
44 | background: '#e1e1e1',
45 | borderWidth: 0,
46 | padding: '3px 5px',
47 | color: 'gray',
48 | borderRadius: 3,
49 | },
50 | select: {
51 | WebkitAppearance: 'none',
52 | MozAppearance: 'none',
53 | borderWidth: 0,
54 | padding: '3px 3px 3px 5px',
55 | outline: 'none',
56 | borderRadius: 0,
57 | borderRight: '3px solid #b7b7b7',
58 | color: 'gray',
59 | width: 75
60 | },
61 | integerInput: {
62 | width: 50,
63 | outline: 'none'
64 | },
65 | textInput: {
66 | marginTop: 2,
67 | outline: 'none',
68 | width: '100%',
69 | padding: 3,
70 | },
71 | colorInput: {
72 | width: 18,
73 | height: 18,
74 | borderRadius: '50%',
75 | display: 'inline-block',
76 | background: 'white',
77 | marginRight: 3,
78 | },
79 | color: {
80 | marginLeft: 2,
81 | marginTop: 2,
82 | width: 14,
83 | height: 14,
84 | display: 'inline-block',
85 | borderRadius: '50%'
86 | },
87 | colorCover: {
88 | position: 'fixed',
89 | top: 0,
90 | right: 0,
91 | bottom: 0,
92 | left: 0,
93 | },
94 | colorPopover: {
95 | position: 'absolute',
96 | marginTop: 8,
97 | zIndex: 999999
98 | },
99 | empty: {
100 | display: 'none',
101 | },
102 | button: {
103 | color: '#515151',
104 | textDecoration: 'none',
105 | display: 'block',
106 | padding: '2px 0',
107 | },
108 | item: {
109 | padding: '2px 6px',
110 | cursor: 'default'
111 | },
112 |
113 | highlightedItem: {
114 | color: 'white',
115 | background: 'hsl(200, 50%, 50%)',
116 | padding: '2px 6px',
117 | cursor: 'default'
118 | },
119 | };
120 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | mode: 'development',
6 | resolve: {
7 | extensions: ['.js', '.jsx', '.js', '.css'],
8 | mainFields: [
9 | 'webpack',
10 | 'browser',
11 | 'web',
12 | 'browserify',
13 | ['jam', 'main'],
14 | 'main',
15 | 'index'
16 | ]
17 | },
18 | devtool: 'eval',
19 | entry: [
20 | 'webpack-dev-server/client?http://localhost:3000',
21 | 'webpack/hot/only-dev-server',
22 | './examples'
23 | ],
24 | output: {
25 | path: path.join(__dirname, 'dist'),
26 | filename: 'bundle.js',
27 | publicPath: '/dist/'
28 | },
29 | plugins: [new webpack.HotModuleReplacementPlugin()],
30 | module: {
31 | rules: [
32 | { test: /\.(png|svg)$/, loader: 'url-loader?limit=8192' },
33 | {
34 | test: /^((?!\.module).)*\.css$/,
35 | loaders: ['style-loader', 'css-loader']
36 | },
37 | {
38 | test: /\.module\.css$/,
39 | loaders: [
40 | 'style-loader',
41 | 'css-loader?modules&localIdentName=[name]__[local]___[hash:base64:5]!'
42 | ]
43 | },
44 | {
45 | test: /\.js$/,
46 | loaders: ['react-hot-loader/webpack', 'babel-loader'],
47 | include: [path.join(__dirname, 'src'), path.join(__dirname, 'example')]
48 | }
49 | ]
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/webpack.production.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 | var node_modules_dir = path.resolve(__dirname, 'node_modules');
4 |
5 | module.exports = {
6 | resolve: {
7 | extensions: ['.js', '.jsx', '.js', '.css'],
8 | mainFields: [
9 | 'webpack',
10 | 'browser',
11 | 'web',
12 | 'browserify',
13 | ['jam', 'main'],
14 | 'main',
15 | 'index'
16 | ]
17 | },
18 | entry: ['./examples'],
19 | output: {
20 | path: path.join(__dirname, 'dist'),
21 | filename: 'bundle.js',
22 | publicPath: '/static/'
23 | },
24 | plugins: [],
25 | module: {
26 | rules: [
27 | {
28 | test: /^((?!\.module).)*\.css$/,
29 | loaders: ['style-loader', 'css-loader']
30 | },
31 | {
32 | test: /\.module\.css$/,
33 | loaders: [
34 | 'style-loader',
35 | 'css-loader?modules&localIdentName=[name]__[local]___[hash:base64:5]!'
36 | ]
37 | },
38 | {
39 | test: /\.js$/,
40 | loaders: ['babel-loader'],
41 | exclude: [node_modules_dir],
42 | include: [path.join(__dirname, 'src'), path.join(__dirname, 'example')]
43 | }
44 | ]
45 | }
46 | };
47 |
--------------------------------------------------------------------------------