├── .babelrc ├── .eslintrc ├── .gitignore ├── .jshintrc ├── .npmrc ├── LICENSE ├── README.md ├── examples ├── App.js ├── App.module.css ├── components │ ├── Malevich.js │ ├── MockupDesigner.js │ ├── Mondrian.js │ ├── SwissStyle.js │ ├── TshirtDesigner.js │ └── components │ │ └── h1.hs └── index.js ├── glasses ├── index.html ├── package-lock.json ├── package.json ├── server.js ├── src ├── Designer.js ├── Handler.js ├── Icon.js ├── Preview.js ├── SVGRenderer.js ├── actions │ ├── Dragger.js │ ├── Rotator.js │ ├── Scaler.js │ └── index.js ├── constants.js ├── editors │ └── BezierEditor.js ├── index.js ├── objects │ ├── Circle.js │ ├── Image.js │ ├── Path.js │ ├── Rect.js │ ├── Text.js │ ├── Vector.js │ └── index.js └── panels │ ├── ArrangePanel.js │ ├── Button.js │ ├── ColorInput.js │ ├── Column.js │ ├── Columns.js │ ├── ImagePanel.js │ ├── InsertMenu.js │ ├── PanelList.js │ ├── PropertyGroup.js │ ├── SizePanel.js │ ├── StylePanel.js │ ├── SwitchState.js │ ├── TextPanel.js │ ├── index.js │ └── styles.js ├── webpack.config.js └── webpack.production.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-export-default-from", 5 | "@babel/plugin-proposal-class-properties", 6 | "css-modules-transform" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": true 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "parser": "babel-eslint", 11 | "rules": { 12 | "quotes": [2, "single"], 13 | "strict": [2, "never"], 14 | "react/jsx-uses-react": 2, 15 | "react/jsx-uses-vars": 2, 16 | "react/react-in-jsx-scope": 2 17 | }, 18 | "plugins": [ 19 | "react" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | lib 5 | .idea 6 | 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "newcap": false 6 | } 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | unsafe-perm = true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution 4.0 International (CC BY 4.0) 2 | https://creativecommons.org/licenses/by/4.0/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *This project is **back to being maintained**, redirect any new PRs and issues to [@wassgha](https://github.com/wassgha/)* 2 | 3 | React-designer 4 | ============== 5 | 6 | [![Join the chat at https://gitter.im/fatiherikli/react-designer](https://badges.gitter.im/fatiherikli/react-designer.svg)](https://gitter.im/fatiherikli/react-designer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | 8 | Easy to configure, lightweight, editable vector graphics in your react components. 9 | 10 | - Supports polygon and shape designing (with bezier curves) 11 | - Implemented default scale, rotate, drag, and arrange actions 12 | - Custom object types and custom panels 13 | 14 | 15 | Examples and demonstration: 16 | 17 | 18 | ![bezier editor](http://i.imgur.com/cqTleWB.gif) 19 | 20 | ## Keymap 21 | 22 | | Parameter | Default | 23 | | :------------- |:------------------------------- 24 | | `del` or `back` | Removes the current object | 25 | | `arrows` | Move the current object by 1px | 26 | | `shift` + `arrows` | Move the currnet object by 10px | 27 | | `enter` | Close the drawing path | 28 | 29 | 30 | ## Usage 31 | All the entities are pure react components except action strategies in react-designer. I have tried to explain that. I'm starting with components. 32 | 33 | ### Component: Designer 34 | 35 | This is the main canvas component which holds the all toolset and manages all drawing data. You could use this component to create drawing canvas. 36 | 37 | An example with default configuration: 38 | 39 | ```javascript 40 | import Designer, {Text, Rectangle} from 'react-designer'; 41 | 42 | class App() { 43 | state = { 44 | objects: [ 45 | {type: "text", x: 10, y: 20, text: "Hello!", fill: "red"}, 46 | {type: "rect", x: 50, y: 70, fill: "red"} 47 | ] 48 | } 49 | 50 | render() { 51 | return ( 52 | this.setState({objects})} 58 | objects={this.state.objects} /> 59 | ) 60 | } 61 | } 62 | ``` 63 | 64 | The `Designer` component expects the following parameters: 65 | 66 | | Parameter | Default | | 67 | | :------------- |:------------------------------- | :----- | 68 | | width | 300 | The width of document | 69 | | height | 300 | The height of document | 70 | | canvasWidth | null | The width of canvas. Same with document if it's null. | 71 | | canvasHeight | null | The height of canvas. Same with document if it's null. | 72 | | objects | [] | Your object set. | 73 | | onUpdate | [] | Your update callback. | 74 | | objectTypes | Text, Circle, Rectangle, Path | Mapping of object types. | 75 | | insertMenu | | Insert menu component. You can set null for hiding 76 | | snapToGrid | 1 | Snaps the objects accordingly this multipier. | 77 | | rotator | rotate({object, mouse}) | The rotating strategy of objects 78 | | scale | scale({object, mouse}) | The scaling strategy of objects 79 | | drag | drag({object, mouse}) | The dragging strategy of objects 80 | 81 | 82 | Object types are pure react components which are derived from `Vector`. 83 | 84 | ### Component: Vector 85 | 86 | You can create an object type by subclassing `Vector` component. Each object types have static `meta` object which contains `icon` and `initial`, and optionally `editor` value. 87 | 88 | Example implementation: 89 | 90 | ```javascript 91 | class MyRectangle extends Vector { 92 | static meta = { 93 | icon: , 94 | initial: { 95 | width: 5, 96 | height: 5, 97 | strokeWidth: 0, 98 | fill: "yellow", 99 | radius: 5, 100 | blendMode: "normal" 101 | } 102 | }; 103 | 104 | render() { 105 | let {object, index} = this.props; 106 | return ( 107 | 112 | ); 113 | } 114 | } 115 | ``` 116 | 117 | You can register this object type in your `Designer` instance. 118 | 119 | ```javascript 120 | 127 | ``` 128 | 129 | Apart from meta options, the vectors have `panels` static definition which contains the available panels of their. 130 | 131 | Here are default panels in Vector component: 132 | 133 | ```javascript 134 | static panels = [ 135 | SizePanel, 136 | TextPanel, 137 | StylePanel, 138 | ArrangePanel 139 | ]; 140 | ``` 141 | 142 | ### Component: Preview 143 | 144 | You can use `Preview` component to disable editing tool set and controllers. This component just renders the SVG output of your data. It may be useful for presenting edited or created graphic, instead of building a SVG file. 145 | 146 | The parameters are same with Designer component, except for two: the onUpdate callback is not necessary and an additional `responsive` option can be added, which given the original `width` and `height` will expand the preview to cover the width and height of its parent component, scaling its SVG while keeping the original aspect ratio of elements. Note that the original `width` and `height` still need to be provided in order for the responsive `Preview` to work. 147 | 148 | ```javascript 149 | 155 | ``` 156 | 157 | ### Action strategies 158 | 159 | The actions of `rotate`, `scale`, `drag` are pure functions. You can change this actions by passing your strategy. The action functions calling with the following object bundle. 160 | 161 | ```javascript 162 | { 163 | object, // the current object 164 | mouse, // mouse coordinates bundle. it have x and y attribtues 165 | startPoint, // starting points of mouse and object bundles. 166 | objectIndex, // the index of the object in the documen, 167 | objectRefs, // DOM references of objects in the document 168 | } 169 | ``` 170 | 171 | Here are default action strategies: 172 | 173 | #### Dragger 174 | Moves the object to mouse bundle by the center of object. 175 | 176 | ```javascript 177 | // dragger.js 178 | export default ({object, startPoint, mouse}) => { 179 | return { 180 | ...object, 181 | x: mouse.x - (startPoint.clientX - startPoint.objectX), 182 | y: mouse.y - (startPoint.clientY - startPoint.objectY) 183 | }; 184 | }; 185 | ``` 186 | 187 | #### Scaler 188 | Scales the object by the difference with startPoint and current mouse bundle. If the difference lower than zero, changes the position of object. 189 | 190 | ```javascript 191 | // scale.js 192 | export default ({object, startPoint, mouse}) => { 193 | let {objectX, objectY, clientX, clientY} = startPoint; 194 | let width = startPoint.width + mouse.x - clientX; 195 | let height = startPoint.height + mouse.y - clientY; 196 | 197 | return { 198 | ...object, 199 | x: width > 0 ? objectX: objectX + width, 200 | y: height > 0 ? objectY: objectY + height, 201 | width: Math.abs(width), 202 | height: Math.abs(height) 203 | }; 204 | }; 205 | ``` 206 | 207 | #### Rotator 208 | Changes the rotation as degree of object. This action may needs some improvement, I'm calculating with a base value (45 degree) because of the rotator anchor is on the upper right corner of object. 209 | 210 | ```javascript 211 | // rotate.js 212 | export default ({object, startPoint, mouse}) => { 213 | let angle = Math.atan2( 214 | startPoint.objectX + (object.width || 0) / 2 - mouse.x, 215 | startPoint.objectY + (object.height || 0) / 2 - mouse.y 216 | ); 217 | 218 | let asDegree = angle * 180 / Math.PI; 219 | let rotation = (asDegree + 45) * -1; 220 | 221 | return { 222 | ...object, 223 | rotate: rotation 224 | }; 225 | }; 226 | ``` 227 | 228 | 229 | ### To-do 230 | 231 | I built this project to create user-designed areas in my side project. So, this was just a hobby project, there may be things missing for a svg editor. I'm open to pull requests and feedback, and I need help to maintain. 232 | 233 | Here is a todo list that in my mind. You could extend this list. 234 | 235 | - Implement `Export` panel 236 | - Export selected object 237 | - Export document 238 | - Write initial tests and setup test environment 239 | - Add a key map to keep the ratio of objects when scaling 240 | - Implement theme support for UI 241 | 242 | ## Release Notes 243 | 244 | ### 1.0.8 245 | 246 | - Move React-dom dependency to dev-dependencies 247 | 248 | ### 1.0.6 249 | 250 | - `Designer` component exported as default now. 251 | - Added `insertMenu` prop to `Designer` component. 252 | 253 | ### Contributors (You can add your name here in your pull-request) 254 | 255 | - Fatih Erikli - [fatiherikli](https://github.com/fatiherikli/) 256 | - Wassim Gharbi - [wassgha](https://github.com/wassgha/) 257 | - [iamraffe](https://github.com/iamraffe/) 258 | - [thatneat](https://github.com/thatneat/) 259 | -------------------------------------------------------------------------------- /examples/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import ReactDOMServer from 'react-dom/server'; 4 | import {Rect, Vector, Text} from '../src/objects'; 5 | import classes from './App.module' 6 | 7 | import MondrianExample from './components/Mondrian'; 8 | import MalevichExample from './components/Malevich'; 9 | import SwissStyleExample from './components/SwissStyle'; 10 | import MockupDesignerExample from './components/MockupDesigner'; 11 | import TshirtDesignerExample from './components/TshirtDesigner'; 12 | 13 | 14 | export default class App extends Component { 15 | render() { 16 | return ( 17 |
18 |
19 |

