├── .gitignore ├── readme_ja.md ├── variations ├── readme_ja.md ├── img │ ├── desc_circlepack_in_a_circle.png │ └── desc_circlepack_in_a_circle_web.png ├── circlepacking_in_a_circle_web │ ├── index.html │ ├── circlepacking_in_a_circle_web.js │ └── lib │ │ └── canvas2svg.js ├── readme.md └── circlepacking_in_a_circle.jsx ├── img ├── desc_circlepack01a.png ├── desc_circlepack02.png ├── desc_circlepack03a.png ├── desc_circlepack04a.png ├── desc_circlepack05b.png └── desc_circlepack06.png ├── LICENSE.txt ├── readme.md └── circlepacking.jsx /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shspage/illustrator-circlepacking/HEAD/.gitignore -------------------------------------------------------------------------------- /readme_ja.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shspage/illustrator-circlepacking/HEAD/readme_ja.md -------------------------------------------------------------------------------- /variations/readme_ja.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shspage/illustrator-circlepacking/HEAD/variations/readme_ja.md -------------------------------------------------------------------------------- /img/desc_circlepack01a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shspage/illustrator-circlepacking/HEAD/img/desc_circlepack01a.png -------------------------------------------------------------------------------- /img/desc_circlepack02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shspage/illustrator-circlepacking/HEAD/img/desc_circlepack02.png -------------------------------------------------------------------------------- /img/desc_circlepack03a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shspage/illustrator-circlepacking/HEAD/img/desc_circlepack03a.png -------------------------------------------------------------------------------- /img/desc_circlepack04a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shspage/illustrator-circlepacking/HEAD/img/desc_circlepack04a.png -------------------------------------------------------------------------------- /img/desc_circlepack05b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shspage/illustrator-circlepacking/HEAD/img/desc_circlepack05b.png -------------------------------------------------------------------------------- /img/desc_circlepack06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shspage/illustrator-circlepacking/HEAD/img/desc_circlepack06.png -------------------------------------------------------------------------------- /variations/img/desc_circlepack_in_a_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shspage/illustrator-circlepacking/HEAD/variations/img/desc_circlepack_in_a_circle.png -------------------------------------------------------------------------------- /variations/img/desc_circlepack_in_a_circle_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shspage/illustrator-circlepacking/HEAD/variations/img/desc_circlepack_in_a_circle_web.png -------------------------------------------------------------------------------- /variations/circlepacking_in_a_circle_web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | circle pack example 4 | 5 | 6 | 14 | 15 |

reload 16 | | export

