forked from adrian17/Gallifreyan
-
Notifications
You must be signed in to change notification settings - Fork 0
/
gallifreyan.js
674 lines (590 loc) · 26.5 KB
/
gallifreyan.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
"use strict";
var canvasSize = 1000.0; //the image resolution in pixels
var canvasScale = canvasSize / 800.0; //800=the canvas size on the screen
var midPoint = canvasSize / 2.0; //the (x, y) of the centerpoint
var outerR = midPoint * 0.9; //radius of the outermost circle
var lineWidth = 3.0 * canvasScale;
var PI = Math.PI;
var allCircles = [],
currentCircle = null, //points to a wordCircle which contains selectedCircle
selectedCircle = null, //points to selected circle
snapMode = true; //disabling this disables some rule checking; can't be toggled for now
var lines = [],
selectedLine = null, //points to selected line
lineEnd = 0; //tells which end of the line is selected
var dirtyRender = true; //whether GUI and red dots will be drawn
var deleteLineMode = false;//whether next selected line will be deleted
// Both ends of a line move with a cursor. It looks like you're placing the first end of the line.
// A click will disable this mode, and you'll normally control the other end of the line.
var addLineMode = false;
Array.prototype.contains = function(k) {
return (this.indexOf(k) != -1);
};
Array.prototype.remove = function(index) {
this.splice(index, 1);
return this;
};
Array.prototype.removeItem = function(item) {
var index = this.indexOf(item);
return index > -1 ? this.remove(index) : this;
};
Number.prototype.clamp = function(min, max) {
return Math.min(Math.max(this, min), max);
};
function pointFromAngle(obj, r, angle) {
return [obj.x + Math.cos(angle) * r, obj.y + Math.sin(angle) * r]
}
//math
function dist(a, b) { return Math.sqrt(Math.pow((a.x - b.x), 2) + Math.pow((a.y - b.y), 2)); }
function normalizeAngle(angle) { while (angle > PI) angle -= 2 * PI; while (angle < -PI) angle += 2 * PI; return angle; } //caps to (-PI, PI)
function angleDifference(a, b) { return normalizeAngle(a-b); } // capped to (-PI, PI);
function angleBetweenCircles(circle, second) {
var d = dist(circle, second);
var angle = Math.acos((second.r*second.r - d*d - circle.r*circle.r) / (-2*d*circle.r));
return angle;
}
//since we are drawing mostly circles, it's not like we need control over beginPath() and stroke() anyway
function drawCircle(x, y, r) { ctx.beginPath(); ctx.arc(x, y, r, 0, PI * 2); ctx.stroke(); }
function drawArc(x, y, r, a1, a2) { ctx.beginPath(); ctx.arc(x, y, r, a1, a2); ctx.stroke(); }
function drawLine(x1, y1, x2, y2) { ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); }
function drawDot(x, y, r) { ctx.beginPath(); ctx.arc(x, y, r, 0, PI * 2); ctx.fill(); }
//draws a red dot in a given location, signifying a circle you can select
function drawRedDot(x, y) { ctx.fillStyle = "red"; drawDot(x, y, 3 + lineWidth / 3); ctx.fillStyle = "black"; }
function drawBigRedDot(x, y) { ctx.fillStyle = "red"; drawDot(x, y, 3 + lineWidth); ctx.fillStyle = "black"; }
$(document).ready(function() {
$("input").val(localStorage.getItem("input"));
prepareCanvas();
createGUI();
redraw();
});
//resets everything and parses the text
function updateText() {
resetZoom();
allCircles = []; lines = []; currentCircle = null; selectedCircle = null; selectedLine = null;
var text = $("input").val().trim().toLowerCase().split(" ");
localStorage.setItem("input", $("input").val());
var words = [];
for (var toParse of text) {
var word = [];
for (var i = 0; i < toParse.length; i++) {
if (toParse.substring(i, i + 2).match("(ch|sh|th|ng|qu)")) {
word.push(toParse.substring(i, i + 2));
i++;
} else if (toParse[i] === "c") {
//soft c comes usually before i, e or y
if (i+1 < toParse.length && toParse[i+1].match("[iey]"))
word.push("s");
else
word.push("k");
} else {
word.push(toParse[i]);
}
}
words.push(word);
}
generateWords(words);
}
//discard any unfinished manual actions (line addition/deletion)
function resetModes() {
deleteLineMode = false;
if (addLineMode) {
addLineMode = false;
deleteLine(selectedLine);
selectedLine = null;
}
}
//create a new line and let user to position it
function addNewLine() {
addLineMode = true;
selectedLine = new Line(allCircles[0], -PI/2, allCircles[0], -PI/2);
lines.push(selectedLine);
}
//disconnect the line from both circles it's connected to, and remove it from the global list
function deleteLine(line) {
line.points[0].circle.lines.removeItem(line);
line.points[1].circle.lines.removeItem(line);
lines.removeItem(line);
}
//a line is always defined be the circles it is connected to and angles in relation to these circles.
//thus, it will always be connected to the circles' borders.
function Line(circle1, a1, circle2, a2) {
this.draw = function() {
ctx.strokeStyle = (selectedLine === this) ? "grey" : "black";
drawLine(this.points[0].x, this.points[0].y, this.points[1].x, this.points[1].y);
if (dirtyRender && this.selectable) {
if (deleteLineMode || (addLineMode && selectedLine === this)) {
drawBigRedDot(this.points[0].x, this.points[0].y);
drawBigRedDot(this.points[1].x, this.points[1].y);
} else {
drawRedDot(this.points[0].x, this.points[0].y);
drawRedDot(this.points[1].x, this.points[1].y);
}
}
};
this.update = function() {
for (var point of this.points)
[point.x, point.y] = pointFromAngle(point.circle, point.circle.r, point.a);
};
this.updatePoint = function(end, circle, a) {
var point = this.points[end];
point.circle.lines.removeItem(this);
point.circle = circle; circle.lines.push(this);
point.a = a;
this.update();
};
this.points = [{ circle: circle1, a: a1 },
{ circle: circle2, a: a2 }];
this.selectable = true;
circle1.lines.push(this); circle2.lines.push(this);
this.update();
}
//every circle or arc you can see is of this class.
//every circle has:
//an owner - the location is always calculated in relation to its owner's position and angle
//a type - which corresponds to the row of the alphabet
//a subtype - which corresponds to the column of the alphabet
//if the letter is a vowel, then type=5 (when it's a standalone letter) or 6 (when it's connected to a consonant)
//a list of other circles and lines connected to it, so they can easily updated in a cascading style
function Circle(owner, type, subtype, d, r, a) {
this.draw = function() {
ctx.strokeStyle = (selectedCircle === this) ? "grey" : "black";
if (this.isWordCircle) { //it's a wordCircle so we need to make a gap for B- and T- row letters
var angles = []; //a list of intersections with these letters
for (var child of this.children) {
if (child.hasGaps) {
var an = angleBetweenCircles(this, child);
angles.push(child.a + an, child.a - an);
}
}
if (angles.length === 0) angles = [0, 2 * PI];
for (var i = angles.length; i > 0; i -= 2) { //we're going in the oppposite direction as that's how arc() draws
drawArc(this.x, this.y, this.r, angles[i % angles.length], angles[i - 1]);
}
}
else if (this.hasGaps) { //so it's not a wordCircle; now let's check if it's a B- or T- row letter
var an = angleBetweenCircles(this, this.owner);
drawArc(this.x, this.y, this.r, this.a + PI - an, this.a + PI + an);
}
else { //if not, we can just draw a circle there
drawCircle(this.x, this.y, this.r);
}
if (this.dots) { //drawing the dots
var dotR = 3 + lineWidth / 2;
var r = this.r - 1 - 3 * dotR
var delta = (0.2 * this.owner.r / this.r);
for (var i = -1; i < this.dots - 1; i++)
drawDot(...pointFromAngle(this, r, this.a + delta * i + PI), dotR);
}
if (dirtyRender && this.selectable)
drawRedDot(this.x, this.y);
};
this.update = function(d, a) { //recalculates the position, forces other circles/lines connected to it to update too
var oldA = this.a;
[this.x, this.y] = pointFromAngle(this.owner, d, a);
this.d = d;
this.a = normalizeAngle(a);
for (var child of this.children) {
if (this.isWordCircle)
child.update(child.d, child.a); // don't change word orientation
else
child.update(child.d, child.a - oldA + this.a); // adjust vowel's orienatation
}
for (var line of this.lines)
line.update();
};
this.owner = owner;
this.children = [];
this.type = type; this.subtype = subtype;
// currently only word circles lay on main circle; this may change in the future
this.isWordCircle = owner == allCircles[0];
this.isVowel = this.type === 5 || this.type === 6;
this.isConsonant = ! this.isVowel;
this.hasGaps = this.type === 1 || this.type === 3;
this.dots = this.isConsonant ? [null, 0, 2, 3, 0, 0, 0][this.subtype] : 0;
this.nLines = 0; //expected number of lines, according to rules
this.lines = [];
this.selectable = true;
this.r = r;
this.update(d, a);
}
//selects the circle/line. Checks whether any buttons are pressed.
function doClick(e) {
var mouse = getMouse(e);
if (selectedCircle != null) { selectedCircle = null; redraw(); return; }
if (selectedLine != null && !addLineMode) { selectedLine = null; redraw(); return; }
for (var button of buttons) {
if (button.click(e)) return;
}
var minD = 40;
for (var circle of allCircles) {
if (!circle.selectable) continue;
var d = dist(circle, mouse);
if (d < minD) {
minD = d;
selectedCircle = circle;
if (selectedCircle.type === 6) currentCircle = selectedCircle.owner.owner;
else currentCircle = selectedCircle.owner;
}
}
for (var line of lines) {
if (!line.selectable) continue;
for (var j = 0; j < 2; ++j) {
var d = dist(line.points[j], mouse);
if (d < minD) {
minD = d;
selectedLine = line;
lineEnd = j;
}
}
}
if (selectedLine != null) {
selectedCircle = null; //if we've selected a line, let's unselect a circle
if (deleteLineMode) {
deleteLine(selectedLine);
selectedLine = null;
}
}
if (deleteLineMode) {
deleteLineMode = false;
redraw();
}
if (addLineMode)
addLineMode = false; //don't move both ends anymore; now it's a normal selectedLine with one attached end
}
//makes sure that the correct distance from the base circle is kept according to language rules
function correctCircleLocation(selected, d, a) {
if (!snapMode) { selected.update(d, a); return; }
switch (selected.type) {
case 1: //B-row
d = d.clamp(selected.owner.r - selected.r + 1, selected.owner.r - selected.r * 0.5);
break;
case 2: //J-row
d = d.clamp(0, selected.owner.r - selected.r - 5);
break;
case 3: //T-row
d = d.clamp(selected.owner.r, selected.owner.r + selected.r * 0.8);
break;
case 4: //TH-row
d = selected.owner.r;
break;
case 5: //vowels, laying on a wordCircle
switch (selected.subtype) {
case 1: d = d.clamp(selected.owner.r + selected.r + 5, Infinity); break;
case 2:
case 3:
case 5:
d = selected.owner.r; break;
case 4: d = d.clamp(0, selected.owner.r - selected.r - 5); break;
} break;
case 6: //vowels, connected to consonants
switch (selected.subtype) {
case 1:
if (selected.owner.type === 1) { d = d.clamp(selected.r * 2, Infinity); a = selected.owner.a; }
if (selected.owner.type === 2) { d = d.clamp(selected.owner.r + selected.r, Infinity); a = selected.owner.a; }
if (selected.owner.type === 3) { d = d.clamp(selected.owner.r / 2, Infinity); a = selected.owner.a; }
if (selected.owner.type === 4) { d = d.clamp(selected.r, selected.owner.r - selected.r); a = selected.owner.a; }
break;
case 2:
case 3:
case 5:
if (selected.owner.type === 3) { d = selected.owner.d - selected.owner.owner.r; a = selected.owner.a + PI; }//locked
else d = 0;
break;
case 4:
d = selected.owner.r; break;
} break;
}
selected.update(d, a);
for (var child of selected.children)
correctCircleLocation(child, child.d, child.a);
}
function getCircleAngleLimits(circle) {
var index = currentCircle.children.indexOf(circle);
// first/last letter of a word are limited to PI/2
var nextAngle = PI / 2;
var previousAngle = PI / 2;
if (index + 1 < currentCircle.children.length)
nextAngle = currentCircle.children[index + 1].a;
if (index >= 1)
previousAngle = currentCircle.children[index - 1].a;
return [nextAngle, previousAngle];
}
//manages the movement of circles and lines. In case of circles, correctCircleLocation() is called to enforce language rules
$("canvas").mousemove(function(e) {
var mouse = getMouse(e);
if (selectedCircle != null) {
var selected = selectedCircle;
var a = Math.atan2(mouse.y - selected.owner.y, mouse.x - selected.owner.x);
a = normalizeAngle(a);
var d = dist(mouse, selected.owner);
if (selected.type != 6 && currentCircle.children.length > 2) {
var [nextAngle, previousAngle] = getCircleAngleLimits(selectedCircle);
if (nextAngle > previousAngle) { a > 0 ? previousAngle += 2 * PI : nextAngle -= 2 * PI; } //still buggy
if (a - nextAngle > 2 * PI || a - previousAngle > 2 * PI) a -= 2 * PI; if (a - nextAngle < -2 * PI || a - previousAngle < -2 * PI) a += 2 * PI;
a = a.clamp(nextAngle, previousAngle);
}
correctCircleLocation(selected, d, a);
redraw();
return;
}
if (selectedLine != null) {
var selected = selectedLine;
var minD = 50;
for (var circle of allCircles) {
var d = Math.abs(dist(mouse, circle) - circle.r);
if (d < minD) {
var a = Math.atan2(mouse.y - circle.y, mouse.x - circle.x);
// this tries to prevent you from moving the line to a gap part of the circle.
// it's not perfect though, since you can still get there if that pixel happens to be black for some other reason
if (isPixelWhite(...pointFromAngle(circle, circle.r, a)))
return;
minD = d;
selected.updatePoint(lineEnd, circle, a);
if (addLineMode)
selected.updatePoint((lineEnd+1) % 2, circle, a); //moving both ends at once looks like a single red dot
}
}
redraw();
return;
}
});
//changes the circle's radius
$("canvas").mousewheel(function(event, delta, deltaX, deltaY) {
if (selectedCircle != null) {
var selected = selectedCircle;
var oldR = selected.r;
if (delta > 0 || deltaX > 0 || deltaY > 0) selected.r += 2; else selected.r -= 2;
if (selected.isVowel)
selected.r = selected.r.clamp(10, Infinity);
else
selected.r = selected.r.clamp(selected.owner.r * 0.1, Infinity);
for (var child of selected.children) {
child.r *= (selected.r / oldR);
child.update(child.d * (selected.r / oldR), child.a);
}
correctCircleLocation(selected, selected.d, selected.a);
redraw();
}
return false;
});
//draws red lines to signify the min/max angles that the circle can move within
function drawAngles() {
if (currentCircle.children.length < 3) return;
var len = selectedCircle.owner.r * 1.3;
var [nextAngle, previousAngle] = getCircleAngleLimits(selectedCircle);
ctx.strokeStyle = "red";
drawLine(currentCircle.x, currentCircle.y, ...pointFromAngle(currentCircle, len, nextAngle));
drawLine(currentCircle.x, currentCircle.y, ...pointFromAngle(currentCircle, len, previousAngle));
ctx.strokeStyle = "black";
}
//generates the sentence
function generateWords(words) {
allCircles.push(new Circle({ x: midPoint, y: midPoint, a: 0 }, 4, 0, 0, outerR, 0));
allCircles[0].selectable = false;
var delta = 2 * PI / words.length;
var angle = PI / 2;
var r = words.length === 1 ? outerR * 0.8 : 2.5 * outerR / (words.length + 4);
var d = words.length === 1 ? 0 : outerR - r * 1.2;
for (var word of words) {
var wordL = 0; //approximates the number of letters, taking into account that some will be merged
for (var j = 0; j < word.length; j++) {
if (j > 0 && word[j].match("^(a|e|i|o|u)$") && !(word[j - 1].match("^(a|e|i|o|u)$"))) continue;
wordL++;
}
generateWord(word, wordL, r, d, angle);
angle -= delta; angle = normalizeAngle(angle);
}
redraw();
createLines();
redraw();
}
//assigns the subtype
var map = {
"b": 1, "ch": 2, "d": 3, "f": 4, "g": 5, "h": 6,
"j": 1, "k": 2, "l": 3, "m": 4, "n": 5, "p": 6,
"t": 1, "sh": 2, "r": 3, "s": 4, "v": 5, "w": 6,
"th": 1, "y": 2, "z": 3, "ng": 4, "qu": 5, "x": 6,
"a": 1, "e": 2, "i": 3, "o": 4, "u": 5
};
//generates a single word
function generateWord(word, wordL, mcR, dist, mainAngle) {
var delta = 2 * PI / wordL;
var angle = PI / 2;
var globalR = 1.8 * mcR / (wordL + 2);
var newMainCircle = new Circle(allCircles[0], 2, 0, dist, mcR, mainAngle);
allCircles.push(newMainCircle);
allCircles[0].children.push(newMainCircle);
for (var letter of word) {
var newCircle = null;
var owner = newMainCircle;
var type = 0, r = 0, d = 0;
var subtype = map[letter];
var nLines = [0, 0, 0, 3, 1, 2][subtype - 1];
if (letter.match("^(b|ch|d|f|g|h)$")) {
type = 1, r = globalR, d = mcR - r + 1;
newCircle = new Circle(owner, type, subtype, d, r, angle);
}
else if (letter.match("^(j|k|l|m|n|p)$")) {
type = 2, r = globalR, d = mcR - r - 5;
newCircle = new Circle(owner, type, subtype, d, r, angle);
}
else if (letter.match("^(t|sh|r|s|v|w)$")) {
type = 3, r = globalR * 1.3, d = mcR * 1.1;
newCircle = new Circle(owner, type, subtype, d, r, angle);
}
else if (letter.match("^(th|y|z|ng|qu|x)$")) {
type = 4, r = globalR, d = mcR;
newCircle = new Circle(owner, type, subtype, d, r, angle);
}
else if (letter.match("^(a|e|i|o|u)$")) {
nLines = [0, 0, 1, 0, 1][subtype - 1];
var previous = owner.children[owner.children.length - 1];
r = globalR * 0.25;
if (previous && subtype != 4 && previous.type === 3) { //let's not attach to this as floating letters look ugly
type = 5, d = mcR;
angle += delta / 2;
newCircle = new Circle(owner, type, subtype, owner.r, r, angle);
angle += delta / 2;
}
else if (previous && previous.isConsonant && previous.children.length === 0) { //are we free to attach?
type = 6;
owner = previous;
angle += delta;
newCircle = new Circle(owner, type, subtype, owner.r / 2, r, owner.a + PI + PI / 8);
if ([2, 3, 5].contains(subtype)) newCircle.selectable = false;
}
else { //let's just add this normally then.
type = 5, d = mcR;
newCircle = new Circle(owner, type, subtype, owner.r, r, angle);
}
}
if (newCircle === null) continue; //skip, if the letter wasn't found
newCircle.nLines = nLines;
correctCircleLocation(newCircle, newCircle.d, newCircle.a);
owner.children.push(newCircle);
allCircles.push(newCircle);
angle -= delta; angle = normalizeAngle(angle);
}
}
//checks if a line end is too close to an another line
//will bug out around the PI=-PI point but let's ignore it for now
function isLineTooClose(circle, angle) {
for (var line of circle.lines) {
var diff;
diff = Math.abs(angleDifference(line.points[0].a, angle));
if (line.points[0].circle === circle && diff < 0.1) return true;
diff = Math.abs(angleDifference(line.points[1].a, angle));
if (line.points[1].circle === circle && diff < 0.1) return true;
}
return false;
}
function isPixelWhite(x, y) {
// note: x, y are window coords, not world coords (unless zoom==1)
var data = ctx.getImageData(Math.floor(x), Math.floor(y), 1, 1).data;
// actually, the default "white" on canvas is transparent black
return data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0;
}
//generates the lines after all the circles are created
function createLines() {
var baseLineAngle = circle => {
if (circle.type === 6)
return circle.subtype === 3 ? circle.owner.a + PI : circle.owner.a;
else if (circle.type === 5)
return circle.subtype === 3 ? circle.a + PI : circle.a;
else
return circle.a + PI;
}
var allowedOffset = circle => circle.type === 6 ? PI / 6 : PI / 2;
var checkedCircles = allCircles.slice(1); // without main circle
// note: currently bestAngle is not reset. That means a new line may happen to have the same angle as a previous one
var bestAngle = 0;
for (var circle of checkedCircles) {
if (circle.nLines === 0) continue;
var passes = 0;
while (circle.lines.length < circle.nLines) {
//looks for the best path to the base circle if there are no other options left
if (passes > 100 || (circle.isVowel && circle.subtype === 5)) {
circle2 = allCircles[0]; //the only one left
//let's look for the path with the least intersections
var minInter = 1000;
for (var n = 0; n < 100; ++n) {
var inter = 0;
var randAngle = baseLineAngle(circle) + (Math.random() - 0.5) * allowedOffset(circle);
var [x, y] = pointFromAngle(circle, circle.r, randAngle);
var intersection = findIntersection(circle2.x, circle2.y, circle2.r, x, y, randAngle);
var maxT = intersection.t;
if (isLineTooClose(circle, randAngle)) continue;
if (isLineTooClose(circle2, intersection.a)) continue;
for (var circle3 of checkedCircles) {
if (circle3 === circle) continue;
intersection = findIntersection(circle3.x, circle3.y, circle3.r, x, y, randAngle);
if (intersection === 0) continue;
if (intersection.t < maxT) inter++;
}
if (inter < minInter) { minInter = inter; bestAngle = randAngle; }
}
var [x, y] = pointFromAngle(circle, circle.r, bestAngle);
var intersection = findIntersection(circle2.x, circle2.y, circle2.r, x, y, bestAngle);
lines.push(new Line(circle, bestAngle, circle2, intersection.a));
if (circle.isVowel) break;
else continue;
}
//normal routine, searches for pairs that still need circles
for (var circle2 of checkedCircles) {
if (circle2 === circle) continue;
if (circle2.lines.length >= circle2.nLines) continue;
if (circle2.isVowel && circle2.subtype === 5) continue;
var angle = Math.atan2(circle2.y - circle.y, circle2.x - circle.x);
var [x, y] = pointFromAngle(circle, circle.r, angle);
var intersection = findIntersection(circle2.x, circle2.y, circle2.r, x, y, angle);
if (intersection === 0) continue;
var angle2 = intersection.a;
if (Math.floor(Math.random() + 0.6)) continue; //some extra randomness
var rand = (Math.random() - 0.5) * PI / 4;
angle += rand;
angle2 -= rand;
if (Math.abs(angleDifference(angle, baseLineAngle(circle))) > allowedOffset(circle))
continue;
if (Math.abs(angleDifference(angle2, baseLineAngle(circle2))) > allowedOffset(circle2))
continue;
if (isLineTooClose(circle, angle)) continue;
if (isLineTooClose(circle2, angle2)) continue;
//let's just check if we don't run into a white section of a circle
if (circle.hasGaps)
if (isPixelWhite(...pointFromAngle(circle, circle.r, angle)))
continue;
if (circle2.hasGaps)
if (isPixelWhite(...pointFromAngle(circle2, circle2.r, angle2)))
continue;
//nothing more to check, let's make a line there
lines.push(new Line(circle, angle, circle2, angle2));
if (circle.lines.length >= circle.nLines) break;
}
passes++;
if (passes > 103) break;
}
}
}
//checks whether all the circles have a correct amount of lines connected
function checkLines() {
for (var circle of allCircles.slice(1)) { //we don't check the first circle
if (circle.isWordCircle) continue; //also skip wordCircles
if (circle.nLines != circle.lines.length) return 0;
}
return 1;
}
//the core drawing routine
function redraw() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvasSize, canvasSize);
var data = scrollerObj.getValues();
ctx.setTransform(data.zoom, 0, 0, data.zoom, -data.left * canvasScale, -data.top * canvasScale);
ctx.lineWidth = lineWidth;
for (var circle of allCircles)
circle.draw();
for (var line of lines)
line.draw();
if (selectedCircle != null && selectedCircle.type != 6) drawAngles();
ctx.setTransform(1, 0, 0, 1, 0, 0);
if (dirtyRender) { drawGUI(); }
}