react-designer

20 |

Easy to configure, lightweight, editable vector graphics in your react components.

21 | 27 |
28 |
29 |

Usage

30 |

You should provide your objects and object types. The objects might be empty array if you want to create a 31 | blank canvas.

32 |
{`
 33 | import Designer, {Text, Rect} from 'react-designer';
 34 | 
 35 | class App() {
 36 |   state = {
 37 |     objects: [
 38 |       {type: "text", x: 10, y: 20, text: "Hello!", fill: "red"},
 39 |       {type: "rect", x: 50, y: 70, width: 30, height: 40, fill: "red"}
 40 |     ]
 41 |   };
 42 | 
 43 |   render() {
 44 |     return (
 45 |        this.setState({objects})}
 51 |         objects={this.state.objects} />
 52 |     )
 53 |   }
 54 | }
 55 |           `.trim()}
56 | You should listen onUpdate callback to update your objects. React-designer will invoke this 57 | callback in every update. 58 |
59 |

Examples

60 |
61 |
62 |

Mondrian

63 |

Default configuration with initial rectangle objects set on the internal state of 64 | container component.

65 |

Enabled all default drawing tool set and panels.

66 |

67 | Show example on github 68 |

69 |
70 |
71 | 72 |
73 |
74 | 75 |
76 |
77 |

Malevich

78 |

Default configuration with initial shapes (bezier curves) set on the internal state of 79 | container component.

80 | 81 |

You can double-click to edit shapes.

82 |

83 | Show example on github 84 |

85 |
86 |
87 | 88 |
89 |
90 | 91 |
92 |
93 |

The Swiss Style

94 |

Default configuration with initial text and shapes with blending modes on the internal state of 95 | container component.

96 | 97 |

98 | Show example on github 99 |

100 |
101 |
102 | 103 |
104 |
105 | 106 |
107 |
108 |

Mockup Designer

109 |

An extended toolset for very simple mockup designing tool.

110 | 111 |

Custom components on this demo:

112 | 119 | 120 |

These components are derived from Vector, but they are still pure React components.

121 | 122 |

123 | Show example on github 124 |

125 |
126 |
127 | 128 |
129 |
130 | 131 |
132 |
133 |

Tshirt Designer

134 |

Default toolset with customized designer canvas for building simple tshirt design tool.

135 | 136 |

Price calculation is a simple pure function that receives object list and returns total price.

137 | 138 |

In this demo, price is relative to covered areas of objects on the canvas. 139 | For example price is instantly changing while you resizing objects or typing a text.

140 | 141 |

142 | Show example on github
143 |

144 |
145 |
146 | 147 |
148 |

152 | Here is price calculation logic: 153 |

154 |
{`
158 | const priceMap = {
159 |   'text': ({text, fontSize}) => text.length * fontSize * 0.01,
160 |   'rectangle': ({width, height}) => width * height * 0.001,
161 |   'circle': ({width, height}) => width * (height || width) * 0.001
162 | };
163 | 
164 | const calculatePrice = (objects, initialCost = 5) => (
165 |   objects.map(
166 |     ({type, ...rest}) => priceMap[type](rest)
167 |   ).reduce(
168 |     (a, b) => a + b,
169 |     initialCost
170 |   )
171 | );
172 |           `}
173 |

174 | We are calling this function in our `handleUpdate` method. 175 |