17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Hiroyuki Sato 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the Software 9 | is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /variations/readme.md: -------------------------------------------------------------------------------- 1 | illustrator-circlepacking : variations 2 | ====================== 3 | [readme in Japanese](https://github.com/shspage/illustrator-circlepacking/blob/master/variations/readme_ja.md) 4 | 5 | ## circlepacking_in_a_circle.jsx 6 | 7 | ![desc_circlepack_in_a_circle](https://github.com/shspage/illustrator-circlepacking/raw/master/variations/img/desc_circlepack_in_a_circle.png) 8 | 9 | **USAGE** : Select a circle (or already drawn circles in a circle), and run this script. 10 | 11 | In this variation, I added some restriction in moving range to the original script. 12 | It is still based on Euclidean geometry. 13 | 14 | Because of the restriction, it seems that the convergence is relatively slow. 15 | So I set a large value for "max_dist_err_last_phase_threshold" -- one of the optional values. 16 | The error can be small enough after the last phase, or it can't be. 17 | Please note that **the final error can be noticeable.** 18 | You can continue the process for adjusting, by selecting circles and running this script again. 19 | Considering the usage like this, I added the process that excludes the outer circle from the selection. 20 | So you can select it with circles. 21 | 22 | 23 | ## circlepacking_in_a_circle_web.js 24 | 25 | ![desc_circlepack_in_a_circle_web](https://github.com/shspage/illustrator-circlepacking/raw/master/variations/img/desc_circlepack_in_a_circle_web.png) 26 | 27 | This is a variation of circlepacking_in_a_circle.jsx which draws on HTML5 canvas. 28 | You can see the transition of convergence with its animation. 29 | [See it in action](http://shspage.com/ex/circlepacking_in_a_circle/index.html). 30 | 31 | 32 | ---------------------- 33 | Copyright(c) 2016 Hiroyuki Sato 34 | [https://github.com/shspage](https://github.com/shspage) 35 | This script is distributed under the MIT License. 36 | See the LICENSE file for details. 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | illustrator-circlepacking 2 | ====================== 3 | [readme in Japanese](https://github.com/shspage/illustrator-circlepacking/blob/master/readme_ja.md) 4 | 5 | **Use Download button (on the right side of this page) to download ZIP file.** 6 | **If you use right click on each file to save, you'll get an HTML file.** 7 | 8 | This is a script for Adobe Illustrator that draws non overlapping tangent circles, in Euclidean geometry. 9 | 10 | ![desc_circlepack01a](https://github.com/shspage/illustrator-circlepacking/raw/master/img/desc_circlepack01a.png) 11 | 12 | **USAGE**: Select a rectangle path or circles, and run this script. 13 | A window with an output field and buttons will open. 14 | 15 | ## buttons 16 | * __exec__ : starts new process. 17 | * __abort__ : aborts the process. 18 | * __close__ : closes the window. 19 | 20 | ## modes 21 | **mode-1** : if only 1 object is selected : generates random points inside it. 22 | **mode-2** : if 3 or more path is selected : arranges selected circles. (draws arranged circles **anew**) 23 | 24 | You can edit the number of random points and other settings. They are at the beginning of the script. 25 | 26 | You'll notice big circles on the edge. Please think them like the heel of bread. 27 | 28 | 29 | ## error value 30 | While running, this script displays "**max dist err**" in the window. 31 | It means the maximum distance between circles. Of course 0.0 is ideal. But 2.0 or lower value seemed good enough. 32 | If it keeps growing or is seemed hard to converge, something is wrong. You should abort the process. 33 | 34 | The circle which has "max dist err" is marked with red like the following image. 35 | If you don't like this coloring, edit the beginning part of the script to set "**mark_with_red_for_max_dist_err_circle**" to **false**. (ver.1.0.3) 36 | ![desc_circlepack06](https://github.com/shspage/illustrator-circlepacking/raw/master/img/desc_circlepack06.png) 37 | 38 | If you feel the error is still noticeable, you can select drawn circles and run this script again to continue adjusting process. 39 | (Make sure not to select the rectangle frame at this time.) 40 | 41 | 42 | ## example 43 | This is an example image which applied path-offset effect after generated circles. 44 | 45 | ![desc_circlepack04a](https://github.com/shspage/illustrator-circlepacking/raw/master/img/desc_circlepack04a.png) 46 | 47 | 48 | ## variations 49 | There're some variations inside "variations" folder. The following image is a web browser version. 50 | 51 | ![desc_circlepack_in_a_circle_web](https://github.com/shspage/illustrator-circlepacking/raw/master/variations/img/desc_circlepack_in_a_circle_web.png) 52 | 53 | 54 | ## algorithm 55 | I don't know if this is the best way. But it produces the fastest and the most accurate result for now. I guess there're more mathematically convincing methods. 56 | 57 | 1. In mode-1, it generates random points inside the selected object (supposed a rectangle path). 58 | 59 | 2. Applies the Delaunay triangulation to the points. 60 | Assuming the drawn triangles are ideal for packing (though it never be), it determines an ideal radius for each vertex, using tangents of the incircle. 61 | ![desc_circlepack02](https://github.com/shspage/illustrator-circlepacking/raw/master/img/desc_circlepack02.png) 62 | 63 | 3. Finds the best position to each surrounding circles. And averages them to find temporary center. 64 | 65 | 4. Finds the best radius to each surrounding circles. And averages them to find temporary radius. 66 | ![desc_circlepack03a](https://github.com/shspage/illustrator-circlepacking/raw/master/img/desc_circlepack03a.png) 67 | 68 | 5. Repeats from "2" to "4" while the error value is greater than the threshold. 69 | 70 | 6. Repeats "3" and "4" many times for finishing. 71 | 72 | 73 | ## todo 74 | Implementing Delaunay refinement algorithm. 75 | Doing this in Hyperbolic geometry. 76 | 77 | ## references 78 | To implement the Delaunay triangulation, I referred to the following page (in Japanese) by Tercel. Thanks for helpful description of algorithm. 79 | [http://tercel-sakuragaoka.blogspot.jp/2011/06/processingdelaunay.html](http://tercel-sakuragaoka.blogspot.jp/2011/06/processingdelaunay.html) 80 | 81 | ---------------------- 82 | Copyright(c) 2016 Hiroyuki Sato 83 | [https://github.com/shspage](https://github.com/shspage) 84 | This script is distributed under the MIT License. 85 | See the LICENSE file for details. 86 | -------------------------------------------------------------------------------- /circlepacking.jsx: -------------------------------------------------------------------------------- 1 | #target "illustrator" 2 | 3 | // circlepacking 4 | // draws non overlapping tangent circles. 5 | // mode-1 : if only 1 object is selected : generates random points inside it. 6 | // mode-2 : if 3 or more path is selected : arranges selected circles. 7 | // (draws arranged circles anew) 8 | 9 | // Copyright(c) 2016 Hiroyuki Sato 10 | // https://github.com/shspage 11 | // This script is distributed under the MIT License. 12 | // See the LICENSE file for details. 13 | 14 | // ver.1.2.0 15 | 16 | var _opt = { 17 | number_of_random_points : 100, // in random point mode 18 | min_initial_radius : 4, // in random point mode 19 | 20 | stroke_width : 0.5, 21 | 22 | // Perhaps you don't need to change the followings 23 | max_dist_err_threshold : 0.2, // 2.0 seemed good enough 24 | max_dist_err_last_phase_threshold : 5.5, 25 | normal_loop_count : 50, 26 | last_phase_loop_count : 500, // re-configured at below 27 | large_angle_threshold : Math.cos(Math.PI * 2 / 3), 28 | 29 | // marks with red for the circle which has max error 30 | mark_with_red_for_max_dist_err_circle : true, 31 | // draws delaunay triangles and circles without arranging 32 | just_show_initial_status : false, 33 | // size of the text area in the dialog 34 | edittext_width : 400, 35 | edittext_height : 400 36 | } 37 | _opt.last_phase_loop_count = Math.max( 38 | _opt.last_phase_loop_count, _opt.number_of_random_points); 39 | 40 | var _g = { 41 | win : null, // window 42 | et : null, // edittext 43 | cancel : false // true if canceled. 44 | } 45 | 46 | var _WPI = Math.PI * 2; 47 | // ------------------------------------------------ 48 | function circlePackMain(){ 49 | var timer = new TimeChecker(); 50 | if(app.documents.length < 1){ 51 | putTextln("error: no document"); 52 | return; 53 | } 54 | 55 | var points = getInitialPoints(); 56 | if(points.length < 3){ 57 | putTextln("error: at least 3 points required"); 58 | return; 59 | } 60 | 61 | var circles; 62 | var loopTimes = 0; 63 | var isLast = false; 64 | var max_dist_err = _opt.max_dist_err_threshold + 1; 65 | var max_dist_err_idx = -1; 66 | 67 | while(max_dist_err > _opt.max_dist_err_threshold){ 68 | if(_g.cancel) break; 69 | 70 | loopTimes++; 71 | putTextln("loop: " + loopTimes); 72 | 73 | circles = getCirclesByTriangulation(points); 74 | if( _opt.just_show_initial_status) break; 75 | if(_g.cancel) break; 76 | 77 | circles = arrangeCircles(circles, isLast); 78 | if(_g.cancel) break; 79 | 80 | // check errors 81 | var max_dist_err_squared = 0; 82 | max_dist_err_idx = -1; 83 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 84 | if(_g.cancel) break; 85 | 86 | var error = circles[ci].verifyR(); 87 | if(error > max_dist_err_squared){ 88 | max_dist_err_squared = error; 89 | max_dist_err_idx = circles[ci].idx; 90 | } 91 | } 92 | if(_g.cancel) break; 93 | max_dist_err = Math.sqrt(max_dist_err_squared) 94 | putTextln("-- max_dist_err=" + max_dist_err); 95 | 96 | if(isLast) break; 97 | if(max_dist_err < _opt.max_dist_err_last_phase_threshold){ 98 | isLast = true; 99 | putTextln("#### next loop is the last phase"); 100 | } 101 | 102 | points = []; 103 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 104 | if(_g.cancel) break; 105 | points.push(circles[ci].o); 106 | } 107 | if(_g.cancel) break; 108 | } 109 | 110 | // draws circles 111 | if(_opt.mark_with_red_for_max_dist_err_circle){ 112 | putTextln("-- max dist err index = " + max_dist_err_idx); 113 | 114 | for(var i = 0, iEnd = circles.length; i < iEnd; i++){ 115 | if(_g.cancel) break; 116 | var c = circles[i]; 117 | drawCircle2(c, c.idx == max_dist_err_idx); 118 | } 119 | } else { 120 | for(var i = 0, iEnd = circles.length; i < iEnd; i++){ 121 | if(_g.cancel) break; 122 | var c = circles[i]; 123 | drawCircle(c.o, c.r); 124 | } 125 | } 126 | app.redraw(); 127 | if(_g.cancel) putTextln("### CANCELED ###"); 128 | timer.showResult(); 129 | } 130 | // ------------------------------------------------ 131 | // only 1 object is selected -> random point mode : generates points inside the object. 132 | // otherwise -> selected circles mode : gets selected paths (= circles). 133 | // returns : an array of Point 134 | function getInitialPoints(){ 135 | var points = []; 136 | 137 | var sel = app.activeDocument.selection; 138 | if(sel.length < 1){ 139 | alert("ERROR\nNothing selected."); 140 | 141 | } else if(sel.length == 1){ 142 | // random point mode 143 | points = distributeRandomPointsInRect( 144 | sel[0], _opt.number_of_random_points, _opt.min_initial_radius * 2); 145 | } else { 146 | // selected circles mode 147 | var paths = extractPaths(sel); 148 | if(paths.length < 3){ 149 | alert("ERROR\nSelect a rectangle or circles."); 150 | return; 151 | } 152 | for(var i = 0, iEnd = paths.length; i < iEnd; i++){ 153 | var c = getCircle(paths, i); 154 | points.push(c.o); 155 | } 156 | } 157 | putTextln("-- " + points.length + " points prepared"); 158 | return points; 159 | } 160 | // ------------------------------------------------ 161 | // frame : a rectangle path or other object which has geometricBounds 162 | // count : number of random points to generate 163 | // min_dist : minimum distance between points 164 | // returns : an array of Point 165 | function distributeRandomPointsInRect(frame, count, min_dist){ 166 | var points = []; 167 | var p, ok; 168 | 169 | var min_dist2 = min_dist * min_dist; 170 | var gb = frame.geometricBounds; 171 | var rect = { left:gb[0], top:gb[1], right:gb[2], bottom:gb[3], 172 | width:gb[2] - gb[0], height:gb[1] - gb[3] }; 173 | 174 | for(var i = 0; i < count; i++){ 175 | if(i % 100 == 0) putText(i + " "); 176 | while(true){ 177 | if(_g.cancel) break; 178 | p = new Point(Math.random() * rect.width + rect.left, 179 | Math.random() * rect.height + rect.bottom, i); 180 | ok = true; 181 | for(var pi = 0, piEnd = points.length; pi < piEnd; pi++){ 182 | if(points[pi].dist2(p) < min_dist2){ 183 | ok = false; 184 | break; 185 | } 186 | } 187 | if(ok) break; 188 | } 189 | if(_g.cancel) break; 190 | points.push(p); 191 | } 192 | 193 | return points; 194 | } 195 | // -------------------------------------- 196 | // points : an array of Point 197 | // returns : an array of Circle 198 | function getCirclesByTriangulation(points){ 199 | var p, c; 200 | var triangles = delaunay(points); 201 | 202 | // gets tmp radius 203 | for(var key in triangles){ 204 | triangles[key].findTmpRadiusForEachVertex(); 205 | } 206 | 207 | // sets radius 208 | for(var i = 0, iEnd = points.length; i < iEnd; i++){ 209 | points[i].fixInitialRadius(); 210 | } 211 | 212 | // creates circles 213 | var circles = []; 214 | for(var i = 0, iEnd = points.length; i < iEnd; i++){ 215 | p = points[i]; 216 | if(p.r == 0) continue; 217 | c = new Circle(p, p.r, i); 218 | circles.push(c); 219 | } 220 | 221 | // correlates vertex of triangles to each circle 222 | for(var key in triangles){ 223 | var t = triangles[key]; 224 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 225 | circles[ci].addTriangle(t, circles); 226 | } 227 | } 228 | 229 | // converts vertice to circles and detect if each circle is surrounded by others 230 | var tmp_circles = []; 231 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 232 | c = circles[ci]; 233 | c.fixCircles(circles); 234 | c.detectSurrounded(); 235 | } 236 | 237 | // removes circles unsuitable to tangent 238 | tmp_circles = []; 239 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 240 | c = circles[ci]; 241 | c.removeInvalidCircles(); 242 | if(c.circles.length > 0) tmp_circles.push(c); 243 | } 244 | circles = tmp_circles; 245 | 246 | // draws the initial status 247 | if(_opt.just_show_initial_status){ 248 | var grpTri = activeDocument.activeLayer.groupItems.add(); 249 | for(var key in triangles){ 250 | var tri = triangles[key]; 251 | var polygon = triangles[key].draw(); 252 | polygon.move(grpTri, ElementPlacement.PLACEATEND); 253 | } 254 | } 255 | 256 | return circles; 257 | } 258 | // -------------------------------------- 259 | // arranges center and radius of each circle 260 | // circles : an array of Circle 261 | // isLast : true if it is the last loop 262 | function arrangeCircles(circles, isLast){ 263 | putTextln("-- arrange: " + circles.length + " circles"); 264 | 265 | var loop_times = _opt.normal_loop_count; 266 | if(isLast) loop_times = _opt.last_phase_loop_count; 267 | var notify_loop_count = 50; 268 | 269 | var i = 0; 270 | for(; i < loop_times; i++){ 271 | if(_g.cancel) break; 272 | c_len = circles.length; 273 | for(var ci = 0; ci < c_len; ci++) circles[ci].findO(); 274 | for(var ci = 0; ci < c_len; ci++) circles[ci].fixO(); 275 | 276 | for(var ci = 0; ci < c_len; ci++) circles[ci].findR(); 277 | for(var ci = 0; ci < c_len; ci++) circles[ci].fixR(); 278 | 279 | if(i > 0 && i % notify_loop_count == 0) putText(" " + i); 280 | } 281 | if(i > notify_loop_count) putTextln("."); 282 | return circles; 283 | } 284 | 285 | // -------------------------------------- 286 | // delaunay triangulation 287 | // points : an array of Point 288 | 289 | // To implement the Delaunay triangulation, I referred to the following page 290 | // (in Japanese) by Tercel. Thanks for helpful description of algorithm. 291 | // http://tercel-sakuragaoka.blogspot.jp/2011/06/processingdelaunay.html 292 | function delaunay(points){ 293 | function addElementToRedundanciesMap(set, tri){ 294 | if(tri.key in set){ 295 | set[tri.key].overlap = true; 296 | } else { 297 | set[tri.key] = tri; 298 | } 299 | } 300 | 301 | // key : Triangle._getKey 302 | // value : Triangle instance 303 | var triangles = {}; 304 | 305 | var hugeTriangle = getHugeTriangle(points); 306 | triangles["huge"] = hugeTriangle; 307 | 308 | for(var i = 0, iEnd = points.length; i < iEnd; i++){ 309 | var p = points[i]; 310 | var tmp_triangles = {}; 311 | 312 | for(var key in triangles){ 313 | var t = triangles[key]; 314 | 315 | if(t.isInsideCircle(p)){ 316 | addElementToRedundanciesMap(tmp_triangles, new Triangle(p, t.p1, t.p2)); 317 | addElementToRedundanciesMap(tmp_triangles, new Triangle(p, t.p2, t.p3)); 318 | addElementToRedundanciesMap(tmp_triangles, new Triangle(p, t.p3, t.p1)); 319 | delete triangles[key]; 320 | } 321 | } 322 | 323 | for(var key in tmp_triangles){ 324 | var t = tmp_triangles[key]; 325 | if( ! t.overlap) triangles[t.key] = t; 326 | } 327 | } 328 | 329 | for(var key in triangles){ 330 | if(hugeTriangle.hasCommonPoints(triangles[key])) delete triangles[key]; 331 | } 332 | 333 | return triangles; 334 | /* for(var key in triangles){ 335 | triangles[key].draw(); 336 | } */ 337 | } 338 | // ------------------------------------------------ 339 | // points : an array of Point 340 | // returns : Triangle 341 | function getHugeTriangle(points){ 342 | var rect = getRectForPoints(points); 343 | 344 | var center = new Point((rect.left + rect.right) / 2, 345 | (rect.top + rect.bottom) / 2); 346 | var topleft = new Point(rect.left, rect.top); 347 | var radius = center.dist(topleft); 348 | 349 | var x1 = center.x - Math.sqrt(3) * radius; 350 | var y1 = center.y - radius; 351 | var p1 = new Point(x1, y1, -1); 352 | 353 | var x2 = center.x + Math.sqrt(3) * radius; 354 | var y2 = center.y - radius; 355 | var p2 = new Point(x2, y2, -2); 356 | 357 | var x3 = center.x; 358 | var y3 = center.y + 2 * radius; 359 | var p3 = new Point(x3, y3, -3); 360 | 361 | return new Triangle(p1, p2, p3); 362 | } 363 | // ------------------------------------------------ 364 | // t : Triangle 365 | // returns : Circle 366 | function getCircumscribedCirclesOfTriangle(t){ 367 | var x1 = t.p1.x; 368 | var y1 = t.p1.y; 369 | var x2 = t.p2.x; 370 | var y2 = t.p2.y; 371 | var x3 = t.p3.x; 372 | var y3 = t.p3.y; 373 | 374 | var c = 2.0 * ((x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1)); 375 | var x = ((y3 - y1) * (x2 * x2 - x1 * x1 + y2 * y2 - y1 * y1) 376 | + (y1 - y2) * (x3 * x3 - x1 * x1 + y3 * y3 - y1 * y1))/c; 377 | var y = ((x1 - x3) * (x2 * x2 - x1 * x1 + y2 * y2 - y1 * y1) 378 | + (x2 - x1) * (x3 * x3 - x1 * x1 + y3 * y3 - y1 * y1))/c; 379 | 380 | var center = new Point(x, y); 381 | var radius = center.dist(t.p1); 382 | 383 | return new Circle(center, radius); 384 | } 385 | // ------------------------------------------------ 386 | // Point 387 | // x, y : float 388 | // idx : unique index 389 | var Point = function(x, y, idx){ 390 | this.x = x; 391 | this.y = y; 392 | this.idx = idx; 393 | 394 | this.tmpRs = []; 395 | this.r = 0; 396 | 397 | this.distFromCenter = 0; 398 | } 399 | Point.prototype = { 400 | eq : function(p){ // p : point 401 | return this.x == p.x && this.y == p.y; 402 | }, 403 | set : function(x, y){ 404 | this.x = x; 405 | this.y = y; 406 | }, 407 | setP : function(p){ // p : point 408 | this.x = p.x; 409 | this.y = p.y; 410 | }, 411 | dist2 : function(p){ // p : point 412 | var dx = this.x - p.x; 413 | var dy = this.y - p.y; 414 | return dx * dx + dy * dy; 415 | }, 416 | dist : function(p){ // p : point 417 | return Math.sqrt(this.dist2(p)) || 0; 418 | }, 419 | fixInitialRadius : function(){ 420 | if(this.tmpRs.length > 0) this.r = average(this.tmpRs); 421 | }, 422 | toArray : function(){ 423 | return [this.x, this.y]; 424 | }, 425 | toString : function(){ 426 | return "[" + this.x + ", " + this.y + "]"; 427 | } 428 | } 429 | 430 | // ------------------------------------------------ 431 | // Triangle 432 | // p1, p2, p3 : vertice of a triangle (Point) 433 | var Triangle = function(p1, p2, p3){ 434 | this.p1 = p1; 435 | this.p2 = p2; 436 | this.p3 = p3; 437 | 438 | // circumcircle 439 | var circumCircle = getCircumscribedCirclesOfTriangle(this); 440 | this.o = circumCircle.o; 441 | this.r2 = circumCircle.r * circumCircle.r; 442 | this.key = this._getKey(); 443 | this.overlap; 444 | } 445 | Triangle.prototype = { 446 | // for delaunay : begin 447 | _getKey : function(){ 448 | var r = [this.p1.idx, this.p2.idx, this.p3.idx]; 449 | r.sort(); 450 | return r.join("_"); 451 | }, 452 | _hasCommon_sub : function(p, tri){ // p : Point, tri : Triangle 453 | return p.eq(tri.p1) || p.eq(tri.p2) || p.eq(tri.p3); 454 | }, 455 | hasCommonPoints : function(tri){ // tri : Triangle 456 | return this._hasCommon_sub(this.p1, tri) 457 | || this._hasCommon_sub(this.p2, tri) 458 | || this._hasCommon_sub(this.p3, tri); 459 | }, 460 | isInsideCircle : function(p){ // p : point 461 | return p.dist2(this.o) <= this.r2; 462 | }, 463 | // for delaunay : end 464 | 465 | // for circle packing : start 466 | findTmpRadiusForEachVertex : function(){ 467 | var d1 = this.p1.dist(this.p2); 468 | var d2 = this.p2.dist(this.p3); 469 | var d3 = this.p3.dist(this.p1); 470 | var r1 = (d1 + d3 - d2) / 2; 471 | this.p1.tmpRs.push(r1); 472 | this.p2.tmpRs.push(d2 - r1); 473 | this.p3.tmpRs.push(d3 - r1); 474 | }, 475 | // for circle packing : end 476 | 477 | draw : function(){ 478 | return drawPolygon([this.p1.toArray(), this.p2.toArray(), this.p3.toArray()]); 479 | }, 480 | toString : function(){ 481 | return "Triangle[" + (this.key || "-") + "]"; 482 | } 483 | } 484 | 485 | // -------------------------------------- 486 | // Circle 487 | // o : center (Point) 488 | // r : radius (float) 489 | // idx : unique index 490 | var Circle = function(o, r, idx){ 491 | this.o = o; 492 | this.r = r; 493 | this.idx = idx; 494 | 495 | this.circles = []; 496 | this.verticeIndexCounter = {}; // key:Point.idx, value:Point.idx or -1 497 | this.circleIdxs = {}; // key:Circle.idx, value:index in this.circles 498 | 499 | this.isSurrounded = false; 500 | this.angle = 0; 501 | 502 | this.tmpR = 0; 503 | this.tmpO = new Point(0, 0); 504 | } 505 | Circle.prototype = { 506 | _addIdx : function(idx1, idx2){ 507 | this._addVerticeIndexCounter(idx1, idx2); 508 | this._addVerticeIndexCounter(idx2, idx1); 509 | }, 510 | _addVerticeIndexCounter : function(idx1, idx2){ 511 | if(idx1 in this.verticeIndexCounter){ 512 | this.verticeIndexCounter[idx1] = -1; 513 | } else { 514 | this.verticeIndexCounter[idx1] = idx2; 515 | } 516 | }, 517 | addTriangle : function(t, circles){ 518 | if(t.p1.idx == this.idx){ 519 | this._addIdx(t.p2.idx, t.p3.idx); 520 | } else if(t.p2.idx == this.idx){ 521 | this._addIdx(t.p1.idx, t.p3.idx); 522 | } else if(t.p3.idx == this.idx){ 523 | this._addIdx(t.p1.idx, t.p2.idx); 524 | } 525 | }, 526 | fixCircles : function(circles){ 527 | for(var idx in this.verticeIndexCounter){ 528 | var circle = circles[idx]; 529 | this.circles.push(circle); 530 | this.circleIdxs[idx] = this.circles.length - 1; 531 | } 532 | }, 533 | findO : function(){ 534 | var x = 0; 535 | var y = 0; 536 | var len = this.circles.length; 537 | for(var i = 0; i < len; i++){ 538 | var circle = this.circles[i]; 539 | var t = getAngle(circle.o, this.o); 540 | x += Math.cos(t) * (this.r + circle.r) + circle.o.x; 541 | y += Math.sin(t) * (this.r + circle.r) + circle.o.y; 542 | } 543 | this.tmpO.set(x / len, y / len); 544 | }, 545 | fixO : function(){ 546 | this.o.setP(this.tmpO); 547 | }, 548 | findR : function(){ 549 | var totalLen = 0; 550 | var len = this.circles.length; 551 | for(var i = 0; i < len; i++){ 552 | var circle = this.circles[i]; 553 | totalLen += circle.o.dist(this.o) - circle.r; 554 | } 555 | this.tmpR = totalLen / len; 556 | }, 557 | fixR : function(){ 558 | this.r = this.tmpR; 559 | }, 560 | detectSurrounded : function(){ 561 | this.isSurrounded = true; 562 | for(var idx in this.verticeIndexCounter){ 563 | if(this.verticeIndexCounter[idx] >= 0){ 564 | this.isSurrounded = false; 565 | break; 566 | } 567 | } 568 | }, 569 | removeInvalidCircles : function(){ 570 | if( ! this.isSurrounded){ 571 | var invalid_idx = []; 572 | 573 | for(var i = 0, iEnd = this.circles.length; i < iEnd; i++){ 574 | var c = this.circles[i]; 575 | 576 | if(c.idx in this.verticeIndexCounter && (! c.isSurrounded)){ 577 | var idx1 = this.verticeIndexCounter[c.idx]; // Circle.idx 578 | if(idx1 >= 0){ 579 | // index in this.circles 580 | var other_idx = this.circleIdxs[idx1]; 581 | 582 | if(hasLargeAngle(this.o, this.circles[other_idx].o, c.o)){ 583 | invalid_idx.push(i); 584 | } 585 | } 586 | } 587 | } 588 | 589 | if(invalid_idx.length > 1){ 590 | invalid_idx.sort(); 591 | for(var i = invalid_idx.length - 1; i >= 0; i--){ 592 | this.circles.splice(invalid_idx[i], 1); 593 | } 594 | } else if(invalid_idx.length > 0){ 595 | this.circles.splice(invalid_idx[i], 1); 596 | } 597 | } 598 | }, 599 | verifyR : function(){ 600 | var max_dist_err_squared = 0; 601 | for(var i = 0, iEnd = this.circles.length; i < iEnd; i++){ 602 | var circle = this.circles[i]; 603 | var rr = this.r + circle.r; 604 | var error = Math.abs(this.o.dist2(circle.o) - rr * rr); 605 | if(error > max_dist_err_squared){ 606 | max_dist_err_squared = error; 607 | } 608 | } 609 | return max_dist_err_squared; 610 | }, 611 | toString : function(){ 612 | return "Circle[" + this.idx + "]"; 613 | } 614 | } 615 | 616 | // ------------------------------------------------ 617 | // extracts paths inside selected groups 618 | // sel : an array of pageitems ( ex. selection) 619 | // paths : an empty array (undefined at the initial state) 620 | // returns : an array of pathitems 621 | function extractPaths(sel, paths){ 622 | if( ! paths) paths = []; 623 | for(var i = 0, iEnd = sel.length; i < iEnd; i++){ 624 | if(sel[i].typename == "PathItem"){ 625 | paths.push(sel[i]); 626 | } else if(sel[i].typename == "GroupItem"){ 627 | extractPaths(sel[i].pageItems, paths); 628 | } else if(sel[i].typename == "CompoundPathItem"){ 629 | extractPaths(sel[i].pathItems, paths); 630 | } 631 | } 632 | return paths; 633 | } 634 | 635 | // ------------------------------------------------ 636 | // returns black color object 637 | function getBlack(){ 638 | var col = new GrayColor(); 639 | col.gray = 100; 640 | return col; 641 | } 642 | // ------------------------------------------------ 643 | // returns red color object 644 | function getRed(){ 645 | var col; 646 | if(activeDocument.documentColorSpace == DocumentColorSpace.CMYK){ 647 | col = new CMYKColor(); 648 | col.magenta = 100; col.yellow = 100; 649 | col.cyan = 0; col.black = 0; 650 | return col; 651 | } else { // RGB 652 | col = new RGBColor(); 653 | col.red = 255; col.green = 0; col.blue = 0; 654 | return col; 655 | } 656 | } 657 | // ------------------------------------------------ 658 | // draws a polygon 659 | // points : an array of coordinates [x, y]([float, float]) 660 | // returns : a pathitem drawn 661 | function drawPolygon(points){ 662 | for(var i = 0, iEnd = points.length; i < iEnd; i++){ 663 | var point = points[i]; 664 | points[i] = (point instanceof Point) ? point.toArray() : point; 665 | } 666 | var p = app.activeDocument.activeLayer.pathItems.add(); 667 | p.setEntirePath(points); 668 | p.closed = true; 669 | p.filled = false; 670 | p.strokeColor = getBlack(); 671 | p.strokeWidth = _opt.stroke_width; 672 | return p; 673 | } 674 | // ------------------------------------------------ 675 | // draws a circle 676 | // o : center (Point) 677 | // r : radius (float) 678 | function drawCircle(o, r){ 679 | var circle = app.activeDocument.activeLayer.pathItems.ellipse( 680 | o.y + r, o.x - r, r*2, r*2); 681 | circle.filled = false; 682 | circle.strokeColor = getBlack(); 683 | circle.strokeWidth = _opt.stroke_width; 684 | } 685 | // ------------------------------------------------ 686 | // c : Circle 687 | // has_max_err : bool 688 | function drawCircle2(c, has_max_err){ 689 | var r = c.r; 690 | var circle = app.activeDocument.activeLayer.pathItems.ellipse( 691 | c.o.y + r, c.o.x - r, r*2, r*2); 692 | circle.filled = false; 693 | circle.strokeColor = has_max_err ? getRed() : getBlack(); 694 | circle.strokeWidth = _opt.stroke_width; 695 | } 696 | // ------------------------------------------------ 697 | // sum 698 | // r : an array of numbers 699 | function sum(r){ 700 | var total = 0; 701 | for(var i = 0, iEnd = r.length; i < iEnd; i++){ 702 | total += r[i]; 703 | } 704 | return total; 705 | } 706 | // ------------------------------------------------ 707 | // average 708 | // r : an array of numbers 709 | function average(r){ 710 | if(r.length == 0) return 0; 711 | return sum(r) / r.length; 712 | } 713 | // ------------------------------------------------ 714 | // returns angle of line drawn from p1 to p2 715 | // p1, p2 : Point 716 | // returns : angle (radian) (float) 717 | function getAngle(p1, p2) { 718 | var x = p2.x - p1.x; 719 | var y = p2.y - p1.y; 720 | return Math.atan2(y, x); 721 | } 722 | // ------------------------------------------------ 723 | // tests if a triangle has large angle at the vertex p1 724 | // large angle = the cosine value is lower than _opt.large_angle_threshold 725 | // o, p1, p2 : Point (vertex of a triangle) 726 | // returns : true if there's large angle at p1 727 | function hasLargeAngle(o, p1, p2){ 728 | var d1 = o.dist(p1); 729 | var d2 = o.dist(p2); 730 | var d3 = p1.dist(p2); 731 | 732 | return findCos(d2, d3, d1) < _opt.large_angle_threshold; 733 | } 734 | // ------------------------------------------------ 735 | // law of cosines 736 | function findCos(d1, d2, d3){ 737 | return (d1 * d1 + d2 * d2 - d3 * d3) / (2 * d1 * d2); 738 | } 739 | // ------------------------------------------------ 740 | // finds spec of a rectangle which surrounds given points 741 | // points : an array of Point 742 | // returns : rect object 743 | function getRectForPoints(points){ 744 | var p = points[0]; 745 | var rect = { left:p.x, top:p.y, right:p.x, bottom:p.y }; // pointsを囲む矩形 746 | 747 | for(var i = 1, iEnd = points.length; i < iEnd; i++){ 748 | p = points[i]; 749 | if(p.x < rect.left) rect.left = p.x; 750 | if(p.x > rect.right) rect.right = p.x; 751 | if(p.y > rect.top) rect.top = p.y; 752 | if(p.y < rect.bottom) rect.bottom = p.y; 753 | } 754 | return rect; 755 | } 756 | // ------------------------------------------------ 757 | // returns a Circle instance 758 | // sel : selection 759 | // idx : idx of the target item 760 | // returns : Circle 761 | function getCircle(sel, idx){ 762 | var gb = sel[idx].geometricBounds; // left, top, right, bottom 763 | var center = new Point((gb[0] + gb[2]) / 2, (gb[1] + gb[3]) / 2, idx); 764 | var radius = (gb[2] - gb[0]) / 2; 765 | center.r = radius; 766 | return new Circle(center, radius, idx); 767 | } 768 | // -------------------------------------- 769 | // function to display elapsed time at the end 770 | var TimeChecker = function(){ 771 | this.start_time = new Date(); 772 | 773 | this.showResult = function(){ 774 | var stop_time = new Date(); 775 | var ms = stop_time.getTime() - this.start_time.getTime(); 776 | var hours = Math.floor(ms / (60 * 60 * 1000)); 777 | ms -= (hours * 60 * 60 * 1000); 778 | var minutes = Math.floor(ms / (60 * 1000)); 779 | ms -= (minutes * 60 * 1000); 780 | var seconds = Math.floor(ms / 1000); 781 | ms -= seconds * 1000; 782 | putTextln("END: " + hours + "h " + minutes + "m " + seconds + "s " + ms); 783 | } 784 | } 785 | // -------------------------------------- 786 | function putText(txt){ 787 | _g.et.text += txt; 788 | _g.win.update(); 789 | }; 790 | // -------------------------------------- 791 | function putTextln(txt){ 792 | _g.et.text += txt + "\n"; 793 | _g.win.update(); 794 | }; 795 | // -------------------------------------- 796 | function main(){ 797 | _g.win = new Window("dialog", "circlePacking", undefined, {closeButton:true} ); 798 | _g.et = _g.win.add("edittext",[0, 0, _opt.edittext_width, _opt.edittext_height], "", 799 | { multiline:true, scrolling:true }); 800 | _g.cancel = false; 801 | 802 | var gr = _g.win.add("group"); 803 | var btn_ok = gr.add("button", undefined, "exec"); 804 | var btn_cancel = gr.add("button", undefined, "abort"); 805 | var btn_close = gr.add("button", undefined, "close"); 806 | 807 | btn_ok.onClick = function(){ 808 | try{ 809 | this.enabled = false; 810 | circlePackMain(); 811 | } catch(error){ 812 | alert(error); 813 | } 814 | }; 815 | btn_cancel.onClick = function(){ 816 | try{ 817 | _g.cancel = true; 818 | }catch(error){ 819 | alert(error); 820 | } 821 | }; 822 | btn_close.onClick = function(){ 823 | try{ 824 | _g.win.close(); 825 | }catch(error){ 826 | alert(error); 827 | } 828 | }; 829 | _g.win.show(); 830 | } 831 | main(); 832 | 833 | -------------------------------------------------------------------------------- /variations/circlepacking_in_a_circle.jsx: -------------------------------------------------------------------------------- 1 | #target "illustrator" 2 | 3 | // circlepacking 4 | // draws non overlapping tangent circles. 5 | // mode-1 : if only 1 object is selected : generates random points inside it. 6 | // mode-2 : if 3 or more path is selected : arranges selected circles. 7 | // (draws arranged circles anew) 8 | 9 | // Copyright(c) 2016 Hiroyuki Sato 10 | // https://github.com/shspage 11 | // This script is distributed under the MIT License. 12 | // See the LICENSE file for details. 13 | 14 | // ver.1.2.0+ 15 | // in a circle variation ver.1.3.0 16 | 17 | var _opt = { 18 | number_of_random_points : 50, // in random point mode 19 | min_initial_radius : 2, // in random point mode 20 | 21 | stroke_width : 0.5, 22 | 23 | max_dist_err_threshold : 0.0, // 2.0 seemed good enough 24 | 25 | // In this variation, I added some restriction in moving range to the original script. 26 | // It is still based on Euclidean geometry. 27 | // Because of the restriction, it seems that the convergence is relatively slow. 28 | // So I set a large value for "max_dist_err_last_phase_threshold". 29 | // The error can be small enough after the last phase, or it can't be. 30 | // Please note that **the final error can be noticeable.** 31 | // You can continue the process for adjusting, by selecting circles and running this script again. 32 | // Considering the usage like this, I added the process that excludes the outer circle from the selection. 33 | // So you can select it with circles. 34 | max_dist_err_last_phase_threshold : 18, //5.5, 35 | normal_loop_count : 50, 36 | last_phase_loop_count : 500, // re-configured at below 37 | large_angle_threshold : Math.cos(Math.PI * 2 / 3), 38 | 39 | // marks with red for the circle which has max error 40 | mark_with_red_for_max_dist_err_circle : true, 41 | // draws delaunay triangles and circles without arranging 42 | just_show_initial_status : false, 43 | // size of the text area in the dialog 44 | edittext_width : 400, 45 | edittext_height : 400 46 | } 47 | _opt.last_phase_loop_count = Math.max( 48 | _opt.last_phase_loop_count, _opt.number_of_random_points); 49 | 50 | var _g = { 51 | win : null, // window 52 | et : null, // edittext 53 | cancel : false // true if canceled. 54 | } 55 | 56 | var _origin; 57 | var _radius; 58 | var _r2; 59 | 60 | var _WPI = Math.PI * 2; 61 | var _HPI = Math.PI / 2; 62 | 63 | // ------------------------------------------------ 64 | function circlePackMain(){ 65 | var timer = new TimeChecker(); 66 | if(app.documents.length < 1){ 67 | putTextln("error: no document"); 68 | return; 69 | } 70 | 71 | var points = getInitialPoints(); 72 | if(points.length < 3){ 73 | putTextln("error: at least 3 points required"); 74 | return; 75 | } 76 | 77 | var circles; 78 | var loopTimes = 0; 79 | var isLast = false; 80 | var max_dist_err = _opt.max_dist_err_threshold + 1; 81 | var max_dist_err_idx = -1; 82 | 83 | while(max_dist_err > _opt.max_dist_err_threshold){ 84 | if(_g.cancel) break; 85 | 86 | loopTimes++; 87 | putTextln("loop: " + loopTimes); 88 | 89 | circles = getCirclesByTriangulation(points); 90 | if( _opt.just_show_initial_status) break; 91 | if(_g.cancel) break; 92 | 93 | circles = arrangeCircles(circles, isLast); 94 | if(_g.cancel) break; 95 | 96 | // check errors 97 | var max_dist_err_squared = 0; 98 | max_dist_err_idx = -1; 99 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 100 | if(_g.cancel) break; 101 | 102 | var error = circles[ci].verifyR(); 103 | if(error > max_dist_err_squared){ 104 | max_dist_err_squared = error; 105 | max_dist_err_idx = circles[ci].idx; 106 | } 107 | } 108 | if(_g.cancel) break; 109 | max_dist_err = Math.sqrt(max_dist_err_squared) 110 | putTextln("-- max_dist_err=" + max_dist_err); 111 | 112 | if(isLast) break; 113 | if(max_dist_err < _opt.max_dist_err_last_phase_threshold){ 114 | isLast = true; 115 | putTextln("#### next loop is the last phase"); 116 | } 117 | 118 | points = []; 119 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 120 | if(_g.cancel) break; 121 | points.push(circles[ci].o); 122 | } 123 | if(_g.cancel) break; 124 | } 125 | 126 | // draws circles 127 | if(_opt.mark_with_red_for_max_dist_err_circle){ 128 | putTextln("-- max dist err index = " + max_dist_err_idx); 129 | 130 | for(var i = 0, iEnd = circles.length; i < iEnd; i++){ 131 | if(_g.cancel) break; 132 | var c = circles[i]; 133 | drawCircle2(c, c.idx == max_dist_err_idx); 134 | } 135 | } else { 136 | for(var i = 0, iEnd = circles.length; i < iEnd; i++){ 137 | if(_g.cancel) break; 138 | var c = circles[i]; 139 | drawCircle(c.o, c.r); 140 | } 141 | } 142 | 143 | app.redraw(); 144 | if(_g.cancel) putTextln("### CANCELED ###"); 145 | timer.showResult(); 146 | } 147 | // ------------------------------------------------ 148 | // only 1 object is selected -> random point mode : generates points inside the object. 149 | // otherwise -> selected circles mode : gets selected paths (= circles). 150 | // returns : an array of Point 151 | function getInitialPoints(){ 152 | var points = []; 153 | 154 | var sel = app.activeDocument.selection; 155 | if(sel.length < 1){ 156 | alert("ERROR\nNothing selected."); 157 | 158 | } else if(sel.length == 1){ 159 | // random point mode 160 | points = distributeRandomPointsInRect( 161 | sel[0], _opt.number_of_random_points, _opt.min_initial_radius * 2); 162 | } else { 163 | // selected circles mode 164 | var paths = extractPaths(sel); 165 | if(paths.length < 3){ 166 | //alert("ERROR\nSelect a rectangle or circles."); 167 | alert("ERROR\nSelect (a circle) or (3 or more circles)."); 168 | return; 169 | } 170 | 171 | for(var i = 0, iEnd = paths.length; i < iEnd; i++){ 172 | var c = getCircle(paths, i); 173 | points.push(c.o); 174 | } 175 | 176 | // if the outer circle is selected, removes it 177 | var gb = paths[0].geometricBounds; 178 | var rect = { left:gb[0], top:gb[1], right:gb[2], bottom:gb[3] }; 179 | 180 | for(var i = 1, iEnd = paths.length; i < iEnd; i++){ 181 | var gb = paths[i].geometricBounds; 182 | if(gb[0] < rect.left) rect.left = gb[0]; 183 | if(gb[2] > rect.right) rect.right = gb[2]; 184 | if(gb[1] > rect.top) rect.top = gb[1]; 185 | if(gb[3] < rect.bottom) rect.bottom = gb[3]; 186 | } 187 | 188 | _origin = new Point((rect.left + rect.right)/2, 189 | (rect.top + rect.bottom)/2); 190 | _radius = (rect.right - rect.left) / 2; 191 | _r2 = _radius * _radius; 192 | 193 | var radius_limit = _radius * 0.99; 194 | for(var i = 0, iEnd = points.length; i < iEnd; i++){ 195 | if(points[i].r > radius_limit){ 196 | points.splice(i, 1); 197 | paths[i].selected = false; 198 | break; 199 | } 200 | } 201 | } 202 | 203 | putTextln("-- " + points.length + " points prepared"); 204 | return points; 205 | } 206 | // ------------------------------------------------ 207 | // frame : a rectangle path or other object which has geometricBounds 208 | // count : number of random points to generate 209 | // min_dist : minimum distance between points 210 | // returns : an array of Point 211 | function distributeRandomPointsInRect(frame, count, min_dist){ 212 | var points = []; 213 | var p, ok; 214 | 215 | var min_dist2 = min_dist * min_dist; 216 | var gb = frame.geometricBounds; 217 | var rect = { left:gb[0], top:gb[1], right:gb[2], bottom:gb[3], 218 | width:gb[2] - gb[0], height:gb[1] - gb[3] }; 219 | 220 | _origin = new Point((rect.left + rect.right)/2, 221 | (rect.top + rect.bottom)/2); 222 | _radius = (rect.width / 2); 223 | _r2 = _radius * _radius; 224 | 225 | var r = _radius - _opt.min_initial_radius; 226 | 227 | for(var i = 0; i < count; i++){ 228 | if(i % 100 == 0) putText(i + " "); 229 | while(true){ 230 | if(_g.cancel) break; 231 | var t = Math.random() * Math.PI; 232 | var a = Math.random() * Math.PI; 233 | 234 | p = new Point(r * Math.sin(t) * Math.cos(a) + _origin.x, 235 | r * Math.cos(t) + _origin.y , i); 236 | 237 | ok = true; 238 | for(var pi = 0, piEnd = points.length; pi < piEnd; pi++){ 239 | if(points[pi].dist2(p) < min_dist2){ 240 | ok = false; 241 | break; 242 | } 243 | } 244 | if(ok) break; 245 | } 246 | if(_g.cancel) break; 247 | points.push(p); 248 | } 249 | 250 | return points; 251 | } 252 | // -------------------------------------- 253 | // points : an array of Point 254 | // returns : an array of Circle 255 | function getCirclesByTriangulation(points){ 256 | var p, c; 257 | var triangles = delaunay(points); 258 | 259 | // gets tmp radius 260 | for(var key in triangles){ 261 | triangles[key].findTmpRadiusForEachVertex(); 262 | } 263 | 264 | // sets radius 265 | for(var i = 0, iEnd = points.length; i < iEnd; i++){ 266 | points[i].fixInitialRadius(); 267 | } 268 | 269 | // creates circles 270 | var circles = []; 271 | for(var i = 0, iEnd = points.length; i < iEnd; i++){ 272 | p = points[i]; 273 | if(p.r == 0) continue; 274 | c = new Circle(p, p.r, i); 275 | circles.push(c); 276 | } 277 | 278 | // correlates vertex of triangles to each circle 279 | for(var key in triangles){ 280 | var t = triangles[key]; 281 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 282 | circles[ci].addTriangle(t, circles); 283 | } 284 | } 285 | 286 | // converts vertice to circles and detect if each circle is surrounded by others 287 | var tmp_circles = []; 288 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 289 | c = circles[ci]; 290 | c.fixCircles(circles); 291 | c.detectSurrounded(); 292 | } 293 | 294 | // removes circles unsuitable to tangent 295 | tmp_circles = []; 296 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 297 | c = circles[ci]; 298 | c.removeInvalidCircles(); 299 | if(c.circles.length > 0) tmp_circles.push(c); 300 | } 301 | circles = tmp_circles; 302 | 303 | // draws the initial status 304 | if(_opt.just_show_initial_status){ 305 | var grpTri = activeDocument.activeLayer.groupItems.add(); 306 | for(var key in triangles){ 307 | var tri = triangles[key]; 308 | var polygon = triangles[key].draw(); 309 | polygon.move(grpTri, ElementPlacement.PLACEATEND); 310 | } 311 | } 312 | 313 | return circles; 314 | } 315 | // -------------------------------------- 316 | // arranges center and radius of each circle 317 | // circles : an array of Circle 318 | // isLast : true if it is the last loop 319 | function arrangeCircles(circles, isLast){ 320 | putTextln("-- arrange: " + circles.length + " circles"); 321 | 322 | var loop_times = _opt.normal_loop_count; 323 | if(isLast) loop_times = _opt.last_phase_loop_count; 324 | var notify_loop_count = 50; 325 | 326 | var i = 0; 327 | for(; i < loop_times; i++){ 328 | if(_g.cancel) break; 329 | c_len = circles.length; 330 | for(var ci = 0; ci < c_len; ci++) circles[ci].findO(); 331 | for(var ci = 0; ci < c_len; ci++) circles[ci].fixO(); 332 | 333 | for(var ci = 0; ci < c_len; ci++) circles[ci].findR(); 334 | for(var ci = 0; ci < c_len; ci++) circles[ci].fixR(); 335 | 336 | if(i > 0 && i % notify_loop_count == 0) putText(" " + i); 337 | } 338 | if(i > notify_loop_count) putTextln("."); 339 | return circles; 340 | } 341 | 342 | // -------------------------------------- 343 | // delaunay triangulation 344 | // points : an array of Point 345 | 346 | // To implement the Delaunay triangulation, I referred to the following page 347 | // (in Japanese) by Tercel. Thanks for helpful description of algorithm. 348 | // http://tercel-sakuragaoka.blogspot.jp/2011/06/processingdelaunay.html 349 | function delaunay(points){ 350 | function addElementToRedundanciesMap(set, tri){ 351 | if(tri.key in set){ 352 | set[tri.key].overlap = true; 353 | } else { 354 | set[tri.key] = tri; 355 | } 356 | } 357 | 358 | // key : Triangle._getKey 359 | // value : Triangle instance 360 | var triangles = {}; 361 | 362 | var hugeTriangle = getHugeTriangle(points); 363 | triangles["huge"] = hugeTriangle; 364 | 365 | for(var i = 0, iEnd = points.length; i < iEnd; i++){ 366 | var p = points[i]; 367 | var tmp_triangles = {}; 368 | 369 | for(var key in triangles){ 370 | var t = triangles[key]; 371 | 372 | if(t.isInsideCircle(p)){ 373 | addElementToRedundanciesMap(tmp_triangles, new Triangle(p, t.p1, t.p2)); 374 | addElementToRedundanciesMap(tmp_triangles, new Triangle(p, t.p2, t.p3)); 375 | addElementToRedundanciesMap(tmp_triangles, new Triangle(p, t.p3, t.p1)); 376 | delete triangles[key]; 377 | } 378 | } 379 | 380 | for(var key in tmp_triangles){ 381 | var t = tmp_triangles[key]; 382 | if( ! t.overlap) triangles[t.key] = t; 383 | } 384 | } 385 | 386 | for(var key in triangles){ 387 | if(hugeTriangle.hasCommonPoints(triangles[key])) delete triangles[key]; 388 | } 389 | 390 | return triangles; 391 | /* for(var key in triangles){ 392 | triangles[key].draw(); 393 | } */ 394 | } 395 | // ------------------------------------------------ 396 | // points : an array of Point 397 | // returns : Triangle 398 | function getHugeTriangle(points){ 399 | var rect = getRectForPoints(points); 400 | 401 | var center = new Point((rect.left + rect.right) / 2, 402 | (rect.top + rect.bottom) / 2); 403 | var topleft = new Point(rect.left, rect.top); 404 | var radius = center.dist(topleft); 405 | 406 | var x1 = center.x - Math.sqrt(3) * radius; 407 | var y1 = center.y - radius; 408 | var p1 = new Point(x1, y1, -1); 409 | 410 | var x2 = center.x + Math.sqrt(3) * radius; 411 | var y2 = center.y - radius; 412 | var p2 = new Point(x2, y2, -2); 413 | 414 | var x3 = center.x; 415 | var y3 = center.y + 2 * radius; 416 | var p3 = new Point(x3, y3, -3); 417 | 418 | return new Triangle(p1, p2, p3); 419 | } 420 | // ------------------------------------------------ 421 | // t : Triangle 422 | // returns : Circle 423 | function getCircumscribedCirclesOfTriangle(t){ 424 | var x1 = t.p1.x; 425 | var y1 = t.p1.y; 426 | var x2 = t.p2.x; 427 | var y2 = t.p2.y; 428 | var x3 = t.p3.x; 429 | var y3 = t.p3.y; 430 | 431 | var c = 2.0 * ((x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1)); 432 | var x = ((y3 - y1) * (x2 * x2 - x1 * x1 + y2 * y2 - y1 * y1) 433 | + (y1 - y2) * (x3 * x3 - x1 * x1 + y3 * y3 - y1 * y1))/c; 434 | var y = ((x1 - x3) * (x2 * x2 - x1 * x1 + y2 * y2 - y1 * y1) 435 | + (x2 - x1) * (x3 * x3 - x1 * x1 + y3 * y3 - y1 * y1))/c; 436 | 437 | var center = new Point(x, y); 438 | var radius = center.dist(t.p1); 439 | 440 | return new Circle(center, radius); 441 | } 442 | // ------------------------------------------------ 443 | // Point 444 | // x, y : float 445 | // idx : unique index 446 | var Point = function(x, y, idx){ 447 | this.x = x; 448 | this.y = y; 449 | this.idx = idx; 450 | 451 | this.tmpRs = []; 452 | this.r = 0; 453 | 454 | this.distFromCenter = 0; 455 | } 456 | Point.prototype = { 457 | eq : function(p){ // p : point 458 | return this.x == p.x && this.y == p.y; 459 | }, 460 | set : function(x, y){ 461 | this.x = x; 462 | this.y = y; 463 | }, 464 | setP : function(p){ // p : point 465 | this.x = p.x; 466 | this.y = p.y; 467 | }, 468 | dist2 : function(p){ // p : point 469 | var dx = this.x - p.x; 470 | var dy = this.y - p.y; 471 | return dx * dx + dy * dy; 472 | }, 473 | dist : function(p){ // p : point 474 | return Math.sqrt(this.dist2(p)) || 0; 475 | }, 476 | fixInitialRadius : function(){ 477 | if(this.tmpRs.length > 0){ 478 | var r = average(this.tmpRs); 479 | var d = this.dist(_origin); 480 | if(d + r > _radius) r = _radius - d; 481 | this.r = r; 482 | } 483 | }, 484 | toArray : function(){ 485 | return [this.x, this.y]; 486 | }, 487 | toString : function(){ 488 | return "[" + this.x + ", " + this.y + "]"; 489 | } 490 | } 491 | 492 | // ------------------------------------------------ 493 | // Triangle 494 | // p1, p2, p3 : vertice of a triangle (Point) 495 | var Triangle = function(p1, p2, p3){ 496 | this.p1 = p1; 497 | this.p2 = p2; 498 | this.p3 = p3; 499 | 500 | // circumcircle 501 | var circumCircle = getCircumscribedCirclesOfTriangle(this); 502 | this.o = circumCircle.o; 503 | this.r2 = circumCircle.r * circumCircle.r; 504 | this.key = this._getKey(); 505 | this.overlap; 506 | } 507 | Triangle.prototype = { 508 | // for delaunay : begin 509 | _getKey : function(){ 510 | var r = [this.p1.idx, this.p2.idx, this.p3.idx]; 511 | r.sort(); 512 | return r.join("_"); 513 | }, 514 | _hasCommon_sub : function(p, tri){ // p : Point, tri : Triangle 515 | return p.eq(tri.p1) || p.eq(tri.p2) || p.eq(tri.p3); 516 | }, 517 | hasCommonPoints : function(tri){ // tri : Triangle 518 | return this._hasCommon_sub(this.p1, tri) 519 | || this._hasCommon_sub(this.p2, tri) 520 | || this._hasCommon_sub(this.p3, tri); 521 | }, 522 | isInsideCircle : function(p){ // p : point 523 | return p.dist2(this.o) <= this.r2; 524 | }, 525 | // for delaunay : end 526 | 527 | // for circle packing : start 528 | findTmpRadiusForEachVertex : function(){ 529 | var d1 = this.p1.dist(this.p2); 530 | var d2 = this.p2.dist(this.p3); 531 | var d3 = this.p3.dist(this.p1); 532 | var r1 = (d1 + d3 - d2) / 2; 533 | this.p1.tmpRs.push(r1); 534 | this.p2.tmpRs.push(d2 - r1); 535 | this.p3.tmpRs.push(d3 - r1); 536 | }, 537 | // for circle packing : end 538 | 539 | draw : function(){ 540 | return drawPolygon([this.p1.toArray(), this.p2.toArray(), this.p3.toArray()]); 541 | }, 542 | toString : function(){ 543 | return "Triangle[" + (this.key || "-") + "]"; 544 | } 545 | } 546 | 547 | // -------------------------------------- 548 | // Circle 549 | // o : center (Point) 550 | // r : radius (float) 551 | // idx : unique index 552 | var Circle = function(o, r, idx){ 553 | this.o = o; 554 | this.r = r; 555 | this.idx = idx; 556 | 557 | this.circles = []; 558 | this.verticeIndexCounter = {}; // key:Point.idx, value:Point.idx or -1 559 | this.circleIdxs = {}; // key:Circle.idx, value:index in this.circles 560 | 561 | this.isSurrounded = false; 562 | this.angle = 0; 563 | 564 | this.tmpR = 0; 565 | this.tmpO = new Point(0, 0); 566 | } 567 | Circle.prototype = { 568 | _addIdx : function(idx1, idx2){ 569 | this._addVerticeIndexCounter(idx1, idx2); 570 | this._addVerticeIndexCounter(idx2, idx1); 571 | }, 572 | _addVerticeIndexCounter : function(idx1, idx2){ 573 | if(idx1 in this.verticeIndexCounter){ 574 | this.verticeIndexCounter[idx1] = -1; 575 | } else { 576 | this.verticeIndexCounter[idx1] = idx2; 577 | } 578 | }, 579 | addTriangle : function(t, circles){ 580 | if(t.p1.idx == this.idx){ 581 | this._addIdx(t.p2.idx, t.p3.idx); 582 | } else if(t.p2.idx == this.idx){ 583 | this._addIdx(t.p1.idx, t.p3.idx); 584 | } else if(t.p3.idx == this.idx){ 585 | this._addIdx(t.p1.idx, t.p2.idx); 586 | } 587 | }, 588 | fixCircles : function(circles){ 589 | for(var idx in this.verticeIndexCounter){ 590 | var circle = circles[idx]; 591 | this.circles.push(circle); 592 | this.circleIdxs[idx] = this.circles.length - 1; 593 | } 594 | }, 595 | findO : function(){ 596 | var x = 0; 597 | var y = 0; 598 | var len = this.circles.length; 599 | for(var i = 0; i < len; i++){ 600 | var circle = this.circles[i]; 601 | var t = getAngle(circle.o, this.o); 602 | x += Math.cos(t) * (this.r + circle.r) + circle.o.x; 603 | y += Math.sin(t) * (this.r + circle.r) + circle.o.y; 604 | } 605 | 606 | if( ! this.isSurrounded){ 607 | var t = getAngle(_origin, this.o); 608 | x += Math.cos(t) * (_radius - this.r) + _origin.x; 609 | y += Math.sin(t) * (_radius - this.r) + _origin.y; 610 | len++; 611 | } 612 | this.tmpO.set(x / len, y / len); 613 | }, 614 | fixO : function(){ 615 | var d2 = _origin.dist2(this.tmpO); 616 | if(d2 > _r2){ 617 | var t = getAngle(_origin, this.tmpO); 618 | var r = _radius - this.r; 619 | this.tmpO = new Point(Math.cos(t) * r + _origin.x, 620 | Math.sin(t) * r + _origin.y); 621 | } 622 | this.o.setP(this.tmpO); 623 | }, 624 | findR : function(){ 625 | var totalLen = 0; 626 | var len = this.circles.length; 627 | for(var i = 0; i < len; i++){ 628 | var circle = this.circles[i]; 629 | totalLen += circle.o.dist(this.o) - circle.r; 630 | } 631 | 632 | if( ! this.isSurrounded){ 633 | totalLen += _radius - _origin.dist(this.o); 634 | len++; 635 | } 636 | this.tmpR = totalLen / len; 637 | }, 638 | fixR : function(){ 639 | var d = _origin.dist(this.o); 640 | if(d + this.tmpR > _radius){ 641 | this.tmpR = Math.max(_radius - d, 1); 642 | } 643 | this.r = this.tmpR; 644 | }, 645 | detectSurrounded : function(){ 646 | this.isSurrounded = true; 647 | for(var idx in this.verticeIndexCounter){ 648 | if(this.verticeIndexCounter[idx] >= 0){ 649 | this.isSurrounded = false; 650 | break; 651 | } 652 | } 653 | }, 654 | removeInvalidCircles : function(){ 655 | if( ! this.isSurrounded){ 656 | var invalid_idx = []; 657 | 658 | for(var i = 0, iEnd = this.circles.length; i < iEnd; i++){ 659 | var c = this.circles[i]; 660 | 661 | if(c.idx in this.verticeIndexCounter && (! c.isSurrounded)){ 662 | var idx1 = this.verticeIndexCounter[c.idx]; // Circle.idx 663 | if(idx1 >= 0){ 664 | // index in this.circles 665 | var other_idx = this.circleIdxs[idx1]; 666 | 667 | if(hasLargeAngle(this.o, this.circles[other_idx].o, c.o)){ 668 | invalid_idx.push(i); 669 | } 670 | } 671 | } 672 | } 673 | 674 | if(invalid_idx.length > 1){ 675 | invalid_idx.sort(); 676 | for(var i = invalid_idx.length - 1; i >= 0; i--){ 677 | this.circles.splice(invalid_idx[i], 1); 678 | } 679 | } else if(invalid_idx.length > 0){ 680 | this.circles.splice(invalid_idx[i], 1); 681 | } 682 | } 683 | }, 684 | verifyR : function(){ 685 | var max_dist_err_squared = 0; 686 | for(var i = 0, iEnd = this.circles.length; i < iEnd; i++){ 687 | var circle = this.circles[i]; 688 | var rr = this.r + circle.r; 689 | var error = Math.abs(this.o.dist2(circle.o) - rr * rr); 690 | if(error > max_dist_err_squared){ 691 | max_dist_err_squared = error; 692 | } 693 | } 694 | return max_dist_err_squared; 695 | }, 696 | toString : function(){ 697 | return "Circle[" + this.idx + "]"; 698 | } 699 | } 700 | 701 | // ------------------------------------------------ 702 | // extracts paths inside selected groups 703 | // sel : an array of pageitems ( ex. selection) 704 | // paths : an empty array (undefined at the initial state) 705 | // returns : an array of pathitems 706 | function extractPaths(sel, paths){ 707 | if( ! paths) paths = []; 708 | for(var i = 0, iEnd = sel.length; i < iEnd; i++){ 709 | if(sel[i].typename == "PathItem"){ 710 | paths.push(sel[i]); 711 | } else if(sel[i].typename == "GroupItem"){ 712 | extractPaths(sel[i].pageItems, paths); 713 | } else if(sel[i].typename == "CompoundPathItem"){ 714 | extractPaths(sel[i].pathItems, paths); 715 | } 716 | } 717 | return paths; 718 | } 719 | 720 | // ------------------------------------------------ 721 | // returns black color object 722 | function getBlack(){ 723 | var col = new GrayColor(); 724 | col.gray = 100; 725 | return col; 726 | } 727 | // ------------------------------------------------ 728 | // returns red color object 729 | function getRed(){ 730 | var col; 731 | if(activeDocument.documentColorSpace == DocumentColorSpace.CMYK){ 732 | col = new CMYKColor(); 733 | col.magenta = 100; col.yellow = 100; 734 | col.cyan = 0; col.black = 0; 735 | return col; 736 | } else { // RGB 737 | col = new RGBColor(); 738 | col.red = 255; col.green = 0; col.blue = 0; 739 | return col; 740 | } 741 | } 742 | // ------------------------------------------------ 743 | // draws a polygon 744 | // points : an array of coordinates [x, y]([float, float]) 745 | // returns : a pathitem drawn 746 | function drawPolygon(points){ 747 | for(var i = 0, iEnd = points.length; i < iEnd; i++){ 748 | var point = points[i]; 749 | points[i] = (point instanceof Point) ? point.toArray() : point; 750 | } 751 | var p = app.activeDocument.activeLayer.pathItems.add(); 752 | p.setEntirePath(points); 753 | p.closed = true; 754 | p.filled = false; 755 | p.strokeColor = getBlack(); 756 | p.strokeWidth = _opt.stroke_width; 757 | return p; 758 | } 759 | // ------------------------------------------------ 760 | // draws a circle 761 | // o : center (Point) 762 | // r : radius (float) 763 | function drawCircle(o, r){ 764 | var circle = app.activeDocument.activeLayer.pathItems.ellipse( 765 | o.y + r, o.x - r, r*2, r*2); 766 | circle.filled = false; 767 | circle.strokeColor = getBlack(); 768 | circle.strokeWidth = _opt.stroke_width; 769 | } 770 | // ------------------------------------------------ 771 | // c : Circle 772 | // has_max_err : bool 773 | function drawCircle2(c, has_max_err){ 774 | var r = c.r; 775 | var circle = app.activeDocument.activeLayer.pathItems.ellipse( 776 | c.o.y + r, c.o.x - r, r*2, r*2); 777 | circle.filled = false; 778 | circle.strokeColor = has_max_err ? getRed() : getBlack(); 779 | circle.strokeWidth = _opt.stroke_width; 780 | } 781 | // ------------------------------------------------ 782 | // sum 783 | // r : an array of numbers 784 | function sum(r){ 785 | var total = 0; 786 | for(var i = 0, iEnd = r.length; i < iEnd; i++){ 787 | total += r[i]; 788 | } 789 | return total; 790 | } 791 | // ------------------------------------------------ 792 | // average 793 | // r : an array of numbers 794 | function average(r){ 795 | if(r.length == 0) return 0; 796 | return sum(r) / r.length; 797 | } 798 | // ------------------------------------------------ 799 | // returns angle of line drawn from p1 to p2 800 | // p1, p2 : Point 801 | // returns : angle (radian) (float) 802 | function getAngle(p1, p2) { 803 | var x = p2.x - p1.x; 804 | var y = p2.y - p1.y; 805 | return Math.atan2(y, x); 806 | } 807 | // ------------------------------------------------ 808 | // tests if a triangle has large angle at the vertex p1 809 | // large angle = the cosine value is lower than _opt.large_angle_threshold 810 | // o, p1, p2 : Point (vertex of a triangle) 811 | // returns : true if there's large angle at p1 812 | function hasLargeAngle(o, p1, p2){ 813 | var d1 = o.dist(p1); 814 | var d2 = o.dist(p2); 815 | var d3 = p1.dist(p2); 816 | 817 | return findCos(d2, d3, d1) < _opt.large_angle_threshold; 818 | } 819 | // ------------------------------------------------ 820 | // law of cosines 821 | function findCos(d1, d2, d3){ 822 | return (d1 * d1 + d2 * d2 - d3 * d3) / (2 * d1 * d2); 823 | } 824 | // ------------------------------------------------ 825 | // finds spec of a rectangle which surrounds given points 826 | // points : an array of Point 827 | // returns : rect object 828 | function getRectForPoints(points){ 829 | var p = points[0]; 830 | var rect = { left:p.x, top:p.y, right:p.x, bottom:p.y }; // pointsを囲む矩形 831 | 832 | for(var i = 1, iEnd = points.length; i < iEnd; i++){ 833 | p = points[i]; 834 | if(p.x < rect.left) rect.left = p.x; 835 | if(p.x > rect.right) rect.right = p.x; 836 | if(p.y > rect.top) rect.top = p.y; 837 | if(p.y < rect.bottom) rect.bottom = p.y; 838 | } 839 | return rect; 840 | } 841 | // ------------------------------------------------ 842 | // returns a Circle instance 843 | // sel : selection 844 | // idx : idx of the target item 845 | // returns : Circle 846 | function getCircle(sel, idx){ 847 | var gb = sel[idx].geometricBounds; // left, top, right, bottom 848 | var center = new Point((gb[0] + gb[2]) / 2, (gb[1] + gb[3]) / 2, idx); 849 | var radius = (gb[2] - gb[0]) / 2; 850 | center.r = radius; 851 | return new Circle(center, radius, idx); 852 | } 853 | // -------------------------------------- 854 | // function to display elapsed time at the end 855 | var TimeChecker = function(){ 856 | this.start_time = new Date(); 857 | 858 | this.showResult = function(){ 859 | var stop_time = new Date(); 860 | var ms = stop_time.getTime() - this.start_time.getTime(); 861 | var hours = Math.floor(ms / (60 * 60 * 1000)); 862 | ms -= (hours * 60 * 60 * 1000); 863 | var minutes = Math.floor(ms / (60 * 1000)); 864 | ms -= (minutes * 60 * 1000); 865 | var seconds = Math.floor(ms / 1000); 866 | ms -= seconds * 1000; 867 | putTextln("END: " + hours + "h " + minutes + "m " + seconds + "s " + ms); 868 | } 869 | } 870 | // -------------------------------------- 871 | function putText(txt){ 872 | _g.et.text += txt; 873 | _g.win.update(); 874 | }; 875 | // -------------------------------------- 876 | function putTextln(txt){ 877 | _g.et.text += txt + "\n"; 878 | _g.win.update(); 879 | }; 880 | // -------------------------------------- 881 | function main(){ 882 | _g.win = new Window("dialog", "circlePacking", undefined, {closeButton:true} ); 883 | _g.et = _g.win.add("edittext",[0, 0, _opt.edittext_width, _opt.edittext_height], "", 884 | { multiline:true, scrolling:true }); 885 | _g.cancel = false; 886 | 887 | var gr = _g.win.add("group"); 888 | var btn_ok = gr.add("button", undefined, "exec"); 889 | var btn_cancel = gr.add("button", undefined, "abort"); 890 | var btn_close = gr.add("button", undefined, "close"); 891 | 892 | btn_ok.onClick = function(){ 893 | try{ 894 | this.enabled = false; 895 | circlePackMain(); 896 | } catch(error){ 897 | alert(error); 898 | } 899 | }; 900 | btn_cancel.onClick = function(){ 901 | try{ 902 | _g.cancel = true; 903 | }catch(error){ 904 | alert(error); 905 | } 906 | }; 907 | btn_close.onClick = function(){ 908 | try{ 909 | _g.win.close(); 910 | }catch(error){ 911 | alert(error); 912 | } 913 | }; 914 | _g.win.show(); 915 | } 916 | main(); 917 | 918 | -------------------------------------------------------------------------------- /variations/circlepacking_in_a_circle_web/circlepacking_in_a_circle_web.js: -------------------------------------------------------------------------------- 1 | // circlepacking 2 | // draws non overlapping tangent circles. 3 | 4 | // Copyright(c) 2016 Hiroyuki Sato 5 | // https://github.com/shspage 6 | // This script is distributed under the MIT License. 7 | // See the LICENSE file for details. 8 | 9 | // ver.1.2.0+ 10 | // in a circle variation ver.1.2.0+ 11 | // in a circle in a browser variation ver.1.2 12 | 13 | // * exportSVGtoFile function requires canvas2svg.js. 14 | // (Copyright (c) 2014 Gliffy Inc. 15 | // https://github.com/gliffy/canvas2svg ) 16 | 17 | var CirclePack; 18 | 19 | (function(){ 20 | var _opt = { 21 | number_of_random_points : 200, 22 | min_initial_radius : 2, 23 | 24 | stroke_width : 1, 25 | 26 | max_dist_err_threshold : 0.0, // 2.0 seemed good enough 27 | 28 | max_dist_err_last_phase_threshold : 5.5, //5.5, 29 | normal_loop_count : 50, 30 | last_phase_loop_count : 500, // re-configured at below 31 | large_angle_threshold : Math.cos(Math.PI * 2 / 3), 32 | 33 | // marks with red for the circle which has max error 34 | mark_with_red_for_max_dist_err_circle : true, 35 | // draws delaunay triangles 36 | draw_triangles : true, 37 | // draws without arranging 38 | just_show_initial_status : false 39 | } 40 | _opt.last_phase_loop_count = Math.max( 41 | _opt.last_phase_loop_count, _opt.number_of_random_points); 42 | 43 | var _canvas; 44 | 45 | var _origin; 46 | var _radius; 47 | var _r2; 48 | 49 | var _timer; 50 | var _interval; 51 | 52 | var _circles = null; 53 | var _export_url = null; 54 | 55 | var _WPI = Math.PI * 2; 56 | var _HPI = Math.PI / 2; 57 | 58 | var _EXPORT_SVG_FILENAME = "output.svg"; 59 | 60 | // ------------------------------------------------ 61 | function circlePackMain(canvas){ 62 | _canvas = canvas; 63 | _timer = new TimeChecker(); 64 | _circles = null; 65 | 66 | var points = getInitialPoints(); 67 | if(points.length < 3) return; 68 | 69 | var circles; 70 | var loopTimes = 0; 71 | var isLast = false; 72 | var max_dist_err = _opt.max_dist_err_threshold + 1; 73 | var max_dist_err_idx = -1; 74 | 75 | var circlePackLoop = function(){ 76 | loopTimes++; 77 | console.log("loop: " + loopTimes); 78 | 79 | clearRect(); 80 | 81 | circles = getCirclesByTriangulation(points); 82 | if( _opt.just_show_initial_status){ 83 | clearInterval(_interval); 84 | _timer.showResult(); 85 | } 86 | 87 | circles = arrangeCircles(circles, isLast); 88 | 89 | // check errors 90 | var max_dist_err_squared = 0; 91 | max_dist_err_idx = -1; 92 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 93 | var error = circles[ci].verifyR(); 94 | if(error > max_dist_err_squared){ 95 | max_dist_err_squared = error; 96 | max_dist_err_idx = circles[ci].idx; 97 | } 98 | } 99 | max_dist_err = Math.sqrt(max_dist_err_squared) 100 | console.log("-- max_dist_err=" + max_dist_err); 101 | 102 | var pfx = isLast ? "done : " : ""; 103 | var textColor = isLast ? "#f00" : "#000"; 104 | drawText(pfx + "loop " + loopTimes + " : max err = " + max_dist_err, 10, 10, textColor); 105 | 106 | drawCircles(circles, max_dist_err_idx); 107 | _circles = circles; 108 | 109 | if(isLast){ 110 | clearInterval(_interval); 111 | _timer.showResult(); 112 | } else { 113 | if(max_dist_err < _opt.max_dist_err_last_phase_threshold){ 114 | isLast = true; 115 | console.log("#### next loop is the last phase"); 116 | } 117 | 118 | points = []; 119 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 120 | points.push(circles[ci].o); 121 | } 122 | } 123 | } 124 | _interval = setInterval(circlePackLoop, 100); 125 | } 126 | // ------------------------------------------------ 127 | function clearRect(){ 128 | _canvas.getContext("2d").clearRect(0, 0, _canvas.width, _canvas.height); 129 | } 130 | // ------------------------------------------------ 131 | function drawCircles(circles, max_dist_err_idx, as_svg){ 132 | //if( ! _opt.just_show_initial_status) clearRect(); 133 | var ctx = as_svg && C2S 134 | ? new C2S(_canvas.width, _canvas.height) 135 | : _canvas.getContext("2d"); 136 | 137 | if(_opt.mark_with_red_for_max_dist_err_circle){ 138 | console.log("-- max dist err index = " + max_dist_err_idx); 139 | 140 | for(var i = 0, iEnd = circles.length; i < iEnd; i++){ 141 | var c = circles[i]; 142 | drawCircle2(ctx, c, c.idx == max_dist_err_idx); 143 | } 144 | } else { 145 | for(var i = 0, iEnd = circles.length; i < iEnd; i++){ 146 | var c = circles[i]; 147 | drawCircle(ctx, c.o, c.r); 148 | } 149 | } 150 | 151 | if(as_svg){ 152 | return ctx.getSerializedSvg(); 153 | } 154 | } 155 | // ------------------------------------------------ 156 | // only 1 object is selected -> random point mode : generates points inside the object. 157 | // otherwise -> selected circles mode : gets selected paths (= circles). 158 | // returns : an array of Point 159 | function getInitialPoints(){ 160 | var points = []; 161 | 162 | // random point mode 163 | points = distributeRandomPointsInRect( 164 | _opt.number_of_random_points, _opt.min_initial_radius * 2); 165 | 166 | console.log("-- " + points.length + " points prepared"); 167 | return points; 168 | } 169 | // ------------------------------------------------ 170 | // count : number of random points to generate 171 | // min_dist : minimum distance between points 172 | // returns : an array of Point 173 | function distributeRandomPointsInRect(count, min_dist){ 174 | var points = []; 175 | var p, ok; 176 | 177 | var min_dist2 = min_dist * min_dist; 178 | var rect = { left:0, top:0, right:_canvas.width, bottom:canvas.height, 179 | width:_canvas.width, height:_canvas.height }; 180 | 181 | _origin = new Point((rect.left + rect.right)/2, 182 | (rect.top + rect.bottom)/2); 183 | _radius = (rect.width / 2); 184 | _r2 = _radius * _radius; 185 | 186 | var r = _radius - _opt.min_initial_radius; 187 | 188 | for(var i = 0; i < count; i++){ 189 | while(true){ 190 | var t = Math.random() * Math.PI; 191 | var a = Math.random() * Math.PI; 192 | 193 | p = new Point(r * Math.sin(t) * Math.cos(a) + _origin.x, 194 | r * Math.cos(t) + _origin.y , i); 195 | 196 | ok = true; 197 | for(var pi = 0, piEnd = points.length; pi < piEnd; pi++){ 198 | if(points[pi].dist2(p) < min_dist2){ 199 | ok = false; 200 | break; 201 | } 202 | } 203 | if(ok) break; 204 | } 205 | points.push(p); 206 | } 207 | 208 | return points; 209 | } 210 | // -------------------------------------- 211 | // points : an array of Point 212 | // returns : an array of Circle 213 | function getCirclesByTriangulation(points){ 214 | var p, c; 215 | var triangles = delaunay(points); 216 | 217 | // gets tmp radius 218 | for(var key in triangles){ 219 | triangles[key].findTmpRadiusForEachVertex(); 220 | } 221 | 222 | // sets radius 223 | for(var i = 0, iEnd = points.length; i < iEnd; i++){ 224 | points[i].fixInitialRadius(); 225 | } 226 | 227 | // creates circles 228 | var circles = []; 229 | for(var i = 0, iEnd = points.length; i < iEnd; i++){ 230 | p = points[i]; 231 | if(p.r == 0) continue; 232 | c = new Circle(p, p.r, i); 233 | circles.push(c); 234 | } 235 | 236 | // correlates vertex of triangles to each circle 237 | for(var key in triangles){ 238 | var t = triangles[key]; 239 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 240 | circles[ci].addTriangle(t, circles); 241 | } 242 | } 243 | 244 | // converts vertice to circles and detect if each circle is surrounded by others 245 | var tmp_circles = []; 246 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 247 | c = circles[ci]; 248 | c.fixCircles(circles); 249 | c.detectSurrounded(); 250 | } 251 | 252 | // removes circles unsuitable to tangent 253 | tmp_circles = []; 254 | for(var ci = 0, ciEnd = circles.length; ci < ciEnd; ci++){ 255 | c = circles[ci]; 256 | c.removeInvalidCircles(); 257 | if(c.circles.length > 0) tmp_circles.push(c); 258 | } 259 | circles = tmp_circles; 260 | 261 | // draws the initial status 262 | if(_opt.just_show_initial_status || _opt.draw_triangles){ 263 | for(var key in triangles){ 264 | var tri = triangles[key]; 265 | triangles[key].draw(); 266 | } 267 | } 268 | 269 | return circles; 270 | } 271 | // -------------------------------------- 272 | // arranges center and radius of each circle 273 | // circles : an array of Circle 274 | // isLast : true if it is the last loop 275 | function arrangeCircles(circles, isLast){ 276 | console.log("-- arrange: " + circles.length + " circles"); 277 | 278 | var loop_times = _opt.normal_loop_count; 279 | if(isLast) loop_times = _opt.last_phase_loop_count; 280 | var notify_loop_count = 200; 281 | 282 | var i = 0; 283 | for(; i < loop_times; i++){ 284 | c_len = circles.length; 285 | for(var ci = 0; ci < c_len; ci++) circles[ci].findO(); 286 | for(var ci = 0; ci < c_len; ci++) circles[ci].fixO(); 287 | 288 | for(var ci = 0; ci < c_len; ci++) circles[ci].findR(); 289 | for(var ci = 0; ci < c_len; ci++) circles[ci].fixR(); 290 | 291 | if(i > 0 && i % notify_loop_count == 0) console.log(" " + i); 292 | } 293 | // if(i > notify_loop_count) console.log("."); 294 | return circles; 295 | } 296 | 297 | // -------------------------------------- 298 | // delaunay triangulation 299 | // points : an array of Point 300 | 301 | // To implement the Delaunay triangulation, I referred to the following page 302 | // (in Japanese) by Tercel. Thanks for helpful description of algorithm. 303 | // http://tercel-sakuragaoka.blogspot.jp/2011/06/processingdelaunay.html 304 | function delaunay(points){ 305 | function addElementToRedundanciesMap(set, tri){ 306 | if(tri.key in set){ 307 | set[tri.key].overlap = true; 308 | } else { 309 | set[tri.key] = tri; 310 | } 311 | } 312 | 313 | // key : Triangle._getKey 314 | // value : Triangle instance 315 | var triangles = {}; 316 | 317 | var hugeTriangle = getHugeTriangle(points); 318 | triangles["huge"] = hugeTriangle; 319 | 320 | for(var i = 0, iEnd = points.length; i < iEnd; i++){ 321 | var p = points[i]; 322 | var tmp_triangles = {}; 323 | 324 | for(var key in triangles){ 325 | var t = triangles[key]; 326 | 327 | if(t.isInsideCircle(p)){ 328 | addElementToRedundanciesMap(tmp_triangles, new Triangle(p, t.p1, t.p2)); 329 | addElementToRedundanciesMap(tmp_triangles, new Triangle(p, t.p2, t.p3)); 330 | addElementToRedundanciesMap(tmp_triangles, new Triangle(p, t.p3, t.p1)); 331 | delete triangles[key]; 332 | } 333 | } 334 | 335 | for(var key in tmp_triangles){ 336 | var t = tmp_triangles[key]; 337 | if( ! t.overlap) triangles[t.key] = t; 338 | } 339 | } 340 | 341 | for(var key in triangles){ 342 | if(hugeTriangle.hasCommonPoints(triangles[key])) delete triangles[key]; 343 | } 344 | 345 | return triangles; 346 | /* for(var key in triangles){ 347 | triangles[key].draw(); 348 | } */ 349 | } 350 | // ------------------------------------------------ 351 | // points : an array of Point 352 | // returns : Triangle 353 | function getHugeTriangle(points){ 354 | var rect = getRectForPoints(points); 355 | 356 | var center = new Point((rect.left + rect.right) / 2, 357 | (rect.top + rect.bottom) / 2); 358 | var topleft = new Point(rect.left, rect.top); 359 | var radius = center.dist(topleft); 360 | 361 | var x1 = center.x - Math.sqrt(3) * radius; 362 | var y1 = center.y + radius; 363 | var p1 = new Point(x1, y1, -1); 364 | 365 | var x2 = center.x + Math.sqrt(3) * radius; 366 | var y2 = center.y + radius; 367 | var p2 = new Point(x2, y2, -2); 368 | 369 | var x3 = center.x; 370 | var y3 = center.y - 2 * radius; 371 | var p3 = new Point(x3, y3, -3); 372 | 373 | return new Triangle(p1, p2, p3); 374 | } 375 | // ------------------------------------------------ 376 | // t : Triangle 377 | // returns : Circle 378 | function getCircumscribedCirclesOfTriangle(t){ 379 | var x1 = t.p1.x; 380 | var y1 = t.p1.y; 381 | var x2 = t.p2.x; 382 | var y2 = t.p2.y; 383 | var x3 = t.p3.x; 384 | var y3 = t.p3.y; 385 | 386 | var c = 2.0 * ((x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1)); 387 | var x = ((y3 - y1) * (x2 * x2 - x1 * x1 + y2 * y2 - y1 * y1) 388 | + (y1 - y2) * (x3 * x3 - x1 * x1 + y3 * y3 - y1 * y1))/c; 389 | var y = ((x1 - x3) * (x2 * x2 - x1 * x1 + y2 * y2 - y1 * y1) 390 | + (x2 - x1) * (x3 * x3 - x1 * x1 + y3 * y3 - y1 * y1))/c; 391 | 392 | var center = new Point(x, y); 393 | var radius = center.dist(t.p1); 394 | 395 | return new Circle(center, radius); 396 | } 397 | // ------------------------------------------------ 398 | // Point 399 | // x, y : float 400 | // idx : unique index 401 | var Point = function(x, y, idx){ 402 | this.x = x; 403 | this.y = y; 404 | this.idx = idx; 405 | 406 | this.tmpRs = []; 407 | this.r = 0; 408 | 409 | this.distFromCenter = 0; 410 | } 411 | Point.prototype = { 412 | eq : function(p){ // p : point 413 | return this.x == p.x && this.y == p.y; 414 | }, 415 | set : function(x, y){ 416 | this.x = x; 417 | this.y = y; 418 | }, 419 | setP : function(p){ // p : point 420 | this.x = p.x; 421 | this.y = p.y; 422 | }, 423 | dist2 : function(p){ // p : point 424 | var dx = this.x - p.x; 425 | var dy = this.y - p.y; 426 | return dx * dx + dy * dy; 427 | }, 428 | dist : function(p){ // p : point 429 | return Math.sqrt(this.dist2(p)) || 0; 430 | }, 431 | fixInitialRadius : function(){ 432 | if(this.tmpRs.length > 0){ 433 | var r = average(this.tmpRs); 434 | var d = this.dist(_origin); 435 | if(d + r > _radius) r = _radius - d; 436 | this.r = r; 437 | } 438 | }, 439 | toArray : function(){ 440 | return [this.x, this.y]; 441 | }, 442 | toString : function(){ 443 | return "[" + this.x + ", " + this.y + "]"; 444 | } 445 | } 446 | 447 | // ------------------------------------------------ 448 | // Triangle 449 | // p1, p2, p3 : vertice of a triangle (Point) 450 | var Triangle = function(p1, p2, p3){ 451 | this.p1 = p1; 452 | this.p2 = p2; 453 | this.p3 = p3; 454 | 455 | // circumcircle 456 | var circumCircle = getCircumscribedCirclesOfTriangle(this); 457 | this.o = circumCircle.o; 458 | this.r2 = circumCircle.r * circumCircle.r; 459 | this.key = this._getKey(); 460 | this.overlap; 461 | } 462 | Triangle.prototype = { 463 | // for delaunay : begin 464 | _getKey : function(){ 465 | var r = [this.p1.idx, this.p2.idx, this.p3.idx]; 466 | r.sort(); 467 | return r.join("_"); 468 | }, 469 | _hasCommon_sub : function(p, tri){ // p : Point, tri : Triangle 470 | return p.eq(tri.p1) || p.eq(tri.p2) || p.eq(tri.p3); 471 | }, 472 | hasCommonPoints : function(tri){ // tri : Triangle 473 | return this._hasCommon_sub(this.p1, tri) 474 | || this._hasCommon_sub(this.p2, tri) 475 | || this._hasCommon_sub(this.p3, tri); 476 | }, 477 | isInsideCircle : function(p){ // p : point 478 | return p.dist2(this.o) <= this.r2; 479 | }, 480 | // for delaunay : end 481 | 482 | // for circle packing : start 483 | findTmpRadiusForEachVertex : function(){ 484 | var d1 = this.p1.dist(this.p2); 485 | var d2 = this.p2.dist(this.p3); 486 | var d3 = this.p3.dist(this.p1); 487 | var r1 = (d1 + d3 - d2) / 2; 488 | this.p1.tmpRs.push(r1); 489 | this.p2.tmpRs.push(d2 - r1); 490 | this.p3.tmpRs.push(d3 - r1); 491 | }, 492 | // for circle packing : end 493 | 494 | draw : function(){ 495 | drawPolygon([this.p1, this.p2, this.p3]); 496 | }, 497 | toString : function(){ 498 | return "Triangle[" + (this.key || "-") + "]"; 499 | } 500 | } 501 | 502 | // -------------------------------------- 503 | // Circle 504 | // o : center (Point) 505 | // r : radius (float) 506 | // idx : unique index 507 | var Circle = function(o, r, idx){ 508 | this.o = o; 509 | this.r = r; 510 | this.idx = idx; 511 | 512 | this.circles = []; 513 | this.verticeIndexCounter = {}; // key:Point.idx, value:Point.idx or -1 514 | this.circleIdxs = {}; // key:Circle.idx, value:index in this.circles 515 | 516 | this.isSurrounded = false; 517 | this.angle = 0; 518 | 519 | this.tmpR = 0; 520 | this.tmpO = new Point(0, 0); 521 | } 522 | Circle.prototype = { 523 | _addIdx : function(idx1, idx2){ 524 | this._addVerticeIndexCounter(idx1, idx2); 525 | this._addVerticeIndexCounter(idx2, idx1); 526 | }, 527 | _addVerticeIndexCounter : function(idx1, idx2){ 528 | if(idx1 in this.verticeIndexCounter){ 529 | this.verticeIndexCounter[idx1] = -1; 530 | } else { 531 | this.verticeIndexCounter[idx1] = idx2; 532 | } 533 | }, 534 | addTriangle : function(t, circles){ 535 | if(t.p1.idx == this.idx){ 536 | this._addIdx(t.p2.idx, t.p3.idx); 537 | } else if(t.p2.idx == this.idx){ 538 | this._addIdx(t.p1.idx, t.p3.idx); 539 | } else if(t.p3.idx == this.idx){ 540 | this._addIdx(t.p1.idx, t.p2.idx); 541 | } 542 | }, 543 | fixCircles : function(circles){ 544 | for(var idx in this.verticeIndexCounter){ 545 | var circle = circles[idx]; 546 | this.circles.push(circle); 547 | this.circleIdxs[idx] = this.circles.length - 1; 548 | } 549 | }, 550 | findO : function(){ 551 | var x = 0; 552 | var y = 0; 553 | var len = this.circles.length; 554 | for(var i = 0; i < len; i++){ 555 | var circle = this.circles[i]; 556 | var t = getAngle(circle.o, this.o); 557 | x += Math.cos(t) * (this.r + circle.r) + circle.o.x; 558 | y += Math.sin(t) * (this.r + circle.r) + circle.o.y; 559 | } 560 | 561 | if( ! this.isSurrounded){ 562 | var t = getAngle(_origin, this.o); 563 | x += Math.cos(t) * (_radius - this.r) + _origin.x; 564 | y += Math.sin(t) * (_radius - this.r) + _origin.y; 565 | len++; 566 | } 567 | this.tmpO.set(x / len, y / len); 568 | }, 569 | fixO : function(){ 570 | var d2 = _origin.dist2(this.tmpO); 571 | if(d2 > _r2){ 572 | var t = getAngle(_origin, this.tmpO); 573 | var r = _radius - this.r; 574 | this.tmpO = new Point(Math.cos(t) * r + _origin.x, 575 | Math.sin(t) * r + _origin.y); 576 | } 577 | this.o.setP(this.tmpO); 578 | }, 579 | findR : function(){ 580 | var totalLen = 0; 581 | var len = this.circles.length; 582 | for(var i = 0; i < len; i++){ 583 | var circle = this.circles[i]; 584 | totalLen += circle.o.dist(this.o) - circle.r; 585 | } 586 | 587 | if( ! this.isSurrounded){ 588 | totalLen += _radius - _origin.dist(this.o); 589 | len++; 590 | } 591 | this.tmpR = totalLen / len; 592 | }, 593 | fixR : function(){ 594 | var d = _origin.dist(this.o); 595 | if(d + this.tmpR > _radius){ 596 | this.tmpR = Math.max(_radius - d, 1); 597 | } 598 | this.r = this.tmpR; 599 | }, 600 | detectSurrounded : function(){ 601 | this.isSurrounded = true; 602 | for(var idx in this.verticeIndexCounter){ 603 | if(this.verticeIndexCounter[idx] >= 0){ 604 | this.isSurrounded = false; 605 | break; 606 | } 607 | } 608 | }, 609 | removeInvalidCircles : function(){ 610 | if( ! this.isSurrounded){ 611 | var invalid_idx = []; 612 | 613 | for(var i = 0, iEnd = this.circles.length; i < iEnd; i++){ 614 | var c = this.circles[i]; 615 | 616 | if(c.idx in this.verticeIndexCounter && (! c.isSurrounded)){ 617 | var idx1 = this.verticeIndexCounter[c.idx]; // Circle.idx 618 | if(idx1 >= 0){ 619 | // index in this.circles 620 | var other_idx = this.circleIdxs[idx1]; 621 | 622 | if(hasLargeAngle(this.o, this.circles[other_idx].o, c.o)){ 623 | invalid_idx.push(i); 624 | } 625 | } 626 | } 627 | } 628 | 629 | if(invalid_idx.length > 1){ 630 | invalid_idx.sort(); 631 | for(var i = invalid_idx.length - 1; i >= 0; i--){ 632 | this.circles.splice(invalid_idx[i], 1); 633 | } 634 | } else if(invalid_idx.length > 0){ 635 | this.circles.splice(invalid_idx[i], 1); 636 | } 637 | } 638 | }, 639 | verifyR : function(){ 640 | var max_dist_err_squared = 0; 641 | for(var i = 0, iEnd = this.circles.length; i < iEnd; i++){ 642 | var circle = this.circles[i]; 643 | var rr = this.r + circle.r; 644 | var error = Math.abs(this.o.dist2(circle.o) - rr * rr); 645 | if(error > max_dist_err_squared){ 646 | max_dist_err_squared = error; 647 | } 648 | } 649 | return max_dist_err_squared; 650 | }, 651 | toString : function(){ 652 | return "Circle[" + this.idx + "]"; 653 | } 654 | } 655 | 656 | // ------------------------------------------------ 657 | // draws a polygon 658 | // points : an array of coordinates [x, y]([float, float]) 659 | // returns : a pathitem drawn 660 | function drawPolygon(points, strokeColor){ 661 | if( ! strokeColor) strokeColor = "#ccc"; 662 | 663 | var ctx = _canvas.getContext("2d"); 664 | ctx.beginPath(); 665 | ctx.moveTo(points[0].x, points[0].y); 666 | for(var i = 1; i < points.length; i++){ 667 | ctx.lineTo(points[i].x, points[i].y); 668 | } 669 | ctx.closePath(); 670 | ctx.lineWidth = _opt.stroke_width; 671 | ctx.strokeStyle = strokeColor; 672 | ctx.stroke(); 673 | } 674 | // ------------------------------------------------ 675 | // draws a circle 676 | // o : center (Point) 677 | // r : radius (float) 678 | function drawCircle(ctx, o, r, strokeColor){ 679 | if( ! strokeColor) strokeColor = "#000"; 680 | 681 | //var ctx = _canvas.getContext("2d"); 682 | ctx.beginPath(); 683 | ctx.arc(o.x, o.y, r, 0, _WPI, true); 684 | ctx.lineWidth = _opt.stroke_width; 685 | ctx.strokeStyle = strokeColor; 686 | ctx.stroke(); 687 | } 688 | // ------------------------------------------------ 689 | // c : Circle 690 | // has_max_err : bool 691 | function drawCircle2(ctx, c, has_max_err){ 692 | var strokeColor = has_max_err ? "#f00" : "#000"; 693 | drawCircle(ctx, c.o, c.r, strokeColor); 694 | } 695 | // ------------------------------------------------ 696 | function drawText(txt, x, y, color){ 697 | if( ! color) color = "#000"; 698 | 699 | var ctx = _canvas.getContext("2d"); 700 | ctx.fillStyle = color; 701 | ctx.font= '10px sans-serif'; 702 | ctx.fillText(txt, x, y); 703 | } 704 | // ------------------------------------------------ 705 | // sum 706 | // r : an array of numbers 707 | function sum(r){ 708 | var total = 0; 709 | for(var i = 0, iEnd = r.length; i < iEnd; i++){ 710 | total += r[i]; 711 | } 712 | return total; 713 | } 714 | // ------------------------------------------------ 715 | // average 716 | // r : an array of numbers 717 | function average(r){ 718 | if(r.length == 0) return 0; 719 | return sum(r) / r.length; 720 | } 721 | // ------------------------------------------------ 722 | // returns angle of line drawn from p1 to p2 723 | // p1, p2 : Point 724 | // returns : angle (radian) (float) 725 | function getAngle(p1, p2) { 726 | var x = p2.x - p1.x; 727 | var y = p2.y - p1.y; 728 | return Math.atan2(y, x); 729 | } 730 | // ------------------------------------------------ 731 | // tests if a triangle has large angle at the vertex p1 732 | // large angle = the cosine value is lower than _opt.large_angle_threshold 733 | // o, p1, p2 : Point (vertex of a triangle) 734 | // returns : true if there's large angle at p1 735 | function hasLargeAngle(o, p1, p2){ 736 | var d1 = o.dist(p1); 737 | var d2 = o.dist(p2); 738 | var d3 = p1.dist(p2); 739 | 740 | return findCos(d2, d3, d1) < _opt.large_angle_threshold; 741 | } 742 | // ------------------------------------------------ 743 | // law of cosines 744 | function findCos(d1, d2, d3){ 745 | return (d1 * d1 + d2 * d2 - d3 * d3) / (2 * d1 * d2); 746 | } 747 | // ------------------------------------------------ 748 | // finds spec of a rectangle which surrounds given points 749 | // points : an array of Point 750 | // returns : rect object 751 | function getRectForPoints(points){ 752 | var p = points[0]; 753 | var rect = { left:p.x, top:p.y, right:p.x, bottom:p.y }; 754 | 755 | for(var i = 1, iEnd = points.length; i < iEnd; i++){ 756 | p = points[i]; 757 | if(p.x < rect.left) rect.left = p.x; 758 | if(p.x > rect.right) rect.right = p.x; 759 | if(p.y < rect.top) rect.top = p.y; 760 | if(p.y > rect.bottom) rect.bottom = p.y; 761 | } 762 | return rect; 763 | } 764 | // -------------------------------------- 765 | // function to display elapsed time at the end 766 | var TimeChecker = function(){ 767 | this.start_time = new Date(); 768 | 769 | this.showResult = function(){ 770 | var stop_time = new Date(); 771 | var ms = stop_time.getTime() - this.start_time.getTime(); 772 | var hours = Math.floor(ms / (60 * 60 * 1000)); 773 | ms -= (hours * 60 * 60 * 1000); 774 | var minutes = Math.floor(ms / (60 * 1000)); 775 | ms -= (minutes * 60 * 1000); 776 | var seconds = Math.floor(ms / 1000); 777 | ms -= seconds * 1000; 778 | console.log("END: " + hours + "h " + minutes + "m " + seconds + "s " + ms); 779 | } 780 | } 781 | // -------------------------------------- 782 | function exportSVGtoFile(){ 783 | if(C2S && _circles){ 784 | try{ 785 | if(confirm(getMessage("export_svg_confirm"))){ 786 | if(_export_url) URL.revokeObjectURL(_export_url); 787 | var svg = drawCircles(_circles, -1, true); 788 | _export_url = handleDownload(svg); 789 | alert(getMessage("exported")); 790 | } 791 | } catch(e){ 792 | console.log(e); 793 | alert(getMessage("failed_to_export")); 794 | } 795 | } else { 796 | alert(getMessage("nothing_to_export")); 797 | } 798 | } 799 | // -------------------------------------- 800 | function handleDownload(text) { 801 | var blob = new Blob([ text ], { "type" : "image/svg+xml" }); 802 | var a = document.createElement("a"); 803 | a.target = '_blank'; 804 | a.download = _EXPORT_SVG_FILENAME; 805 | var url; 806 | var winURL = window.URL || window.webkitURL; 807 | if(winURL && winURL.createObject) { //chrome 808 | url = winURL.createObjectURL(blob); 809 | a.href = url; 810 | a.click(); 811 | } else if(window.URL && window.URL.createObjectURL) { //firefox 812 | url = window.URL.createObjectURL(blob); 813 | a.href = url; 814 | document.body.appendChild(a); 815 | a.click(); 816 | document.body.removeChild(a); 817 | } 818 | return url; 819 | } 820 | // -------------------------------------- 821 | function getLang(){ 822 | //return "en"; 823 | return navigator.language.startsWith("ja") ? "ja" : "en"; 824 | } 825 | // -------------------------------------- 826 | function getMessage(entry){ 827 | var obj = _messages[entry]; 828 | var lang = getLang(); 829 | 830 | if(obj){ 831 | return obj[lang] || obj["en"].toString(); 832 | } else { 833 | return "undefined:[" + entry + "]"; 834 | } 835 | } 836 | // -------------------------------------- 837 | _messages = { 838 | export_svg_confirm : { 839 | "en" : "export SVG file?", 840 | "ja" : "SVGファイルをエクスポートしますか?" 841 | }, 842 | exported : { 843 | "en" : "exported", 844 | "ja" : "エクスポート完了" 845 | }, 846 | failed_to_export : { 847 | "en" : "failed to export", 848 | "ja" : "エクスポートに失敗しました" 849 | }, 850 | nothing_to_export : { 851 | "en" : "there's nothing to export yet", 852 | "ja" : "エクスポートできるものがありません" 853 | } 854 | } 855 | // -------------------------------------- 856 | CirclePack = { 857 | draw : function(canvas){ circlePackMain(canvas); }, 858 | exportSVGtoFile : exportSVGtoFile 859 | } 860 | 861 | //circlePackMain(); 862 | })(); 863 | 864 | -------------------------------------------------------------------------------- /variations/circlepacking_in_a_circle_web/lib/canvas2svg.js: -------------------------------------------------------------------------------- 1 | /*!! 2 | * Canvas 2 Svg v1.0.19 3 | * A low level canvas to SVG converter. Uses a mock canvas context to build an SVG document. 4 | * 5 | * Licensed under the MIT license: 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 8 | * Author: 9 | * Kerry Liu 10 | * 11 | * Copyright (c) 2014 Gliffy Inc. 12 | */ 13 | 14 | ;(function () { 15 | "use strict"; 16 | 17 | var STYLES, ctx, CanvasGradient, CanvasPattern, namedEntities; 18 | 19 | //helper function to format a string 20 | function format(str, args) { 21 | var keys = Object.keys(args), i; 22 | for (i=0; i 1) { 226 | options = defaultOptions; 227 | options.width = arguments[0]; 228 | options.height = arguments[1]; 229 | } else if ( !o ) { 230 | options = defaultOptions; 231 | } else { 232 | options = o; 233 | } 234 | 235 | if (!(this instanceof ctx)) { 236 | //did someone call this without new? 237 | return new ctx(options); 238 | } 239 | 240 | //setup options 241 | this.width = options.width || defaultOptions.width; 242 | this.height = options.height || defaultOptions.height; 243 | this.enableMirroring = options.enableMirroring !== undefined ? options.enableMirroring : defaultOptions.enableMirroring; 244 | 245 | this.canvas = this; ///point back to this instance! 246 | this.__document = options.document || document; 247 | 248 | // allow passing in an existing context to wrap around 249 | // if a context is passed in, we know a canvas already exist 250 | if (options.ctx) { 251 | this.__ctx = options.ctx; 252 | } else { 253 | this.__canvas = this.__document.createElement("canvas"); 254 | this.__ctx = this.__canvas.getContext("2d"); 255 | } 256 | 257 | this.__setDefaultStyles(); 258 | this.__stack = [this.__getStyleState()]; 259 | this.__groupStack = []; 260 | 261 | //the root svg element 262 | this.__root = this.__document.createElementNS("http://www.w3.org/2000/svg", "svg"); 263 | this.__root.setAttribute("version", 1.1); 264 | this.__root.setAttribute("xmlns", "http://www.w3.org/2000/svg"); 265 | this.__root.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); 266 | this.__root.setAttribute("width", this.width); 267 | this.__root.setAttribute("height", this.height); 268 | 269 | //make sure we don't generate the same ids in defs 270 | this.__ids = {}; 271 | 272 | //defs tag 273 | this.__defs = this.__document.createElementNS("http://www.w3.org/2000/svg", "defs"); 274 | this.__root.appendChild(this.__defs); 275 | 276 | //also add a group child. the svg element can't use the transform attribute 277 | this.__currentElement = this.__document.createElementNS("http://www.w3.org/2000/svg", "g"); 278 | this.__root.appendChild(this.__currentElement); 279 | }; 280 | 281 | 282 | /** 283 | * Creates the specified svg element 284 | * @private 285 | */ 286 | ctx.prototype.__createElement = function (elementName, properties, resetFill) { 287 | if (typeof properties === "undefined") { 288 | properties = {}; 289 | } 290 | 291 | var element = this.__document.createElementNS("http://www.w3.org/2000/svg", elementName), 292 | keys = Object.keys(properties), i, key; 293 | if (resetFill) { 294 | //if fill or stroke is not specified, the svg element should not display. By default SVG's fill is black. 295 | element.setAttribute("fill", "none"); 296 | element.setAttribute("stroke", "none"); 297 | } 298 | for (i=0; i 0) { 498 | if (this.__currentElement.nodeName === "path") { 499 | if (!this.__currentElementsToStyle) this.__currentElementsToStyle = {element: parent, children: []}; 500 | this.__currentElementsToStyle.children.push(this.__currentElement) 501 | this.__applyCurrentDefaultPath(); 502 | } 503 | 504 | var group = this.__createElement("g"); 505 | parent.appendChild(group); 506 | this.__currentElement = group; 507 | } 508 | 509 | var transform = this.__currentElement.getAttribute("transform"); 510 | if (transform) { 511 | transform += " "; 512 | } else { 513 | transform = ""; 514 | } 515 | transform += t; 516 | this.__currentElement.setAttribute("transform", transform); 517 | }; 518 | 519 | /** 520 | * scales the current element 521 | */ 522 | ctx.prototype.scale = function (x, y) { 523 | if (y === undefined) { 524 | y = x; 525 | } 526 | this.__addTransform(format("scale({x},{y})", {x:x, y:y})); 527 | }; 528 | 529 | /** 530 | * rotates the current element 531 | */ 532 | ctx.prototype.rotate = function (angle) { 533 | var degrees = (angle * 180 / Math.PI); 534 | this.__addTransform(format("rotate({angle},{cx},{cy})", {angle:degrees, cx:0, cy:0})); 535 | }; 536 | 537 | /** 538 | * translates the current element 539 | */ 540 | ctx.prototype.translate = function (x, y) { 541 | this.__addTransform(format("translate({x},{y})", {x:x,y:y})); 542 | }; 543 | 544 | /** 545 | * applies a transform to the current element 546 | */ 547 | ctx.prototype.transform = function (a, b, c, d, e, f) { 548 | this.__addTransform(format("matrix({a},{b},{c},{d},{e},{f})", {a:a, b:b, c:c, d:d, e:e, f:f})); 549 | }; 550 | 551 | /** 552 | * Create a new Path Element 553 | */ 554 | ctx.prototype.beginPath = function () { 555 | var path, parent; 556 | 557 | // Note that there is only one current default path, it is not part of the drawing state. 558 | // See also: https://html.spec.whatwg.org/multipage/scripting.html#current-default-path 559 | this.__currentDefaultPath = ""; 560 | this.__currentPosition = {}; 561 | 562 | path = this.__createElement("path", {}, true); 563 | parent = this.__closestGroupOrSvg(); 564 | parent.appendChild(path); 565 | this.__currentElement = path; 566 | }; 567 | 568 | /** 569 | * Helper function to apply currentDefaultPath to current path element 570 | * @private 571 | */ 572 | ctx.prototype.__applyCurrentDefaultPath = function () { 573 | var currentElement = this.__currentElement; 574 | if (currentElement.nodeName === "path") { 575 | currentElement.setAttribute("d", this.__currentDefaultPath); 576 | } else { 577 | console.error("Attempted to apply path command to node", currentElement.nodeName); 578 | } 579 | }; 580 | 581 | /** 582 | * Helper function to add path command 583 | * @private 584 | */ 585 | ctx.prototype.__addPathCommand = function (command) { 586 | this.__currentDefaultPath += " "; 587 | this.__currentDefaultPath += command; 588 | }; 589 | 590 | /** 591 | * Adds the move command to the current path element, 592 | * if the currentPathElement is not empty create a new path element 593 | */ 594 | ctx.prototype.moveTo = function (x,y) { 595 | if (this.__currentElement.nodeName !== "path") { 596 | this.beginPath(); 597 | } 598 | 599 | // creates a new subpath with the given point 600 | this.__currentPosition = {x: x, y: y}; 601 | this.__addPathCommand(format("M {x} {y}", {x:x, y:y})); 602 | }; 603 | 604 | /** 605 | * Closes the current path 606 | */ 607 | ctx.prototype.closePath = function () { 608 | if (this.__currentDefaultPath) { 609 | this.__addPathCommand("Z"); 610 | } 611 | }; 612 | 613 | /** 614 | * Adds a line to command 615 | */ 616 | ctx.prototype.lineTo = function (x, y) { 617 | this.__currentPosition = {x: x, y: y}; 618 | if (this.__currentDefaultPath.indexOf('M') > -1) { 619 | this.__addPathCommand(format("L {x} {y}", {x:x, y:y})); 620 | } else { 621 | this.__addPathCommand(format("M {x} {y}", {x:x, y:y})); 622 | } 623 | }; 624 | 625 | /** 626 | * Add a bezier command 627 | */ 628 | ctx.prototype.bezierCurveTo = function (cp1x, cp1y, cp2x, cp2y, x, y) { 629 | this.__currentPosition = {x: x, y: y}; 630 | this.__addPathCommand(format("C {cp1x} {cp1y} {cp2x} {cp2y} {x} {y}", 631 | {cp1x:cp1x, cp1y:cp1y, cp2x:cp2x, cp2y:cp2y, x:x, y:y})); 632 | }; 633 | 634 | /** 635 | * Adds a quadratic curve to command 636 | */ 637 | ctx.prototype.quadraticCurveTo = function (cpx, cpy, x, y) { 638 | this.__currentPosition = {x: x, y: y}; 639 | this.__addPathCommand(format("Q {cpx} {cpy} {x} {y}", {cpx:cpx, cpy:cpy, x:x, y:y})); 640 | }; 641 | 642 | 643 | /** 644 | * Return a new normalized vector of given vector 645 | */ 646 | var normalize = function (vector) { 647 | var len = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1]); 648 | return [vector[0] / len, vector[1] / len]; 649 | }; 650 | 651 | /** 652 | * Adds the arcTo to the current path 653 | * 654 | * @see http://www.w3.org/TR/2015/WD-2dcontext-20150514/#dom-context-2d-arcto 655 | */ 656 | ctx.prototype.arcTo = function (x1, y1, x2, y2, radius) { 657 | // Let the point (x0, y0) be the last point in the subpath. 658 | var x0 = this.__currentPosition && this.__currentPosition.x; 659 | var y0 = this.__currentPosition && this.__currentPosition.y; 660 | 661 | // First ensure there is a subpath for (x1, y1). 662 | if (typeof x0 == "undefined" || typeof y0 == "undefined") { 663 | return; 664 | } 665 | 666 | // Negative values for radius must cause the implementation to throw an IndexSizeError exception. 667 | if (radius < 0) { 668 | throw new Error("IndexSizeError: The radius provided (" + radius + ") is negative."); 669 | } 670 | 671 | // If the point (x0, y0) is equal to the point (x1, y1), 672 | // or if the point (x1, y1) is equal to the point (x2, y2), 673 | // or if the radius radius is zero, 674 | // then the method must add the point (x1, y1) to the subpath, 675 | // and connect that point to the previous point (x0, y0) by a straight line. 676 | if (((x0 === x1) && (y0 === y1)) 677 | || ((x1 === x2) && (y1 === y2)) 678 | || (radius === 0)) { 679 | this.lineTo(x1, y1); 680 | return; 681 | } 682 | 683 | // Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) all lie on a single straight line, 684 | // then the method must add the point (x1, y1) to the subpath, 685 | // and connect that point to the previous point (x0, y0) by a straight line. 686 | var unit_vec_p1_p0 = normalize([x0 - x1, y0 - y1]); 687 | var unit_vec_p1_p2 = normalize([x2 - x1, y2 - y1]); 688 | if (unit_vec_p1_p0[0] * unit_vec_p1_p2[1] === unit_vec_p1_p0[1] * unit_vec_p1_p2[0]) { 689 | this.lineTo(x1, y1); 690 | return; 691 | } 692 | 693 | // Otherwise, let The Arc be the shortest arc given by circumference of the circle that has radius radius, 694 | // and that has one point tangent to the half-infinite line that crosses the point (x0, y0) and ends at the point (x1, y1), 695 | // and that has a different point tangent to the half-infinite line that ends at the point (x1, y1), and crosses the point (x2, y2). 696 | // The points at which this circle touches these two lines are called the start and end tangent points respectively. 697 | 698 | // note that both vectors are unit vectors, so the length is 1 699 | var cos = (unit_vec_p1_p0[0] * unit_vec_p1_p2[0] + unit_vec_p1_p0[1] * unit_vec_p1_p2[1]); 700 | var theta = Math.acos(Math.abs(cos)); 701 | 702 | // Calculate origin 703 | var unit_vec_p1_origin = normalize([ 704 | unit_vec_p1_p0[0] + unit_vec_p1_p2[0], 705 | unit_vec_p1_p0[1] + unit_vec_p1_p2[1] 706 | ]); 707 | var len_p1_origin = radius / Math.sin(theta / 2); 708 | var x = x1 + len_p1_origin * unit_vec_p1_origin[0]; 709 | var y = y1 + len_p1_origin * unit_vec_p1_origin[1]; 710 | 711 | // Calculate start angle and end angle 712 | // rotate 90deg clockwise (note that y axis points to its down) 713 | var unit_vec_origin_start_tangent = [ 714 | -unit_vec_p1_p0[1], 715 | unit_vec_p1_p0[0] 716 | ]; 717 | // rotate 90deg counter clockwise (note that y axis points to its down) 718 | var unit_vec_origin_end_tangent = [ 719 | unit_vec_p1_p2[1], 720 | -unit_vec_p1_p2[0] 721 | ]; 722 | var getAngle = function (vector) { 723 | // get angle (clockwise) between vector and (1, 0) 724 | var x = vector[0]; 725 | var y = vector[1]; 726 | if (y >= 0) { // note that y axis points to its down 727 | return Math.acos(x); 728 | } else { 729 | return -Math.acos(x); 730 | } 731 | }; 732 | var startAngle = getAngle(unit_vec_origin_start_tangent); 733 | var endAngle = getAngle(unit_vec_origin_end_tangent); 734 | 735 | // Connect the point (x0, y0) to the start tangent point by a straight line 736 | this.lineTo(x + unit_vec_origin_start_tangent[0] * radius, 737 | y + unit_vec_origin_start_tangent[1] * radius); 738 | 739 | // Connect the start tangent point to the end tangent point by arc 740 | // and adding the end tangent point to the subpath. 741 | this.arc(x, y, radius, startAngle, endAngle); 742 | }; 743 | 744 | /** 745 | * Sets the stroke property on the current element 746 | */ 747 | ctx.prototype.stroke = function () { 748 | if (this.__currentElement.nodeName === "path") { 749 | this.__currentElement.setAttribute("paint-order", "fill stroke markers"); 750 | } 751 | this.__applyCurrentDefaultPath(); 752 | this.__applyStyleToCurrentElement("stroke"); 753 | }; 754 | 755 | /** 756 | * Sets fill properties on the current element 757 | */ 758 | ctx.prototype.fill = function () { 759 | if (this.__currentElement.nodeName === "path") { 760 | this.__currentElement.setAttribute("paint-order", "stroke fill markers"); 761 | } 762 | this.__applyCurrentDefaultPath(); 763 | this.__applyStyleToCurrentElement("fill"); 764 | }; 765 | 766 | /** 767 | * Adds a rectangle to the path. 768 | */ 769 | ctx.prototype.rect = function (x, y, width, height) { 770 | if (this.__currentElement.nodeName !== "path") { 771 | this.beginPath(); 772 | } 773 | this.moveTo(x, y); 774 | this.lineTo(x+width, y); 775 | this.lineTo(x+width, y+height); 776 | this.lineTo(x, y+height); 777 | this.lineTo(x, y); 778 | this.closePath(); 779 | }; 780 | 781 | 782 | /** 783 | * adds a rectangle element 784 | */ 785 | ctx.prototype.fillRect = function (x, y, width, height) { 786 | var rect, parent; 787 | rect = this.__createElement("rect", { 788 | x : x, 789 | y : y, 790 | width : width, 791 | height : height 792 | }, true); 793 | parent = this.__closestGroupOrSvg(); 794 | parent.appendChild(rect); 795 | this.__currentElement = rect; 796 | this.__applyStyleToCurrentElement("fill"); 797 | }; 798 | 799 | /** 800 | * Draws a rectangle with no fill 801 | * @param x 802 | * @param y 803 | * @param width 804 | * @param height 805 | */ 806 | ctx.prototype.strokeRect = function (x, y, width, height) { 807 | var rect, parent; 808 | rect = this.__createElement("rect", { 809 | x : x, 810 | y : y, 811 | width : width, 812 | height : height 813 | }, true); 814 | parent = this.__closestGroupOrSvg(); 815 | parent.appendChild(rect); 816 | this.__currentElement = rect; 817 | this.__applyStyleToCurrentElement("stroke"); 818 | }; 819 | 820 | 821 | /** 822 | * Clear entire canvas: 823 | * 1. save current transforms 824 | * 2. remove all the childNodes of the root g element 825 | */ 826 | ctx.prototype.__clearCanvas = function () { 827 | var current = this.__closestGroupOrSvg(), 828 | transform = current.getAttribute("transform"); 829 | var rootGroup = this.__root.childNodes[1]; 830 | var childNodes = rootGroup.childNodes; 831 | for (var i = childNodes.length - 1; i >= 0; i--) { 832 | if (childNodes[i]) { 833 | rootGroup.removeChild(childNodes[i]); 834 | } 835 | } 836 | this.__currentElement = rootGroup; 837 | //reset __groupStack as all the child group nodes are all removed. 838 | this.__groupStack = []; 839 | if (transform) { 840 | this.__addTransform(transform); 841 | } 842 | }; 843 | 844 | /** 845 | * "Clears" a canvas by just drawing a white rectangle in the current group. 846 | */ 847 | ctx.prototype.clearRect = function (x, y, width, height) { 848 | //clear entire canvas 849 | if (x === 0 && y === 0 && width === this.width && height === this.height) { 850 | this.__clearCanvas(); 851 | return; 852 | } 853 | var rect, parent = this.__closestGroupOrSvg(); 854 | rect = this.__createElement("rect", { 855 | x : x, 856 | y : y, 857 | width : width, 858 | height : height, 859 | fill : "#FFFFFF" 860 | }, true); 861 | parent.appendChild(rect); 862 | }; 863 | 864 | /** 865 | * Adds a linear gradient to a defs tag. 866 | * Returns a canvas gradient object that has a reference to it's parent def 867 | */ 868 | ctx.prototype.createLinearGradient = function (x1, y1, x2, y2) { 869 | var grad = this.__createElement("linearGradient", { 870 | id : randomString(this.__ids), 871 | x1 : x1+"px", 872 | x2 : x2+"px", 873 | y1 : y1+"px", 874 | y2 : y2+"px", 875 | "gradientUnits" : "userSpaceOnUse" 876 | }, false); 877 | this.__defs.appendChild(grad); 878 | return new CanvasGradient(grad, this); 879 | }; 880 | 881 | /** 882 | * Adds a radial gradient to a defs tag. 883 | * Returns a canvas gradient object that has a reference to it's parent def 884 | */ 885 | ctx.prototype.createRadialGradient = function (x0, y0, r0, x1, y1, r1) { 886 | var grad = this.__createElement("radialGradient", { 887 | id : randomString(this.__ids), 888 | cx : x1+"px", 889 | cy : y1+"px", 890 | r : r1+"px", 891 | fx : x0+"px", 892 | fy : y0+"px", 893 | "gradientUnits" : "userSpaceOnUse" 894 | }, false); 895 | this.__defs.appendChild(grad); 896 | return new CanvasGradient(grad, this); 897 | 898 | }; 899 | 900 | /** 901 | * Parses the font string and returns svg mapping 902 | * @private 903 | */ 904 | ctx.prototype.__parseFont = function () { 905 | var regex = /^\s*(?=(?:(?:[-a-z]+\s*){0,2}(italic|oblique))?)(?=(?:(?:[-a-z]+\s*){0,2}(small-caps))?)(?=(?:(?:[-a-z]+\s*){0,2}(bold(?:er)?|lighter|[1-9]00))?)(?:(?:normal|\1|\2|\3)\s*){0,3}((?:xx?-)?(?:small|large)|medium|smaller|larger|[.\d]+(?:\%|in|[cem]m|ex|p[ctx]))(?:\s*\/\s*(normal|[.\d]+(?:\%|in|[cem]m|ex|p[ctx])))?\s*([-,\'\"\sa-z0-9]+?)\s*$/i; 906 | var fontPart = regex.exec( this.font ); 907 | var data = { 908 | style : fontPart[1] || 'normal', 909 | size : fontPart[4] || '10px', 910 | family : fontPart[6] || 'sans-serif', 911 | weight: fontPart[3] || 'normal', 912 | decoration : fontPart[2] || 'normal', 913 | href : null 914 | }; 915 | 916 | //canvas doesn't support underline natively, but we can pass this attribute 917 | if (this.__fontUnderline === "underline") { 918 | data.decoration = "underline"; 919 | } 920 | 921 | //canvas also doesn't support linking, but we can pass this as well 922 | if (this.__fontHref) { 923 | data.href = this.__fontHref; 924 | } 925 | 926 | return data; 927 | }; 928 | 929 | /** 930 | * Helper to link text fragments 931 | * @param font 932 | * @param element 933 | * @return {*} 934 | * @private 935 | */ 936 | ctx.prototype.__wrapTextLink = function (font, element) { 937 | if (font.href) { 938 | var a = this.__createElement("a"); 939 | a.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", font.href); 940 | a.appendChild(element); 941 | return a; 942 | } 943 | return element; 944 | }; 945 | 946 | /** 947 | * Fills or strokes text 948 | * @param text 949 | * @param x 950 | * @param y 951 | * @param action - stroke or fill 952 | * @private 953 | */ 954 | ctx.prototype.__applyText = function (text, x, y, action) { 955 | var font = this.__parseFont(), 956 | parent = this.__closestGroupOrSvg(), 957 | textElement = this.__createElement("text", { 958 | "font-family" : font.family, 959 | "font-size" : font.size, 960 | "font-style" : font.style, 961 | "font-weight" : font.weight, 962 | "text-decoration" : font.decoration, 963 | "x" : x, 964 | "y" : y, 965 | "text-anchor": getTextAnchor(this.textAlign), 966 | "dominant-baseline": getDominantBaseline(this.textBaseline) 967 | }, true); 968 | 969 | textElement.appendChild(this.__document.createTextNode(text)); 970 | this.__currentElement = textElement; 971 | this.__applyStyleToCurrentElement(action); 972 | parent.appendChild(this.__wrapTextLink(font,textElement)); 973 | }; 974 | 975 | /** 976 | * Creates a text element 977 | * @param text 978 | * @param x 979 | * @param y 980 | */ 981 | ctx.prototype.fillText = function (text, x, y) { 982 | this.__applyText(text, x, y, "fill"); 983 | }; 984 | 985 | /** 986 | * Strokes text 987 | * @param text 988 | * @param x 989 | * @param y 990 | */ 991 | ctx.prototype.strokeText = function (text, x, y) { 992 | this.__applyText(text, x, y, "stroke"); 993 | }; 994 | 995 | /** 996 | * No need to implement this for svg. 997 | * @param text 998 | * @return {TextMetrics} 999 | */ 1000 | ctx.prototype.measureText = function (text) { 1001 | this.__ctx.font = this.font; 1002 | return this.__ctx.measureText(text); 1003 | }; 1004 | 1005 | /** 1006 | * Arc command! 1007 | */ 1008 | ctx.prototype.arc = function (x, y, radius, startAngle, endAngle, counterClockwise) { 1009 | // in canvas no circle is drawn if no angle is provided. 1010 | if (startAngle === endAngle) { 1011 | return; 1012 | } 1013 | startAngle = startAngle % (2*Math.PI); 1014 | endAngle = endAngle % (2*Math.PI); 1015 | if (startAngle === endAngle) { 1016 | //circle time! subtract some of the angle so svg is happy (svg elliptical arc can't draw a full circle) 1017 | endAngle = ((endAngle + (2*Math.PI)) - 0.001 * (counterClockwise ? -1 : 1)) % (2*Math.PI); 1018 | } 1019 | var endX = x+radius*Math.cos(endAngle), 1020 | endY = y+radius*Math.sin(endAngle), 1021 | startX = x+radius*Math.cos(startAngle), 1022 | startY = y+radius*Math.sin(startAngle), 1023 | sweepFlag = counterClockwise ? 0 : 1, 1024 | largeArcFlag = 0, 1025 | diff = endAngle - startAngle; 1026 | 1027 | // https://github.com/gliffy/canvas2svg/issues/4 1028 | if (diff < 0) { 1029 | diff += 2*Math.PI; 1030 | } 1031 | 1032 | if (counterClockwise) { 1033 | largeArcFlag = diff > Math.PI ? 0 : 1; 1034 | } else { 1035 | largeArcFlag = diff > Math.PI ? 1 : 0; 1036 | } 1037 | 1038 | this.lineTo(startX, startY); 1039 | this.__addPathCommand(format("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}", 1040 | {rx:radius, ry:radius, xAxisRotation:0, largeArcFlag:largeArcFlag, sweepFlag:sweepFlag, endX:endX, endY:endY})); 1041 | 1042 | this.__currentPosition = {x: endX, y: endY}; 1043 | }; 1044 | 1045 | /** 1046 | * Generates a ClipPath from the clip command. 1047 | */ 1048 | ctx.prototype.clip = function () { 1049 | var group = this.__closestGroupOrSvg(), 1050 | clipPath = this.__createElement("clipPath"), 1051 | id = randomString(this.__ids), 1052 | newGroup = this.__createElement("g"); 1053 | 1054 | this.__applyCurrentDefaultPath(); 1055 | group.removeChild(this.__currentElement); 1056 | clipPath.setAttribute("id", id); 1057 | clipPath.appendChild(this.__currentElement); 1058 | 1059 | this.__defs.appendChild(clipPath); 1060 | 1061 | //set the clip path to this group 1062 | group.setAttribute("clip-path", format("url(#{id})", {id:id})); 1063 | 1064 | //clip paths can be scaled and transformed, we need to add another wrapper group to avoid later transformations 1065 | // to this path 1066 | group.appendChild(newGroup); 1067 | 1068 | this.__currentElement = newGroup; 1069 | 1070 | }; 1071 | 1072 | /** 1073 | * Draws a canvas, image or mock context to this canvas. 1074 | * Note that all svg dom manipulation uses node.childNodes rather than node.children for IE support. 1075 | * http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-drawimage 1076 | */ 1077 | ctx.prototype.drawImage = function () { 1078 | //convert arguments to a real array 1079 | var args = Array.prototype.slice.call(arguments), 1080 | image=args[0], 1081 | dx, dy, dw, dh, sx=0, sy=0, sw, sh, parent, svg, defs, group, 1082 | currentElement, svgImage, canvas, context, id; 1083 | 1084 | if (args.length === 3) { 1085 | dx = args[1]; 1086 | dy = args[2]; 1087 | sw = image.width; 1088 | sh = image.height; 1089 | dw = sw; 1090 | dh = sh; 1091 | } else if (args.length === 5) { 1092 | dx = args[1]; 1093 | dy = args[2]; 1094 | dw = args[3]; 1095 | dh = args[4]; 1096 | sw = image.width; 1097 | sh = image.height; 1098 | } else if (args.length === 9) { 1099 | sx = args[1]; 1100 | sy = args[2]; 1101 | sw = args[3]; 1102 | sh = args[4]; 1103 | dx = args[5]; 1104 | dy = args[6]; 1105 | dw = args[7]; 1106 | dh = args[8]; 1107 | } else { 1108 | throw new Error("Invalid number of arguments passed to drawImage: " + arguments.length); 1109 | } 1110 | 1111 | parent = this.__closestGroupOrSvg(); 1112 | currentElement = this.__currentElement; 1113 | var translateDirective = "translate(" + dx + ", " + dy + ")"; 1114 | if (image instanceof ctx) { 1115 | //canvas2svg mock canvas context. In the future we may want to clone nodes instead. 1116 | //also I'm currently ignoring dw, dh, sw, sh, sx, sy for a mock context. 1117 | svg = image.getSvg().cloneNode(true); 1118 | if (svg.childNodes && svg.childNodes.length > 1) { 1119 | defs = svg.childNodes[0]; 1120 | while(defs.childNodes.length) { 1121 | id = defs.childNodes[0].getAttribute("id"); 1122 | this.__ids[id] = id; 1123 | this.__defs.appendChild(defs.childNodes[0]); 1124 | } 1125 | group = svg.childNodes[1]; 1126 | if (group) { 1127 | //save original transform 1128 | var originTransform = group.getAttribute("transform"); 1129 | var transformDirective; 1130 | if (originTransform) { 1131 | transformDirective = originTransform+" "+translateDirective; 1132 | } else { 1133 | transformDirective = translateDirective; 1134 | } 1135 | group.setAttribute("transform", transformDirective); 1136 | parent.appendChild(group); 1137 | } 1138 | } 1139 | } else if (image.nodeName === "CANVAS" || image.nodeName === "IMG") { 1140 | //canvas or image 1141 | svgImage = this.__createElement("image"); 1142 | svgImage.setAttribute("width", dw); 1143 | svgImage.setAttribute("height", dh); 1144 | svgImage.setAttribute("preserveAspectRatio", "none"); 1145 | 1146 | if (sx || sy || sw !== image.width || sh !== image.height) { 1147 | //crop the image using a temporary canvas 1148 | canvas = this.__document.createElement("canvas"); 1149 | canvas.width = dw; 1150 | canvas.height = dh; 1151 | context = canvas.getContext("2d"); 1152 | context.drawImage(image, sx, sy, sw, sh, 0, 0, dw, dh); 1153 | image = canvas; 1154 | } 1155 | svgImage.setAttribute("transform", translateDirective); 1156 | svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", 1157 | image.nodeName === "CANVAS" ? image.toDataURL() : image.getAttribute("src")); 1158 | parent.appendChild(svgImage); 1159 | } 1160 | }; 1161 | 1162 | /** 1163 | * Generates a pattern tag 1164 | */ 1165 | ctx.prototype.createPattern = function (image, repetition) { 1166 | var pattern = this.__document.createElementNS("http://www.w3.org/2000/svg", "pattern"), id = randomString(this.__ids), 1167 | img; 1168 | pattern.setAttribute("id", id); 1169 | pattern.setAttribute("width", image.width); 1170 | pattern.setAttribute("height", image.height); 1171 | if (image.nodeName === "CANVAS" || image.nodeName === "IMG") { 1172 | img = this.__document.createElementNS("http://www.w3.org/2000/svg", "image"); 1173 | img.setAttribute("width", image.width); 1174 | img.setAttribute("height", image.height); 1175 | img.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", 1176 | image.nodeName === "CANVAS" ? image.toDataURL() : image.getAttribute("src")); 1177 | pattern.appendChild(img); 1178 | this.__defs.appendChild(pattern); 1179 | } else if (image instanceof ctx) { 1180 | pattern.appendChild(image.__root.childNodes[1]); 1181 | this.__defs.appendChild(pattern); 1182 | } 1183 | return new CanvasPattern(pattern, this); 1184 | }; 1185 | 1186 | ctx.prototype.setLineDash = function (dashArray) { 1187 | if (dashArray && dashArray.length > 0) { 1188 | this.lineDash = dashArray.join(","); 1189 | } else { 1190 | this.lineDash = null; 1191 | } 1192 | }; 1193 | 1194 | /** 1195 | * Not yet implemented 1196 | */ 1197 | ctx.prototype.drawFocusRing = function () {}; 1198 | ctx.prototype.createImageData = function () {}; 1199 | ctx.prototype.getImageData = function () {}; 1200 | ctx.prototype.putImageData = function () {}; 1201 | ctx.prototype.globalCompositeOperation = function () {}; 1202 | ctx.prototype.setTransform = function () {}; 1203 | 1204 | //add options for alternative namespace 1205 | if (typeof window === "object") { 1206 | window.C2S = ctx; 1207 | } 1208 | 1209 | // CommonJS/Browserify 1210 | if (typeof module === "object" && typeof module.exports === "object") { 1211 | module.exports = ctx; 1212 | } 1213 | 1214 | }()); 1215 | --------------------------------------------------------------------------------