├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── dev-client.js ├── dev-server.js ├── dev-styles.css ├── example1.png ├── example2.png ├── example3.png ├── index.html ├── package.json ├── src ├── Blobber.js └── lib │ ├── convex-hull │ ├── index.js │ ├── orthoConvexHull.js │ └── roundedSVGPath.js │ ├── polygon-union │ ├── index.js │ ├── makePolygonGroups.js │ └── roundedSVGPath.js │ └── rect2rectPoints.js ├── test └── test.js └── webpack.dev.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "quotes": [1, "single"], 5 | "semi": [1, "always"], 6 | "comma-dangle": [0, "always-multiline"], 7 | "no-eval": 2, 8 | "no-console": 0, 9 | "no-undef": 0, 10 | "no-unused-vars": 0, 11 | "react/jsx-uses-react": 2, 12 | "react/jsx-uses-vars": 2, 13 | "react/react-in-jsx-scope": 2, 14 | "react/jsx-key": 1, 15 | "react/jsx-quotes": [1, "single"] 16 | }, 17 | "env": { 18 | "es6": true, 19 | "browser": true, 20 | "node": true, 21 | "mocha": true 22 | }, 23 | "ecmaFeatures": { 24 | "jsx": true, 25 | "modules": true 26 | }, 27 | "extends": "eslint:recommended", 28 | "plugins": [ 29 | "react" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | .gitignore 4 | .eslintrc 5 | .babelrc 6 | .travis.yml 7 | webpack.dev.config.js 8 | dev-client.js 9 | dev-server.js 10 | dev-styles.css 11 | index.html 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "4" 5 | - "5" 6 | notifications: 7 | slack: standardanalytics:vPQA8IowAphWwzDgGtHkdkRk 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Standard Analytics IO, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-blobber 2 | 3 | [![build status](https://img.shields.io/travis/scienceai/react-blobber/master.svg?style=flat-square)](https://travis-ci.org/scienceai/react-blobber) 4 | [![npm version](https://img.shields.io/npm/v/react-blobber.svg?style=flat-square)](https://www.npmjs.com/package/react-blobber) 5 | 6 | Create orthogonal blobs from grouped arrays of rectangles 7 | 8 | Example of convex-hull algorithm: 9 | 10 |

11 | 12 |

13 | 14 | Examples of polygon-union algorithm: 15 | 16 |

17 | 18 | 19 |

20 | 21 | ### Usage 22 | 23 | ``` 24 | npm install react-blobber --save 25 | ``` 26 | 27 | ```js 28 | import React from 'react'; 29 | import Blobber from 'react-blobber'; 30 | 31 | const groupLabels = [ 32 | ['Mercury', 'Venus', 'Mars'], 33 | ['quarks', 'leptons', 'bosons'], 34 | ['heart', 'lungs', 'brain'], 35 | ]; 36 | 37 | const groupColors = ['#D24D57', '#F5D76E', '#19B5FE']; 38 | 39 | const groupRectangles = [ 40 | [ 41 | { x: 142, y: 154, width: 150, height: 24 }, 42 | { x: 254, y: 102, width: 150, height: 24 }, 43 | { x: 306, y: 294, width: 150, height: 24 }, 44 | ], 45 | [ 46 | { x: 219, y: 245, width: 150, height: 24 }, 47 | { x: 102, y: 289, width: 150, height: 24 }, 48 | { x: 102, y: 209, width: 150, height: 24 }, 49 | ], 50 | [ 51 | { x: 310, y: 190, width: 150, height: 24 }, 52 | { x: 393, y: 246, width: 150, height: 24 }, 53 | { x: 392, y: 130, width: 150, height: 24 }, 54 | ], 55 | ]; 56 | 57 | class Example extends React.Component { 58 | 59 | render() { 60 | 61 | const exampleBlobs = groupRectangles.map((rectGroup, i) => ( 62 | 71 | )); 72 | 73 | return ( 74 |
75 | {exampleBlobs} 76 |
77 | ); 78 | } 79 | } 80 | ``` 81 | 82 | ##### Props 83 | 84 | + `rects`: an array of rectangles for one blob group (example: elements of `groupRectangles` above). A rectangle object consists of `x` and `y` top-left coordinates as well as `width` and `height`. 85 | 86 | + `pathOffset`: blob padding, in pixels 87 | 88 | + `cornerRadius`: blob corner radius, in pixels 89 | 90 | + `containerStyle`: style object for container div 91 | 92 | + `svgStyle`: style object for svg paths 93 | 94 | + `algorithm`: options are `convex-hull` or `polygon-union`. There are minor differences in appearance between the two algorithms. `convex-hull` may not produce optimal results for very complex layouts or groupings, due to convexity requirements. The way `polygon-union` creates extensions between elements may make it more amenable to complex groupings. 95 | 96 | ### Development 97 | 98 | `npm run dev` to start the webpack dev server with hot reloading, then go to [http://localhost:3000](http://localhost:3000). 99 | 100 | ### Build 101 | 102 | ``` 103 | npm run build 104 | ``` 105 | 106 | Outputs to `dist/`. 107 | 108 | ### Test 109 | 110 | ``` 111 | npm test 112 | ``` 113 | 114 | ### License 115 | 116 | [Apache 2.0](https://github.com/scienceai/blobber/blob/master/LICENSE) 117 | -------------------------------------------------------------------------------- /dev-client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Blobber from './src/Blobber'; 4 | 5 | import './dev-styles.css'; 6 | 7 | class Main extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | algorithm: 'convex-hull', 13 | 14 | dragGroup: -1, 15 | dragging: -1, 16 | //rects: [{x1:30, y1:80, x2:180, y2:120},{x1:60, y1:60, x2:155, y2:100}, {x1:115, y1:15, x2:275, y2:50}] 17 | rectGroups: [ 18 | [{x:142, y:154, width:150, height: 24}, 19 | {x:254, y:102, width:150, height: 24}, 20 | {x:306, y:294, width:150, height: 24}], 21 | 22 | [{x:219, y:245, width:150, height: 24}, 23 | {x:102, y:289, width:150, height: 24}, 24 | {x:102, y:209, width:150, height: 24}], 25 | 26 | [{x:310, y:190, width:150, height: 24}, 27 | {x:393, y:246, width:150, height: 24}, 28 | {x:392, y:130, width:150, height: 24}] 29 | ] 30 | }; 31 | } 32 | 33 | componentDidMount() { 34 | document.addEventListener('mousemove', this.handleMouseMove.bind(this)); 35 | //document.addEventListener('onmouseup', this.dragEnd.bind(this)); 36 | } 37 | 38 | componentWillUnmount() { 39 | document.removeEventListener('mousemove', this.handleMouseMove.bind(this)); 40 | //document.removeEventListener('onmouseup', this.dragEnd.bind(this)); 41 | } 42 | 43 | dragStart(group, i, event) { 44 | //console.log('mousedown', i, event); 45 | this.setState({dragGroup: group, dragging: i}); 46 | } 47 | 48 | dragEnd(event) { 49 | //console.log('dragEnd'); 50 | this.setState({dragging: -1}); 51 | } 52 | 53 | handleMouseMove(e) { 54 | //console.log('drag: ', this.state.dragging); 55 | if (this.state.dragGroup == -1 || this.state.dragging == -1) return; 56 | var newRects = this.state.rectGroups.slice(); 57 | newRects[this.state.dragGroup][this.state.dragging].x = e.pageX-30; 58 | newRects[this.state.dragGroup][this.state.dragging].y = e.pageY-10; 59 | //console.log('x ', e.pageX); 60 | this.setState({ 61 | rectGroups: newRects 62 | }); 63 | e.stopPropagation(); 64 | e.preventDefault(); 65 | } 66 | 67 | render() { 68 | 69 | const labels = [ 70 | ['Mercury', 'Venus', 'Mars'], 71 | ['quarks', 'leptons', 'bosons'], 72 | ['heart', 'lungs', 'brain'], 73 | ]; 74 | 75 | const rectDivs = this.state.rectGroups.map((rectGroup, i) => { 76 | const cells = rectGroup.map((rect, j) => { 77 | //console.log('rect ', rect); 78 | const width = rect.width; 79 | const height = rect.height; 80 | const top = rect.y; 81 | const left = rect.x; 82 | //console.log('rect loc ', width, height, top, left); 83 | const divStyle = {top:top, left:left, width:width, height:height, zIndex:100}; 84 | return( 85 |
86 |
{labels[i][j]}
87 |
88 | ); 89 | }); 90 | return cells; 91 | }); 92 | 93 | const blobColors = ['#D24D57', '#F5D76E', '#19B5FE']; 94 | const blobbers = this.state.rectGroups.map((rectGroup, i)=> { 95 | return( 96 | 103 | ); 104 | }); 105 | 106 | return( 107 |
108 |
109 | 110 | {blobbers} 111 | {rectDivs} 112 | 113 |
114 | 118 |
119 | ); 120 | } 121 | 122 | } 123 | 124 | 125 | ReactDOM.render(
, document.getElementById('root')); 126 | -------------------------------------------------------------------------------- /dev-server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.dev.config'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | stats: { colors: true }, 9 | }).listen(3000, (err) => { 10 | if (err) return console.error(err); 11 | console.log('🚧 server listening on http://localhost:3000. 🚧'); 12 | }); 13 | -------------------------------------------------------------------------------- /dev-styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | 5 | .cell { 6 | border-radius: 12px; 7 | line-height: 24px; 8 | height:24px; 9 | padding: 0px 12px 0px 12px; 10 | position: absolute; 11 | box-sizing: border-box; 12 | text-align: center; 13 | } 14 | 15 | .cell:hover { 16 | cursor: move; 17 | } 18 | 19 | .blobber { 20 | width: 100%; 21 | height: 100%; 22 | position: absolute; 23 | top: 0px; 24 | left: 0px; 25 | border: 1px solid lightgray; 26 | } 27 | 28 | select { 29 | font-size: 2em; 30 | margin-top: 10px; 31 | } 32 | -------------------------------------------------------------------------------- /example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scienceai/react-blobber/809152aad8af6d6e688ca46dd94d9c522022fdcb/example1.png -------------------------------------------------------------------------------- /example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scienceai/react-blobber/809152aad8af6d6e688ca46dd94d9c522022fdcb/example2.png -------------------------------------------------------------------------------- /example3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scienceai/react-blobber/809152aad8af6d6e688ca46dd94d9c522022fdcb/example3.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-blobber development 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-blobber", 3 | "version": "0.4.0", 4 | "description": "React component for creating blobs around elements", 5 | "main": "dist/Blobber.js", 6 | "scripts": { 7 | "dev": "node dev-server.js", 8 | "build": "rm -rf dist && babel src --out-dir dist", 9 | "prepublish": "npm run build", 10 | "test": "node_modules/.bin/mocha --compilers js:babel-core/register" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/scienceai/react-blobber.git" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "blobber" 19 | ], 20 | "author": "Erik Wysocan", 21 | "contributors": [ 22 | { 23 | "name": "Leon Chen" 24 | } 25 | ], 26 | "license": "Apache-2.0", 27 | "bugs": { 28 | "url": "https://github.com/scienceai/react-blobber/issues" 29 | }, 30 | "homepage": "https://github.com/scienceai/react-blobber", 31 | "peerDependencies": { 32 | "react": "^15.0.1" 33 | }, 34 | "devDependencies": { 35 | "babel-cli": "6.7.5", 36 | "babel-core": "6.7.6", 37 | "babel-eslint": "6.0.2", 38 | "babel-loader": "6.2.4", 39 | "babel-plugin-react-transform": "2.0.2", 40 | "babel-preset-es2015": "6.6.0", 41 | "babel-preset-react": "6.5.0", 42 | "css-loader": "0.23.1", 43 | "enzyme": "2.2.0", 44 | "eslint": "2.7.0", 45 | "eslint-plugin-react": "4.3.0", 46 | "mocha": "2.4.5", 47 | "postcss-cssnext": "2.5.2", 48 | "postcss-loader": "0.8.2", 49 | "react": "15.0.1", 50 | "react-addons-test-utils": "15.0.1", 51 | "react-dom": "15.0.1", 52 | "react-transform-hmr": "1.0.4", 53 | "style-loader": "0.13.1", 54 | "webpack": "1.12.15", 55 | "webpack-dev-server": "1.14.1" 56 | }, 57 | "dependencies": { 58 | "poly-bool": "1.0.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Blobber.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import rect2rectPoints from './lib/rect2rectPoints'; 3 | import * as convexHullAlgos from './lib/convex-hull'; 4 | import * as polygonUnionAlgos from './lib/polygon-union'; 5 | 6 | const Blobber = ({ 7 | rects = [], 8 | pathOffset = 5, 9 | cornerRadius = 10, 10 | containerStyle = { position:'absolute', left:'0px', top:'0px', width:'100%', height:'100%' }, 11 | svgStyle = {}, 12 | algorithm = 'convex-hull', 13 | }) => { 14 | 15 | const rectsAsPoints = rects.map(rect => rect2rectPoints(rect)); 16 | 17 | const { 18 | fill = 'lightgray', 19 | opacity = '0.5', 20 | stroke = 'lightgray', 21 | } = svgStyle; 22 | 23 | if (algorithm === 'convex-hull') { 24 | 25 | const hull = convexHullAlgos.orthoConvexHull(rectsAsPoints, pathOffset); 26 | const roundedHullStr = convexHullAlgos.roundedSVGPath(hull, cornerRadius); 27 | 28 | let svgPointsStr = ''; 29 | for (let i = 0; i < hull.length; i++) { 30 | svgPointsStr += `${hull[i].x},${hull[i].y} `; 31 | } 32 | 33 | // for development 34 | const svgRects = rectsAsPoints.map(({ x1, y1, x2, y2 }, idx) => 35 | 36 | ); 37 | const polyline = ( 38 | 39 | ); 40 | 41 | return( 42 |
46 | 47 | {/*svgRects*/} 48 | {/*polyline*/} 49 | 50 | 51 |
52 | ); 53 | 54 | } else if (algorithm === 'polygon-union') { 55 | 56 | const polygons = polygonUnionAlgos.makePolygonGroups(rectsAsPoints, pathOffset); 57 | 58 | const svgPaths = polygons.map((polygon, i) => { 59 | const roundedPolygon = polygonUnionAlgos.roundedSVGPath(polygon, cornerRadius); 60 | 61 | let svgPointsStr = ''; 62 | polygon.forEach(point => { 63 | svgPointsStr += `${point[0]},${point[1]} `; 64 | }); 65 | 66 | // for development 67 | const polygonOutline = ( 68 | 69 | ); 70 | const polygonCircle = ( 71 | 72 | ); 73 | 74 | return( 75 | 76 | {/*polygonOutline*/} 77 | 78 | {/*polygonCircle*/} 79 | 80 | ); 81 | }); 82 | 83 | // for development 84 | const svgRects = rectsAsPoints.map(({ x1, y1, x2, y2 }, idx) => 85 | 86 | ); 87 | const svgCircles = rects.map(({ x, y, width, height }, idx) => 88 | 89 | ); 90 | 91 | return( 92 |
96 | 97 | {/* 98 | {svgRects} 99 | {svgCircles} 100 | */} 101 | {svgPaths} 102 | 103 |
104 | ); 105 | 106 | } else { 107 | throw new Error('Invalid algorithm prop'); 108 | } 109 | 110 | }; 111 | 112 | export default Blobber; 113 | -------------------------------------------------------------------------------- /src/lib/convex-hull/index.js: -------------------------------------------------------------------------------- 1 | import orthoConvexHull from './orthoConvexHull'; 2 | import roundedSVGPath from './roundedSVGPath'; 3 | 4 | export { orthoConvexHull, roundedSVGPath }; 5 | -------------------------------------------------------------------------------- /src/lib/convex-hull/orthoConvexHull.js: -------------------------------------------------------------------------------- 1 | export default function orthoConvexHull(rects, offset) { 2 | //Orthogonal/X-Y Convex Hull 3 | let points = []; 4 | 5 | // find islands 6 | let islands = []; 7 | for (let i = 0; i< rects.length; i++) { 8 | let isIsland = true; 9 | 10 | for (let j = 0; j < rects.length; j++ ) { 11 | if (i != j) { 12 | if (rects[i].x1 >= rects[j].x1 && rects[i].x1 <= rects[j].x2) { 13 | isIsland = false; 14 | } else if (rects[i].x2 >= rects[j].x1 && rects[i].x2 <= rects[j].x2) { 15 | isIsland = false; 16 | } else if (rects[i].x1 <= rects[j].x1 && rects[i].x2 >= rects[j].x2) { 17 | isIsland = false; 18 | } 19 | if (rects[i].y1 >= rects[j].y1 && rects[i].y1 <= rects[j].y2) { 20 | isIsland = false; 21 | } else if (rects[i].y2 >= rects[j].y1 && rects[i].y2 <= rects[j].y2) { 22 | isIsland = false; 23 | } else if (rects[i].y1 <= rects[j].y1 && rects[i].y2 >= rects[j].y2) { 24 | isIsland = false; 25 | } 26 | } 27 | } 28 | if (isIsland === true) islands.push(i); 29 | //console.log('isLand?', i, isIsland); 30 | } 31 | //console.log('islands', islands); 32 | // find nearest neighbor 33 | 34 | let nearestRect; 35 | const connectorThickness = 1; 36 | let rectConnects = []; 37 | for (let i = 0; i< islands.length; i++) { 38 | nearestRect = false; 39 | let xMin = 100000; 40 | let yMin = 100000; 41 | let deltaMin = 100000; 42 | for (let j = 0; j < rects.length; j++ ) { 43 | //console.log(i,j); 44 | 45 | if (islands[i] != j) { 46 | 47 | let xDelta = (rects[islands[i]].x1 - rects[j].x2 ); 48 | //console.log('delta for ',i , rects[islands[i]].x1, '-', rects[j].x2 ); 49 | 50 | let yDelta = (rects[islands[i]].y2 - rects[j].y1); 51 | let delta = Math.sqrt(Math.abs(xDelta * xDelta) + Math.abs(yDelta * yDelta)); 52 | if (delta < deltaMin) { 53 | deltaMin = delta; 54 | nearestRect = j; 55 | xMin = xDelta; 56 | yMin = yDelta; 57 | } 58 | // let xDelta = (rects[islands[i]].x2 - rects[j].x1); 59 | // let yDelta = (rects[islands[i]].y1 - rects[j].y2); 60 | // let delta = Math.sqrt(Math.abs(xDelta * xDelta) + Math.abs(yDelta * yDelta)); 61 | // if (delta < deltaMin) { 62 | // deltaMin = delta; 63 | // nearestRect = j; 64 | // } 65 | 66 | // if ( Math.abs(rects[islands[i]].x2 - rects[j].x1) < Math.abs(xMin) ) { 67 | // xMin =(rects[islands[i]].x2 - rects[j].x1); 68 | // if (Math.abs(xMin) < deltaMin) { 69 | // deltaMin = Math.abs(xMin); 70 | // nearestRect = j; 71 | // } 72 | // } 73 | // if (Math.abs(rects[islands[i]].y2 - rects[j].y1) < Math.abs(yMin)) { 74 | // yMin =(rects[islands[i]].y2 - rects[j].y1); 75 | // if (Math.abs(yMin) < deltaMin) { 76 | // deltaMin = Math.abs(yMin); 77 | // nearestRect = j; 78 | // } 79 | // } 80 | // if (Math.abs(rects[islands[i]].x1 - rects[j].x2) < Math.abs(xMin)) { 81 | // xMin =(rects[islands[i]].x1 - rects[j].x2); 82 | // if (Math.abs(xMin) < deltaMin) { 83 | // deltaMin = Math.abs(xMin); 84 | // nearestRect = j; 85 | // } 86 | // } 87 | // if (Math.abs(rects[islands[i]].y1 - rects[j].y2) < Math.abs(yMin)) { 88 | // yMin =(rects[islands[i]].y1 - rects[j].y2); 89 | // if (Math.abs(yMin) < deltaMin) { 90 | // deltaMin = Math.abs(yMin); 91 | // nearestRect = j; 92 | // } 93 | // } 94 | 95 | } 96 | } 97 | if (nearestRect !== false) { 98 | //console.log('island neighbors: ', islands[i], nearestRect ); 99 | //rectConnects.push({x1:rects[islands[i]].x1, y1:rects[islands[i]].y1, x2:rects[islands[i]].x1+10, y2:rects[islands[i]].y1+10}); 100 | //rectConnects.push({x1:rects[nearestRect].x1, y1:rects[nearestRect].y1, x2:rects[nearestRect].x1+offset, y2:rects[nearestRect].y1+offset}); 101 | let cX1, cX2, cY1, cY2; 102 | if (xMin > 0) { 103 | //console.log('island on right'); 104 | // island is to the right of neighbor 105 | cX1 = rects[nearestRect].x2 - connectorThickness; 106 | cX2 = rects[islands[i]].x1 + connectorThickness; 107 | //rectConnects.push({x1:rects[nearestRect].x2, y1:rects[nearestRect].y1, x2:rects[islands[i]].x1+offset, y2:rects[nearestRect].y1+offset}); 108 | } else { 109 | //island is to the left 110 | //console.log('island on left'); 111 | cX1 = rects[islands[i]].x2 - connectorThickness; 112 | cX2 = rects[nearestRect].x1 + connectorThickness; 113 | //rectConnects.push({ x1:rects[islands[i]].x2, y1:rects[islands[i]].y1, x2:rects[nearestRect].x1 + offset, y2:rects[islands[i]].y1 + offset,}); 114 | } 115 | if (yMin> 0) { 116 | // island is below neighbor 117 | //cY1 = rects[islands[i]].y1; 118 | //cY2 = rects[islands[i]].y1 + connectorThickness; 119 | cY1 = rects[nearestRect].y2 + ((rects[islands[i]].y1 - rects[nearestRect].y2) / 2); 120 | cY2 = cY1 + connectorThickness; 121 | } else { 122 | // island is above neighbor 123 | //cY1 = rects[nearestRect].y1; 124 | cY1 = rects[islands[i]].y2 + ((rects[nearestRect].y1 -rects[islands[i]].y2)/2); 125 | cY2 = cY1 + connectorThickness; 126 | } 127 | //console.log('connector ', cX1, cY1, cX2, cY2); 128 | rectConnects.push({x1:cX1, y1:cY1, x2:cX2, y2:cY2}); 129 | 130 | } 131 | } 132 | 133 | // add connectors to rects 134 | rects.push.apply(rects, rectConnects); 135 | 136 | // get all the points in the rect Array and add offset 137 | for (let i = 0; i < rects.length; i++) { 138 | rects[i].x1 = rects[i].x1 - offset; 139 | rects[i].y1 = rects[i].y1 - offset; 140 | rects[i].x2 = rects[i].x2 + offset; 141 | rects[i].y2 = rects[i].y2 + offset; 142 | } 143 | 144 | // convert rects to points 145 | for (let i = 0; i < rects.length; i++) { 146 | points.push({x: rects[i].x1, y: rects[i].y1}); 147 | points.push({x: rects[i].x1, y: rects[i].y2}); 148 | points.push({x: rects[i].x2, y: rects[i].y2}); 149 | points.push({x: rects[i].x2, y: rects[i].y1}); 150 | } 151 | 152 | 153 | //console.log('points: ', JSON.stringify(points)); 154 | // find min and max values of y 155 | let min_y = Number.POSITIVE_INFINITY; 156 | let min_yPoint; 157 | let max_y = Number.NEGATIVE_INFINITY; 158 | let max_yPoint; 159 | let min_x = Number.POSITIVE_INFINITY; 160 | let min_xPoint; 161 | let max_x = Number.NEGATIVE_INFINITY; 162 | let max_xPoint; 163 | let tmp; 164 | for (let i = points.length-1; i>=0; i--) { 165 | tmp = points[i].y; 166 | if (tmp < min_y) { 167 | min_yPoint = points[i]; 168 | min_y = points[i].y; 169 | } else if (tmp > max_y) { 170 | max_yPoint = points[i]; 171 | max_y = points[i].y; 172 | } 173 | tmp = points[i].x; 174 | if (tmp < min_x) { 175 | min_xPoint = points[i]; 176 | min_x = points[i].x; 177 | } else if (tmp > max_x) { 178 | max_xPoint = points[i]; 179 | max_x = points[i].x; 180 | } 181 | } 182 | // sort points by x coordinate, then by y 183 | points.sort(function(a,b) { 184 | if (a.x == b.x) return a.y-b.y; 185 | return a.x-b.x; 186 | 187 | }); 188 | 189 | //console.log('sorted points: ', JSON.stringify(points)); 190 | 191 | //console.log('minYPoint: ', min_yPoint); 192 | //console.log('maxYPoint: ', max_yPoint); 193 | 194 | // duplicate array 195 | let reversedPoints = points.slice(0); 196 | // and reverse 197 | reversedPoints.reverse(); 198 | 199 | // construct upper paths 200 | let upperLeft = orthoBuildUpperLeft(points, min_yPoint); 201 | //console.log('upperLeft: ', upperLeft ); 202 | let upperRight = orthoBuildUpperRight(reversedPoints, max_xPoint); 203 | //console.log('upperRight: ', upperRight ); 204 | 205 | // construct lower paths 206 | let lowerLeft = orthoBuildLowerLeft(points, max_yPoint); 207 | //console.log('lowerLeft: ', lowerLeft); 208 | 209 | // resort by y then x 210 | points.sort(function(a,b) { 211 | if (a.y == b.y) return a.x-b.x; 212 | return b.y-a.y; 213 | }); 214 | let lowerRight = orthoBuildLowerRight(points, max_yPoint); 215 | //console.log('lowerRight: ', lowerRight); 216 | 217 | upperRight.reverse(); 218 | lowerRight.reverse(); 219 | lowerLeft.reverse(); 220 | 221 | 222 | let hull = upperLeft.concat(upperRight, lowerRight, lowerLeft); 223 | //console.log('hull', JSON.stringify(hull)); 224 | 225 | // remove all duplicate points 226 | hull = hull.reduce((prev, curr) => { 227 | if (!prev.some(point => point.x === curr.x && point.y === curr.y)) { 228 | prev.push(curr); 229 | } 230 | return prev; 231 | }, []); 232 | 233 | // clean up redundant points on a line 234 | let hullLength = 0; 235 | while(hull.length != hullLength && hull.length > 2) { 236 | 237 | hullLength = hull.length; 238 | 239 | for (let i = 0; i hull.length-1) x2i = (x2i - hull.length); 242 | let x3i = i+2; 243 | if (x3i > hull.length-1) x3i = (x3i - hull.length); 244 | //console.log('is ', hull.length, i, x2i, x3i ); 245 | if (hull[i].x == hull[x2i].x && hull[i].x == hull[x3i].x) { 246 | //console.log('remove: ', hull[i-2].x, hull[i-1].x, hull[i].x ); 247 | hull.splice(x2i, 1); 248 | } 249 | } 250 | for (let i = 0; i hull.length-1) y2i = (y2i - hull.length); 253 | let y3i = i+2; 254 | if (y3i > hull.length-1) y3i = (y3i - hull.length); 255 | if (hull[i].y == hull[y2i].y && hull[i].y == hull[y3i].y) { 256 | //console.log('remove: ', hull[i-2].x, hull[i-1].x, hull[i].x ); 257 | hull.splice(y2i, 1); 258 | } 259 | } 260 | } 261 | let closeLine = [ 262 | {x: hull[hull.length-1].x, y: hull[hull.length-1].y}, 263 | {x: hull[0].x, y: hull[0].y} 264 | ]; 265 | hull = hull.concat([{x: hull[0].x, y: hull[0].y}]); 266 | 267 | //console.log('closeLine: ', JSON.stringify(closeLine)); 268 | //console.log('cleaned hull', JSON.stringify(hull)); 269 | return(hull); 270 | } 271 | 272 | 273 | function orthoBuildLowerLeft(points) { 274 | let section = [points[0]]; 275 | 276 | for (let i = 0; i < points.length; i++) { 277 | //console.log('x,y: ', points[i].x, points[i].y); 278 | if (section.length > 1) { 279 | if (points[i].y == section[section.length-1].y) { 280 | // horizontal line 281 | if (points[i].x >section[section.length-1].x) { 282 | //right 283 | let y = section[section.length-1].y; 284 | let x = points[i].x; 285 | section.push({x: x, y: y}); 286 | section.push(points[i]); 287 | } 288 | } 289 | if (points[i].x == section[section.length-1].x) { 290 | // vertical line 291 | if (points[i].y > section[section.length-1].y) { 292 | 293 | section.push(points[i]); 294 | } 295 | } 296 | if (points[i].y > section[section.length-1].y) { 297 | if (points[i].x > section[section.length-1].x) { 298 | // down right 299 | //section[section.length-1].y = points[i].y 300 | let y = section[section.length-1].y; 301 | let x = points[i].x; 302 | section.push({x: x, y: y}); 303 | section.push(points[i]); 304 | } 305 | } 306 | } else { 307 | // first point 308 | section.push(points[i]); 309 | } 310 | } 311 | return section; 312 | } 313 | 314 | function orthoBuildLowerRight(points, max_yPoint) { 315 | let section = [max_yPoint]; 316 | 317 | for (let i = 0; i < points.length; i++) { 318 | //console.log('lr x,y: ', points[i].x, points[i].y); 319 | if (section.length >= 1) { 320 | if (points[i].y == section[section.length-1].y) { 321 | // horizontal line 322 | if (points[i].x >= section[section.length-1].x) { 323 | //right 324 | section.push(points[i]); 325 | } 326 | } 327 | if (points[i].x == section[section.length-1].x) { 328 | // vertical line 329 | if (points[i].y <= section[section.length-1].y) { 330 | section.push(points[i]); 331 | } 332 | } 333 | if (points[i].x >= section[section.length-1].x) { 334 | if (points[i].y <= section[section.length-1].y) { 335 | // up right 336 | //section[section.length-1].x = points[i].x 337 | //console.log('up right') 338 | let x = section[section.length-1].x; 339 | let y = points[i].y; 340 | section.push({x: x, y: y}); 341 | section.push(points[i]); 342 | } 343 | } 344 | } 345 | } 346 | return section; 347 | } 348 | 349 | function orthoBuildUpperLeft(points, min_yPoint) { 350 | let section = [points[0]]; 351 | 352 | for (let i = 0; i < points.length; i++) { 353 | //console.log('x,y: ', points[i].x, points[i].y); 354 | if (section.length > 1) { 355 | if (points[i].y == section[section.length-1].y) { 356 | // horizontal line 357 | if (points[i].x > section[section.length-1].x) { 358 | //right 359 | section.push(points[i]); 360 | } 361 | } 362 | if (points[i].x == section[section.length-1].x) { 363 | // vertical line 364 | if (points[i].y < section[section.length-1].y) { 365 | section.push(points[i]); 366 | } 367 | } 368 | if (points[i].y < section[section.length-1].y) { 369 | if (points[i].x > section[section.length-1].x) { 370 | // up right 371 | //section[section.length-1].x = points[i].x 372 | let y = section[section.length-1].y; 373 | let x = points[i].x; 374 | section.push({x: x, y: y}); 375 | section.push(points[i]); 376 | } 377 | } 378 | } else { 379 | // first point 380 | section.push(points[i]); 381 | } 382 | } 383 | return section; 384 | } 385 | 386 | function orthoBuildUpperRight(points, max_xPoint) { 387 | let section = []; 388 | //console.log('buildUpperRight'); 389 | for (let i = 0; i < points.length; i++) { 390 | if (section.length > 1) { 391 | if (points[i].y == section[section.length-1].y) { 392 | // horizontal line 393 | if (points[i].x < section[section.length-1].x) { 394 | section.push(points[i]); 395 | } 396 | } 397 | if (points[i].x == section[section.length-1].x) { 398 | // vertical line 399 | if (points[i].y < section[section.length-1].y) { 400 | section.push(points[i]); 401 | } 402 | } 403 | if (points[i].y < section[section.length-1].y) { 404 | if (points[i].x < section[section.length-1].x) { 405 | // up left 406 | //section[section.length-1].x = points[i].x 407 | let y = section[section.length-1].y; 408 | let x = points[i].x; 409 | section.push({x: x, y: y}); 410 | section.push(points[i]); 411 | } 412 | } 413 | } else { 414 | // first point 415 | if (points[i].x == max_xPoint.x) { 416 | section.push(points[i]); 417 | } 418 | } 419 | } 420 | return section; 421 | } 422 | -------------------------------------------------------------------------------- /src/lib/convex-hull/roundedSVGPath.js: -------------------------------------------------------------------------------- 1 | export default function roundedSVGPath(points, r) { 2 | 3 | let svgPath; 4 | 5 | if (!points.length) return; 6 | 7 | //compute the middle of the first line as start-stop-point: 8 | let deltaX, deltaY, deltaX2, deltaY2, deltaX3, deltaY3, xPerY, startX, startY; 9 | let radius1, radius2, radius3; 10 | 11 | svgPath = 'M'; 12 | let prevRadius, radius; 13 | prevRadius = r; 14 | radius = r; 15 | deltaX = points[1].x - points[0].x; 16 | deltaY = points[1].y - points[0].y; 17 | if (deltaX != 0 && Math.abs(deltaX)/2 < prevRadius) { 18 | prevRadius = Math.abs(deltaX)/2; 19 | } 20 | if (deltaY != 0 && Math.abs(deltaY)/2 < prevRadius) { 21 | prevRadius = Math.abs(deltaY)/2; 22 | } 23 | radius = prevRadius; 24 | //console.log('prevRadius', prevRadius); 25 | 26 | for (let i = 1; i < points.length; i++) { 27 | 28 | // some logic to deel with closing the loop 29 | if (i == 0) { 30 | //console.log('i==0'); 31 | deltaX = points[i].x - points[points.length-1].x; 32 | deltaY = points[i].y - points[points.length-1].y; 33 | deltaX2 = points[i+1].x - points[i].x; 34 | deltaY2 = points[i+1].y - points[i].y; 35 | deltaX3 = points[i+2].x - points[i+1].x; 36 | deltaY3 = points[i+2].y - points[i+1].y; 37 | } else if (i < points.length-2) { 38 | //console.log('i 0) { 110 | // up then right 111 | //console.log(' then curve right ', radius); 112 | svgPath += ' s 0 ' + negRadius + ' ' + radius + ' ' + negRadius; 113 | } 114 | } else if (deltaY > 0) { 115 | //down 116 | //console.log('down ', deltaY); 117 | if (svgPath == 'M') svgPath += ' ' + points[0].x + ' ' + (points[0].y + radius); 118 | 119 | svgPath += ' l ' + (deltaX) + ' ' + (deltaY - (prevRadius + radius) ); 120 | if (deltaX2 < 0) { 121 | // down then left 122 | svgPath += ' s 0 ' + radius + ' ' + negRadius + ' ' + radius; 123 | } else if (deltaX2 > 0) { 124 | // down then right 125 | svgPath += ' s 0 ' + ' ' + radius + ' ' + radius + ' ' + radius; 126 | } 127 | } else if (deltaX < 0) { 128 | // left 129 | 130 | if (svgPath == 'M') { 131 | //console.log('start ', points[0].x, '-', radius, ',', points[0].y); 132 | svgPath += ' ' + (points[0].x - radius) + ' ' + (points[0].y); 133 | } 134 | 135 | //console.log('left ', deltaX); 136 | 137 | svgPath += ' l ' + (deltaX + (prevRadius + radius)) + ' ' + (deltaY); 138 | 139 | if (deltaY2 < 0) { 140 | // left, then up 141 | //console.log('- then curve up ', radius); 142 | svgPath += ' s ' + negRadius + ' 0 ' + negRadius + ' ' + negRadius; 143 | } 144 | if (deltaY2 > 0) { 145 | // left, then down 146 | //console.log('- then curve down ', radius); 147 | svgPath += ' s ' + negRadius + ' 0 ' + negRadius + ' '+ radius; 148 | } 149 | } else if (deltaX > 0) { 150 | // right 151 | 152 | if (svgPath == 'M') { 153 | //console.log('start ', points[0].x, '+', radius, ',', points[0].y); 154 | svgPath += ' ' + (points[0].x + prevRadius) + ' ' + (points[0].y); 155 | } 156 | //console.log('right ', deltaX, ' - (', prevRadius, ' + ' + radius, ')'); 157 | 158 | svgPath += ' l ' + (deltaX - (prevRadius + radius)) + ' ' + (deltaY); 159 | if (deltaY2 > 0) { 160 | //console.log('- then curve down ', radius); 161 | // right, then down 162 | svgPath += ' s ' + radius + ' 0 ' + radius + ' ' + radius; 163 | } 164 | if (deltaY2 < 0) { 165 | //console.log('- then curve up ', radius); 166 | // right, then up 167 | svgPath += ' s ' + ' ' + radius + ' 0 ' + radius + ' ' + negRadius; 168 | } 169 | } 170 | 171 | } 172 | // close the shape 173 | //console.log('svgPath: ', svgPath); 174 | return svgPath; 175 | } 176 | -------------------------------------------------------------------------------- /src/lib/polygon-union/index.js: -------------------------------------------------------------------------------- 1 | import makePolygonGroups from './makePolygonGroups'; 2 | import roundedSVGPath from './roundedSVGPath'; 3 | 4 | export { makePolygonGroups, roundedSVGPath }; 5 | -------------------------------------------------------------------------------- /src/lib/polygon-union/makePolygonGroups.js: -------------------------------------------------------------------------------- 1 | import polygonBoolean from 'poly-bool'; 2 | 3 | export default function makePolygonGroups(rects, pathOffset) { 4 | const polygons = unionRects(offsetRects(rects, pathOffset), pathOffset); 5 | const cleanedPolygons = polygons.map(poly => cleanPolygon(poly)); 6 | return cleanedPolygons; 7 | } 8 | 9 | function offsetRects(rects, offset) { 10 | // offsets all the points in a rect 11 | for (let i = 0; i < rects.length; i++) { 12 | rects[i].x1 = rects[i].x1 - offset; 13 | rects[i].y1 = rects[i].y1 - offset; 14 | rects[i].x2 = rects[i].x2 + offset; 15 | rects[i].y2 = rects[i].y2 + offset; 16 | } 17 | return rects; 18 | } 19 | 20 | function unionRects(rects, pathOffset) { 21 | //console.log('unionRects: ', rects); 22 | let initialPoly = polygonPointArr(rects[0]); 23 | let polygons = [initialPoly];// todo - don't init? 24 | //console.log(' - polygon:', polygon); 25 | rects.forEach((rect, i) => { 26 | //console.log(' - rect:', rect); 27 | let pointArr = [polygonPointArr(rect)]; 28 | //console.log(' - pointArr:', pointArr); 29 | polygons = polygonBoolean(pointArr, polygons, 'or'); 30 | //console.log(' - polygon:', polygon); 31 | }); 32 | //console.log(' - unionRects returning: ', polygons); 33 | if(polygons.length > 1){ 34 | // connect polygon islands 35 | polygons = joinPolygons(polygons, pathOffset, 'elastic'); 36 | //console.log(' - joinPolygons returning: ', polygons); 37 | } 38 | return polygons; 39 | } 40 | 41 | function polygonPointArr(rect) { 42 | const { x1, y1, x2, y2 } = rect; 43 | return [ [x1, y1], [x2, y1], [x2, y2], [x1, y2] ]; 44 | } 45 | 46 | function joinPolygons(polygonsArr, minBridgeThickness, style){ 47 | //console.log('joinPolygons start data ', JSON.stringify(polygonsArr)); 48 | 49 | // join the polygons until there is only 1 polygon 50 | // limit prevents infinite loops on unsolvable problem (probably indicating a bug) 51 | let i = 0; 52 | let limit = polygonsArr.length +2; 53 | while(polygonsArr.length > 1 && i <= limit){ 54 | i++; 55 | let bridgeData = findNearestPointsInAll(polygonsArr); 56 | //console.log('join', polygonsArr); 57 | 58 | let bridgeRect; 59 | let startLine = bridgeData.lineA; 60 | let endLine = bridgeData.lineB; 61 | let startPoint = bridgeData.pointA; 62 | let endPoint = bridgeData.pointB; 63 | 64 | if(bridgeData.pointA[0] > bridgeData.pointB[0]){ 65 | startLine = bridgeData.lineB; 66 | endLine = bridgeData.lineA; 67 | startPoint = bridgeData.pointB; 68 | endPoint = bridgeData.pointA; 69 | } 70 | 71 | if(bridgeData.overlap == true){ 72 | if (startLine[0][1] == startLine[1][1]){ 73 | //console.log('horizontal parallel lines - make vertical bridge') 74 | // find the overlapping x pixels - make a vertical bridge 75 | let rectX1 = startLine[0][0] > endLine[0][0] ? startLine[0][0] : endLine[0][0]; 76 | let rectX2 = startLine[1][0] < endLine[1][0] ? startLine[1][0] : endLine[1][0]; 77 | let rectY1 = startLine[0][1]; 78 | let rectY2 = endLine[0][1]; 79 | 80 | rectX2 = rectX2 - rectX1 < minBridgeThickness ? rectX1 + minBridgeThickness : rectX2; 81 | let deltaX = Math.abs(rectX2 - rectX1); 82 | let deltaY = Math.abs(rectY2 - rectY1); 83 | let thickness = minBridgeThickness; 84 | 85 | if(style == 'elastic'){ 86 | thickness = deltaX * ((1/(deltaY*deltaY))*deltaX); 87 | thickness = thickness < minBridgeThickness ? minBridgeThickness : thickness; 88 | thickness = thickness > deltaX ? deltaX : thickness; 89 | } 90 | let bridgeX1 = ((rectX2 - rectX1)/2) - (thickness/2) + rectX1; 91 | let bridgeX2 = ((rectX2 - rectX1)/2) + (thickness/2) + rectX1; 92 | 93 | // bridgeX1 = bridgeX1 < rectX1 ? rectX1 : bridgeX1; 94 | // bridgeX2 = bridgeX2 > rectX2 ? rectX2 : bridgeX2; 95 | 96 | bridgeRect = [[bridgeX1, rectY1], [bridgeX2, rectY1], [bridgeX2, rectY2], [bridgeX1, rectY2]]; 97 | //console.log('bridgeRect ', bridgeRect); 98 | polygonsArr = polygonBoolean(polygonsArr, bridgeRect, 'or'); 99 | 100 | } else { 101 | // lineDirection == 'v' - build horizontal bridge 102 | //console.log('vertical parallel lines - make horizontal bridge'); 103 | // - find the overlapping Y pixels 104 | let rectX1 = startLine[0][0]; 105 | let rectX2 = endLine[0][0]; 106 | let rectY1 = startLine[0][1] > endLine[0][1] ? startLine[0][1] : endLine[0][1]; 107 | let rectY2 = startLine[1][1] < endLine[1][1] ? startLine[1][1] : endLine[1][1]; 108 | rectY2 = rectY2 - rectY1 < minBridgeThickness ? rectY1 + minBridgeThickness : rectY2; 109 | 110 | let deltaX = Math.abs(rectX2 - rectX1); 111 | let deltaY = Math.abs(rectY2 - rectY1); 112 | let thickness = minBridgeThickness; 113 | 114 | if(style == 'elastic'){ 115 | thickness = deltaY * ((1/(deltaX*deltaX))*deltaY); 116 | thickness = thickness < minBridgeThickness ? minBridgeThickness : thickness; 117 | thickness = thickness > deltaY ? deltaY : thickness; 118 | } 119 | let bridgeY1 = ((rectY2 - rectY1)/2) - (thickness/2) + rectY1; 120 | let bridgeY2 = ((rectY2 - rectY1)/2) + (thickness/2) + rectY1; 121 | 122 | bridgeRect = [[rectX1, bridgeY1], [rectX2, bridgeY1], [rectX2, bridgeY2], [rectX1, bridgeY2]]; 123 | //console.log('bridgeRect ', bridgeRect); 124 | polygonsArr = polygonBoolean(polygonsArr, bridgeRect, 'or'); 125 | 126 | } 127 | } else { 128 | // points are orthoganol to each other 129 | //console.log('adjascent'); 130 | 131 | if(startLine[0][0] == startLine[1][0]){ 132 | //start line is vertical - build horizontal bridge segment 133 | //console.log('- h then v'); 134 | let startX = startPoint[0]; 135 | let endX = endPoint[0] + minBridgeThickness; 136 | //let startY = startPoint[1] - minBridgeThickness; 137 | //let endY = startPoint[1]; 138 | // if(startPoint[1] == startLine[0][1] && startPoint[1] < startLine[1][1]){ 139 | // startY = startPoint[1]; 140 | // endY = startPoint[1] + minBridgeThickness; 141 | // } 142 | 143 | let startY = startPoint[1] < startLine[1][1] ? startPoint[1] : startPoint[1] - minBridgeThickness; 144 | let endY = startPoint[1] < startLine[1][1] ? startPoint[1] + minBridgeThickness : startPoint[1]; 145 | 146 | let rectX1 = startX; 147 | let rectX2 = endX; 148 | let rectY1 = startY; 149 | let rectY2 = endY; 150 | 151 | bridgeRect = [[rectX1, rectY1], [rectX2, rectY1], [rectX2, rectY2], [rectX1, rectY2]]; 152 | //console.log('bridgeRect ', bridgeRect); 153 | polygonsArr = polygonBoolean(polygonsArr, bridgeRect, 'or'); 154 | 155 | // build vertical bridge segment 156 | startY = rectY1; 157 | endY = endPoint[1]; 158 | startX = rectX2 - minBridgeThickness; 159 | endX = rectX2; 160 | if(startPoint[0] == startLine[0][0] && startPoint[0] < startLine[1][0]){ 161 | startX = startPoint[0]; 162 | endX = startPoint[0] + minBridgeThickness; 163 | } 164 | rectX1 = startX; 165 | rectX2 = endX; 166 | rectY1 = startY; 167 | rectY2 = endY; 168 | 169 | bridgeRect = [[rectX1, rectY1], [rectX2, rectY1], [rectX2, rectY2], [rectX1, rectY2]]; 170 | //console.log('bridgeRect ', bridgeRect); 171 | polygonsArr = polygonBoolean(polygonsArr, bridgeRect, 'or'); 172 | 173 | } else { 174 | // line A is Horizontal - build vertical bridge segment 175 | //console.log('- v then h'); 176 | 177 | let startY = startPoint[1]; 178 | let endY = endPoint[1]; 179 | 180 | // let startX = startPoint[0] < endPoint[0] ? startPoint[0] - minBridgeThickness : startPoint[0]; 181 | // let endX = startPoint[0] < endPoint[0] ? startPoint[0]: startPoint[0] - minBridgeThickness; 182 | 183 | let startX = startPoint[0] < startLine[1][0] ? startPoint[0] : startPoint[0] - minBridgeThickness; 184 | let endX = startPoint[0] < startLine[1][0] ? startPoint[0] + minBridgeThickness : startPoint[0] ; 185 | 186 | let rectX1 = startX; 187 | let rectX2 = endX; 188 | let rectY1 = startY; 189 | let rectY2 = endY; 190 | 191 | bridgeRect = [[rectX1, rectY1], [rectX2, rectY1], [rectX2, rectY2], [rectX1, rectY2]]; 192 | //console.log('bridgeRect ', bridgeRect); 193 | polygonsArr = polygonBoolean(polygonsArr, bridgeRect, 'or'); 194 | 195 | // build horizontal line segment 196 | 197 | //console.log('startLine: ', startLine); 198 | //console.log('endLine: ', endLine); 199 | startY = endPoint[1] < endLine[1][1] ? endPoint[1] : endPoint[1] - minBridgeThickness; 200 | endY = endPoint[1] < endLine[1][1] ? endPoint[1] + minBridgeThickness : endPoint[1]; 201 | 202 | startX = rectX1; 203 | endX = endPoint[0] + minBridgeThickness; 204 | if(startPoint[0] == startLine[0][0] && startPoint[0] < startLine[1][0]){ 205 | startX = startPoint[0]; 206 | endX = startPoint[0] + minBridgeThickness; 207 | } 208 | rectX1 = startX; 209 | rectX2 = endX; 210 | rectY1 = startY; 211 | rectY2 = endY; 212 | 213 | bridgeRect = [[rectX1, rectY1], [rectX2, rectY1], [rectX2, rectY2], [rectX1, rectY2]]; 214 | //console.log('bridgeRect ', bridgeRect); 215 | polygonsArr = polygonBoolean(polygonsArr, bridgeRect, 'or'); 216 | } 217 | } 218 | 219 | // console.log('bridgePoints result data', bridgeRect); 220 | 221 | //console.log('joinPolygons result data', JSON.stringify(polygonsArr)); 222 | } 223 | return(polygonsArr); 224 | } 225 | 226 | // a function to find the closest points between all of the polygons 227 | function findNearestPointsInAll(polygonArr){ 228 | let shortestDelta = Number.POSITIVE_INFINITY; 229 | let shortestResult; 230 | for (let i = 0; i < polygonArr.length; i++) { 231 | for (let j = i+1; j < polygonArr.length; j++) { 232 | let result = findNearestPoints(polygonArr[i], polygonArr[j]); 233 | if(result.delta < shortestDelta){ 234 | shortestDelta = result.delta; 235 | shortestResult = result; 236 | } 237 | } 238 | } 239 | return shortestResult; 240 | } 241 | 242 | function findNearestPoints(polygonA, polygonB) { 243 | let closestDelta = Number.POSITIVE_INFINITY; 244 | let closestPoint = polygonA[0]; 245 | let closestPointIndex = 0; 246 | let closestLine = [polygonB[0], polygonB[1]]; 247 | 248 | // compare every point in PolygonA to every line in PolygonB and find closest distance 249 | polygonA.forEach( (pointA, indexA, arrA) => { 250 | let thisClosestPoint; 251 | let thisClosestLine; 252 | let thisClosestPointIndex; 253 | 254 | let thisClosestDelta = polygonB.reduce( (prev, curr, i, arr) => { 255 | let minDistance = 0; 256 | if (i < arr.length -1) { 257 | minDistance = pDistance(pointA[0], pointA[1], arr[i][0], arr[i][1], arr[i+1][0], arr[i+1][1]); 258 | } else { 259 | minDistance = pDistance(pointA[0], pointA[1], arr[i][0], arr[i][1], arr[0][0], arr[0][1]); 260 | } 261 | //console.log('minDistance ', minDistance); 262 | if (minDistance < prev) { 263 | thisClosestPoint = pointA; 264 | thisClosestPointIndex = indexA; 265 | 266 | let thisNextPointI = i+1 < arr.length ? i+1 : 0; 267 | let thisPrevPointI = i-1 >= 0 ? i-1 : arr.length-1; 268 | 269 | thisClosestLine = [arr[i], arr[thisNextPointI]]; 270 | 271 | //console.log('compare: ', thisClosestPoint[1], arr[i][1], arr[thisNextPointI][1]); 272 | if((thisClosestPoint[0] == arr[i][0]) && (thisClosestPoint[0] == arr[thisNextPointI][0])){ 273 | // all x's are same- aligned on Y axis 274 | thisClosestLine = [arr[i], arr[thisPrevPointI]]; 275 | //console.log('swapping vertical line'); 276 | } else if ((thisClosestPoint[1] == arr[i][1]) && (thisClosestPoint[1] == arr[thisNextPointI][1])){ 277 | // all Y's are same- aligned on X axis 278 | thisClosestLine = [arr[i], arr[thisPrevPointI]]; 279 | //console.log('swapping H line'); 280 | } 281 | 282 | return minDistance; 283 | } else { 284 | return prev; 285 | } 286 | }, Number.POSITIVE_INFINITY); 287 | 288 | //console.log('thisClosestDelta ', thisClosestDelta); 289 | if (thisClosestDelta < closestDelta){ 290 | closestDelta = thisClosestDelta; 291 | closestPoint = thisClosestPoint; 292 | closestLine = thisClosestLine; 293 | closestPointIndex = thisClosestPointIndex; 294 | } 295 | 296 | }); 297 | 298 | //console.log('closestDelta: ', closestDelta); 299 | let lineDirection = 'p'; 300 | let closestPointB = closestLine[0][0]; 301 | let overlap = false; 302 | let lineA = [[0,0],[0,10]]; 303 | 304 | // find if points are alligned 305 | //if(closestPoint[0] == closestLine[0][0]){ 306 | // x coordinate is alligned 307 | //overlap = true; 308 | //console.log("x align"); 309 | //} else if (closestPoint[1] == closestLine[0][1]){ 310 | // y coordinate is alligned 311 | //overlap = true; 312 | //console.log("y align"); 313 | // } 314 | 315 | let prevPointIndex = closestPointIndex-1; 316 | let nextPointIndex = closestPointIndex+1; 317 | 318 | if (prevPointIndex < 0) prevPointIndex = polygonA.length - 1; 319 | if (nextPointIndex > polygonA.length-1) nextPointIndex = 0; 320 | 321 | if(closestLine[0][0] == closestLine[1][0]){ 322 | // Manage Vertical Lines 323 | lineDirection = 'v' 324 | // reorder points on line 325 | if(closestLine[0][1] > closestLine[1][1]){ 326 | closestLine = [closestLine[1], closestLine[0]] 327 | } 328 | if(Math.abs(closestPoint[1] - closestLine[0][1]) < Math.abs(closestPoint[1] - closestLine[1][1]) ){ 329 | closestPointB = closestLine[0]; 330 | } else { 331 | closestPointB = closestLine[1]; 332 | } 333 | if(closestPoint[1] >= closestLine[0][1] && closestPoint[1] <= closestLine[1][1]){ 334 | overlap = true; 335 | if (polygonA[prevPointIndex][0] == closestPoint[0]){ 336 | // prev point on PolygonA is vertically offset 337 | lineA = [polygonA[prevPointIndex], polygonA[closestPointIndex]]; 338 | } else if (polygonA[nextPointIndex][0] == closestPoint[0]) { 339 | // next point is vertically offset 340 | lineA = [polygonA[nextPointIndex], polygonA[closestPointIndex]]; 341 | } 342 | if(lineA[0][1] > lineA[1][1]){ 343 | lineA = [lineA[1], lineA[0]] 344 | } 345 | } else { 346 | // nearest line is orthoganol (non overlaping) so select a perpendicular lineA 347 | if (polygonA[prevPointIndex][1] == closestPoint[1]){ 348 | // prev point on PolygonA is horizontally offset 349 | lineA = [polygonA[nextPointIndex], polygonA[closestPointIndex]]; 350 | } else if (polygonA[nextPointIndex][1] == closestPoint[1]) { 351 | // next point is horizontally offset 352 | lineA = [polygonA[prevPointIndex], polygonA[closestPointIndex]]; 353 | } 354 | if(lineA[0][1] > lineA[1][1]){ 355 | lineA = [lineA[1], lineA[0]]; 356 | } 357 | } 358 | 359 | } else if(closestLine[0][1] == closestLine[1][1]){ 360 | // closestLine is Horizontal 361 | 362 | lineDirection = 'h'; 363 | // reorder points in line 364 | if(closestLine[0][0] > closestLine[1][0]){ 365 | closestLine = [closestLine[1], closestLine[0]] 366 | } 367 | // find closest point on line 368 | if(Math.abs(closestPoint[0] - closestLine[0][0]) < Math.abs(closestPoint[0] - closestLine[1][0]) ){ 369 | closestPointB = closestLine[0]; 370 | } else { 371 | closestPointB = closestLine[1]; 372 | } 373 | // find if lines overlap in X pixels 374 | if(closestPoint[0] >= closestLine[0][0] && closestPoint[0] <= closestLine[1][0]){ 375 | overlap = true; 376 | // find the parallel line on polygonA 377 | if (polygonA[prevPointIndex][1] == closestPoint[1]){ 378 | // prev point on PolygonA is horizontally offset 379 | lineA = [polygonA[prevPointIndex], polygonA[closestPointIndex]]; 380 | } else if (polygonA[nextPointIndex][1] == closestPoint[1]) { 381 | // next point is horizontally offset 382 | lineA = [polygonA[nextPointIndex], polygonA[closestPointIndex]]; 383 | } 384 | if(lineA[0][0] > lineA[1][0]){ 385 | lineA = [lineA[1], lineA[0]]; 386 | } 387 | } else { 388 | // nearest line is orthoganol (non overlaping) so select a perpendicular lineA 389 | if (polygonA[prevPointIndex][1] == closestPoint[1]){ 390 | // prev point on PolygonA is horizontally offset 391 | lineA = [polygonA[nextPointIndex], polygonA[closestPointIndex]]; 392 | } else if (polygonA[nextPointIndex][1] == closestPoint[1]) { 393 | // next point is horizontally offset 394 | lineA = [polygonA[prevPointIndex], polygonA[closestPointIndex]]; 395 | } 396 | if(lineA[0][1] > lineA[1][1]){ 397 | lineA = [lineA[1], lineA[0]]; 398 | } 399 | } 400 | } 401 | 402 | return ({ delta: closestDelta, 403 | pointA: closestPoint, 404 | pointB: closestPointB, 405 | lineA: lineA, 406 | lineB: closestLine, 407 | overlap: overlap}); 408 | } 409 | 410 | // finds shortest distance between point and line 411 | function pDistance(x, y, x1, y1, x2, y2) { 412 | var A = x - x1; 413 | var B = y - y1; 414 | var C = x2 - x1; 415 | var D = y2 - y1; 416 | 417 | var dot = A * C + B * D; 418 | var len_sq = C * C + D * D; 419 | var param = -1; 420 | if (len_sq != 0) //in case of 0 length line 421 | param = dot / len_sq; 422 | 423 | var xx, yy; 424 | 425 | if (param < 0) { 426 | xx = x1; 427 | yy = y1; 428 | } 429 | else if (param > 1) { 430 | xx = x2; 431 | yy = y2; 432 | } 433 | else { 434 | xx = x1 + param * C; 435 | yy = y1 + param * D; 436 | } 437 | 438 | var dx = x - xx; 439 | var dy = y - yy; 440 | return Math.sqrt(dx * dx + dy * dy); 441 | } 442 | 443 | 444 | 445 | function cleanPolygon(polygon) { 446 | // clean up redundant points on a line 447 | let polygonLength = 0; 448 | 449 | while(polygon.length != polygonLength && polygon.length > 2) { 450 | 451 | polygonLength = polygon.length; 452 | for (let i=0; i polygon.length-1) x2i = (x2i - polygon.length); 455 | var x3i = i+2; 456 | if (x3i > polygon.length-1) x3i = (x3i - polygon.length); 457 | //console.log('is ', hull.length, i, x2i, x3i ); 458 | if (polygon[i][0] == polygon[x2i][0] && polygon[i][0] == polygon[x3i][0]) { 459 | //console.log('remove: ', hull[i-2].x, hull[i-1].x, hull[i].x ); 460 | polygon.splice(x2i, 1); 461 | } 462 | } 463 | for (let i=0; i < polygon.length; i++) { 464 | var y2i = i+1; 465 | if (y2i > polygon.length-1) y2i = (y2i - polygon.length); 466 | var y3i = i+2; 467 | if (y3i > polygon.length-1) y3i = (y3i - polygon.length); 468 | if (polygon[i][1] == polygon[y2i][1] && polygon[i][1] == polygon[y3i][1]) { 469 | //console.log('remove: ', hull[i-2].x, hull[i-1].x, hull[i].x ); 470 | polygon.splice(y2i, 1); 471 | } 472 | } 473 | 474 | } 475 | return polygon; 476 | } 477 | 478 | 479 | 480 | function findNearestRect(rectA, rects) { 481 | 482 | let neighbors = rects.forEach((rectB) => { 483 | // make sure rects are not intersecting 484 | if (rectA !== rectB && !isIntersecting(rectA, rectB)) { 485 | // find closest faces 486 | let minDist = Number.POSITIVE_INFINITY; 487 | } 488 | }); 489 | } 490 | 491 | // determine if two rects intersect 492 | function isIntersecting(rectA, rectB) { 493 | let overlappingX = false; 494 | let overlappingY = false; 495 | 496 | if (rectA.x1 == rectB.x1 && rectA.x2 == rectB.x2 && 497 | rectA.y1 == rectB.y1 && rectA.y2 == rectB.y2) { 498 | //same 499 | return true; 500 | } 501 | if (rectA.x1 >= rectB.x1 && rectA.x1 <= rectB.x2) { 502 | overlappingX = true; 503 | } else if (rectA.x2 >= rectB.x1 && rectA.x2 <= rectB.x2) { 504 | overlappingX = true; 505 | } 506 | if (rectA.y1 >= rectB.y1 && rectA.y1 <= rectB.y2) { 507 | overlappingY = true; 508 | } else if (rectA.y2 >= rectB.y1 && rectA.y2 <= rectB.y2) { 509 | overlappingY = true; 510 | } 511 | //console.log(' - result x y: ' , overlappingX, overlappingY); 512 | if (overlappingY && overlappingX) { 513 | //console.log('overlapping: ', JSON.stringify(rectA), JSON.stringify(rectB)); 514 | return true; 515 | } else { 516 | return false; 517 | } 518 | } 519 | 520 | function findIntersecting(rects) { 521 | let overlappingX = false; 522 | let overlappingY = false; 523 | 524 | let overlappingRects = rects.filter((rectA, i, rects) => 525 | { 526 | //console.log('rectPoints ', JSON.stringify(rectPoints)); 527 | for (let rectB of rects) { 528 | overlappingX = false; 529 | overlappingY = false; 530 | // debugger; 531 | //console.log('checking: ' , JSON.stringify(rectA), JSON.stringify(rectB)); 532 | if (!(rectA.x1 === rectB.x1 && rectA.x2 === rectB.x2 && 533 | rectA.y1 === rectB.y1 && rectA.y2 === rectB.y2)) { 534 | if (rectA.x1 >= rectB.x1 && rectA.x1 <= rectB.x2) { 535 | overlappingX = true; 536 | } else if (rectA.x2 >= rectB.x1 && rectA.x2 <= rectB.x2) { 537 | overlappingX = true; 538 | } 539 | if (rectA.y1 >= rectB.y1 && rectA.y1 <= rectB.y2) { 540 | overlappingY = true; 541 | } else if (rectA.y2 >= rectB.y1 && rectA.y2 <= rectB.y2) { 542 | overlappingY = true; 543 | } 544 | //console.log(' - result x y: ' , overlappingX, overlappingY); 545 | if (overlappingY && overlappingX) { 546 | //console.log('overlapping: ', JSON.stringify(rectA), JSON.stringify(rectB)); 547 | return true; 548 | } 549 | } 550 | } 551 | return false; 552 | }); 553 | return(overlappingRects); 554 | } 555 | -------------------------------------------------------------------------------- /src/lib/polygon-union/roundedSVGPath.js: -------------------------------------------------------------------------------- 1 | export default function roundedSVGPath(points, r) { 2 | 3 | let svgPath; 4 | 5 | if (!points.length) return; 6 | 7 | //console.log('roundedSVGPath: ', points); 8 | 9 | // see if path is a closed loop; 10 | if (points[0][0] !== points[points.length-1][0] || points[0][1] !== points[points.length-1][1]) { 11 | // close the loop 12 | points.push(points[0]); 13 | //console.log('closed roundedSVGPath: ', points); 14 | } 15 | 16 | //compute the middle of the first line as start-stop-point: 17 | let deltaX, deltaY, deltaX2, deltaY2, deltaX3, deltaY3, startX, startY; 18 | let radius1, radius2, radius3; 19 | 20 | svgPath = 'M'; 21 | let prevRadius, radius; 22 | prevRadius = r; 23 | radius = r; 24 | deltaX = points[1][0] - points[0][0]; 25 | deltaY = points[1][1] - points[0][1]; 26 | 27 | if (deltaX != 0 && Math.abs(deltaX)/2 < prevRadius) { 28 | prevRadius = Math.abs(deltaX)/2; 29 | } 30 | if (deltaY != 0 && Math.abs(deltaY)/2 < prevRadius) { 31 | prevRadius = Math.abs(deltaY)/2; 32 | } 33 | radius = prevRadius; 34 | //console.log('prevRadius', prevRadius); 35 | 36 | for (let i = 1; i < points.length; i++) { 37 | 38 | // some logic to deel with closing the loop 39 | if (i === 0) { 40 | //console.log('i==0'); 41 | deltaX = points[i][0] - points[points.length-1][0]; 42 | deltaY = points[i][1] - points[points.length-1][1]; 43 | deltaX2 = points[i+1][0] - points[i][0]; 44 | deltaY2 = points[i+1][1] - points[i][1]; 45 | deltaX3 = points[i+2][0] - points[i+1][0]; 46 | deltaY3 = points[i+2][1] - points[i+1][1]; 47 | } else if (i < points.length-2) { 48 | //console.log('i 0) { 120 | // up then right 121 | //console.log(' then curve right ', radius); 122 | svgPath += ' s 0 ' + negRadius + ' ' + radius + ' ' + negRadius; 123 | } 124 | } else if (deltaY > 0) { 125 | //down 126 | 127 | if (svgPath == 'M') { 128 | // console.log('start') 129 | svgPath += ' ' + points[0][0] + ' ' + (points[0][1] + prevRadius); 130 | } 131 | // console.log('down ', deltaY); 132 | 133 | svgPath += ' l ' + (deltaX) + ' ' + (deltaY - (prevRadius + radius) ); 134 | if (deltaX2 < 0) { 135 | // down then left 136 | svgPath += ' s 0 ' + radius + ' ' + negRadius + ' ' + radius; 137 | } else if (deltaX2 > 0) { 138 | // down then right 139 | svgPath += ' s 0 ' + ' ' + radius + ' ' + radius + ' ' + radius; 140 | } 141 | } else if (deltaX < 0) { 142 | // left 143 | 144 | if (svgPath == 'M') { 145 | // console.log('start ', points[0][0], '-', radius, ',', points[0][1]); 146 | svgPath += ' ' + (points[0][0] - (prevRadius)) + ' ' + (points[0][1]); 147 | } 148 | 149 | // console.log('left ', deltaX, prevRadius, radius); 150 | 151 | svgPath += ' l ' + (deltaX + (prevRadius + radius)) + ' ' + (deltaY); 152 | 153 | if (deltaY2 < 0) { 154 | // left, then up 155 | //console.log('- then curve up ', radius); 156 | svgPath += ' s ' + (negRadius) + ' 0 ' + (negRadius) + ' ' + (negRadius); 157 | } 158 | if (deltaY2 > 0) { 159 | // left, then down 160 | //console.log('- then curve down ', radius); 161 | svgPath += ' s ' + negRadius + ' 0 ' + negRadius + ' '+ radius; 162 | } 163 | } else if (deltaX > 0) { 164 | // right 165 | if (svgPath === 'M') { 166 | // console.log('start ', points[0][0], '+', radius, ',', points[0][1]); 167 | svgPath += ' ' + (points[0][0] + prevRadius) + ' ' + (points[0][1]); 168 | } 169 | // console.log('right ', deltaX, ' - (', prevRadius, ' + ' + radius, ')'); 170 | 171 | svgPath += ' l ' + (deltaX - (prevRadius + radius)) + ' ' + (deltaY); 172 | if (deltaY2 > 0) { 173 | //console.log('- then curve down ', radius); 174 | // right, then down 175 | svgPath += ' s ' + radius + ' 0 ' + radius + ' ' + radius; 176 | } 177 | if (deltaY2 < 0) { 178 | //console.log('- then curve up ', radius); 179 | // right, then up 180 | svgPath += ' s ' + ' ' + radius + ' 0 ' + radius + ' ' + negRadius; 181 | } 182 | } 183 | 184 | } 185 | // close the shape 186 | svgPath += ' z'; 187 | // console.log('rounded svgPath: ', svgPath); 188 | return svgPath; 189 | } 190 | -------------------------------------------------------------------------------- /src/lib/rect2rectPoints.js: -------------------------------------------------------------------------------- 1 | export default function rect2rectPoints(rect) { 2 | if ('x1' in rect && 'y1' in rect && 'x2' in rect && 'y2' in rect) return rect; 3 | 4 | if ('x' in rect && 'y' in rect && 'width' in rect && 'height' in rect) { 5 | return { 6 | x1: rect.x, 7 | y1: rect.y, 8 | x2: rect.x + rect.width, 9 | y2: rect.y + rect.height, 10 | }; 11 | } else { 12 | return { x1: 0, y1: 0, x2: 0, y2: 0 }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import assert from 'assert'; 4 | import Blobber from '../src/Blobber'; 5 | 6 | const rects = [ 7 | { x: 30, y: 250, width: 150, height: 24 }, 8 | { x: 100, y: 285, width: 150, height: 24 }, 9 | { x: 200, y: 310, width: 150, height: 24 }, 10 | ]; 11 | 12 | describe('Blobber', () => { 13 | 14 | it('renders the component', () => { 15 | const wrapper = shallow(); 16 | assert(wrapper.find('svg')); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var webpack = require('webpack'); 4 | 5 | var babelrcObj = JSON.parse(fs.readFileSync('./.babelrc')); 6 | babelrcObj.plugins = []; 7 | babelrcObj.plugins.push(['react-transform', { 8 | transforms: [ 9 | { 10 | transform: 'react-transform-hmr', 11 | imports: ['react'], 12 | locals: ['module'] 13 | } 14 | ] 15 | }]); 16 | 17 | module.exports = { 18 | devtool: 'inline-source-map', 19 | entry: [ 20 | 'webpack-dev-server/client?http://localhost:3000', 21 | './dev-client.js' 22 | ], 23 | output: { 24 | path: path.join(__dirname, 'dist'), 25 | filename: 'react-blobber.js', 26 | publicPath: '/js/' 27 | }, 28 | module: { 29 | loaders: [ 30 | { test: /\.jsx?$/, loader: 'babel?' + JSON.stringify(babelrcObj), exclude: /node_modules/ }, 31 | { test: /\.css$/, loader: 'style-loader!css-loader!postcss-loader' }, 32 | ] 33 | }, 34 | postcss: function () { 35 | return [ 36 | require('postcss-cssnext')(), 37 | ]; 38 | }, 39 | progress: true, 40 | resolve: { 41 | modulesDirectories: [ 42 | 'src', 43 | 'node_modules' 44 | ], 45 | extensions: ['', '.js', '.jsx'] 46 | }, 47 | plugins: [ 48 | new webpack.HotModuleReplacementPlugin(), 49 | new webpack.NoErrorsPlugin(), 50 | ], 51 | }; 52 | --------------------------------------------------------------------------------