176 |
177 |
178 |
179 | Fatih Erikli, 2016
180 | MIT Licensed 181 |
182 |
183 | Ask me anything: @fthrkl 184 |
185 |
186 |
187 | ); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /examples/App.module.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Lora:400,400italic,700); 2 | 3 | .example { 4 | display: flex; 5 | flex-direction: column; 6 | margin-bottom: 80px; 7 | } 8 | 9 | .example h3 { 10 | color: gray; 11 | margin: 10px 0 15px; 12 | font-weight: normal; 13 | float: left; 14 | font-size: 2.5em; 15 | } 16 | 17 | .example h3 + p { 18 | clear: both; 19 | } 20 | 21 | .example h3 span { 22 | color: #c1c1c1; 23 | } 24 | 25 | .example a { 26 | color: #39393a; 27 | } 28 | 29 | .info { 30 | width: 650px; 31 | padding: 20px; 32 | color: gray; 33 | font-size: 0.9em; 34 | line-height: 1.7em; 35 | } 36 | 37 | .preview { 38 | float: left; 39 | margin-bottom: 110px; 40 | } 41 | 42 | .container { 43 | margin: 50px auto auto; 44 | width: 650px; 45 | font-family: "Lora", helvetica, arial, sans-serif; 46 | } 47 | 48 | .landing { 49 | margin: 150px 0 70px; 50 | font-family: "Lora", helvetica, arial, sans-serif; 51 | } 52 | 53 | .landing h1, .landing h2 { 54 | font-weight: 300; 55 | text-align: center; 56 | } 57 | 58 | .landing h1 { 59 | font-size: 4em; 60 | font-weight: bold; 61 | color: black; 62 | font-style: italic; 63 | } 64 | 65 | .landing h2 { 66 | margin-top: -40px; 67 | color: #bababa; 68 | font-style: italic; 69 | font-size: 1.6em; 70 | } 71 | 72 | .nav { 73 | border-top: 1px solid black; 74 | border-bottom: 1px solid black; 75 | text-align: center; 76 | margin-top: 80px; 77 | padding-bottom: 5px; 78 | } 79 | 80 | .nav li { 81 | margin-top: 5px; 82 | display: inline-block; 83 | padding: 5px; 84 | } 85 | 86 | .nav li a { 87 | display: inline-block; 88 | padding: 5px; 89 | color: black; 90 | font-size: 1.2em; 91 | text-decoration: none; 92 | margin-left: 10px; 93 | margin-right: 10px; 94 | } 95 | 96 | .code { 97 | font-family: Monaco; 98 | display: block; 99 | padding: 30px 25px; 100 | background: #f6f6f6; 101 | color: #434746; 102 | line-height: 1.6em; 103 | font-size: 0.9em; 104 | } 105 | 106 | .current a { 107 | border-bottom: 2px solid #e1e1e1; 108 | line-height: 0.3em; 109 | } 110 | 111 | .footer { 112 | padding: 0 50px 120px; 113 | margin-top: -30px; 114 | font-size: 1.3em; 115 | line-height: 1.5em; 116 | overflow: hidden; 117 | color: #424242; 118 | } 119 | 120 | .footer a { 121 | color: #424242; 122 | text-decoration: none; 123 | } 124 | 125 | .footerLeft { 126 | float: left; 127 | } 128 | 129 | .footerRight { 130 | float: right; 131 | } 132 | 133 | .mainTitle { 134 | text-align: center; 135 | font-size: 1.4em; 136 | } 137 | 138 | .usage { 139 | margin-top: -20px; 140 | margin-bottom: 50px; 141 | line-height: 1.6em; 142 | } 143 | -------------------------------------------------------------------------------- /examples/components/Malevich.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Designer from '../../src/Designer'; 3 | 4 | export default class extends Component { 5 | state = { 6 | objects: [{ 7 | "width": 239, 8 | "rotate": 0, 9 | "fill": "rgba(236, 181, 114, 1)", 10 | "strokeWidth": 0, 11 | "blendMode": "normal", 12 | "type": "circle", 13 | "x": 116, 14 | "y": 34.5625, 15 | "height": null 16 | }, { 17 | "fill": "rgba(220, 134, 0, 1)", 18 | "closed": true, 19 | "rotate": 0, 20 | "moveX": 47, 21 | "moveY": 65.5625, 22 | "path": [{"x1": 47, "y1": 65.5625, "x2": 57, "y2": 61.5625, "x": 57, "y": 61.5625}, { 23 | "x1": 57, 24 | "y1": 61.5625, 25 | "x2": 182, 26 | "y2": 356.5625, 27 | "x": 182, 28 | "y": 356.5625 29 | }, {"x1": 182, "y1": 356.5625, "x2": 171, "y2": 362.5625, "x": 171, "y": 362.5625}, { 30 | "x1": 171, 31 | "y1": 362.5625, 32 | "x2": 47, 33 | "y2": 65.5625, 34 | "x": 47, 35 | "y": 65.5625 36 | }], 37 | "stroke": "gray", 38 | "strokeWidth": "0", 39 | "type": "polygon", 40 | "x": 47, 41 | "y": 65.5625 42 | }, { 43 | "fill": "rgba(160, 41, 1, 1)", 44 | "closed": true, 45 | "rotate": 0, 46 | "moveX": 22, 47 | "moveY": 259.5625, 48 | "path": [{"x1": 22, "y1": 259.5625, "x2": 53, "y2": 244.5625, "x": 53, "y": 244.5625}, { 49 | "x1": 53, 50 | "y1": 244.5625, 51 | "x2": 71, 52 | "y2": 278.5625, 53 | "x": 71, 54 | "y": 278.5625 55 | }, {"x1": 71, "y1": 278.5625, "x2": 43, "y2": 297.5625, "x": 43, "y": 297.5625}, { 56 | "x1": 43, 57 | "y1": 297.5625, 58 | "x2": 22, 59 | "y2": 259.5625, 60 | "x": 22, 61 | "y": 259.5625 62 | }], 63 | "stroke": "gray", 64 | "strokeWidth": "0", 65 | "type": "polygon", 66 | "x": 22, 67 | "y": 259.5625 68 | }, { 69 | "fill": "rgba(135, 103, 82, 1)", 70 | "closed": true, 71 | "rotate": 0, 72 | "moveX": 47, 73 | "moveY": 212.5625, 74 | "path": [{"x1": 47, "y1": 212.5625, "x2": 69, "y2": 200.5625, "x": 69, "y": 200.5625}, { 75 | "x1": 69, 76 | "y1": 200.5625, 77 | "x2": 99, 78 | "y2": 274.5625, 79 | "x": 99, 80 | "y": 274.5625 81 | }, {"x1": 99, "y1": 274.5625, "x2": 76, "y2": 283.5625, "x": 76, "y": 283.5625}, { 82 | "x1": 76, 83 | "y1": 283.5625, 84 | "x2": 47, 85 | "y2": 212.5625, 86 | "x": 47, 87 | "y": 212.5625 88 | }], 89 | "stroke": "gray", 90 | "strokeWidth": "0", 91 | "type": "polygon", 92 | "x": 48, 93 | "y": 212.5625 94 | }, { 95 | "fill": "rgba(220, 134, 0, 1)", 96 | "closed": true, 97 | "rotate": 0, 98 | "moveX": 71, 99 | "moveY": 183.5625, 100 | "path": [{"x1": 71, "y1": 183.5625, "x2": 80, "y2": 178.5625, "x": 80, "y": 178.5625}, { 101 | "x1": 80, 102 | "y1": 178.5625, 103 | "x2": 114, 104 | "y2": 275.5625, 105 | "x": 114, 106 | "y": 275.5625 107 | }, {"x1": 114, "y1": 275.5625, "x2": 106, "y2": 278.5625, "x": 106, "y": 278.5625}, { 108 | "x1": 106, 109 | "y1": 278.5625, 110 | "x2": 71, 111 | "y2": 183.5625, 112 | "x": 71, 113 | "y": 183.5625 114 | }], 115 | "stroke": "gray", 116 | "strokeWidth": "0", 117 | "type": "polygon", 118 | "x": 71, 119 | "y": 183.5625 120 | }, { 121 | "fill": "rgba(111, 134, 124, 1)", 122 | "closed": true, 123 | "rotate": 0, 124 | "moveX": 77, 125 | "moveY": 314.5625, 126 | "path": [{"x1": 77, "y1": 314.5625, "x2": 181, "y2": 256.5625, "x": 181, "y": 256.5625}, { 127 | "x1": 181, 128 | "y1": 256.5625, 129 | "x2": 208, 130 | "y2": 307.5625, 131 | "x": 208, 132 | "y": 307.5625 133 | }, {"x1": 208, "y1": 307.5625, "x2": 106, "y2": 365.5625, "x": 106, "y": 365.5625}, { 134 | "x1": 106, 135 | "y1": 365.5625, 136 | "x2": 77, 137 | "y2": 314.5625, 138 | "x": 77, 139 | "y": 314.5625 140 | }], 141 | "stroke": "gray", 142 | "strokeWidth": "0", 143 | "type": "polygon", 144 | "x": 77, 145 | "y": 314.5625 146 | }, { 147 | "fill": "rgba(111, 134, 124, 1)", 148 | "closed": true, 149 | "rotate": 0, 150 | "moveX": 178, 151 | "moveY": 385.5625, 152 | "path": [{"x1": 178, "y1": 385.5625, "x2": 197, "y2": 377.5625, "x": 197, "y": 377.5625}, { 153 | "x1": 197, 154 | "y1": 377.5625, 155 | "x2": 202, 156 | "y2": 387.5625, 157 | "x": 202, 158 | "y": 387.5625 159 | }, {"x1": 202, "y1": 387.5625, "x2": 183, "y2": 397.5625, "x": 183, "y": 397.5625}, { 160 | "x1": 183, 161 | "y1": 397.5625, 162 | "x2": 178, 163 | "y2": 385.5625, 164 | "x": 178, 165 | "y": 385.5625 166 | }], 167 | "stroke": "gray", 168 | "strokeWidth": "0", 169 | "type": "polygon", 170 | "x": 176, 171 | "y": 381.5625 172 | }, { 173 | "fill": "rgba(163, 172, 127, 1)", 174 | "closed": true, 175 | "rotate": 0, 176 | "moveX": 96, 177 | "moveY": 6.5625, 178 | "path": [{"x1": 96, "y1": 6.5625, "x2": 96, "y2": 70.5625, "x": 96, "y": 70.5625}, { 179 | "x1": 96, 180 | "y1": 70.5625, 181 | "x2": 104, 182 | "y2": 70.5625, 183 | "x": 104, 184 | "y": 70.5625 185 | }, {"x1": 104, "y1": 70.5625, "x2": 106, "y2": 8.5625, "x": 106, "y": 8.5625}, { 186 | "x1": 106, 187 | "y1": 8.5625, 188 | "x2": 96, 189 | "y2": 6.5625, 190 | "x": 96, 191 | "y": 6.5625 192 | }], 193 | "stroke": "gray", 194 | "strokeWidth": "0", 195 | "type": "polygon", 196 | "x": 96, 197 | "y": 6.5625 198 | }, { 199 | "fill": "rgba(163, 172, 127, 1)", 200 | "closed": true, 201 | "rotate": 0, 202 | "moveX": 70, 203 | "moveY": 39.5625, 204 | "path": [{"x1": 70, "y1": 39.5625, "x2": 124, "y2": 32.5625, "x": 124, "y": 32.5625}, { 205 | "x1": 124, 206 | "y1": 32.5625, 207 | "x2": 124, 208 | "y2": 43.5625, 209 | "x": 124, 210 | "y": 43.5625 211 | }, {"x1": 124, "y1": 43.5625, "x2": 72, "y2": 49.5625, "x": 72, "y": 49.5625}, { 212 | "x1": 72, 213 | "y1": 49.5625, 214 | "x2": 70, 215 | "y2": 39.5625, 216 | "x": 70, 217 | "y": 39.5625 218 | }], 219 | "stroke": "gray", 220 | "strokeWidth": "0", 221 | "type": "polygon", 222 | "x": 70, 223 | "y": 39.5625 224 | }, { 225 | "fill": "rgba(69, 69, 41, 1)", 226 | "closed": true, 227 | "rotate": 0, 228 | "moveX": 87, 229 | "moveY": 53.5625, 230 | "path": [{"x1": 87, "y1": 53.5625, "x2": 116, "y2": 49.5625, "x": 116, "y": 49.5625}, { 231 | "x1": 116, 232 | "y1": 49.5625, 233 | "x2": 118, 234 | "y2": 59.5625, 235 | "x": 118, 236 | "y": 59.5625 237 | }, {"x1": 118, "y1": 59.5625, "x2": 89, "y2": 63.5625, "x": 89, "y": 63.5625}, { 238 | "x1": 89, 239 | "y1": 63.5625, 240 | "x2": 87, 241 | "y2": 53.5625, 242 | "x": 87, 243 | "y": 53.5625 244 | }], 245 | "stroke": "gray", 246 | "strokeWidth": "0", 247 | "type": "polygon", 248 | "x": 86, 249 | "y": 51.5625 250 | }, { 251 | "fill": "rgba(206, 67, 0, 1)", 252 | "closed": true, 253 | "rotate": 0, 254 | "moveX": 123, 255 | "moveY": 168.5625, 256 | "path": [{"x1": 123, "y1": 168.5625, "x2": 349, "y2": 36.5625, "x": 349, "y": 36.5625}, { 257 | "x1": 349, 258 | "y1": 36.5625, 259 | "x2": 349, 260 | "y2": 50.5625, 261 | "x": 349, 262 | "y": 50.5625 263 | }, {"x1": 349, "y1": 50.5625, "x2": 136, "y2": 193.5625, "x": 136, "y": 193.5625}, { 264 | "x1": 136, 265 | "y1": 193.5625, 266 | "x2": 123, 267 | "y2": 168.5625, 268 | "x": 123, 269 | "y": 168.5625 270 | }], 271 | "stroke": "gray", 272 | "strokeWidth": "0", 273 | "type": "polygon", 274 | "x": 123, 275 | "y": 168.5625 276 | }, { 277 | "fill": "rgba(0, 0, 0, 1)", 278 | "closed": true, 279 | "rotate": 0, 280 | "moveX": 135, 281 | "moveY": 120.5625, 282 | "path": [{"x1": 135, "y1": 120.5625, "x2": 154, "y2": 112.5625, "x": 154, "y": 112.5625}, { 283 | "x1": 154, 284 | "y1": 112.5625, 285 | "x2": 203, 286 | "y2": 216.5625, 287 | "x": 203, 288 | "y": 216.5625 289 | }, {"x1": 203, "y1": 216.5625, "x2": 184, "y2": 226.5625, "x": 184, "y": 226.5625}, { 290 | "x1": 184, 291 | "y1": 226.5625, 292 | "x2": 135, 293 | "y2": 120.5625, 294 | "x": 135, 295 | "y": 120.5625 296 | }], 297 | "stroke": "gray", 298 | "strokeWidth": "0", 299 | "type": "polygon", 300 | "x": 135, 301 | "y": 120.5625 302 | }, { 303 | "fill": "rgba(133, 34, 8, 1)", 304 | "closed": true, 305 | "rotate": 0, 306 | "moveX": 337, 307 | "moveY": 78.5625, 308 | "path": [{"x1": 337, "y1": 78.5625, "x2": 346, "y2": 92.5625, "x": 346, "y": 92.5625}, { 309 | "x1": 346, 310 | "y1": 92.5625, 311 | "x2": 252, 312 | "y2": 183.5625, 313 | "x": 252, 314 | "y": 183.5625 315 | }, {"x1": 252, "y1": 183.5625, "x2": 236, "y2": 161.5625, "x": 236, "y": 161.5625}, { 316 | "x1": 236, 317 | "y1": 161.5625, 318 | "x2": 337, 319 | "y2": 78.5625, 320 | "x": 337, 321 | "y": 78.5625 322 | }], 323 | "stroke": "gray", 324 | "strokeWidth": "0", 325 | "type": "polygon", 326 | "x": 337, 327 | "y": 78.5625 328 | }, { 329 | "fill": "rgba(0, 0, 0, 1)", 330 | "closed": true, 331 | "rotate": 0, 332 | "moveX": 138, 333 | "moveY": 51.5625, 334 | "path": [{"x1": 138, "y1": 51.5625, "x2": 152, "y2": 45.5625, "x": 152, "y": 45.5625}, { 335 | "x1": 152, 336 | "y1": 45.5625, 337 | "x2": 247, 338 | "y2": 246.5625, 339 | "x": 247, 340 | "y": 246.5625 341 | }, {"x1": 247, "y1": 246.5625, "x2": 226, "y2": 257.5625, "x": 226, "y": 257.5625}, { 342 | "x1": 226, 343 | "y1": 257.5625, 344 | "x2": 138, 345 | "y2": 51.5625, 346 | "x": 138, 347 | "y": 51.5625 348 | }], 349 | "stroke": "gray", 350 | "strokeWidth": "0", 351 | "type": "polygon", 352 | "x": 138, 353 | "y": 51.5625 354 | }, { 355 | "fill": "rgba(0, 0, 0, 1)", 356 | "closed": true, 357 | "rotate": 0, 358 | "moveX": 8, 359 | "moveY": 292.5625, 360 | "path": [{"x1": 8, "y1": 292.5625, "x2": 15, "y2": 303.5625, "x": 15, "y": 303.5625}, { 361 | "x1": 15, 362 | "y1": 303.5625, 363 | "x2": 255, 364 | "y2": 135.5625, 365 | "x": 255, 366 | "y": 135.5625 367 | }, {"x1": 255, "y1": 135.5625, "x2": 252, "y2": 130.5625, "x": 252, "y": 130.5625}, { 368 | "x1": 252, 369 | "y1": 130.5625, 370 | "x2": 8, 371 | "y2": 292.5625, 372 | "x": 8, 373 | "y": 292.5625 374 | }], 375 | "stroke": "gray", 376 | "strokeWidth": "0", 377 | "type": "polygon", 378 | "x": 8, 379 | "y": 292.5625 380 | }, { 381 | "fill": "rgba(0, 0, 0, 1)", 382 | "closed": true, 383 | "rotate": 0, 384 | "moveX": 314, 385 | "moveY": 6.5625, 386 | "path": [{"x1": 266, "y1": 49.5625, "x2": 224, "y2": 49.5625, "x": 204, "y": 69.5625}, { 387 | "x1": 169, 388 | "y1": 99.5625, 389 | "x2": 189, 390 | "y2": 152.5625, 391 | "x": 243, 392 | "y": 125.5625 393 | }, {"x1": 275, "y1": 109.5625, "x2": 288, "y2": 72.5625, "x": 307, "y": 26.5625}, { 394 | "x1": 321, 395 | "y1": -5.4375, 396 | "x2": 314, 397 | "y2": 6.5625, 398 | "x": 314, 399 | "y": 6.5625 400 | }], 401 | "stroke": "gray", 402 | "strokeWidth": "0", 403 | "type": "polygon", 404 | "x": 316, 405 | "y": -7.4375 406 | }] 407 | }; 408 | 409 | handleUpdate(objects) { 410 | this.setState({objects}); 411 | } 412 | 413 | render() { 414 | return ( 415 | 420 | ); 421 | } 422 | } -------------------------------------------------------------------------------- /examples/components/MockupDesigner.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Designer from '../../src/Designer'; 3 | import {ArrangePanel, SizePanel} from '../../src/panels' 4 | import {Rect, Vector, Text} from '../../src/objects/index'; 5 | 6 | 7 | export class H1 extends Text { 8 | static meta = { 9 | icon:
H1
, 10 | initial: { 11 | ...Text.meta.initial, 12 | text: "Hello", 13 | fill: "black", 14 | fontSize: 20 15 | } 16 | }; 17 | } 18 | 19 | export class _Link extends Text { 20 | static meta = { 21 | icon:
Anc
, 22 | initial: { 23 | ...Text.meta.initial, 24 | text: "Hello", 25 | textDecoration: "underline", 26 | fill: "blue", 27 | fontSize: 15 28 | } 29 | }; 30 | } 31 | 32 | export class _Button extends Vector { 33 | static meta = { 34 | icon:
Btn
, 35 | initial: { 36 | ...Text.meta.initial, 37 | width: 100, 38 | height: 30, 39 | fill: "#b9b9b9", 40 | stroke: "#646464", 41 | strokeWidth: 2, 42 | radius: 5, 43 | text: "Button" 44 | } 45 | }; 46 | 47 | render() { 48 | let {object} = this.props; 49 | let style = { 50 | dominantBaseline: 'central', 51 | textAnchor: 'middle', 52 | fontWeight: object.fontWeight, 53 | fontStyle: object.fontStyle, 54 | textDecoration: object.textDecoration 55 | }; 56 | return ( 57 | 58 | 63 | {object.text} 68 | 69 | ); 70 | } 71 | } 72 | 73 | export class _Input extends Vector { 74 | static meta = { 75 | icon:
Inp
, 76 | initial: { 77 | ...Text.meta.initial, 78 | width: 100, 79 | height: 40, 80 | fill: "#fff", 81 | stroke: "#646464", 82 | strokeWidth: 2, 83 | radius: 0, 84 | text: 'Label' 85 | } 86 | }; 87 | 88 | render() { 89 | let {object, index} = this.props; 90 | let style = { 91 | dominantBaseline: 'central', 92 | fontWeight: object.fontWeight, 93 | fontStyle: object.fontStyle, 94 | textDecoration: object.textDecoration 95 | }; 96 | return ( 97 | 98 | {object.text} 101 | 107 | 108 | ); 109 | } 110 | } 111 | 112 | 113 | export class Fieldset extends Vector { 114 | static meta = { 115 | icon:
Fst
, 116 | initial: { 117 | ...Text.meta.initial, 118 | width: 250, 119 | height: 100, 120 | strokeWidth: 2, 121 | fill: "#e3e3e3", 122 | stroke: "gray", 123 | radius: 0 124 | } 125 | }; 126 | 127 | render() { 128 | let {object} = this.props; 129 | 130 | let textStyle = { 131 | dominantBaseline: 'central', 132 | textAnchor: 'left', 133 | fontWeight: object.fontWeight, 134 | fontStyle: object.fontStyle, 135 | textDecoration: object.textDecoration 136 | }; 137 | 138 | return ( 139 | 140 | 145 | 146 | 152 | 153 | {object.text} 158 | 159 | ); 160 | } 161 | } 162 | 163 | 164 | export default class extends Component { 165 | state = { 166 | objects: [{ 167 | "width": 270, 168 | "height": 305, 169 | "strokeWidth": 2, 170 | "fill": "#e3e3e3", 171 | "stroke": "gray", 172 | "radius": 0, 173 | "blendMode": "normal", 174 | "text": "Login", 175 | "type": "fieldset", 176 | "fontFamily": "Open Sans", 177 | "x": 40, 178 | "y": 55 179 | }, { 180 | "width": 215, 181 | "height": 40, 182 | "rotate": 0, 183 | "fill": "#fff", 184 | "stroke": "#646464", 185 | "strokeWidth": 2, 186 | "radius": 0, 187 | "text": "Username", 188 | "type": "input", 189 | "fontFamily": "Open Sans", 190 | "x": 65, 191 | "y": 105 192 | }, { 193 | "width": 215, 194 | "height": "40", 195 | "rotate": 0, 196 | "fill": "#fff", 197 | "stroke": "#646464", 198 | "strokeWidth": 2, 199 | "radius": 0, 200 | "text": "Password", 201 | "type": "input", 202 | "fontFamily": "Open Sans", 203 | "x": 65, 204 | "y": 175 205 | }, { 206 | "width": 90, 207 | "height": 30, 208 | "rotate": 0, 209 | "fill": "#b9b9b9", 210 | "stroke": "#646464", 211 | "strokeWidth": 2, 212 | "radius": 5, 213 | "text": "Login", 214 | "fontWeight": "normal", 215 | "type": "button", 216 | "fontFamily": "Open Sans", 217 | "x": 65, 218 | "y": 240 219 | }, { 220 | "text": "Forgot your password?", 221 | "rotate": 0, 222 | "fontWeight": "normal", 223 | "fontStyle": "normal", 224 | "textDecoration": "underline", 225 | "fill": "blue", 226 | "fontSize": 15, 227 | "fontFamily": "Open Sans", 228 | "type": "link", 229 | "x": 145, 230 | "y": 300 231 | }, { 232 | "text": "Register", 233 | "rotate": 0, 234 | "fontWeight": "normal", 235 | "fontStyle": "normal", 236 | "textDecoration": "underline", 237 | "fill": "blue", 238 | "fontSize": 15, 239 | "fontFamily": "Open Sans", 240 | "type": "link", 241 | "x": 95, 242 | "y": 330 243 | }] 244 | }; 245 | 246 | download(event) { 247 | event.preventDefault(); 248 | let svgElement = this.designer.svgElement; 249 | 250 | svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); 251 | 252 | let source = svgElement.outerHTML; 253 | let uri = 'data:image/svg+xml;base64,' + btoa(source); 254 | 255 | window.open(uri) 256 | } 257 | 258 | handleUpdate(objects) { 259 | this.setState({objects}); 260 | } 261 | 262 | render() { 263 | return ( 264 |
265 | this.designer = ref} 267 | width={350} 268 | height={400} 269 | snapToGrid={5} 270 | objectTypes={{ 271 | 'button': _Button, 272 | 'input': _Input, 273 | 'h1': H1, 274 | 'link': _Link, 275 | 'fieldset': Fieldset 276 | }} 277 | objects={this.state.objects} 278 | onUpdate={this.handleUpdate.bind(this)}/> 279 |

280 | Export SVG 281 |

282 |
283 | ); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /examples/components/Mondrian.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Designer from '../../src/Designer'; 3 | 4 | export default class extends Component { 5 | state = { 6 | objects: [{ 7 | "width": 163, 8 | "height": 84, 9 | "rotate": 0, 10 | "strokeWidth": 0, 11 | "fill": "rgba(0, 123, 255, 1)", 12 | "radius": "0", 13 | "blendMode": "normal", 14 | "type": "rectangle", 15 | "x": 17, 16 | "y": 15 17 | }, { 18 | "width": 70, 19 | "height": 146, 20 | "rotate": 0, 21 | "strokeWidth": 0, 22 | "fill": "rgba(255, 255, 255, 1)", 23 | "radius": "0", 24 | "blendMode": "normal", 25 | "type": "rectangle", 26 | "x": 19, 27 | "y": 109 28 | }, { 29 | "width": 81, 30 | "height": 69, 31 | "rotate": 0, 32 | "strokeWidth": 0, 33 | "fill": "rgba(241, 97, 99, 1)", 34 | "radius": "0", 35 | "blendMode": "normal", 36 | "type": "rectangle", 37 | "x": 100, 38 | "y": 110 39 | }, { 40 | "width": 231, 41 | "height": 70, 42 | "rotate": 0, 43 | "strokeWidth": 0, 44 | "fill": "rgba(0, 123, 255, 1)", 45 | "radius": "0", 46 | "blendMode": "normal", 47 | "type": "rectangle", 48 | "x": 100, 49 | "y": 187 50 | }, { 51 | "width": 183, 52 | "height": 60, 53 | "rotate": 0, 54 | "strokeWidth": 0, 55 | "fill": "rgba(255, 241, 0, 1)", 56 | "radius": "0", 57 | "blendMode": "normal", 58 | "type": "rectangle", 59 | "x": 19, 60 | "y": 265 61 | }, { 62 | "width": 118, 63 | "height": 119, 64 | "rotate": 0, 65 | "strokeWidth": 0, 66 | "fill": "rgba(241, 97, 99, 1)", 67 | "radius": "0", 68 | "blendMode": "normal", 69 | "type": "rectangle", 70 | "x": 211, 71 | "y": 266 72 | }, { 73 | "width": 82, 74 | "height": 51, 75 | "rotate": 0, 76 | "strokeWidth": 0, 77 | "fill": "rgba(255, 255, 255, 1)", 78 | "radius": "0", 79 | "blendMode": "normal", 80 | "type": "rectangle", 81 | "x": 120, 82 | "y": 333 83 | }, { 84 | "width": 89, 85 | "height": 50, 86 | "rotate": 0, 87 | "strokeWidth": 0, 88 | "fill": "rgba(241, 97, 99, 1)", 89 | "radius": "0", 90 | "blendMode": "normal", 91 | "type": "rectangle", 92 | "x": 21, 93 | "y": 334 94 | }, { 95 | "width": 143, 96 | "height": 160, 97 | "rotate": 0, 98 | "strokeWidth": 0, 99 | "fill": "rgba(255, 241, 0, 1)", 100 | "radius": "0", 101 | "blendMode": "normal", 102 | "type": "rectangle", 103 | "x": 190, 104 | "y": 16 105 | }] 106 | }; 107 | 108 | handleUpdate(objects) { 109 | this.setState({objects}); 110 | } 111 | 112 | render() { 113 | return ( 114 | 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /examples/components/SwissStyle.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Designer from '../../src/Designer'; 3 | 4 | export default class extends Component { 5 | state = { 6 | objects: [{ 7 | "fill": "rgba(255, 57, 57, 1)", 8 | "closed": true, 9 | "rotate": 0, 10 | "moveX": 20, 11 | "moveY": 126, 12 | "path": [{"x1": 20, "y1": 126, "x2": 145, "y2": 123.84375, "x": 145, "y": 123.84375}, { 13 | "x1": 145, 14 | "y1": 123.84375, 15 | "x2": 144, 16 | "y2": 180.84375, 17 | "x": 144, 18 | "y": 180.84375 19 | }, {"x1": 144, "y1": 180.84375, "x2": 249, "y2": 179.84375, "x": 249, "y": 179.84375}, { 20 | "x1": 249, 21 | "y1": 179.84375, 22 | "x2": 249, 23 | "y2": 269.84375, 24 | "x": 249, 25 | "y": 269.84375 26 | }, {"x1": 249, "y1": 269.84375, "x2": 161, "y2": 272.84375, "x": 161, "y": 272.84375}, { 27 | "x1": 161, 28 | "y1": 272.84375, 29 | "x2": 161, 30 | "y2": 305.84375, 31 | "x": 161, 32 | "y": 305.84375 33 | }, {"x1": 161, "y1": 305.84375, "x2": 248, "y2": 305.84375, "x": 248, "y": 305.84375}, { 34 | "x1": 248, 35 | "y1": 305.84375, 36 | "x2": 248, 37 | "y2": 382.84375, 38 | "x": 248, 39 | "y": 382.84375 40 | }, {"x1": 248, "y1": 382.84375, "x2": 19, "y2": 382.84375, "x": 19, "y": 382.84375}, { 41 | "x1": 19, 42 | "y1": 382.84375, 43 | "x2": 20, 44 | "y2": 126, 45 | "x": 20, 46 | "y": 126 47 | }], 48 | "stroke": "gray", 49 | "strokeWidth": "0", 50 | "type": "polygon", 51 | "x": 20, 52 | "y": 126, 53 | "blendMode": "multiply" 54 | }, { 55 | "fill": "rgba(74, 255, 231, 1)", 56 | "closed": true, 57 | "rotate": 0, 58 | "moveX": 21, 59 | "moveY": 126, 60 | "path": [{"x1": 21, "y1": 126, "x2": 144, "y2": 125.84375, "x": 144, "y": 125.84375}, { 61 | "x1": 144, 62 | "y1": 125.84375, 63 | "x2": 144, 64 | "y2": 176.84375, 65 | "x": 144, 66 | "y": 176.84375 67 | }, {"x1": 144, "y1": 176.84375, "x2": 252, "y2": 177.84375, "x": 252, "y": 177.84375}, { 68 | "x1": 252, 69 | "y1": 177.84375, 70 | "x2": 250, 71 | "y2": 271.84375, 72 | "x": 250, 73 | "y": 271.84375 74 | }, {"x1": 250, "y1": 271.84375, "x2": 163, "y2": 275.84375, "x": 163, "y": 275.84375}, { 75 | "x1": 163, 76 | "y1": 275.84375, 77 | "x2": 162, 78 | "y2": 303.84375, 79 | "x": 162, 80 | "y": 303.84375 81 | }, {"x1": 162, "y1": 303.84375, "x2": 246, "y2": 303.84375, "x": 246, "y": 303.84375}, { 82 | "x1": 246, 83 | "y1": 303.84375, 84 | "x2": 246, 85 | "y2": 384.84375, 86 | "x": 246, 87 | "y": 384.84375 88 | }, {"x1": 246, "y1": 384.84375, "x2": 19, "y2": 382.84375, "x": 19, "y": 382.84375}, { 89 | "x1": 19, 90 | "y1": 382.84375, 91 | "x2": 21, 92 | "y2": 126, 93 | "x": 21, 94 | "y": 126 95 | }], 96 | "stroke": "gray", 97 | "strokeWidth": "0", 98 | "type": "polygon", 99 | "x": 33, 100 | "y": 117, 101 | "blendMode": "darken" 102 | }, { 103 | "text": "love", 104 | "rotate": 0, 105 | "fontWeight": "normal", 106 | "fontStyle": "normal", 107 | "textDecoration": "none", 108 | "fill": "rgba(249, 249, 138, 1)", 109 | "fontSize": "90", 110 | "fontFamily": "Helvetica", 111 | "type": "text", 112 | "x": 117, 113 | "y": 220, 114 | "blendMode": "normal" 115 | }, { 116 | "text": "not", 117 | "rotate": 0, 118 | "fontWeight": "normal", 119 | "fontStyle": "normal", 120 | "textDecoration": "none", 121 | "fill": "rgba(247, 247, 109, 1)", 122 | "fontSize": "50", 123 | "fontFamily": "Helvetica", 124 | "type": "text", 125 | "x": 79, 126 | "y": 281, 127 | "blendMode": "normal" 128 | }, { 129 | "text": "war", 130 | "rotate": 0, 131 | "fontWeight": "normal", 132 | "fontStyle": "normal", 133 | "textDecoration": "none", 134 | "fill": "rgba(255, 255, 121, 1)", 135 | "fontSize": "90", 136 | "fontFamily": "Helvetica", 137 | "type": "text", 138 | "x": 117, 139 | "y": 334, 140 | "blendMode": "normal" 141 | }, { 142 | "text": "make", 143 | "rotate": 0, 144 | "fontWeight": "normal", 145 | "fontStyle": "normal", 146 | "textDecoration": "none", 147 | "fill": "rgba(255, 255, 117, 1)", 148 | "fontSize": "40", 149 | "fontFamily": "Helvetica", 150 | "type": "text", 151 | "x": 87, 152 | "y": 159, 153 | "blendMode": "normal" 154 | }, { 155 | "fill": "rgba(255, 236, 54, 1)", 156 | "closed": true, 157 | "rotate": 0, 158 | "moveX": 22, 159 | "moveY": 13, 160 | "path": [{"x1": 22, "y1": 13, "x2": 24, "y2": 99.84375, "x": 24, "y": 99.84375}, { 161 | "x1": 24, 162 | "y1": 99.84375, 163 | "x2": 161, 164 | "y2": 101.84375, 165 | "x": 161, 166 | "y": 101.84375 167 | }, {"x1": 161, "y1": 101.84375, "x2": 163, "y2": 155.84375, "x": 163, "y": 155.84375}, { 168 | "x1": 163, 169 | "y1": 155.84375, 170 | "x2": 268, 171 | "y2": 159.84375, 172 | "x": 268, 173 | "y": 159.84375 174 | }, {"x1": 268, "y1": 159.84375, "x2": 264, "y2": 268.84375, "x": 264, "y": 268.84375}, { 175 | "x1": 264, 176 | "y1": 268.84375, 177 | "x2": 181, 178 | "y2": 275.84375, 179 | "x": 181, 180 | "y": 275.84375 181 | }, {"x1": 181, "y1": 275.84375, "x2": 181, "y2": 288.84375, "x": 181, "y": 288.84375}, { 182 | "x1": 181, 183 | "y1": 288.84375, 184 | "x2": 264, 185 | "y2": 286.84375, 186 | "x": 264, 187 | "y": 286.84375 188 | }, {"x1": 264, "y1": 286.84375, "x2": 265, "y2": 377.84375, "x": 265, "y": 377.84375}, { 189 | "x1": 265, 190 | "y1": 377.84375, 191 | "x2": 340, 192 | "y2": 377.84375, 193 | "x": 340, 194 | "y": 377.84375 195 | }, {"x1": 340, "y1": 377.84375, "x2": 338, "y2": 14.84375, "x": 338, "y": 14.84375}, { 196 | "x1": 338, 197 | "y1": 14.84375, 198 | "x2": 22, 199 | "y2": 13, 200 | "x": 22, 201 | "y": 13 202 | }], 203 | "stroke": "gray", 204 | "strokeWidth": "0", 205 | "type": "polygon", 206 | "x": 11, 207 | "y": 15, 208 | "blendMode": "multiply" 209 | }, { 210 | "text": "love", 211 | "rotate": 0, 212 | "fontWeight": "bold", 213 | "fontStyle": "normal", 214 | "textDecoration": "none", 215 | "fill": "rgba(26, 236, 255, 1)", 216 | "fontSize": "90", 217 | "fontFamily": "Helvetica", 218 | "type": "text", 219 | "x": 115, 220 | "y": 221, 221 | "blendMode": "multiply" 222 | }, { 223 | "text": "war", 224 | "rotate": 0, 225 | "fontWeight": "normal", 226 | "fontStyle": "normal", 227 | "textDecoration": "none", 228 | "fill": "red", 229 | "fontSize": "90", 230 | "fontFamily": "Helvetica", 231 | "type": "text", 232 | "x": 125, 233 | "y": 333, 234 | "blendMode": "difference" 235 | }] 236 | }; 237 | 238 | handleUpdate(objects) { 239 | this.setState({objects}); 240 | } 241 | 242 | render() { 243 | return ( 244 | 248 | ); 249 | } 250 | } -------------------------------------------------------------------------------- /examples/components/TshirtDesigner.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Designer, {Text, Rect, Circle} from '../../src'; 3 | import {styles as canvasStyles} from '../../src/SVGRenderer'; 4 | 5 | const priceMap = { 6 | 'text': ({text, fontSize}) => text.length * fontSize * 0.01, 7 | 'rectangle': ({width, height}) => width * height * 0.001, 8 | 'circle': ({width, height}) => width * (height || width) * 0.001 9 | }; 10 | 11 | const calculatePrice = (objects, 12 | initialCost = 5) => ( 13 | objects.map( 14 | ({type, ...rest}) => 15 | priceMap[type](rest) 16 | ).reduce( 17 | (a, b) => a + b, 18 | initialCost 19 | ) 20 | ); 21 | 22 | const Background = ({style}) => ( 23 | 27 | 43 | 44 | ); 45 | 46 | export default class extends Component { 47 | 48 | state = { 49 | objects: [ 50 | { 51 | "text": "COME TO THE", 52 | "rotate": 0, 53 | "fontWeight": "bold", 54 | "fontStyle": "normal", 55 | "textDecoration": "none", 56 | "fill": "rgba(11, 10, 10, 1)", 57 | "fontSize": "20", 58 | "fontFamily": "Inconsolata", 59 | "type": "text", 60 | "x": 105, 61 | "y": 152 62 | }, { 63 | "text": "FRONT", 64 | "rotate": 0, 65 | "fontWeight": "bold", 66 | "fontStyle": "normal", 67 | "textDecoration": "none", 68 | "fill": "rgba(0, 0, 0, 1)", 69 | "fontSize": "47", 70 | "fontFamily": "Alegreya", 71 | "type": "text", 72 | "x": 106, 73 | "y": 184 74 | }, { 75 | "text": "END", 76 | "rotate": 0, 77 | "fontWeight": "bold", 78 | "fontStyle": "normal", 79 | "textDecoration": "none", 80 | "fill": "rgba(0, 0, 0, 1)", 81 | "fontSize": "25", 82 | "fontFamily": "Inconsolata", 83 | "type": "text", 84 | "x": 211, 85 | "y": 219 86 | } 87 | ] 88 | }; 89 | 90 | handleUpdate(objects) { 91 | this.setState({objects}); 92 | } 93 | 94 | render() { 95 | return ( 96 |
97 | 102 | 112 |
118 | Tshirt Price: {calculatePrice(this.state.objects).toFixed(2)}$ 119 |
120 |
121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /examples/components/components/h1.hs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronaldtech051/React-design/e6be971814df33da6b4f0760489278909547202c/examples/components/components/h1.hs -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /glasses: -------------------------------------------------------------------------------- 1 | split into two pieces 2 | chamber chamber 3 | merge 4 | yoksa doverim 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React-designer 4 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-designer", 3 | "version": "1.1.1", 4 | "description": "Easy to configure, lightweight, editable vector graphics in your react components.", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "deploy": "NODE_ENV=production webpack -p --config webpack.production.js", 9 | "prepare": "babel -d lib/ src/", 10 | "build": "npm run prepare", 11 | "install": "npm run clean && npm run build", 12 | "prepublish": "npm run clean && npm run build", 13 | "clean": "rimraf lib" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/fatiherikli/react-designer" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "reactjs" 22 | ], 23 | "author": "Fatih Erikli (http://fatiherikli.com)", 24 | "license": "MIT", 25 | "homepage": "https://github.com/react-designer/react-designer", 26 | "devDependencies": { 27 | "@babel/core": "^7.4.4", 28 | "@babel/preset-env": "^7.4.4", 29 | "@babel/preset-react": "^7.0.0", 30 | "@babel/cli": "^7.4.4", 31 | "@babel/plugin-proposal-class-properties": "^7.4.4", 32 | "@babel/plugin-proposal-export-default-from": "^7.2.0", 33 | "babel-loader": "^8.0.5", 34 | "babel-plugin-css-modules-transform": "^1.6.2", 35 | "babel-plugin-webpack-loaders": "^0.9.0", 36 | "css-loader": "^2.1.1", 37 | "eslint-plugin-react": "^7.13.0", 38 | "file-loader": "^3.0.1", 39 | "react": "^16.8.6", 40 | "react-autocomplete": "1.8.1", 41 | "react-dom": "^16.8.6", 42 | "react-dropzone": "^10.1.4", 43 | "react-hot-loader": "^4.8.4", 44 | "rimraf": "^2.6.3", 45 | "style-loader": "^0.23.1", 46 | "svg-inline-loader": "^0.8.0", 47 | "url-loader": "^1.1.2", 48 | "webpack": "^4.30.0", 49 | "webpack-dev-server": "^3.3.1" 50 | }, 51 | "peerDependencies": {}, 52 | "dependencies": { 53 | "classnames": "^2.2.6", 54 | "lodash": ">=4.17.11", 55 | "react-color": "^2.17.3", 56 | "react-hotkeys": "^1.1.4", 57 | "react-portal": "^4.2.0", 58 | "superagent": "*", 59 | "webfontloader": "^1.6.28" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.config'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true 8 | }).listen(3000, '127.0.0.1', function (err) { 9 | if (err) { 10 | console.log(err); 11 | } 12 | console.log('Listening at localhost:3000'); 13 | }); 14 | -------------------------------------------------------------------------------- /src/Designer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import _ from 'lodash'; 4 | import {HotKeys} from 'react-hotkeys'; 5 | import Icon from './Icon'; 6 | 7 | import InsertMenu from './panels/InsertMenu'; 8 | import SVGRenderer from './SVGRenderer'; 9 | import Handler from './Handler'; 10 | import {modes} from './constants'; 11 | import * as actions from './actions'; 12 | import {Text, Path, Rect, Circle, Image} from './objects'; 13 | import PanelList from './panels/PanelList'; 14 | 15 | class Designer extends Component { 16 | static defaultProps = { 17 | objectTypes: { 18 | 'text': Text, 19 | 'rectangle': Rect, 20 | 'circle': Circle, 21 | 'polygon': Path, 22 | 'image': Image 23 | }, 24 | snapToGrid: 1, 25 | svgStyle: {}, 26 | insertMenu: InsertMenu 27 | }; 28 | 29 | state = { 30 | mode: modes.FREE, 31 | handler: { 32 | top: 200, 33 | left: 200, 34 | width: 50, 35 | height: 50, 36 | rotate: 0 37 | }, 38 | currentObjectIndex: null, 39 | selectedObjectIndex: null, 40 | selectedTool: null 41 | }; 42 | 43 | keyMap = { 44 | 'removeObject': ['del', 'backspace'], 45 | 'moveLeft': ['left', 'shift+left'], 46 | 'moveRight': ['right', 'shift+right'], 47 | 'moveUp': ['up', 'shift+up'], 48 | 'moveDown': ['down', 'shift+down'], 49 | 'closePath': ['enter'] 50 | }; 51 | 52 | componentWillMount() { 53 | this.objectRefs = {}; 54 | } 55 | 56 | showHandler(index) { 57 | let {mode} = this.state; 58 | let {objects} = this.props; 59 | let object = objects[index]; 60 | 61 | if (mode !== modes.FREE) { 62 | return; 63 | } 64 | 65 | this.updateHandler(index, object); 66 | this.setState({ 67 | currentObjectIndex: index, 68 | showHandler: true 69 | }); 70 | } 71 | 72 | hideHandler() { 73 | let {mode} = this.state; 74 | if (mode === modes.FREE) { 75 | this.setState({ 76 | showHandler: false 77 | }); 78 | } 79 | } 80 | 81 | getStartPointBundle(event, object) { 82 | let {currentObjectIndex} = this.state; 83 | let {objects} = this.props; 84 | let mouse = this.getMouseCoords(event); 85 | object = object || objects[currentObjectIndex]; 86 | return { 87 | clientX: mouse.x, 88 | clientY: mouse.y, 89 | objectX: object.x, 90 | objectY: object.y, 91 | width: object.width, 92 | height: object.height, 93 | rotate: object.rotate 94 | }; 95 | } 96 | 97 | startDrag(mode, event) { 98 | let {currentObjectIndex} = this.state; 99 | this.setState({ 100 | mode: mode, 101 | startPoint: this.getStartPointBundle(event), 102 | selectedObjectIndex: currentObjectIndex 103 | }); 104 | } 105 | 106 | resetSelection() { 107 | this.setState({ 108 | selectedObjectIndex: null 109 | }); 110 | } 111 | 112 | generateUUID() { 113 | var d = new Date().getTime(); 114 | if(window.performance && typeof window.performance.now === "function"){ 115 | d += performance.now(); //use high-precision timer if available 116 | } 117 | var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 118 | var r = (d + Math.random()*16)%16 | 0; 119 | d = Math.floor(d/16); 120 | return (c=='x' ? r : (r&0x3|0x8)).toString(16); 121 | }); 122 | return uuid; 123 | } 124 | 125 | newObject(event) { 126 | let {mode, selectedTool} = this.state; 127 | 128 | this.resetSelection(event); 129 | 130 | if (mode !== modes.DRAW) { 131 | return; 132 | } 133 | 134 | let {meta} = this.getObjectComponent(selectedTool); 135 | let mouse = this.getMouseCoords(event); 136 | 137 | let {objects, onUpdate} = this.props; 138 | let object = { 139 | ...meta.initial, 140 | type: selectedTool, 141 | x: mouse.x, 142 | y: mouse.y, 143 | uuid: this.generateUUID() 144 | }; 145 | 146 | onUpdate([...objects, object]); 147 | 148 | this.setState({ 149 | currentObjectIndex: objects.length, 150 | selectedObjectIndex: objects.length, 151 | startPoint: this.getStartPointBundle(event, object), 152 | mode: meta.editor ? modes.EDIT_OBJECT : modes.SCALE, 153 | selectedTool: null 154 | }); 155 | 156 | } 157 | 158 | updatePath(object) { 159 | let {path} = object; 160 | let diffX = object.x - object.moveX; 161 | let diffY = object.y - object.moveY; 162 | 163 | let newPath = path.map(({x1, y1, x2, y2, x, y}) => ({ 164 | x1: diffX + x1, 165 | y1: diffY + y1, 166 | x2: diffX + x2, 167 | y2: diffY + y2, 168 | x: diffX + x, 169 | y: diffY + y 170 | })); 171 | 172 | return { 173 | ...object, 174 | path: newPath, 175 | moveX: object.x, 176 | moveY: object.y 177 | }; 178 | } 179 | 180 | updateObject(objectIndex, changes, updatePath) { 181 | let {objects, onUpdate} = this.props; 182 | onUpdate(objects.map((object, index) => { 183 | if (index === objectIndex) { 184 | let newObject = { 185 | ...object, 186 | ...changes 187 | }; 188 | 189 | return updatePath 190 | ? this.updatePath(newObject) 191 | : newObject; 192 | } else { 193 | // console.log("ID=> ", object.uuid, "CHANGES :", JSON.stringify(changes)) 194 | return object; 195 | } 196 | })); 197 | } 198 | 199 | getOffset() { 200 | let parent = this.svgElement.getBoundingClientRect(); 201 | let {canvasWidth, canvasHeight} = this.getCanvas(); 202 | return { 203 | x: parent.left, 204 | y: parent.top, 205 | width: canvasWidth, 206 | height: canvasHeight 207 | }; 208 | } 209 | 210 | applyOffset(bundle) { 211 | let offset = this.getOffset(); 212 | return { 213 | ...bundle, 214 | x: bundle.x - offset.x, 215 | y: bundle.y - offset.y 216 | } 217 | } 218 | 219 | updateHandler(index, object) { 220 | let target = this.objectRefs[index]; 221 | let bbox = target.getBoundingClientRect(); 222 | let {canvasOffsetX, canvasOffsetY} = this.getCanvas(); 223 | 224 | let handler = { 225 | ...this.state.handler, 226 | width: object.width || bbox.width, 227 | height: object.height || bbox.height, 228 | top: object.y + canvasOffsetY, 229 | left: object.x + canvasOffsetX, 230 | rotate: object.rotate 231 | }; 232 | 233 | if (!object.width) { 234 | let offset = this.getOffset(); 235 | handler = { 236 | ...handler, 237 | left: bbox.left - offset.x, 238 | top: bbox.top - offset.y 239 | }; 240 | } 241 | 242 | this.setState({ 243 | handler: handler 244 | }); 245 | } 246 | 247 | snapCoordinates({x, y}) { 248 | let {snapToGrid} = this.props; 249 | return { 250 | x: x - (x % snapToGrid), 251 | y: y - (y % snapToGrid) 252 | }; 253 | } 254 | 255 | getMouseCoords({clientX, clientY}) { 256 | let coords = this.applyOffset({ 257 | x: clientX, 258 | y: clientY 259 | }); 260 | 261 | return this.snapCoordinates(coords); 262 | } 263 | 264 | onDrag(event) { 265 | let {currentObjectIndex, startPoint, mode} = this.state; 266 | let {objects} = this.props; 267 | let object = objects[currentObjectIndex]; 268 | let mouse = this.getMouseCoords(event); 269 | 270 | let {scale, rotate, drag} = actions; 271 | 272 | let map = { 273 | [modes.SCALE]: scale, 274 | [modes.ROTATE]: rotate, 275 | [modes.DRAG]: drag 276 | }; 277 | 278 | let action = map[mode]; 279 | 280 | if (action) { 281 | let newObject = action({ 282 | object, 283 | startPoint, 284 | mouse, 285 | objectIndex: currentObjectIndex, 286 | objectRefs: this.objectRefs 287 | }); 288 | 289 | this.updateObject(currentObjectIndex, newObject); 290 | this.updateHandler(currentObjectIndex, newObject); 291 | } 292 | 293 | if (currentObjectIndex !== null) { 294 | this.detectOverlappedObjects(event); 295 | } 296 | } 297 | 298 | detectOverlappedObjects(event) { 299 | let {currentObjectIndex} = this.state; 300 | let {objects} = this.props; 301 | let mouse = this.getMouseCoords(event); 302 | 303 | let refs = this.objectRefs, 304 | keys = Object.keys(refs), 305 | offset = this.getOffset(); 306 | 307 | let currentRect = (refs[currentObjectIndex] 308 | .getBoundingClientRect()); 309 | 310 | keys.filter( 311 | (object, index) => index !== currentObjectIndex 312 | ).forEach((key) => { 313 | let rect = refs[key].getBoundingClientRect(); 314 | let {left, top, width, height} = rect; 315 | 316 | left -= offset.x; 317 | top -= offset.y; 318 | 319 | let isOverlapped = ( 320 | mouse.x > left && mouse.x < left + width && 321 | mouse.y > top && mouse.y < top + height && 322 | currentRect.width > width && 323 | currentRect.height > height 324 | ); 325 | 326 | if (isOverlapped) { 327 | this.showHandler(Number(key)); 328 | } 329 | }); 330 | } 331 | 332 | stopDrag() { 333 | let {mode} = this.state; 334 | 335 | if (_.includes([modes.DRAG, 336 | modes.ROTATE, 337 | modes.SCALE], mode)) { 338 | this.setState({ 339 | mode: modes.FREE 340 | }); 341 | } 342 | } 343 | 344 | showEditor() { 345 | let {selectedObjectIndex} = this.state; 346 | 347 | let {objects} = this.props, 348 | currentObject = objects[selectedObjectIndex], 349 | objectComponent = this.getObjectComponent(currentObject.type); 350 | 351 | if (objectComponent.meta.editor) { 352 | this.setState({ 353 | mode: modes.EDIT_OBJECT, 354 | showHandler: false 355 | }); 356 | } 357 | } 358 | 359 | getObjectComponent(type) { 360 | let {objectTypes} = this.props; 361 | return objectTypes[type]; 362 | } 363 | 364 | getCanvas() { 365 | let {width, height} = this.props; 366 | let { 367 | canvasWidth=width, 368 | canvasHeight=height 369 | } = this.props; 370 | return { 371 | width, height, canvasWidth, canvasHeight, 372 | canvasOffsetX: (canvasWidth - width) / 2, 373 | canvasOffsetY: (canvasHeight - height) / 2 374 | }; 375 | } 376 | 377 | renderSVG() { 378 | let canvas = this.getCanvas(); 379 | let {width, height, canvasOffsetX, canvasOffsetY} = canvas; 380 | let {background, objects, svgStyle, objectTypes} = this.props; 381 | 382 | return ( 383 | this.svgElement = ref} 393 | onMouseDown={this.newObject.bind(this)} /> 394 | ); 395 | } 396 | 397 | selectTool(tool) { 398 | this.setState({ 399 | selectedTool: tool, 400 | mode: modes.DRAW, 401 | currentObjectIndex: null, 402 | showHandler: false, 403 | handler: null 404 | }); 405 | } 406 | 407 | handleObjectChange(key, value) { 408 | let {selectedObjectIndex} = this.state; 409 | // console.log(this.state, key, value) 410 | this.updateObject(selectedObjectIndex, { 411 | [key]: value 412 | }); 413 | } 414 | 415 | handleArrange(arrange) { 416 | let {selectedObjectIndex} = this.state; 417 | let {objects} = this.props; 418 | let object = objects[selectedObjectIndex]; 419 | 420 | let arrangers = { 421 | 'front': (rest, object) => ([[...rest, object], rest.length]), 422 | 'back': (rest, object) => ([[object, ...rest], 0]) 423 | }; 424 | 425 | let rest = objects.filter( 426 | (object, index) => 427 | selectedObjectIndex !== index 428 | ); 429 | 430 | this.setState({ 431 | selectedObjectIndex: null 432 | }, () => { 433 | 434 | let arranger = arrangers[arrange]; 435 | let [arranged, newIndex] = arranger(rest, object); 436 | this.props.onUpdate(arranged); 437 | this.setState({ 438 | selectedObjectIndex: newIndex 439 | }); 440 | }); 441 | } 442 | 443 | removeCurrent() { 444 | let {selectedObjectIndex} = this.state; 445 | let {objects} = this.props; 446 | 447 | let rest = objects.filter( 448 | (object, index) => 449 | selectedObjectIndex !== index 450 | ); 451 | 452 | this.setState({ 453 | currentObjectIndex: null, 454 | selectedObjectIndex: null, 455 | showHandler: false, 456 | handler: null 457 | }, () => { 458 | this.objectRefs = {}; 459 | this.props.onUpdate(rest); 460 | }); 461 | } 462 | 463 | moveSelectedObject(attr, points, event, key) { 464 | let {selectedObjectIndex} = this.state; 465 | let {objects} = this.props; 466 | let object = objects[selectedObjectIndex]; 467 | 468 | if (key.startsWith('shift')) { 469 | points *= 10; 470 | } 471 | 472 | let changes = { 473 | ...object, 474 | [attr]: object[attr] + points 475 | }; 476 | 477 | this.updateObject(selectedObjectIndex, changes); 478 | this.updateHandler(selectedObjectIndex, changes); 479 | } 480 | 481 | getKeymapHandlers() { 482 | let handlers = { 483 | removeObject: this.removeCurrent.bind(this), 484 | moveLeft: this.moveSelectedObject.bind(this, 'x', -1), 485 | moveRight: this.moveSelectedObject.bind(this, 'x', 1), 486 | moveUp: this.moveSelectedObject.bind(this, 'y', -1), 487 | moveDown: this.moveSelectedObject.bind(this, 'y', 1), 488 | closePath: () => this.setState({mode: modes.FREE}) 489 | }; 490 | 491 | return _.mapValues(handlers, (handler) => (event, key) => { 492 | if (event.target.tagName !== 'INPUT') { 493 | event.preventDefault(); 494 | handler(event, key); 495 | } 496 | }); 497 | } 498 | 499 | render() { 500 | let {showHandler, handler, mode, 501 | selectedObjectIndex, selectedTool} = this.state; 502 | 503 | let { 504 | objects, 505 | objectTypes, 506 | insertMenu: InsertMenuComponent 507 | } = this.props; 508 | 509 | let currentObject = objects[selectedObjectIndex], 510 | isEditMode = mode === modes.EDIT_OBJECT, 511 | showPropertyPanel = selectedObjectIndex !== null; 512 | 513 | let {width, height, canvasWidth, canvasHeight} = this.getCanvas(); 514 | 515 | let objectComponent, objectWithInitial, ObjectEditor; 516 | if (currentObject) { 517 | objectComponent = this.getObjectComponent(currentObject.type); 518 | objectWithInitial = { 519 | ...objectComponent.meta.initial, 520 | ...currentObject 521 | }; 522 | ObjectEditor = objectComponent.meta.editor; 523 | } 524 | 525 | return ( 526 | 530 |
538 | 539 | {/* Left Panel: Displays insertion tools (shapes, images, etc.) */} 540 | {InsertMenuComponent && ( 541 | 544 | )} 545 | 546 | {/* Center Panel: Displays the preview */} 547 |
548 | {isEditMode && ObjectEditor && ( 549 | 552 | this.updateObject(selectedObjectIndex, object)} 553 | onClose={() => this.setState({mode: modes.FREE})} 554 | width={width} 555 | height={height} />)} 556 | 557 | {showHandler && ( 558 | )} 568 | 569 | {this.renderSVG()} 570 |
571 | 572 | {/* Right Panel: Displays text, styling and sizing tools */} 573 | {showPropertyPanel && ( 574 | 580 | )} 581 |
582 |
583 | ); 584 | } 585 | } 586 | 587 | export const styles = { 588 | container: { 589 | position: 'relative', 590 | display: 'flex', 591 | flexDirection: 'row' 592 | }, 593 | canvasContainer: { 594 | position: 'relative' 595 | }, 596 | keyboardManager: { 597 | outline: 'none' 598 | } 599 | } 600 | 601 | export default Designer; 602 | -------------------------------------------------------------------------------- /src/Handler.js: -------------------------------------------------------------------------------- 1 | import React, { Component, useState } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Icon from './Icon'; 4 | 5 | function ScaleAnchor(props) { 6 | let {boundingBox} = props; 7 | let style = { 8 | marginTop: boundingBox.height + 5, 9 | marginLeft: boundingBox.width + 5 10 | }; 11 | let [anchorHovered, setAnchorHovered] = useState(false); 12 | return ( 13 |
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 | 23 | 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 |
45 | 48 | 49 | 50 | {show &&
51 |
52 | 56 |
} 57 |
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 | 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 | 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 | --------------------------------------------------------------------------------