├── .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 | 
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 | 
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 | 
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 | 
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 | 
46 |
47 |
48 | ## variations
49 | There're some variations inside "variations" folder. The following image is a web browser version.
50 |
51 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------