├── .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 | [](https://travis-ci.org/scienceai/react-blobber)
4 | [](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 |
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 |
this.setState({ algorithm: e.target.value })} defaultValue='convex-hull'>
115 | algorithm: convex hull
116 | algorithm: polygon union
117 |
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 |
--------------------------------------------------------------------------------