├── buhin.js
├── pointmaker.js
├── sample.js
├── sample.as
├── stroketype.js
├── polygon.js
├── polygons.js
├── 2d.js
├── gothic.js
├── kage.js
├── curve.js
├── bezier.js
├── util.js
├── fontcanvas.js
├── fit-curve.js
├── COPYING
└── mincho.js
/buhin.js:
--------------------------------------------------------------------------------
1 | export class Buhin{
2 | constructor(number){
3 | this.hash = {};
4 | }
5 | set(name, data){ // void
6 | this.hash[name] = data;
7 | }
8 | push(name, data){ // void
9 | this.set(name, data)
10 | }search(name){ // string
11 | if(this.hash[name]){
12 | return this.hash[name];
13 | }
14 | return ""; // no data
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/pointmaker.js:
--------------------------------------------------------------------------------
1 | import {DIR_POSX} from "./util.js";
2 | export class PointMaker{
3 | constructor(x, y, dir, scale){
4 | this.x = x;
5 | this.y = y;
6 | this.dir = dir;
7 | this.scale = scale;
8 | if(!scale){
9 | this.scale = 1;
10 | }
11 | if(!dir){
12 | this.dir = DIR_POSX;//positive x
13 | }
14 | }
15 | setpos(x, y){
16 | this.x = x;
17 | this.y = y;
18 | }
19 | vec(x, y){ // void
20 | return [this.x + this.scale*this.dir.cos*x - this.scale*this.dir.sin*y,
21 | this.y + this.scale*this.dir.sin*x + this.scale*this.dir.cos*y]
22 | }
23 | setdir(dir){
24 | this.dir = dir;
25 | }
26 | setscale(scale){
27 | this.scale = scale;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/sample.js:
--------------------------------------------------------------------------------
1 | // KAGE engine sample script for JavaScript engine
2 | //
3 | // % js sample.js > result.svg (SpiderMonkey)
4 | // % java -jar js.jar sample.js > result.svg (Rhino)
5 |
6 | load("2d.js");
7 | load("buhin.js");
8 | load("curve.js");
9 | load("kage.js");
10 | load("kagecd.js");
11 | load("kagedf.js");
12 | load("polygon.js");
13 | load("polygons.js");
14 |
15 | var kage = new Kage();
16 | var polygons = new Polygons();
17 |
18 | kage.kBuhin.push("u6f22", "99:150:0:9:12:73:200:u6c35-07:0:-10:50$99:0:0:54:10:190:199:u26c29-07");
19 | kage.kBuhin.push("u6c35-07", "2:7:8:42:12:99:23:124:35$2:7:8:20:62:75:71:97:85$2:7:8:12:123:90:151:81:188$2:2:7:63:144:109:118:188:51");
20 | kage.kBuhin.push("u26c29-07", "1:0:0:18:29:187:29$1:0:0:73:10:73:48$1:0:0:132:10:132:48$1:12:13:44:59:44:87$1:2:2:44:59:163:59$1:22:23:163:59:163:87$1:2:2:44:87:163:87$1:0:0:32:116:176:116$1:0:0:21:137:190:137$7:32:7:102:59:102:123:102:176:10:190$2:7:0:105:137:126:169:181:182");
21 |
22 | kage.makeGlyph(polygons, "u6f22");
23 |
24 | print(polygons.generateSVG(false));
25 |
26 |
--------------------------------------------------------------------------------
/sample.as:
--------------------------------------------------------------------------------
1 | #include "2d.js"
2 | #include "buhin.js"
3 | #include "curve.js"
4 | #include "kage.js"
5 | #include "kagecd.js"
6 | #include "kagedf.js"
7 | #include "polygon.js"
8 | #include "polygons.js"
9 |
10 | var kage = new Kage();
11 | kage.kUseCurve = false;
12 | var polygons = new Polygons();
13 |
14 | kage.kBuhin.push("u6f22", "99:0:0:9:12:73:200:u6c35-07$99:0:0:54:10:190:199:u26c29-07");
15 | kage.kBuhin.push("u6c35-07", "2:7:8:42:12:99:23:124:35$2:7:8:20:62:75:71:97:85$2:7:8:12:123:90:151:81:188$2:2:7:63:144:109:118:188:51");
16 | kage.kBuhin.push("u26c29-07", "1:0:0:18:29:187:29$1:0:0:73:10:73:48$1:0:0:132:10:132:48$1:12:13:44:59:44:87$1:2:2:44:59:163:59$1:22:23:163:59:163:87$1:2:2:44:87:163:87$1:0:0:32:116:176:116$1:0:0:21:137:190:137$7:32:7:102:59:102:123:102:176:10:190$2:7:0:105:137:126:169:181:182");
17 |
18 | kage.makeGlyph(polygons, "u6f22");
19 |
20 | _root.lineStyle(0, 0, 100);
21 | for (var i = 0; i < polygons.array.length; i++) {
22 | _root.beginFill(0, 100);
23 | _root.moveTo(polygons.array[i].array[0].x, polygons.array[i].array[0].y);
24 | for (var j = 1; j < polygons.array[i].array.length; j++) {
25 | _root.lineTo(polygons.array[i].array[j].x, polygons.array[i].array[j].y);
26 | }
27 | _root.endFill();
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/stroketype.js:
--------------------------------------------------------------------------------
1 | export const STROKETYPE = {
2 | STRAIGHT : 1,//直線
3 | CURVE : 2,//曲線 quadratic bezier curve
4 | BENDING : 3,//折れ used in "札" "己"
5 | BENDING_ROUND : 4,//used for "乙"
6 | BEZIER : 6,//複曲線 cubic bezier curve
7 | VCURVE : 7,//vertical line and curve. used in the leftmost stroke of "月".
8 | //although the angle of line can be chosen arbitrarily, only vertical line is expected.
9 | REFERENCE : 99,
10 | }
11 | export const STARTTYPE = {
12 | // INTERNAL_FOR_BENDING : 6,
13 | // INTERNAL_PLANE : 1,
14 | OPEN : 0,//simple lines like "三" or "川" (two strokes on the right side)
15 | //also used in the left stroke of "人"
16 | CONNECTING_H : 2,//horizontal strokes connecting to other strokes. used in the center strokes of "日".
17 | UPPER_LEFT_CORNER : 12,//the starting point is at the upper left corner. usually used for vertical lines, like the leftmost stroke of "日".
18 | UPPER_RIGHT_CORNER : 22,//the starting point is at the upper right corner. usually used for vertical lines, like the rightmost stroke of "日".
19 | CONNECTING_V : 32,//vertical strokes connecting to other strokes. used in the center strokes of "工".
20 | THIN : 7,//used in the right stroke of "人"
21 | ROOFED_THIN : 27,//used in the right stroke of "入"
22 | //////custom
23 | CONNECT_THIN: 37,
24 | CONNECTING_MANUAL: 39//the edge is cut in a certain direction; the degree is determined by hidden parameter. This shape is for straight line, but should be encoded as CURVE data
25 | //40-60 is used for curve strokes
26 |
27 | }
28 | export const ENDTYPE = {
29 | // INTERNAL_LAST_FILLED_CIRCLE : 1,
30 | // INTERNAL_TURN_LEFT : 14,//used in original KAGE-engine implementation
31 | // INTERNAL_TURN_UPWARDS : 15,//used in original KAGE-engine implementation
32 |
33 | OPEN : 0,//simple lines like "三" or "川" (two strokes on the right side)
34 | //also used in the right stroke of "人"(L2R sweep)
35 | CONNECTING_H : 2,
36 | CONNECTING_V : 32,///vertical strokes connecting to other strokes. used in the center strokes of "工".
37 | LOWER_LEFT_CORNER : 13,
38 | LOWER_RIGHT_CORNER : 23,
39 | LOWER_LEFT_ZH_OLD : 313,//for characters used in China.
40 | LOWER_LEFT_ZH_NEW : 413,//for characters used in China.
41 |
42 | TURN_LEFT : 4,//adds a short line to the left. used in the rightmost stroke of "月".
43 | LOWER_RIGHT_HT : 24,//for characters used in China.
44 | TURN_UPWARDS : 5,//adds a short upward line. used in the rightmost stroke of "札" or "風".
45 | LEFT_SWEEP : 7, //thin end; used in the left stroke of "人".
46 | STOP : 8,//used in the rightmost stroke of "小" or lower four dots of "魚".
47 | }
48 |
--------------------------------------------------------------------------------
/polygon.js:
--------------------------------------------------------------------------------
1 | export class Polygon{
2 | // resolution : 0.0001
3 | constructor(number){
4 | this.array = new Array();
5 | // initialize
6 | if(number){
7 | for(var i = 0; i < number; i++){
8 | this.push(0, 0, 0);
9 | }
10 | }
11 | }
12 | push(x, y, off){ // void
13 | var temp = new Object();
14 | temp.x = x;
15 | temp.y = y;
16 | if(off != 1 && off != 2){
17 | off = 0;
18 | }
19 | temp.off = off;
20 | this.array.push(temp);
21 | }
22 | push2(p, off){
23 | let [x, y] = p;
24 | this.push(x, y, off);
25 | }
26 | set(index, x, y, off){ // void
27 | this.array[index].x = x;
28 | this.array[index].y = y;
29 | if(off != 1 && off != 2){
30 | off = 0;
31 | }
32 | this.array[index].off = off;
33 | }
34 | to_font1000(){
35 | for(var j = 0; j < this.array.length; j++){
36 | this.array[j].x = this.array[j].x*5;
37 | this.array[j].y = this.array[j].y*-5 - 200;
38 | }
39 | }
40 | reverse(){ // void
41 | this.array.reverse();
42 | }
43 | concat(poly){ // void
44 | this.array = this.array.concat(poly.array);
45 | }
46 | shift(){ // void
47 | this.array.shift();
48 | }
49 | unshift(x, y, off){ // void
50 | var temp = new Object();
51 | temp.x = x;
52 | temp.y = y;
53 | if(off != 1 && off != 2){
54 | off = 0;
55 | }
56 | temp.off = off;
57 | this.array.unshift(temp);
58 | }
59 | get_sub_path_svg(){
60 | let buffer = "";
61 | buffer += "M";
62 | buffer += this.array[0].x + "," + this.array[0].y + " ";
63 | let mode = "";
64 | for(var j = 1; j < this.array.length; j++){
65 | if(this.array[j].off == 1 && mode != "Q"){
66 | buffer += "Q"; mode = "Q";
67 | } else if(this.array[j-1].off == 0 && this.array[j].off == 2 && mode != "C"){
68 | buffer += "C"; mode = "C";
69 | } else if(this.array[j-1].off == 0 && this.array[j].off == 0 && mode != "L"){
70 | buffer += "L"; mode = "L";
71 | }
72 | buffer += this.array[j].x + "," + this.array[j].y + " ";
73 | }
74 | buffer += "Z";
75 | return buffer;
76 | }
77 | get_sub_path_svg_font(){
78 | let buffer = "";
79 | buffer += "M";
80 | buffer += (this.array[0].x*5.250 - 25) + "," + (916 - this.array[0].y*5.250) + " ";
81 | let mode = "";
82 | for(var j = 1; j < this.array.length; j++){
83 | if(this.array[j].off == 1 && mode != "Q"){
84 | buffer += "Q"; mode = "Q";
85 | } else if(this.array[j-1].off == 0 && this.array[j].off == 2 && mode != "C"){
86 | buffer += "C"; mode = "C";
87 | } else if(this.array[j-1].off == 0 && this.array[j].off == 0 && mode != "L"){
88 | buffer += "L"; mode = "L";
89 | }
90 | buffer += (this.array[j].x*5.250 - 25) + "," + (916 - this.array[j].y*5.250) + " ";
91 | }
92 | buffer += "Z";
93 | return buffer;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/polygons.js:
--------------------------------------------------------------------------------
1 | export class Polygons{
2 | // method
3 | constructor(){
4 | this.array = new Array();
5 | }
6 | clear(){ // void
7 | this.array = new Array();
8 | }
9 | push(polygon){ // void
10 | // only a simple check
11 | var minx = 200;
12 | var maxx = 0;
13 | var miny = 200;
14 | var maxy = 0;
15 | var error = 0;
16 | for(var i = 0; i < polygon.array.length; i++){
17 | if(polygon.array[i].x < minx){
18 | minx = polygon.array[i].x;
19 | }
20 | if(polygon.array[i].x > maxx){
21 | maxx = polygon.array[i].x;
22 | }
23 | if(polygon.array[i].y < miny){
24 | miny = polygon.array[i].y;
25 | }
26 | if(polygon.array[i].y > maxy){
27 | maxy = polygon.array[i].y;
28 | }
29 | if(isNaN(polygon.array[i].x) || isNaN(polygon.array[i].y)){
30 | error++;
31 | }
32 | }
33 | if(error == 0 && minx != maxx && miny != maxy && polygon.array.length >= 3){
34 | var newArray = new Array();
35 | newArray.push(polygon.array.shift());
36 | while(polygon.array.length != 0){
37 | var temp = polygon.array.shift();
38 | //if(newArray[newArray.length - 1].x != temp.x ||
39 | // newArray[newArray.length - 1].y != temp.y){
40 | newArray.push(temp);
41 | //}
42 | }
43 | if(newArray.length >= 3){
44 | polygon.array = newArray;
45 | this.array.push(polygon);
46 | }
47 | }
48 | }
49 | concat(polygons){
50 | for(let polygon of polygons.array){
51 | this.push(polygon);
52 | }
53 | }
54 | generateSVG(){ // string
55 | var buffer = "";
56 | buffer += "\n";
63 | return buffer;
64 | }
65 | generateSVG2(){ // string
66 | var buffer = "";
67 | buffer += "\n";
72 | return buffer;
73 | }
74 | get_path_svg(){
75 | var buffer = "";
76 | for(var i = 0; i < this.array.length; i++){
77 | buffer += this.array[i].get_sub_path_svg();
78 | }
79 | return buffer;
80 | }
81 | get_path_svg_font(){
82 | var buffer = "";
83 | for(var i = 0; i < this.array.length; i++){
84 | buffer += this.array[i].get_sub_path_svg_font();
85 | }
86 | return buffer;
87 | }
88 | generateEPS(){ // string
89 | var buffer = "";
90 | buffer += "%!PS-Adobe-3.0 EPSF-3.0\n";
91 | buffer += "%%BoundingBox: 0 -208 1024 816\n";
92 | buffer += "%%Pages: 0\n";
93 | buffer += "%%Title: Kanji glyph\n";
94 | buffer += "%%Creator: GlyphWiki powered by KAGE system\n";
95 | buffer += "%%CreationDate: " + new Date() + "\n";
96 | buffer += "%%EndComments\n";
97 | buffer += "%%EndProlog\n";
98 |
99 | for(var i = 0; i < this.array.length; i++){
100 | for(var j = 0; j < this.array[i].array.length; j++){
101 | buffer += (this.array[i].array[j].x * 5) + " " + (1000 - this.array[i].array[j].y * 5 - 200) + " ";
102 | if(j == 0){
103 | buffer += "newpath\nmoveto\n";
104 | } else {
105 | buffer += "lineto\n";
106 | }
107 | }
108 | buffer += "closepath\nfill\n";
109 | }
110 | buffer += "%%EOF\n";
111 | return buffer;
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/2d.js:
--------------------------------------------------------------------------------
1 | // Reference : http://www.cam.hi-ho.ne.jp/strong_warriors/teacher/chapter0{4,5}.html
2 |
3 | function point(x, y){
4 | this.x = x;
5 | this.y = y;
6 | }
7 |
8 | function getCrossPoint(x11, y11, x12, y12, x21, y21, x22, y22){ // point
9 | var a1 = y12 - y11;
10 | var b1 = x11 - x12;
11 | var c1 = -1 * a1 * x11 - b1 * y11;
12 | var a2 = y22 - y21;
13 | var b2 = x21 - x22;
14 | var c2 = -1 * a2 * x21 - b2 * y21;
15 |
16 | var temp = b1 * a2 - b2 * a1;
17 | if(temp == 0){ // parallel
18 | return false;
19 | }
20 | return new point((c1 * b2 - c2 * b1) / temp, (a1 * c2 - a2 * c1) / temp);
21 | }
22 |
23 | function isCross(x11, y11, x12, y12, x21, y21, x22, y22){ // boolean
24 | var temp = getCrossPoint(x11, y11, x12, y12, x21, y21, x22, y22);
25 | if(!temp){ return false; }
26 | if(x11 < x12 && (temp.x < x11 || x12 < temp.x) ||
27 | x11 > x12 && (temp.x < x12 || x11 < temp.x) ||
28 | y11 < y12 && (temp.y < y11 || y12 < temp.y) ||
29 | y11 > y12 && (temp.y < y12 || y11 < temp.y)
30 | ){
31 | return false;
32 | }
33 | if(x21 < x22 && (temp.x < x21 || x22 < temp.x) ||
34 | x21 > x22 && (temp.x < x22 || x21 < temp.x) ||
35 | y21 < y22 && (temp.y < y21 || y22 < temp.y) ||
36 | y21 > y22 && (temp.y < y22 || y21 < temp.y)
37 | ){
38 | return false;
39 | }
40 | return true;
41 | }
42 |
43 | function isCrossBox(x1, y1, x2, y2, bx1, by1, bx2, by2){ // boolean
44 | if(isCross(x1, y1, x2, y2, bx1, by1, bx2, by1)){ return true; }
45 | if(isCross(x1, y1, x2, y2, bx2, by1, bx2, by2)){ return true; }
46 | if(isCross(x1, y1, x2, y2, bx1, by2, bx2, by2)){ return true; }
47 | if(isCross(x1, y1, x2, y2, bx1, by1, bx1, by2)){ return true; }
48 | return false;
49 | }
50 |
51 | export function isCrossBoxWithOthers(strokesArray, i, bx1, by1, bx2, by2){ // boolean
52 | for(var j = 0; j < strokesArray.length; j++){
53 | if(i == j){ continue; }
54 | switch(strokesArray[j][0]){
55 | case 0:
56 | case 8:
57 | case 9:
58 | break;
59 | case 6:
60 | case 7:
61 | if(isCrossBox(strokesArray[j][7],
62 | strokesArray[j][8],
63 | strokesArray[j][9],
64 | strokesArray[j][10],
65 | bx1, by1, bx2, by2)){
66 | return true;
67 | }
68 | case 2:
69 | case 12:
70 | case 3:
71 | case 4:
72 | if(isCrossBox(strokesArray[j][5],
73 | strokesArray[j][6],
74 | strokesArray[j][7],
75 | strokesArray[j][8],
76 | bx1, by1, bx2, by2)){
77 | return true;
78 | }
79 | default:
80 | if(isCrossBox(strokesArray[j][3],
81 | strokesArray[j][4],
82 | strokesArray[j][5],
83 | strokesArray[j][6],
84 | bx1, by1, bx2, by2)){
85 | return true;
86 | }
87 | }
88 | }
89 | return false;
90 | }
91 |
92 | export function isCrossWithOthers(strokesArray, i, bx1, by1, bx2, by2){ // boolean
93 | for(var j = 0; j < strokesArray.length; j++){
94 | if(i == j){ continue; }
95 | switch(strokesArray[j][0]){
96 | case 0:
97 | case 8:
98 | case 9:
99 | break;
100 | case 6:
101 | case 7:
102 | if(isCross(strokesArray[j][7],
103 | strokesArray[j][8],
104 | strokesArray[j][9],
105 | strokesArray[j][10],
106 | bx1, by1, bx2, by2)){
107 | return true;
108 | }
109 | case 2:
110 | case 12:
111 | case 3:
112 | case 4:
113 | if(isCross(strokesArray[j][5],
114 | strokesArray[j][6],
115 | strokesArray[j][7],
116 | strokesArray[j][8],
117 | bx1, by1, bx2, by2)){
118 | return true;
119 | }
120 | default:
121 | if(isCross(strokesArray[j][3],
122 | strokesArray[j][4],
123 | strokesArray[j][5],
124 | strokesArray[j][6],
125 | bx1, by1, bx2, by2)){
126 | return true;
127 | }
128 | }
129 | }
130 | return false;
131 | }
132 |
--------------------------------------------------------------------------------
/gothic.js:
--------------------------------------------------------------------------------
1 | import { FontCanvas } from "./fontcanvas.js";
2 | import {get_extended_dest, get_extended_dest_wrong} from "./util.js";
3 |
4 | export class Gothic{
5 | constructor(size) {
6 | this.kRate = 50;
7 | if (size == 1) {
8 | this.kWidth = 3;
9 | this.kKakato = 1.8;
10 | this.kMage = 6;
11 | } else {
12 | this.kWidth = 5;
13 | this.kKakato = 3;
14 | this.kMage = 10;
15 | }
16 | }
17 | getPolygons(glyphData) {
18 | var cv = new FontCanvas();
19 | for (let glyph of glyphData) {
20 | this.drawStroke(cv, glyph);
21 | }
22 | return cv.getPolygons();
23 | }
24 | drawStroke(cv, s){ // gothic
25 | const a1 = s[0];
26 | const a2 = s[1];
27 | const a3 = s[2];
28 | const x1 = s[3];
29 | const y1 = s[4];
30 | const x2 = s[5];
31 | const y2 = s[6];
32 | const x3 = s[7];
33 | const y3 = s[8];
34 | const x4 = s[9];
35 | const y4 = s[10];
36 | const curve_step = 1000 / this.kRate;
37 | switch(a1 % 100){
38 | case 0:
39 | break;
40 | case 1:
41 | if(a3 == 4){
42 | let [tx1, ty1] = get_extended_dest(x2, y2, x1, y1, -this.kMage);
43 | this.gothicDrawLine(x1, y1, tx1, ty1, a2, 1, cv);
44 | this.gothicDrawCurve(tx1, ty1, x2, y2, x2 - this.kMage * 2, y2 - this.kMage * 0.5, a1, a2, cv);
45 | }
46 | else{
47 | this.gothicDrawLine(x1, y1, x2, y2, a2, a3, cv);
48 | }
49 | break;
50 | case 2:
51 | case 12:{
52 | if(a3 == 4){
53 | let [tx1, ty1] = get_extended_dest_wrong(x3, y3, x2, y2, -this.kMage);
54 | this.gothicDrawCurve(x1, y1, x2, y2, tx1, ty1, a1, a2, cv);
55 | this.gothicDrawCurve(tx1, ty1, x3, y3, x3 - this.kMage * 2, y3 - this.kMage * 0.5, a1, a2, cv);
56 | }
57 | else if(a3 == 5){
58 | const tx1 = x3 + this.kMage;
59 | const ty1 = y3;
60 | const tx2 = tx1 + this.kMage * 0.5;
61 | const ty2 = y3 - this.kMage * 2;
62 | this.gothicDrawCurve(x1, y1, x2, y2, x3, y3, a1, a2, cv);
63 | this.gothicDrawCurve(x3, y3, tx1, ty1, tx2, ty2, a1, a2, cv);
64 | }
65 | else{
66 | this.gothicDrawCurve(x1, y1, x2, y2, x3, y3, a1, a2, cv);
67 | }
68 | break;
69 | }
70 | case 3:{
71 | let [tx1, ty1] = get_extended_dest(x2, y2, x1, y1, -this.kMage);
72 | let [tx2, ty2] = get_extended_dest(x2, y2, x3, y3, -this.kMage);
73 |
74 | if(a3 == 5){
75 | const tx3 = x3 - this.kMage;
76 | const ty3 = y3;
77 | const tx4 = x3 + this.kMage * 0.5;
78 | const ty4 = y3 - this.kMage * 2;
79 | this.gothicDrawLine(x1, y1, tx1, ty1, a2, 1, cv);
80 | this.gothicDrawCurve(tx1, ty1, x2, y2, tx2, ty2, a1, a2, cv);
81 | this.gothicDrawLine(tx2, ty2, tx3, ty3, 1, 1, cv);
82 | this.gothicDrawCurve(tx3, ty3, x3, y3, tx4, ty4, a1, a2, cv);
83 | }
84 | else{
85 | this.gothicDrawLine(x1, y1, tx1, ty1, a2, 1, cv);
86 | this.gothicDrawCurve(tx1, ty1, x2, y2, tx2, ty2, a1, a2, cv);
87 | this.gothicDrawLine(tx2, ty2, x3, y3, 1, a3, cv);
88 | }
89 | break;
90 | }
91 | case 6:
92 | if(a3 == 5){
93 | const tx1 = x4 - this.kMage;
94 | const ty1 = y4;
95 | const tx2 = x4 + this.kMage * 0.5;
96 | const ty2 = y4 - this.kMage * 2;
97 | cv.drawCBezier(x1, y1, x2, y2, x3, y3, tx1, ty1, (t) => { return this.kWidth; }, t => 0, 1000 / this.kRate);
98 | this.gothicDrawCurve(tx1, ty1, x4, y4, tx2, ty2, a1, a2, cv);
99 | }
100 | else{
101 | cv.drawCBezier(x1, y1, x2, y2, x3, y3, x4, y4, (t) => { return this.kWidth; }, t => 0, 1000 / this.kRate);
102 | }
103 | break;
104 | case 7:
105 | this.gothicDrawLine(x1, y1, x2, y2, a2, 1, cv);
106 | this.gothicDrawCurve(x2, y2, x3, y3, x4, y4, a1, a2, cv);
107 | break;
108 | case 9: // may not be exist
109 | //kageCanvas[y1][x1] = 0;
110 | //kageCanvas[y2][x2] = 0;
111 | break;
112 | default:
113 | break;
114 | }
115 | }
116 |
117 | gothicDrawCurve(x1, y1, x2, y2, x3, y3, ta1, ta2, cv) {
118 | var a1, a2;
119 | if (a1 % 10 == 2) {
120 | let [x1ext, y1ext] = get_extended_dest_wrong(x1, y1, x2, y2, this.kWidth);
121 | x1 = x1ext; y1 = y1ext;
122 | } else if (a1 % 10 == 3) {
123 | let [x1ext, y1ext] = get_extended_dest_wrong(x1, y1, x2, y2, this.kWidth * this.kKakato);
124 | x1 = x1ext; y1 = y1ext;
125 | }
126 | if (a2 % 10 == 2) {
127 | let [x2ext, y2ext] = get_extended_dest_wrong(x3, y3, x2, y2, this.kWidth);
128 | x3 = x2ext; y3 = y2ext;
129 | } else if (a2 % 10 == 3) {
130 | let [x2ext, y2ext] = get_extended_dest_wrong(x3, y3, x2, y2, this.kWidth * this.kKakato);
131 | x3 = x2ext; y3 = y2ext;
132 | }
133 | cv.drawQBezier(x1, y1, x2, y2, x3, y3, (t) => { return this.kWidth; }, t => 0, 1000 / this.kRate);
134 | }
135 |
136 | gothicDrawLine(tx1, ty1, tx2, ty2, ta1, ta2, cv) {
137 | var x1 = tx1;
138 | var y1 = ty1;
139 | var x2 = tx2;
140 | var y2 = ty2;
141 | if (ta1 % 10 == 2) {
142 | let [x1ext, y1ext] = get_extended_dest(tx1, ty1, tx2, ty2, this.kWidth);
143 | x1 = x1ext; y1 = y1ext;
144 | } else if (ta1 % 10 == 3) {
145 | let [x1ext, y1ext] = get_extended_dest(tx1, ty1, tx2, ty2, this.kWidth * this.kKakato);
146 | x1 = x1ext; y1 = y1ext;
147 | }
148 | if (ta2 % 10 == 2) {
149 | let [x2ext, y2ext] = get_extended_dest(tx2, ty2, tx1, ty1, this.kWidth);
150 | x2 = x2ext; y2 = y2ext;
151 | } else if (ta2 % 10 == 3) {
152 | let [x2ext, y2ext] = get_extended_dest(tx2, ty2, tx1, ty1, this.kWidth * this.kKakato);
153 | x2 = x2ext; y2 = y2ext;
154 | }
155 | cv.drawLine(x1, y1, x2, y2, this.kWidth);
156 | }
157 | }
--------------------------------------------------------------------------------
/kage.js:
--------------------------------------------------------------------------------
1 | import { Buhin } from "./buhin.js";
2 | import { Gothic } from "./gothic.js";
3 | import { Mincho } from "./mincho.js";
4 | import { STROKETYPE } from "./stroketype.js";
5 | import {getBoundingBox, stretch} from "./util.js";
6 | export const FONTTYPE = {
7 | MINCHO: 0,
8 | GOTHIC: 1,
9 | }
10 | export const KShotai = {
11 | kMincho: 0,
12 | kGothic: 1
13 | }
14 | export class Kage {
15 | constructor(type, size){
16 | this.kBuhin = new Buhin();
17 | this.setFont(type,size);
18 | this.kRate = 100;
19 | }
20 | setFont(type, size){
21 | switch(type){
22 | case FONTTYPE.GOTHIC:{
23 | this.kFont = new Gothic(size);
24 | break;
25 | }
26 | case FONTTYPE.MINCHO:{
27 | this.kFont = new Mincho(size);
28 | break;
29 | }
30 | default:{
31 | this.kFont = new Mincho(size);
32 | break;
33 | }
34 | }
35 | }
36 | makeGlyph(polygons, buhin) { // The word "buhin" means "component". This method converts buhin (KAGE data format) to polygons (path data). The variable buhin may represent a component of kanji or a kanji itself.
37 | var glyphData = this.kBuhin.search(buhin);
38 | this.makeGlyph2(polygons, glyphData);
39 | }
40 | makeGlyph2(polygons, data) {
41 | var kageStrokes = this.getStrokes(data);
42 | polygons.concat(this.kFont.getPolygons(kageStrokes));
43 | }
44 | makeGlyph3(data) { // void
45 | var kageStrokes = this.getStrokes(data);
46 | return this.kFont.getPolygons(kageStrokes);
47 | }
48 |
49 | makeGlyphSeparated2(data) {
50 | const strokesArrays = data.map((subdata) => this.getStrokes(subdata));
51 | return strokesArrays.map((strokesArray) => {
52 | const result = this.kFont.getPolygons(strokesArray)
53 | return result;
54 | });
55 | }
56 | makeGlyphSeparated(data) {
57 | const strokesArrays = data.map((subdata) => this.getStrokes(subdata));
58 | return this.kFont.getPolygonsSeparated(strokesArrays);
59 | }
60 | getStrokes(glyphData) { // strokes array
61 | var strokes = new Array();
62 | var textData = glyphData.split("$");
63 | for (var i = 0; i < textData.length; i++) {
64 | var columns = textData[i].split(":");
65 | if (Math.floor(columns[0]) != STROKETYPE.REFERENCE) {
66 | strokes.push([
67 | Math.floor(columns[0]),
68 | Math.floor(columns[1]),
69 | Math.floor(columns[2]),
70 | Math.floor(columns[3]),
71 | Math.floor(columns[4]),
72 | Math.floor(columns[5]),
73 | Math.floor(columns[6]),
74 | Math.floor(columns[7]),
75 | Math.floor(columns[8]),
76 | Math.floor(columns[9]),
77 | Math.floor(columns[10])
78 | ]);
79 |
80 | } else {
81 | var buhin = this.kBuhin.search(columns[7]);
82 | if (buhin != "") {
83 | strokes = strokes.concat(this.getStrokesOfBuhin(buhin,
84 | Math.floor(columns[3]),
85 | Math.floor(columns[4]),
86 | Math.floor(columns[5]),
87 | Math.floor(columns[6]),
88 | Math.floor(columns[1]),
89 | Math.floor(columns[2]),
90 | Math.floor(columns[9]),
91 | Math.floor(columns[10]))
92 | );
93 | }
94 | }
95 | }
96 | return strokes;
97 | }
98 |
99 | getStrokesOfBuhin(buhin, x1, y1, x2, y2, sx, sy, sx2, sy2) {
100 | var temp = this.getStrokes(buhin);
101 | var result = new Array();
102 | var box = getBoundingBox(temp);
103 | if (sx != 0 || sy != 0) {
104 | if (sx > 100) {
105 | sx -= 200;
106 | } else {
107 | sx2 = 0;
108 | sy2 = 0;
109 | }
110 | }
111 | for (var i = 0; i < temp.length; i++) {
112 | if (sx != 0 || sy != 0) {
113 | temp[i][3] = stretch(sx, sx2, temp[i][3], box.minX, box.maxX);
114 | temp[i][4] = stretch(sy, sy2, temp[i][4], box.minY, box.maxY);
115 | temp[i][5] = stretch(sx, sx2, temp[i][5], box.minX, box.maxX);
116 | temp[i][6] = stretch(sy, sy2, temp[i][6], box.minY, box.maxY);
117 | if (temp[i][0] != STROKETYPE.REFERENCE) {
118 | temp[i][7] = stretch(sx, sx2, temp[i][7], box.minX, box.maxX);
119 | temp[i][8] = stretch(sy, sy2, temp[i][8], box.minY, box.maxY);
120 | temp[i][9] = stretch(sx, sx2, temp[i][9], box.minX, box.maxX);
121 | temp[i][10] = stretch(sy, sy2, temp[i][10], box.minY, box.maxY);
122 | }
123 | }
124 | result.push([temp[i][0],
125 | temp[i][1],
126 | temp[i][2],
127 | x1 + temp[i][3] * (x2 - x1) / 200,
128 | y1 + temp[i][4] * (y2 - y1) / 200,
129 | x1 + temp[i][5] * (x2 - x1) / 200,
130 | y1 + temp[i][6] * (y2 - y1) / 200,
131 | x1 + temp[i][7] * (x2 - x1) / 200,
132 | y1 + temp[i][8] * (y2 - y1) / 200,
133 | x1 + temp[i][9] * (x2 - x1) / 200,
134 | y1 + temp[i][10] * (y2 - y1) / 200]);
135 |
136 | }
137 | return result;
138 | }
139 |
140 | //for compatibility
141 | getEachStrokes(glyphData) {
142 | const ss = this.getStrokes(glyphData);
143 | ss.map(s=>{
144 | s.a1_opt = Math.floor(s[0] / 100);
145 | s.a1_100 = s[0]%100;
146 |
147 | s.a2_opt = Math.floor(s[1] / 100);
148 | s.a2_100 = s[1]%100;
149 | s.a2_opt_1 = s.a2_opt % 10;
150 | s.a2_opt_2 = Math.floor(s.a2_opt / 10) % 10;
151 | s.a2_opt_3 = Math.floor(s.a2_opt / 100);
152 |
153 | s.a3_opt = Math.floor(s[2] / 100);
154 | s.a3_100 = s[2]%100;
155 | s.a3_opt_1 = s.a3_opt % 10;
156 | s.a3_opt_2 = Math.floor(s.a3_opt / 10);
157 |
158 | s.x1=s[3];
159 | s.y1=s[4];
160 | s.x2=s[5];
161 | s.y2=s[6];
162 | s.x3=s[7];
163 | s.y3=s[8];
164 | s.x4=s[9];
165 | s.y4=s[10];
166 | })
167 |
168 | return ss;
169 | }
170 | getBox(strokes){
171 | return getBoundingBox(strokes);
172 | }
173 | stretch(dp, sp, p, min, max){
174 | return stretch(dp, sp, p, min, max);
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/curve.js:
--------------------------------------------------------------------------------
1 | function divide_curve(kage, x1, y1, sx1, sy1, x2, y2, curve, div_curve, off_curve){
2 | var rate = 0.5;
3 | var cut = Math.floor(curve.length * rate);
4 | var cut_rate = cut / curve.length;
5 | var tx1 = x1 + (sx1 - x1) * cut_rate;
6 | var ty1 = y1 + (sy1 - y1) * cut_rate;
7 | var tx2 = sx1 + (x2 - sx1) * cut_rate;
8 | var ty2 = sy1 + (y2 - sy1) * cut_rate;
9 | var tx3 = tx1 + (tx2 - tx1) * cut_rate;
10 | var ty3 = ty1 + (ty2 - ty1) * cut_rate;
11 |
12 | div_curve[0] = new Array();
13 | div_curve[1] = new Array();
14 | off_curve[0] = new Array(6);
15 | off_curve[1] = new Array(6);
16 |
17 | // must think about 0 : <0
18 | var i;
19 | for(i = 0; i <= cut; i++){
20 | div_curve[0].push(curve[i]);
21 | }
22 | off_curve[0][0] = x1;
23 | off_curve[0][1] = y1;
24 | off_curve[0][2] = tx1;
25 | off_curve[0][3] = ty1;
26 | off_curve[0][4] = tx3;
27 | off_curve[0][5] = ty3;
28 |
29 | for(i = cut; i < curve.length; i++){
30 | div_curve[1].push(curve[i]);
31 | }
32 | off_curve[1][0] = tx3;
33 | off_curve[1][1] = ty3;
34 | off_curve[1][2] = tx2;
35 | off_curve[1][3] = ty2;
36 | off_curve[1][4] = x2;
37 | off_curve[1][5] = y2;
38 | }
39 |
40 | // ------------------------------------------------------------------
41 | function find_offcurve(kage, curve, sx, sy, result){
42 | var nx1, ny1, nx2, ny2, tx, ty;
43 | var minx, miny, count, diff;
44 | var tt, t, x, y, ix, iy;
45 | var mindiff = 100000;
46 | var area = 8;
47 | var mesh = 2;
48 | // area = 10 mesh = 5 -> 281 calcs
49 | // area = 10 mesh = 4 -> 180 calcs
50 | // area = 8 mesh = 4 -> 169 calcs
51 | // area = 7.5 mesh = 3 -> 100 calcs
52 | // area = 8 mesh = 2 -> 97 calcs
53 | // area = 7 mesh = 2 -> 80 calcs
54 |
55 | nx1 = curve[0][0];
56 | ny1 = curve[0][1];
57 | nx2 = curve[curve.length - 1][0];
58 | ny2 = curve[curve.length - 1][1];
59 |
60 | for(tx = sx - area; tx < sx + area; tx += mesh){
61 | for(ty = sy - area; ty < sy + area; ty += mesh){
62 | count = 0;
63 | diff = 0;
64 | for(tt = 0; tt < curve.length; tt++){
65 | t = tt / curve.length;
66 |
67 | //calculate a dot
68 | x = ((1.0 - t) * (1.0 - t) * nx1 + 2.0 * t * (1.0 - t) * tx + t * t * nx2);
69 | y = ((1.0 - t) * (1.0 - t) * ny1 + 2.0 * t * (1.0 - t) * ty + t * t * ny2);
70 |
71 | //KATAMUKI of vector by BIBUN
72 | ix = (nx1 - 2.0 * tx + nx2) * 2.0 * t + (-2.0 * nx1 + 2.0 * tx);
73 | iy = (ny1 - 2.0 * ty + ny2) * 2.0 * t + (-2.0 * ny1 + 2.0 * ty);
74 |
75 | diff += (curve[count][0] - x) * (curve[count][0] - x) + (curve[count][1] - y) * (curve[count][1] - y);
76 | if(diff > mindiff){
77 | tt = curve.length;
78 | }
79 | count++;
80 | }
81 | if(diff < mindiff){
82 | minx = tx;
83 | miny = ty;
84 | mindiff = diff;
85 | }
86 | }
87 | }
88 |
89 | for(tx = minx - mesh + 1; tx <= minx + mesh - 1; tx += 0.5){
90 | for(ty = miny - mesh + 1; ty <= miny + mesh - 1; ty += 0.5){
91 | count = 0;
92 | diff = 0;
93 | for(tt = 0; tt < curve.length; tt++){
94 | t = tt / curve.length;
95 |
96 | //calculate a dot
97 | x = ((1.0 - t) * (1.0 - t) * nx1 + 2.0 * t * (1.0 - t) * tx + t * t * nx2);
98 | y = ((1.0 - t) * (1.0 - t) * ny1 + 2.0 * t * (1.0 - t) * ty + t * t * ny2);
99 |
100 | //KATAMUKI of vector by BIBUN
101 | ix = (nx1 - 2.0 * tx + nx2) * 2.0 * t + (-2.0 * nx1 + 2.0 * tx);
102 | iy = (ny1 - 2.0 * ty + ny2) * 2.0 * t + (-2.0 * ny1 + 2.0 * ty);
103 |
104 | diff += (curve[count][0] - x) * (curve[count][0] - x) + (curve[count][1] - y) * (curve[count][1] - y);
105 | if(diff > mindiff){
106 | tt = curve.length;
107 | }
108 | count++;
109 | }
110 | if(diff < mindiff){
111 | minx = tx;
112 | miny = ty;
113 | mindiff = diff;
114 | }
115 | }
116 | }
117 |
118 | result[0] = nx1;
119 | result[1] = ny1;
120 | result[2] = minx;
121 | result[3] = miny;
122 | result[4] = nx2;
123 | result[5] = ny2;
124 | result[6] = mindiff;
125 | }
126 |
127 | // ------------------------------------------------------------------
128 | function get_candidate(kage, curve, a1, a2, x1, y1, sx1, sy1, x2, y2, opt3, opt4){
129 | var x, y, ix, iy, ir, ia, ib, tt, t, deltad;
130 | var hosomi = 0.5;
131 |
132 | curve[0] = new Array();
133 | curve[1] = new Array();
134 |
135 | for(tt = 0; tt <= 1000; tt = tt + kage.kRate){
136 | t = tt / 1000;
137 |
138 | //calculate a dot
139 | x = ((1.0 - t) * (1.0 - t) * x1 + 2.0 * t * (1.0 - t) * sx1 + t * t * x2);
140 | y = ((1.0 - t) * (1.0 - t) * y1 + 2.0 * t * (1.0 - t) * sy1 + t * t * y2);
141 |
142 | //KATAMUKI of vector by BIBUN
143 | ix = (x1 - 2.0 * sx1 + x2) * 2.0 * t + (-2.0 * x1 + 2.0 * sx1);
144 | iy = (y1 - 2.0 * sy1 + y2) * 2.0 * t + (-2.0 * y1 + 2.0 * sy1);
145 | //line SUICHOKU by vector
146 | if(ix != 0 && iy != 0){
147 | ir = Math.atan(iy / ix * -1);
148 | ia = Math.sin(ir) * (kage.kMinWidthT);
149 | ib = Math.cos(ir) * (kage.kMinWidthT);
150 | }
151 | else if(ix == 0){
152 | ia = kage.kMinWidthT;
153 | ib = 0;
154 | }
155 | else{
156 | ia = 0;
157 | ib = kage.kMinWidthT;
158 | }
159 |
160 | if(a1 == 7 && a2 == 0){ // L2RD: fatten
161 | deltad = Math.pow(t, hosomi) * kage.kL2RDfatten;
162 | }
163 | else if(a1 == 7){
164 | deltad = Math.pow(t, hosomi);
165 | }
166 | else if(a2 == 7){
167 | deltad = Math.pow(1.0 - t, hosomi);
168 | }
169 | else if(opt3 > 0){
170 | deltad = (((kage.kMinWidthT - opt4 / 2) - opt3 / 2) / (kage.kMinWidthT - opt4 / 2)) + opt3 / 2 / (kage.kMinWidthT - opt4) * t;
171 | }
172 | else{ deltad = 1; }
173 |
174 | if(deltad < 0.15){
175 | deltad = 0.15;
176 | }
177 | ia = ia * deltad;
178 | ib = ib * deltad;
179 |
180 | //reverse if vector is going 2nd/3rd quadrants
181 | if(ix <= 0){
182 | ia = ia * -1;
183 | ib = ib * -1;
184 | }
185 |
186 | temp = new Array(2);
187 | temp[0] = x - ia;
188 | temp[1] = y - ib;
189 | curve[0].push(temp);
190 | temp = new Array(2);
191 | temp[0] = x + ia;
192 | temp[1] = y + ib;
193 | curve[1].push(temp);
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/bezier.js:
--------------------------------------------------------------------------------
1 | import {unit_normal_vector, rad_to_vector, get_rad} from "./util.js";
2 | import {Polygon} from "./polygon.js";
3 | import {fitCubic_tang, fitCurve} from "./fit-curve.js";
4 | const bezier_steps = 200;
5 | export class Bezier{
6 | static generalBezier(x_fun, y_fun, dx_fun, dy_fun, width_func, width_func_d){
7 | var a1 = [];
8 | var a2 = [];
9 | var tang1 = [];
10 | var tang2 = [];
11 | for (var tt = 0; tt <= bezier_steps; tt++) {
12 | const t = tt / bezier_steps;
13 | const x = x_fun(t);
14 | const y = y_fun(t);
15 | const vx = dx_fun(t);
16 | const vy = dy_fun(t);
17 |
18 | let [ia, ib] = unit_normal_vector(vx, vy);
19 | const deltad = width_func(t);
20 | ia = ia * deltad;
21 | ib = ib * deltad;
22 |
23 | const rad = get_rad(vx, vy);
24 | const velocity = Math.sqrt(vx*vx+vy*vy);
25 | const width_rad = Math.atan(width_func_d(t)/velocity);
26 | a1.push([x - ia, y - ib]);
27 | a2.push([x + ia, y + ib]);
28 | tang1.push(rad_to_vector(rad-width_rad));
29 | tang2.push(rad_to_vector(rad+width_rad-Math.PI));
30 | }
31 | //const bez1 = fitCubic_tang(a1, tang1, 0.03);
32 | //const bez2 = fitCubic_tang(a2.reverse(), tang2.reverse(), 0.03);
33 | const bez1 = fitCurve(a1, 0.03);
34 | const bez2 = fitCurve(a2.reverse(), 0.03);
35 | return [bez1, bez2];
36 | }
37 | static generalBezier2(x_fun, y_fun, dx_fun, dy_fun, width_func, width_func_d, dir_func){
38 | //offset vector (ia, ib) is calculated with dir_func
39 | var a1 = [];
40 | var a2 = [];
41 | var tang1 = [];
42 | var tang2 = [];
43 | for (var tt = 0; tt <= bezier_steps; tt++) {
44 | const t = tt / bezier_steps;
45 | const x = x_fun(t);
46 | const y = y_fun(t);
47 | const vx = dx_fun(t);
48 | const vy = dy_fun(t);
49 |
50 | let [ia, ib] = dir_func(t);
51 | const deltad = width_func(t);
52 | ia = ia * deltad;
53 | ib = ib * deltad;
54 |
55 | const rad = get_rad(vx, vy);
56 | const velocity = Math.sqrt(vx*vx+vy*vy);
57 | const width_rad = Math.atan(width_func_d(t)/velocity);
58 | a1.push([x - ia, y - ib]);
59 | a2.push([x + ia, y + ib]);
60 | tang1.push(rad_to_vector(rad-width_rad));
61 | tang2.push(rad_to_vector(rad+width_rad-Math.PI));
62 | }
63 | //const bez1 = fitCubic_tang(a1, tang1, 0.03);
64 | //const bez2 = fitCubic_tang(a2.reverse(), tang2.reverse(), 0.03);
65 | const bez1 = fitCurve(a1, 0.03);
66 | const bez2 = fitCurve(a2.reverse(), 0.03);
67 | return [bez1, bez2];
68 | }
69 |
70 | static qBezier(x1, y1, sx, sy, x2, y2, width_func, width_func_d){
71 | const x_fun = t => ((1.0 - t) * (1.0 - t) * x1 + 2.0 * t * (1.0 - t) * sx + t * t * x2);
72 | const y_fun = t => ((1.0 - t) * (1.0 - t) * y1 + 2.0 * t * (1.0 - t) * sy + t * t * y2);
73 | const dx_fun = t => (x1 - 2.0 * sx + x2) * 2.0 * t + (-2.0 * x1 + 2.0 * sx);
74 | const dy_fun = t => (y1 - 2.0 * sy + y2) * 2.0 * t + (-2.0 * y1 + 2.0 * sy);
75 | return this.generalBezier(x_fun, y_fun, dx_fun, dy_fun, width_func, width_func_d);
76 | }
77 | static qBezier2(x1, y1, sx, sy, x2, y2, width_func, width_func_d){
78 | //similar to qBezier(), but the direction changes at a constant speed (not decided by normal vector)
79 | const x_fun = t => ((1.0 - t) * (1.0 - t) * x1 + 2.0 * t * (1.0 - t) * sx + t * t * x2);
80 | const y_fun = t => ((1.0 - t) * (1.0 - t) * y1 + 2.0 * t * (1.0 - t) * sy + t * t * y2);
81 | const dx_fun = t => (x1 - 2.0 * sx + x2) * 2.0 * t + (-2.0 * x1 + 2.0 * sx);
82 | const dy_fun = t => (y1 - 2.0 * sy + y2) * 2.0 * t + (-2.0 * y1 + 2.0 * sy);
83 | const cent_x = (x1 + 4*sx + x2) / 6;
84 | const cent_y = (y1 + 4*sy + y2) / 6;
85 |
86 | const rad_begin = Math.atan2(cent_y-y1, cent_x-x1);
87 | var rad_end = Math.atan2(y2-cent_y, x2-cent_x);
88 | if(rad_end - rad_begin > Math.PI) {
89 | rad_end -= Math.PI*2;
90 | }else if(rad_begin - rad_end > Math.PI){
91 | rad_end += Math.PI*2;
92 | }
93 | const dir_func = t => rad_to_vector((1-t)*rad_begin+t*rad_end+Math.PI/2);
94 | return this.generalBezier2(x_fun, y_fun, dx_fun, dy_fun, width_func, width_func_d, dir_func);
95 | }
96 |
97 | static cBezier(x1, y1, sx1, sy1, sx2, sy2, x2, y2, width_func, width_func_d){
98 | const x_fun = t => (1.0 - t) * (1.0 - t) * (1.0 - t) * x1 + 3.0 * t * (1.0 - t) * (1.0 - t) * sx1 + 3 * t * t * (1.0 - t) * sx2 + t * t * t * x2;
99 | const y_fun = t => (1.0 - t) * (1.0 - t) * (1.0 - t) * y1 + 3.0 * t * (1.0 - t) * (1.0 - t) * sy1 + 3 * t * t * (1.0 - t) * sy2 + t * t * t * y2;
100 | const dx_fun = t => t * t * (-3 * x1 + 9 * sx1 + -9 * sx2 + 3 * x2) + t * (6 * x1 + -12 * sx1 + 6 * sx2) + -3 * x1 + 3 * sx1;
101 | const dy_fun = t => t * t * (-3 * y1 + 9 * sy1 + -9 * sy2 + 3 * y2) + t * (6 * y1 + -12 * sy1 + 6 * sy2) + -3 * y1 + 3 * sy1;
102 | return this.generalBezier(x_fun, y_fun, dx_fun, dy_fun, width_func, width_func_d);
103 | }
104 |
105 | static slantBezier(x_fun, y_fun, dx_fun, dy_fun, width_func, width_func_d, dir_x, dir_y){
106 | var a1 = [];
107 | var a2 = [];
108 | var tang1 = [];
109 | var tang2 = [];
110 | let [ia, ib] = unit_normal_vector(dir_x, dir_y);
111 | let len = Math.sqrt(dir_x*dir_x+dir_y*dir_y);
112 | let ex = dir_x/len;
113 | let ey = dir_y/len;
114 |
115 | for (var tt = 0; tt <= bezier_steps; tt++) {
116 | const t = tt / bezier_steps;
117 | const x = x_fun(t);
118 | const y = y_fun(t);
119 | const vx = dx_fun(t);
120 | const vy = dy_fun(t);
121 |
122 | const deltad = width_func(t);
123 |
124 | const velocity = (dir_x*vx+dir_y*vy)/Math.sqrt(dir_x*dir_x+dir_y*dir_y);
125 | const width_tan = width_func_d(t)/velocity;
126 | const bez_tan = (dir_x*vy-dir_y*vx)/(dir_x*vx+dir_y*vy);
127 | const rad1 = Math.atan(bez_tan-width_tan);
128 | const rad2 = Math.atan(bez_tan+width_tan);
129 | a1.push([x - ia * deltad, y - ib * deltad]);
130 | a2.push([x + ia * deltad, y + ib * deltad]);
131 | tang1.push([ Math.cos(rad1)*ex-Math.sin(rad1)*ey, Math.sin(rad1)*ex+Math.cos(rad1)*ey]);
132 | tang2.push([-Math.cos(rad2)*ex+Math.sin(rad2)*ey, -Math.sin(rad2)*ex-Math.cos(rad2)*ey]);
133 | }
134 | //const bez1 = fitCubic_tang(a1, tang1, 0.03);
135 | //const bez2 = fitCubic_tang(a2.reverse(), tang2.reverse(), 0.03);
136 | const bez1 = fitCurve(a1, 0.01);
137 | const bez2 = fitCurve(a2.reverse(), 0.01);
138 | return [bez1, bez2];
139 | }
140 |
141 | static qBezier_slant(x1, y1, sx, sy, x2, y2, width_func, width_func_d){
142 | const x_fun = t => ((1.0 - t) * (1.0 - t) * x1 + 2.0 * t * (1.0 - t) * sx + t * t * x2);
143 | const y_fun = t => ((1.0 - t) * (1.0 - t) * y1 + 2.0 * t * (1.0 - t) * sy + t * t * y2);
144 | const dir_x = x2 - x1;
145 | const dir_y = y2 - y1;
146 | const dx_fun = t => (x1 - 2.0 * sx + x2) * 2.0 * t + (-2.0 * x1 + 2.0 * sx);
147 | const dy_fun = t => (y1 - 2.0 * sy + y2) * 2.0 * t + (-2.0 * y1 + 2.0 * sy);
148 | return this.slantBezier(x_fun, y_fun, dx_fun, dy_fun, width_func, width_func_d, dir_x, dir_y);
149 | }
150 | static cBezier_slant(x1, y1, sx1, sy1, sx2, sy2, x2, y2, width_func, width_func_d){
151 | const x_fun = t => (1.0 - t) * (1.0 - t) * (1.0 - t) * x1 + 3.0 * t * (1.0 - t) * (1.0 - t) * sx1 + 3 * t * t * (1.0 - t) * sx2 + t * t * t * x2;
152 | const y_fun = t => (1.0 - t) * (1.0 - t) * (1.0 - t) * y1 + 3.0 * t * (1.0 - t) * (1.0 - t) * sy1 + 3 * t * t * (1.0 - t) * sy2 + t * t * t * y2;
153 | const dx_fun = t => t * t * (-3 * x1 + 9 * sx1 + -9 * sx2 + 3 * x2) + t * (6 * x1 + -12 * sx1 + 6 * sx2) + -3 * x1 + 3 * sx1;
154 | const dy_fun = t => t * t * (-3 * y1 + 9 * sy1 + -9 * sy2 + 3 * y2) + t * (6 * y1 + -12 * sy1 + 6 * sy2) + -3 * y1 + 3 * sy1;
155 | const dir_x = x2 - x1;
156 | const dir_y = y2 - y1;
157 | return this.slantBezier(x_fun, y_fun, dx_fun, dy_fun, width_func, width_func_d, dir_x, dir_y);
158 | }
159 |
160 | static bez_to_poly(bez){
161 | var poly = new Polygon();
162 | poly.push(bez[0][0][0], bez[0][0][1]);
163 | for(let bez1 of bez){
164 | poly.push(bez1[1][0], bez1[1][1], 2);
165 | poly.push(bez1[2][0], bez1[2][1], 2);
166 | poly.push(bez1[3][0], bez1[3][1]);
167 | }
168 | return poly;
169 | }
170 | }
--------------------------------------------------------------------------------
/util.js:
--------------------------------------------------------------------------------
1 | export const bez_cir = 4*(Math.sqrt(2)-1)/3;
2 | //a constant for drawing circles with Bezier curves
3 | export const CURVE_THIN = 0.1910
4 | //width functions (using the first quadrant of circle of radius p, centered at (1-p, 1-p) )
5 | export function norm2(x, y){
6 | return Math.sqrt(x*x + y*y)
7 | }
8 |
9 | export function widfun(t, x1, y1, x2, y2, wid){
10 | const len = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
11 | const p = 1 + Math.sqrt(100/len);
12 | return ( (Math.sqrt(p*p+(p-1)*(p-1)-(p-t)*(p-t))-(p-1))*(1-CURVE_THIN)+CURVE_THIN )*wid;
13 | }
14 |
15 | export function widfun_d(t, x1, y1, x2, y2, wid){
16 | const len = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
17 | const p = 1 + Math.sqrt(100/len);
18 | return wid*(1-CURVE_THIN)*0.5*2*(p-t) / Math.sqrt(p*p+(p-1)*(p-1)-(p-t)*(p-t));
19 | }
20 |
21 | export function widfun_stop(t, x1, y1, x2, y2, wid){
22 | const len = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
23 | const p = 1 + Math.sqrt(100/len);
24 | return ( (Math.sqrt(p*p+(p-1)*(p-1)-(p-t)*(p-t))-(p-1))*(1.10-CURVE_THIN)+CURVE_THIN )*wid;
25 | }
26 |
27 | export function widfun_stop_d(t, x1, y1, x2, y2, wid){
28 | const len = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
29 | const p = 1 + Math.sqrt(100/len);
30 | return wid*(1.10-CURVE_THIN)*0.5*2*(p-t) / Math.sqrt(p*p+(p-1)*(p-1)-(p-t)*(p-t));
31 | }
32 | export function widfun_stop2(t, x1, y1, x2, y2, wid){
33 | const len = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
34 | const p = 1 + Math.sqrt(100/len);
35 | return ( (1-Math.pow(1-t, 1.21))*(1.10-CURVE_THIN)+CURVE_THIN )*wid;
36 | }
37 |
38 | export function widfun_stop2_d(t, x1, y1, x2, y2, wid){
39 | const len = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
40 | const p = 1 + Math.sqrt(100/len);
41 | return wid*(1.10-CURVE_THIN)*(1* 1.21) * Math.pow(1-t,0.21);
42 | }
43 |
44 | //fat version (used in cubic bezier)
45 | export function widfun_fat(t, x1, y1, x2, y2, wid){
46 | const len = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
47 | const p = 1+ Math.sqrt(40/len);
48 | return ( (Math.sqrt(p*p + (p-1)*(p-1) - (p-t)*(p-t)) - (p-1) )*(1-CURVE_THIN)+CURVE_THIN )*wid;
49 | }
50 |
51 | export function widfun_fat_d(t, x1, y1, x2, y2, wid){
52 | const len = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
53 | const p = 1+ Math.sqrt(40/len);
54 | return wid*(1-CURVE_THIN)*0.5*2*(p-t) / Math.sqrt(p*p + (p-1)*(p-1) - (p-t)*(p-t));
55 | }
56 |
57 | export function get_dir(x, y){
58 | if (y==0){
59 | if(x<0){
60 | return {sin: 0, cos: -1};
61 | }else{
62 | return {sin: 0, cos: 1};
63 | }
64 | }else if(x==0){
65 | if(y<0){
66 | return {sin: -1, cos: 0};
67 | }else{
68 | return {sin: 1, cos: 0};
69 | }
70 | }else{
71 | const rad = Math.atan2(y, x);
72 | return {sin: Math.sin(rad), cos: Math.cos(rad)};
73 | }
74 | }
75 | export const DIR_POSX = {sin: 0, cos: 1};
76 | export const DIR_POSY = {sin: 1, cos: 0};
77 | export const DIR_NEGX = {sin: 0, cos: -1};
78 | export const DIR_NEGY = {sin: -1, cos: 0};
79 |
80 | export function moved_point(x, y, dir, delta){
81 | return [x + delta*dir.cos, y + delta*dir.sin];
82 | }
83 | export function get_extended_dest(destX, destY, srcX, srcY, delta) {
84 | const dir = get_dir(destX - srcX, destY - srcY);
85 | return moved_point(destX, destY, dir, delta);
86 | }
87 |
88 | export function get_extended_dest_wrong(destX, destY, srcX, srcY, delta) {
89 | //The process for lines directed exactly in the negative x-direction or y-direction is not correct, so it's named as "wrong".
90 | var destX_new = destX;
91 | var destY_new = destY;
92 | if (srcX == destX) {
93 | destY_new = destY + delta;
94 | }
95 | else if (srcY == destY) {
96 | destX_new = destX + delta;
97 | }
98 | else {
99 | var v;
100 | const rad = Math.atan((destY - srcY) / (destX - srcX));
101 | if (srcX < destX) { v = 1; } else { v = -1; }
102 | destX_new = destX + delta * Math.cos(rad) * v;
103 | destY_new = destY + delta * Math.sin(rad) * v;
104 | }
105 | return [destX_new, destY_new]
106 | }
107 |
108 | export function unit_normal_vector(ix, iy) {//to the right(clockwise (in the display coordinate))
109 | var ia, ib;
110 | // line SUICHOKU by vector
111 | if (ix != 0 && iy != 0) {
112 | const ir = Math.atan(iy / ix * -1.0);
113 | ia = Math.sin(ir);
114 | ib = Math.cos(ir);
115 | }
116 | else if (ix == 0) {
117 | if (iy < 0) {
118 | ia = -1;
119 | } else {
120 | ia = 1;
121 | }
122 | ib = 0;
123 | }
124 | else {
125 | ia = 0;
126 | ib = 1;
127 | }
128 | //reverse if vector is going 2nd/3rd quadrants
129 | if (ix <= 0) {
130 | ia = ia * -1;
131 | ib = ib * -1;
132 | }
133 | return [ia, ib];
134 | }
135 |
136 | function normal_vector_of_len(v, l){//to the right(clockwise (in the display coordinate))
137 | const len=Math.sqrt(v[0]*v[0]+v[1]*v[1]);
138 | return [-v[1]*l/len,v[0]*l/len];
139 | }
140 |
141 | export function vector_to_len(v, l){
142 | const len=Math.sqrt(v[0]*v[0]+v[1]*v[1]);
143 | return [v[0]*l/len,v[1]*l/len];
144 | }
145 |
146 | function calc_hosomi(x1, y1, x2, y2) {
147 | var hosomi = 0.5;
148 | if (Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) < 50) {
149 | hosomi += 0.4 * (1 - Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) / 50);
150 | }
151 | return hosomi;
152 | }
153 |
154 | export function get_rad(x, y) {
155 | return Math.atan2(y,x);
156 | var rad;
157 | if (x == 0) {
158 | if (y > 0) {
159 | rad = Math.PI / 2;
160 | } else {
161 | rad = -Math.PI / 2;
162 | }
163 | } else {
164 | rad = Math.atan(y / x);
165 | if (x < 0) { rad += Math.PI }
166 | }
167 | return rad;
168 | }
169 |
170 | export function rad_to_vector(rad) {
171 | return [Math.cos(rad), Math.sin(rad)];
172 | }
173 |
174 | export function rad_to_dir(rad) {
175 | return {sin: Math.sin(rad), cos: Math.cos(rad)};
176 | }
177 | /////////////////////////////////////////////////////
178 | export function bezier_to_line(bez, x0, y0, rad){
179 | var rotate_mat_inv = [Math.cos(-rad), -Math.sin(-rad), Math.sin(-rad), Math.cos(-rad)]
180 | function genten_rotate(p){
181 | let [x,y] = p
182 | var x1 = x-x0
183 | var y1 = y-y0
184 | return [rotate_mat_inv[0]*x1+rotate_mat_inv[1]*y1, rotate_mat_inv[2]*x1+rotate_mat_inv[3]*y1]
185 | }
186 |
187 | var rotate_mat = [Math.cos(rad), -Math.sin(rad), Math.sin(rad), Math.cos(rad)]
188 | function genten_rotate_inv(p){
189 | let [x,y] = p
190 | return [rotate_mat[0]*x+rotate_mat[1]*y + x0, rotate_mat[2]*x+rotate_mat[3]*y + y0]
191 | }
192 |
193 | var bezier_genten_rotated = bez.map(genten_rotate);
194 | var bez_edited = bezier_to_y(bezier_genten_rotated, 0)
195 | var bez_edited_return = bez_edited.map(genten_rotate_inv)
196 | bez_edited_return[0] = bez[0] //始点は変わらないはずなので誤差を防ぐため元の値を代入
197 | return bez_edited_return;
198 | }
199 | function stretch_bezier_end(bez, t){
200 | const start = [bez[0][0],
201 | bez[0][1]];
202 | const c1 = [(1-t) * bez[0][0]+t * bez[1][0],
203 | (1-t) * bez[0][1]+t * bez[1][1]];
204 | const c2 = [(1-t) * (1-t) * bez[0][0] + 2.0 * t * (1-t) * bez[1][0] + t * t * bez[2][0],
205 | (1-t) * (1-t) * bez[0][1] + 2.0 * t * (1-t) * bez[1][1] + t * t * bez[2][1]]
206 | const end = [(1-t) * (1-t) * (1-t) * bez[0][0] + 3 * t * (1-t) * (1-t) * bez[1][0] + 3 * t * t * (1-t) * bez[2][0] + t * t * t * bez[3][0],
207 | (1-t) * (1-t) * (1-t) * bez[0][1] + 3 * t * (1-t) * (1-t) * bez[1][1] + 3 * t * t * (1-t) * bez[2][1] + t * t * t * bez[3][1],]
208 | return [start, c1, c2, end];
209 | }
210 | export function bezier_to_y(bez, y){
211 | const a = bez[3][1] - 3 * bez[2][1] + 3 * bez[1][1] - bez[0][1];
212 | const b = 3 * bez[2][1] - 6 * bez[1][1] + 3 * bez[0][1];
213 | const c = 3 * bez[1][1] - 3 * bez[0][1];
214 | const d = bez[0][1];
215 | const yy = solveCubic(a, b, c, d - y);
216 | const ext = get_extreme_points(a, b, c);
217 | function hit_extreme(x1, x2){
218 | for (let e of ext){
219 | if (x1 < e && e < x2) return true;
220 | }
221 | return false;
222 | }
223 | yy.sort(function (a, b) {//descending order
224 | return b - a;
225 | });
226 | for (let i of yy) {
227 | if (0 < i && i < 1) {
228 | return stretch_bezier_end(bez, i);
229 | }
230 | }
231 |
232 | yy.reverse()//ascending order
233 | for (let i of yy) {
234 | if (i > 1) {
235 | if(!hit_extreme(1, i)) return stretch_bezier_end(bez, i);
236 | }
237 | }
238 | return bez;
239 |
240 | //var res = shorten_bezier_to_y(bez, y);
241 | //if(res){return res;}else{
242 | // res = extend_bezier_to_y(bez, y);
243 | // if(res){return res;}else{
244 | // return bez
245 | // }
246 | //}
247 | }
248 | function extend_bezier_to_y(bez, y) {
249 | const a = bez[3][1] - 3 * bez[2][1] + 3 * bez[1][1] - bez[0][1];
250 | const b = 3 * bez[2][1] - 6 * bez[1][1] + 3 * bez[0][1];
251 | const c = 3 * bez[1][1] - 3 * bez[0][1];
252 | const d = bez[0][1];
253 | const yy = solveCubic(a, b, c, d - y);
254 | yy.sort(function (a, b) {//ascending order
255 | return a - b;
256 | });
257 | for (let i of yy) {
258 | if (i > 1) {
259 | return stretch_bezier_end(bez, i);
260 | }
261 | }
262 | return false;
263 | }
264 |
265 | function shorten_bezier_to_y(bez, y) {
266 | const a = bez[3][1] - 3 * bez[2][1] + 3 * bez[1][1] - bez[0][1];
267 | const b = 3 * bez[2][1] - 6 * bez[1][1] + 3 * bez[0][1];
268 | const c = 3 * bez[1][1] - 3 * bez[0][1];
269 | const d = bez[0][1];
270 | const yy = solveCubic(a, b, c, d - y);
271 | yy.sort(function (a, b) {//descending order
272 | return b - a;
273 | });
274 | for (let i of yy) {
275 | if (0 < i && i < 1) {
276 | return stretch_bezier_end(bez, i);
277 | }
278 | }
279 | return false;
280 | }
281 | function get_extreme_points(a0, b0, c0){
282 | let a = a0*3;
283 | let b = b0*2;
284 | let c = c0;
285 | let d = b * b - (4 * a * c);
286 | if(d > 0){
287 | let x1 = ((-1) * b + Math.sqrt(d)) / (2 * a);
288 | let x2 = ((-1) * b - Math.sqrt(d)) / (2 * a);
289 | return [x1, x2]
290 | } else if(d == 0) {
291 | return [(-1) * b / (2 * a)]
292 | } else {
293 | return []
294 | }
295 | }
296 | function solveCubic(a, b, c, d) {
297 | if (Math.abs(a) < 1e-8) { // Quadratic case, ax^2+bx+c=0
298 | a = b; b = c; c = d;
299 | if (Math.abs(a) < 1e-8) { // Linear case, ax+b=0
300 | a = b; b = c;
301 | if (Math.abs(a) < 1e-8) // Degenerate case
302 | return [];
303 | return [-b/a];
304 | }
305 |
306 | var D = b*b - 4*a*c;
307 | if (Math.abs(D) < 1e-8)
308 | return [-b/(2*a)];
309 | else if (D > 0)
310 | return [(-b+Math.sqrt(D))/(2*a), (-b-Math.sqrt(D))/(2*a)];
311 | return [];
312 | }
313 |
314 | // Convert to depressed cubic t^3+pt+q = 0 (subst x = t - b/3a)
315 | var p = (3*a*c - b*b)/(3*a*a);
316 | var q = (2*b*b*b - 9*a*b*c + 27*a*a*d)/(27*a*a*a);
317 | var roots;
318 |
319 | if (Math.abs(p) < 1e-8) { // p = 0 -> t^3 = -q -> t = -q^1/3
320 | roots = [cuberoot(-q)];
321 | } else if (Math.abs(q) < 1e-8) { // q = 0 -> t^3 + pt = 0 -> t(t^2+p)=0
322 | roots = [0].concat(p < 0 ? [Math.sqrt(-p), -Math.sqrt(-p)] : []);
323 | } else {
324 | var D = q*q/4 + p*p*p/27;
325 | if (Math.abs(D) < 1e-8) { // D = 0 -> two roots
326 | roots = [-1.5*q/p, 3*q/p];
327 | } else if (D > 0) { // Only one real root
328 | var u = cuberoot(-q/2 - Math.sqrt(D));
329 | roots = [u - p/(3*u)];
330 | } else { // D < 0, three roots, but needs to use complex numbers/trigonometric solution
331 | var u = 2*Math.sqrt(-p/3);
332 | var t = Math.acos(3*q/p/u)/3; // D < 0 implies p < 0 and acos argument in [-1..1]
333 | var k = 2*Math.PI/3;
334 | roots = [u*Math.cos(t), u*Math.cos(t-k), u*Math.cos(t-2*k)];
335 | }
336 | }
337 |
338 | // Convert back from depressed cubic
339 | for (var i = 0; i < roots.length; i++)
340 | roots[i] -= b/(3*a);
341 |
342 | return roots;
343 | }
344 | function cuberoot(x) {
345 | var y = Math.pow(Math.abs(x), 1/3);
346 | return x < 0 ? -y : y;
347 | }
348 |
349 | export function stretch(dp, sp, p, min, max) { // integer
350 | var p1, p2, p3, p4;
351 | if (p < sp + 100) {
352 | p1 = min;
353 | p3 = min;
354 | p2 = sp + 100;
355 | p4 = dp + 100;
356 | } else {
357 | p1 = sp + 100;
358 | p3 = dp + 100;
359 | p2 = max;
360 | p4 = max;
361 | }
362 | return Math.floor(((p - p1) / (p2 - p1)) * (p4 - p3) + p3);
363 | }
364 |
365 | export function getBoundingBox(strokes) { // minX, minY, maxX, maxY
366 | var a = new Object();
367 | a.minX = 200;
368 | a.minY = 200;
369 | a.maxX = 0;
370 | a.maxY = 0;
371 | for (var i = 0; i < strokes.length; i++) {
372 | if (strokes[i][0] == 0) { continue; }
373 | a.minX = Math.min(a.minX, strokes[i][3]);
374 | a.maxX = Math.max(a.maxX, strokes[i][3]);
375 | a.minY = Math.min(a.minY, strokes[i][4]);
376 | a.maxY = Math.max(a.maxY, strokes[i][4]);
377 | a.minX = Math.min(a.minX, strokes[i][5]);
378 | a.maxX = Math.max(a.maxX, strokes[i][5]);
379 | a.minY = Math.min(a.minY, strokes[i][6]);
380 | a.maxY = Math.max(a.maxY, strokes[i][6]);
381 | if (strokes[i][0] == 1) { continue; }
382 | if (strokes[i][0] == 99) { continue; }
383 | a.minX = Math.min(a.minX, strokes[i][7]);
384 | a.maxX = Math.max(a.maxX, strokes[i][7]);
385 | a.minY = Math.min(a.minY, strokes[i][8]);
386 | a.maxY = Math.max(a.maxY, strokes[i][8]);
387 | if (strokes[i][0] == 2) { continue; }
388 | if (strokes[i][0] == 3) { continue; }
389 | if (strokes[i][0] == 4) { continue; }
390 | a.minX = Math.min(a.minX, strokes[i][9]);
391 | a.maxX = Math.max(a.maxX, strokes[i][9]);
392 | a.minY = Math.min(a.minY, strokes[i][10]);
393 | a.maxY = Math.max(a.maxY, strokes[i][10]);
394 | }
395 | return a;
396 | }
--------------------------------------------------------------------------------
/fontcanvas.js:
--------------------------------------------------------------------------------
1 | import { Polygons } from "./polygons.js";
2 | import { Polygon } from "./polygon.js";
3 | import { PointMaker } from "./pointmaker.js";
4 | import { Bezier} from "./bezier.js";
5 | import {vector_to_len, bez_cir, get_dir, DIR_POSY, rad_to_dir, get_extended_dest} from "./util.js";
6 | export class FontCanvas {
7 | constructor() {
8 | this.polygons = new Polygons();
9 | }
10 | getPolygons() {
11 | return this.polygons;
12 | }
13 | addPolygon(poly) {
14 | this.polygons.push(poly);
15 | }
16 |
17 | //flipping and rotating
18 | //polygons in the rectangle (x1, y1, x2, y2) are flipped or rotated
19 | flip_left_right(x1, y1, x2, y2) {
20 | for (var i = 0; i < this.polygons.array.length; i++) {
21 | var inside = true;
22 | for (var j = 0; j < this.polygons.array[i].array.length; j++) {
23 | if (x1 > this.polygons.array[i].array[j].x || this.polygons.array[i].array[j].x > x2 ||
24 | y1 > this.polygons.array[i].array[j].y || this.polygons.array[i].array[j].y > y2) {
25 | inside = false;
26 | }
27 | }
28 | if (inside) {
29 | for (var j = 0; j < this.polygons.array[i].array.length; j++) {
30 | this.polygons.array[i].array[j].x = x2 - (this.polygons.array[i].array[j].x - x1);
31 | this.polygons.array[i].array[j].x = Math.floor(this.polygons.array[i].array[j].x * 10) / 10;
32 | }
33 | this.polygons.array[i].array.reverse()
34 | }
35 | }
36 | }
37 |
38 | flip_up_down(x1, y1, x2, y2) {
39 | for (var i = 0; i < this.polygons.array.length; i++) {
40 | var inside = true;
41 | for (var j = 0; j < this.polygons.array[i].array.length; j++) {
42 | if (x1 > this.polygons.array[i].array[j].x || this.polygons.array[i].array[j].x > x2 ||
43 | y1 > this.polygons.array[i].array[j].y || this.polygons.array[i].array[j].y > y2) {
44 | inside = false;
45 | }
46 | }
47 | if (inside) {
48 | for (var j = 0; j < this.polygons.array[i].array.length; j++) {
49 | this.polygons.array[i].array[j].y = y2 - (this.polygons.array[i].array[j].y - y1);
50 | this.polygons.array[i].array[j].y = Math.floor(this.polygons.array[i].array[j].y * 10) / 10;
51 | }
52 | this.polygons.array[i].array.reverse()
53 | }
54 | }
55 | }
56 |
57 | rotate90(x1, y1, x2, y2) {
58 | for (var i = 0; i < this.polygons.array.length; i++) {
59 | var inside = true;
60 | for (var j = 0; j < this.polygons.array[i].array.length; j++) {
61 | if (x1 > this.polygons.array[i].array[j].x || this.polygons.array[i].array[j].x > x2 ||
62 | y1 > this.polygons.array[i].array[j].y || this.polygons.array[i].array[j].y > y2) {
63 | inside = false;
64 | }
65 | }
66 | if (inside) {
67 | for (var j = 0; j < this.polygons.array[i].array.length; j++) {
68 | var x = this.polygons.array[i].array[j].x;
69 | var y = this.polygons.array[i].array[j].y;
70 | this.polygons.array[i].array[j].x = x1 + (y2 - y);
71 | this.polygons.array[i].array[j].y = y1 + (x - x1);
72 | this.polygons.array[i].array[j].x = Math.floor(this.polygons.array[i].array[j].x * 10) / 10;
73 | this.polygons.array[i].array[j].y = Math.floor(this.polygons.array[i].array[j].y * 10) / 10;
74 | }
75 | }
76 | }
77 | }
78 |
79 | rotate180(x1, y1, x2, y2) {
80 | for (var i = 0; i < this.polygons.array.length; i++) {
81 | var inside = true;
82 | for (var j = 0; j < this.polygons.array[i].array.length; j++) {
83 | if (x1 > this.polygons.array[i].array[j].x || this.polygons.array[i].array[j].x > x2 ||
84 | y1 > this.polygons.array[i].array[j].y || this.polygons.array[i].array[j].y > y2) {
85 | inside = false;
86 | }
87 | }
88 | if (inside) {
89 | for (var j = 0; j < this.polygons.array[i].array.length; j++) {
90 | var x = this.polygons.array[i].array[j].x;
91 | var y = this.polygons.array[i].array[j].y;
92 | this.polygons.array[i].array[j].x = x2 - (x - x1);
93 | this.polygons.array[i].array[j].y = y2 - (y - y1);
94 | this.polygons.array[i].array[j].x = Math.floor(this.polygons.array[i].array[j].x * 10) / 10;
95 | this.polygons.array[i].array[j].y = Math.floor(this.polygons.array[i].array[j].y * 10) / 10;
96 | }
97 | }
98 | }
99 | }
100 |
101 | rotate270(x1, y1, x2, y2) {
102 | for (var i = 0; i < this.polygons.array.length; i++) {
103 | var inside = true;
104 | for (var j = 0; j < this.polygons.array[i].array.length; j++) {
105 | if (x1 > this.polygons.array[i].array[j].x || this.polygons.array[i].array[j].x > x2 ||
106 | y1 > this.polygons.array[i].array[j].y || this.polygons.array[i].array[j].y > y2) {
107 | inside = false;
108 | }
109 | }
110 | if (inside) {
111 | for (var j = 0; j < this.polygons.array[i].array.length; j++) {
112 | var x = this.polygons.array[i].array[j].x;
113 | var y = this.polygons.array[i].array[j].y;
114 | this.polygons.array[i].array[j].x = x1 + (y - y1);
115 | this.polygons.array[i].array[j].y = y2 - (x - x1);
116 | this.polygons.array[i].array[j].x = Math.floor(this.polygons.array[i].array[j].x * 10) / 10;
117 | this.polygons.array[i].array[j].y = Math.floor(this.polygons.array[i].array[j].y * 10) / 10;
118 | }
119 | }
120 | }
121 | }
122 |
123 | drawUpperLeftCorner(x1, y1, dir, kMinWidthT) {
124 | var p = new PointMaker(x1, y1, dir, kMinWidthT);
125 | var poly = new Polygon();
126 | poly.push2(p.vec(0, 1));
127 | poly.push2(p.vec(0.01,0));//fix_union
128 |
129 | poly.push2(p.vec(0, -1));
130 | poly.push2(p.vec(-1, 1));
131 |
132 | poly.reverse();
133 | this.polygons.push(poly);
134 | }
135 |
136 | drawUpperRightCorner(x1, y1, kMinWidthT, kagekMinWidthY, kagekWidth) {
137 | var p = new PointMaker(x1, y1);
138 | var poly = new Polygon();
139 | poly.push2(p.vec(-kMinWidthT,-kagekMinWidthY));
140 | poly.push2(p.vec(0,-kagekMinWidthY - kagekWidth));
141 | poly.push2(p.vec(kMinWidthT+kagekWidth,kagekMinWidthY));
142 | poly.push2(p.vec(kMinWidthT,kMinWidthT*0.8), 2);
143 | poly.push2(p.vec(0,kMinWidthT*1.2), 2);
144 | poly.push2(p.vec(-kMinWidthT*0.9,kMinWidthT*1.2));
145 | //poly.push2(p.vec(-kMinWidthT,kMinWidthT*1.3));
146 | this.polygons.push(poly);
147 | }
148 |
149 | drawUpperRightCorner2(x1, y1, kMinWidthT, kagekMinWidthY, kagekWidth, is_roofed_thin) {
150 | var p = new PointMaker(x1, y1);
151 | var poly = new Polygon();
152 | poly.push2(p.vec(-kMinWidthT,-kagekMinWidthY));
153 | //poly.push2(p.vec(-kMinWidthT*1.2,-kagekMinWidthY));
154 |
155 | poly.push2(p.vec(0,-kagekMinWidthY - kagekWidth));
156 | //poly.push2(p.vec(-kMinWidthT*0.1,-kagekMinWidthY - kagekWidth*1.1));
157 |
158 | poly.push2(p.vec(kMinWidthT+kagekWidth,kagekMinWidthY));
159 | if (is_roofed_thin) {
160 | poly.push2(p.vec(kMinWidthT*0.3,kMinWidthT*1.15));
161 | poly.push2(p.vec(0,kagekMinWidthY));
162 | }else{
163 | poly.push2(p.vec(kMinWidthT,kMinWidthT*0.8), 2);
164 | poly.push2(p.vec(0,kMinWidthT*1.2), 2);
165 | poly.push2(p.vec(-kMinWidthT*0.9,kMinWidthT*1.2));
166 | //poly.push2(p.vec(-kMinWidthT,kMinWidthT*1.3));
167 | }
168 | this.polygons.push(poly);
169 | }
170 |
171 | drawUpperRightCorner_straight_v(x1, y1, kMinWidthT, kagekMinWidthY, kagekWidth) {//vertical straight line
172 | var p = new PointMaker(x1, y1);
173 | var poly = new Polygon();
174 | poly.push2(p.vec(-kMinWidthT,-kagekMinWidthY));
175 | poly.push2(p.vec(0,-kagekMinWidthY - kagekWidth));
176 | poly.push2(p.vec(kMinWidthT+kagekWidth,kagekMinWidthY));
177 | poly.push2(p.vec(kMinWidthT,kMinWidthT));
178 | poly.push2(p.vec(-kMinWidthT,0));
179 | this.polygons.push(poly);
180 | }
181 |
182 | drawOpenBegin_straight(x1, y1, kMinWidthT, kagekMinWidthY, rad) {
183 | const dir=rad_to_dir((rad+Math.PI*0.621)*0.5);
184 | //const rad_offset = Math.atan(kagekMinWidthY*0.5/kMinWidthT);
185 | //const rad_offset = (0.1*Math.PI);
186 | const rad_diff = (rad - Math.PI*0.621) * 0.5
187 | var poly = new Polygon();
188 | let p0 = new PointMaker(x1, y1, rad_to_dir(rad), kMinWidthT)
189 | poly.push2(p0.vec(1, 1));
190 | poly.push2(p0.vec(Math.sin(rad_diff), 1));
191 | let p1 = new PointMaker(x1, y1, dir);
192 | let[x, y] = p1.vec(0, -kMinWidthT);
193 | //const offs_sin = Math.sin(rad_offset);
194 | //const offs_cos = Math.cos(rad_offset);
195 | //let[x, y] = p1.vec(kMinWidthT*offs_sin/offs_cos, -kMinWidthT);
196 | //const new_dir = {sin: dir.sin*offs_cos+dir.cos*offs_sin, cos: dir.cos*offs_cos-dir.sin*offs_sin};
197 | let p2 = new PointMaker(x, y, dir, kMinWidthT*0.876);
198 | poly.push2(p2.vec(0, 0));
199 | poly.push2(p2.vec(0, -1.4), 2);
200 | poly.push2(p2.vec(0.8, -1.4), 2);
201 | poly.push2(p2.vec(1.5, 0.5));
202 | this.polygons.push(poly);
203 | }
204 |
205 | drawOpenBegin_curve_down2(x1, y1, rad, kMinWidthT, rad_offset, kagekMinWidthY){
206 |
207 | var poly = new Polygon();
208 | const dir = rad_to_dir(rad)
209 | let p1 = new PointMaker(x1, y1, dir);
210 | let [xx, yy] = p1.vec(-kagekMinWidthY*0.79, 0)
211 | let p0 = new PointMaker(xx, yy, dir, kMinWidthT);
212 | let [x0, y0 ] = p1.vec(0, kMinWidthT);
213 | poly.push(x0, y0);
214 |
215 | let p2 = new PointMaker();
216 | p2.setscale(kMinWidthT*0.876);
217 | const offset_limit = Math.atan2(kagekMinWidthY*0.79 , kMinWidthT)
218 | rad_offset = Math.min(rad_offset, offset_limit)
219 | p2.setdir(rad_to_dir(rad+rad_offset));
220 |
221 | if (rad_offset < -offset_limit){
222 | let p3 = new PointMaker(x0, y0, rad_to_dir(rad));
223 | let [x3, y3] = p3.vec(kMinWidthT * 2 * Math.sin(rad_offset), -kMinWidthT * 2)
224 | p2.setpos(x3, y3);
225 | }else
226 | {
227 | poly.push2(p0.vec(-Math.sin(rad_offset), 1));
228 | let[x, y] = p0.vec(Math.sin(rad_offset), -1);
229 | p2.setpos(x, y);
230 | }
231 | poly.push2(p2.vec(0, 0));
232 | poly.push2(p2.vec(0, -1.0), 2);
233 | poly.push2(p2.vec(0.6, -1.0), 2);
234 | poly.push2(p2.vec(1.8, 1.0));
235 | this.polygons.push(poly);
236 | }
237 |
238 | drawOpenBegin_curve_down(x1, y1, rad, kMinWidthT, kagekMinWidthY){
239 | var rad_offset;
240 | /*
241 | if(rad > Math.PI * 0.2){//36 degrees
242 | rad_offset = (0.1*Math.PI)*(rad-Math.PI * 0.2)/(Math.PI*0.3);
243 | }else{
244 | rad_offset = (-0.25*Math.PI)*(Math.PI*0.2-rad)/(Math.PI*0.2);
245 | }*/
246 | rad_offset = (Math.PI*0.621 - rad)*0.5
247 | this.drawOpenBegin_curve_down2(x1, y1, rad, kMinWidthT, rad_offset, kagekMinWidthY);
248 | }
249 |
250 | drawOpenBegin_curve_up(x1, y1, dir, kMinWidthT, kagekMinWidthY) {
251 | var poly = new Polygon();
252 | let p1 = new PointMaker(x1, y1, dir);
253 | const offs_sin = Math.sin(-0.1*Math.PI);
254 | const offs_cos = Math.cos(-0.1*Math.PI);
255 | poly.push2(p1.vec(0, -kMinWidthT));
256 | poly.push2(p1.vec((offs_sin/offs_cos)*2*kMinWidthT, -kMinWidthT));
257 |
258 | let p2 = new PointMaker();
259 | p2.setscale(kMinWidthT*0.876);
260 | p2.setdir({sin: dir.sin*offs_cos+dir.cos*offs_sin, cos: dir.cos*offs_cos-dir.sin*offs_sin});
261 | let[x, y] = p1.vec(0, kMinWidthT);
262 | p2.setpos(x, y);
263 |
264 | poly.push2(p2.vec(0, 0));
265 | poly.push2(p2.vec(0, 1.1), 2);
266 | poly.push2(p2.vec(0.7, 1.1), 2);
267 | poly.push2(p2.vec(1.4, -0.5));
268 | poly.reverse();
269 | this.polygons.push(poly);
270 | }
271 |
272 | drawLowerRightHT_v(x2, y2, kMinWidthT, kagekMinWidthY) {//for T design
273 | var poly = new Polygon();
274 | poly.push(x2, y2 + kagekMinWidthY);
275 | poly.push(x2 + kMinWidthT, y2 - kagekMinWidthY * 3);
276 | poly.push(x2 + kMinWidthT * 2, y2 - kagekMinWidthY);
277 | poly.push(x2 + kMinWidthT * 2, y2 + kagekMinWidthY);
278 | this.polygons.push(poly);
279 | }
280 |
281 | drawLowerRightHT(x2, y2, kMinWidthT, kagekMinWidthY) {//for T design
282 | var poly = new Polygon();
283 | poly.push(x2, y2 + kagekMinWidthY);
284 | poly.push(x2 + kMinWidthT * 0.5, y2 - kagekMinWidthY * 4);
285 | poly.push(x2 + kMinWidthT * 2, y2 - kagekMinWidthY);
286 | poly.push(x2 + kMinWidthT * 2, y2 + kagekMinWidthY);
287 | this.polygons.push(poly);
288 | }
289 |
290 | drawNewGTHbox(x2m, y2, kMinWidthT, kagekMinWidthY) {//for new GTH box's left bottom corner MUKI KANKEINASHI
291 | var poly = new Polygon();
292 | poly.push(x2m, y2 - kagekMinWidthY * 5);
293 | poly.push(x2m - kMinWidthT * 2, y2);
294 | poly.push(x2m - kagekMinWidthY, y2 + kagekMinWidthY * 5);
295 | poly.push(x2m + kMinWidthT, y2 + kagekMinWidthY);
296 | poly.push(x2m, y2);
297 | poly.reverse();
298 | this.polygons.push(poly);
299 | }
300 |
301 | drawNewGTHbox_v(x2, y2, kMinWidthT, kagekMinWidthY) {
302 | var poly = new Polygon();
303 | poly.push(x2 - kMinWidthT, y2 - kagekMinWidthY * 3);
304 | poly.push(x2 - kMinWidthT * 2, y2);
305 | poly.push(x2 - kagekMinWidthY, y2 + kagekMinWidthY * 5);
306 | poly.push(x2 + kMinWidthT, y2 + kagekMinWidthY);
307 | //poly.push(x2 + kMinWidthT, y2);
308 | poly.reverse();
309 | this.polygons.push(poly);
310 | }
311 |
312 | drawTailCircle_tan(tailX, tailY, dir, r, tan1, tan2) {
313 | //draw a (semi)circle on the tail of the line to (tailX, tailY)
314 | var poly = new Polygon();
315 | const vec1 = vector_to_len(tan1, r*bez_cir*0.74);
316 | const vec2 = vector_to_len(tan2, r*bez_cir*0.78);
317 | let p = new PointMaker(tailX, tailY, dir, r);
318 | let [x1, y1] = p.vec(0, -1);
319 | let [x2, y2] = p.vec(0, 1);
320 | poly.push(x1, y1);
321 | poly.push(x1 + vec1[0], y1 + vec1[1], 2);
322 | poly.push2(p.vec(0.94,-bez_cir*1.09), 2);
323 | poly.push2(p.vec(0.94,0));
324 | poly.push2(p.vec(0.94,+bez_cir*1.09), 2);
325 | poly.push(x2 + vec2[0], y2 + vec2[1], 2);
326 | poly.push(x2, y2);
327 | poly.push2(p.vec(-0.01,0));//fix_union
328 | this.polygons.push(poly);
329 | }
330 |
331 | drawTailCircle(tailX, tailY, dir, r) {
332 | //draw a (semi)circle on the tail of the line to (tailX, tailY)
333 | var poly = new Polygon();
334 | var p = new PointMaker(tailX, tailY, dir, r);
335 | poly.push2(p.vec(0, -1));
336 | poly.push2(p.vec(bez_cir, -1), 2);
337 | poly.push2(p.vec(1, -bez_cir), 2);
338 | poly.push2(p.vec(1, 0));
339 | poly.push2(p.vec(1, bez_cir), 2);
340 | poly.push2(p.vec(bez_cir, 1), 2);
341 | poly.push2(p.vec(0, 1));
342 | poly.push2(p.vec(-0.01,0));//fix_union
343 | this.polygons.push(poly);
344 | }
345 |
346 | drawCircle_bend_pos(x2, y2, dir, kMinWidthT2) {
347 | var poly = new Polygon();
348 | var p = new PointMaker(x2, y2, dir, kMinWidthT2);
349 | poly.push2(p.vec(0, -1));
350 | poly.push2(p.vec(1.5, -1), 2);
351 | poly.push2(p.vec(0.9, 1), 2);
352 | poly.push2(p.vec(0, 1));
353 | poly.push2(p.vec(-0.01,0));//fix_union
354 | this.polygons.push(poly);
355 | }
356 |
357 | drawCircle_bend_neg(x2, y2, dir, kMinWidthT2) {
358 | var poly = new Polygon();
359 | var p = new PointMaker(x2, y2, dir, kMinWidthT2);
360 | poly.push2(p.vec(0, 1));
361 | poly.push2(p.vec(1.5, 1), 2);
362 | poly.push2(p.vec(0.9, -1), 2);
363 | poly.push2(p.vec(0, -1));
364 | poly.push2(p.vec(-0.01,0));//fix_union
365 | poly.reverse();
366 | this.polygons.push(poly);
367 | }
368 |
369 | drawL2RSweepEnd(x2, y2, dir, kMinWidthT, kagekL2RDfatten) {
370 | var type = (Math.atan2(Math.abs(dir.sin), Math.abs(dir.cos)) / Math.PI * 2 - 0.6);
371 | if (type > 0) {
372 | type = type * 8;
373 | } else {
374 | type = type * 3;
375 | }
376 | const pm = (type < 0) ? -1 : 1;
377 | var p = new PointMaker(x2, y2, dir);
378 | var poly = new Polygon();
379 | poly.push2(p.vec(0, kMinWidthT * kagekL2RDfatten));
380 | poly.push2(p.vec(-0.1,0));//fix_union
381 |
382 | poly.push2(p.vec(0, -kMinWidthT * kagekL2RDfatten));
383 | poly.push2(p.vec(kMinWidthT * kagekL2RDfatten * Math.abs(type), kMinWidthT * kagekL2RDfatten * pm));
384 |
385 | this.polygons.push(poly);
386 | }
387 |
388 | drawTurnUpwards_pos(x2, y2, kMinWidthT, length_param, dir) {
389 | var poly = new Polygon();
390 | var p = new PointMaker(x2, y2, dir);
391 | poly.push2(p.vec(kMinWidthT*0.7, - kMinWidthT*0.7));
392 | poly.push2(p.vec(kMinWidthT*0.22, - kMinWidthT),2);
393 | poly.push2(p.vec(kMinWidthT*0.167, - kMinWidthT - length_param*0.05),2);
394 | poly.push2(p.vec(kMinWidthT*0.181, - kMinWidthT - length_param*0.15));
395 | poly.push2(p.vec(kMinWidthT*0.3, - kMinWidthT - length_param));
396 | poly.push2(p.vec(0, - kMinWidthT - length_param));
397 | poly.push2(p.vec( - kMinWidthT*0.75, - kMinWidthT - length_param/4));
398 | poly.push2(p.vec( - kMinWidthT*0.875, - kMinWidthT - length_param/8), 2);
399 | poly.push2(p.vec( - kMinWidthT*1.25, - kMinWidthT), 2);
400 | poly.push2(p.vec( - kMinWidthT*1.6, - kMinWidthT));
401 |
402 | poly.reverse(); // for fill-rule
403 | this.polygons.push(poly);
404 | }
405 |
406 | drawTurnUpwards_neg(x2, y2, kMinWidthT, length_param, dir) {
407 | var poly = new Polygon();
408 | var p = new PointMaker(x2, y2, dir);
409 | poly.push2(p.vec(kMinWidthT*0.7, kMinWidthT*0.7));
410 | poly.push2(p.vec(0, kMinWidthT),2);
411 | poly.push2(p.vec(kMinWidthT*0.3, kMinWidthT + length_param/2), 2);
412 | poly.push2(p.vec(kMinWidthT*0.5, kMinWidthT + length_param));
413 | poly.push2(p.vec(0, kMinWidthT + length_param));
414 | poly.push2(p.vec( - kMinWidthT*0.6, kMinWidthT + length_param/4), 2);
415 | poly.push2(p.vec( - kMinWidthT*1.8, kMinWidthT), 2);
416 | poly.push2(p.vec( - kMinWidthT*2.2, kMinWidthT));
417 | this.polygons.push(poly);
418 | }
419 |
420 | drawTurnLeft(x2, y2, kMinWidthT, length_param) {
421 | var poly = new Polygon();
422 | poly.push(x2, y2 - kMinWidthT);
423 | poly.push(x2 - length_param, y2 - kMinWidthT);
424 | poly.push(x2 - length_param, y2 - kMinWidthT * 0.5);
425 | poly.push(x2 - kMinWidthT*0.3, y2 - kMinWidthT * 0.3,2);
426 | poly.push(x2, y2 + kMinWidthT * 0.3, 2);
427 | poly.push(x2, y2 + kMinWidthT);
428 | poly.push(x2+0.1, y2);//fix_union
429 | poly.reverse();
430 | this.polygons.push(poly);
431 | }
432 |
433 | drawUroko_h(x2, y2, kagekMinWidthY, urokoX, urokoY) {
434 | var poly = new Polygon();
435 | poly.push(x2, y2 - kagekMinWidthY);
436 | poly.push(x2 - urokoX, y2);
437 | poly.push(x2 - urokoX / 2, y2 - urokoY);
438 | this.polygons.push(poly);
439 | }
440 |
441 | drawUroko(x2, y2, dir, kagekMinWidthY, urokoX, urokoY) {
442 | var poly = new Polygon();
443 | poly.push(x2 + dir.sin * kagekMinWidthY, y2 - dir.cos * kagekMinWidthY);
444 | poly.push(x2 - dir.cos * urokoX, y2 - dir.sin * urokoX);
445 | poly.push(x2 - dir.cos * urokoX / 2 + dir.sin * urokoY, y2 - dir.sin * urokoX / 2 - dir.cos * urokoY);
446 | this.polygons.push(poly);
447 | }
448 |
449 | drawLine(x1, y1, x2, y2, halfWidth) {
450 | //draw a line(rectangle).
451 | var poly = new Polygon;
452 | const dir = get_dir(x2-x1, y2-y1);
453 | let p = new PointMaker(x1, y1, dir);
454 | poly.push2(p.vec(0, halfWidth));
455 | poly.push2(p.vec(0, -halfWidth));
456 | p.setpos(x2, y2);
457 | poly.push2(p.vec(0, -halfWidth));
458 | poly.push2(p.vec(0, halfWidth));
459 | this.polygons.push(poly);
460 | }
461 |
462 | drawOffsetLine(x1, y1, x2, y2, halfWidth, off_left1, off_right1, off_left2, off_right2) {
463 | //Draw a line(rectangle), and adjust the rectangle to trapezoid.
464 | //off_left1 means how much the start point of the left side (seeing from (x1, y1)) of the rectangle is stretched.
465 | //Note that the positive Y-axis goes in the downwards.
466 | //off_left2 deals with the end point. The right side is managed similarly.
467 | //The generated polygon will be clockwise.
468 | var poly = new Polygon;
469 | const dir = get_dir(x2-x1, y2-y1);
470 | let p = new PointMaker(x1, y1, dir);
471 | poly.push2(p.vec(-off_right1, halfWidth));
472 | poly.push2(p.vec(-off_left1, -halfWidth));
473 | p.setpos(x2, y2);
474 | poly.push2(p.vec(off_left2, -halfWidth));
475 | poly.push2(p.vec(off_right2, halfWidth));
476 | this.polygons.push(poly);
477 | }
478 |
479 | drawCBezier(x1, y1, sx1, sy1, sx2, sy2, x2, y2, width_func, width_func_d, curve_step) {
480 | let [bez1, bez2] = Bezier.cBezier(x1, y1, sx1, sy1, sx2, sy2, x2, y2, width_func, width_func_d, curve_step);
481 | var poly = Bezier.bez_to_poly(bez1);
482 | poly.concat(Bezier.bez_to_poly(bez2));
483 | this.polygons.push(poly);
484 | }
485 | drawQBezier(x1, y1, sx, sy, x2, y2, width_func, width_func_d, curve_step, fix_begin, fix_end) {
486 | let [bez1, bez2] = Bezier.qBezier(x1, y1, sx, sy, x2, y2, width_func, width_func_d, curve_step);
487 | var poly = Bezier.bez_to_poly(bez1);
488 | if (fix_end) {
489 | var p = new PointMaker(x2, y2, get_dir(x2-sx, y2-sy), 1);
490 | poly.push2(p.vec(0.1, -width_func(1)))//fix_union
491 | poly.push2(p.vec(0.1, width_func(1)))//fix_union
492 |
493 | //poly.push2(p.vec(0.01, -0.99))//fix_union
494 | //poly.push2(p.vec(0.01, 0.99))//fix_union
495 | }
496 | poly.concat(Bezier.bez_to_poly(bez2));
497 | if (fix_begin) {
498 | var p = new PointMaker(x1, y1, get_dir(x1-sx, y1-sy), 1);
499 | poly.push2(p.vec(0.1, -width_func(0)))//fix_union
500 | poly.push2(p.vec(0.1, width_func(0)))//fix_union
501 | //poly.push2(p.vec(0.01, -0.99))//fix_union
502 | //poly.push2(p.vec(0.01, 0.99))//fix_union
503 | }
504 | this.polygons.push(poly);
505 | }
506 | drawQBezier2(x1, y1, sx, sy, x2, y2, width_func, width_func_d, curve_step) {
507 | let [bez1, bez2] = Bezier.qBezier2(x1, y1, sx, sy, x2, y2, width_func, width_func_d, curve_step);
508 | var poly = Bezier.bez_to_poly(bez1);
509 | poly.concat(Bezier.bez_to_poly(bez2));
510 | this.polygons.push(poly);
511 | }
512 | }
--------------------------------------------------------------------------------
/fit-curve.js:
--------------------------------------------------------------------------------
1 | // ==ClosureCompiler==
2 | // @output_file_name fit-curve.min.js
3 | // @compilation_level SIMPLE_OPTIMIZATIONS
4 | // ==/ClosureCompiler==
5 |
6 | /**
7 | * @preserve JavaScript implementation of
8 | * Algorithm for Automatically Fitting Digitized Curves
9 | * by Philip J. Schneider
10 | * "Graphics Gems", Academic Press, 1990
11 | *
12 | * The MIT License (MIT)
13 | *
14 | * https://github.com/soswow/fit-curves
15 | */
16 |
17 | /**
18 | * Fit one or more Bezier curves to a set of points.
19 | *
20 | * @param {Array>} points - Array of digitized points, e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]]
21 | * @param {Number} maxError - Tolerance, squared error between points and fitted curve
22 | * @returns {Array>>} Array of Bezier curves, where each element is [first-point, control-point-1, control-point-2, second-point] and points are [x, y]
23 | */
24 | export function fitCurve(points, maxError, progressCallback) {
25 | if (!Array.isArray(points)) {
26 | throw new TypeError("First argument should be an array");
27 | }
28 | points.forEach((point) => {
29 | if(!Array.isArray(point) || point.some(item => typeof item !== 'number')
30 | || point.length !== points[0].length) {
31 | throw Error("Each point should be an array of numbers. Each point should have the same amount of numbers.");
32 | }
33 | });
34 |
35 | // Remove duplicate points
36 | points = points.filter((point, i) =>
37 | i === 0 || !point.every((val, j) => val === points[i-1][j])
38 | );
39 |
40 | if (points.length < 2) {
41 | return [];
42 | }
43 |
44 | const len = points.length;
45 | const leftTangent = createTangent(points[1], points[0]);
46 | const rightTangent = createTangent(points[len - 2], points[len - 1]);
47 |
48 | return fitCubic(points, leftTangent, rightTangent, maxError, progressCallback);
49 | }
50 |
51 | /**
52 | * Fit a Bezier curve to a (sub)set of digitized points.
53 | * Your code should not call this function directly. Use {@link fitCurve} instead.
54 | *
55 | * @param {Array>} points - Array of digitized points, e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]]
56 | * @param {Array} leftTangent - Unit tangent vector at start point
57 | * @param {Array} rightTangent - Unit tangent vector at end point
58 | * @param {Number} error - Tolerance, squared error between points and fitted curve
59 | * @returns {Array>>} Array of Bezier curves, where each element is [first-point, control-point-1, control-point-2, second-point] and points are [x, y]
60 | */
61 |
62 | export function fitCubic_tang(points, tangents, error, progressCallback) {
63 | const MaxIterations = 20; //Max times to try iterating (to find an acceptable curve)
64 |
65 | var bezCurve, //Control points of fitted Bezier curve
66 | u, //Parameter values for point
67 | uPrime, //Improved parameter values
68 | maxError, prevErr, //Maximum fitting error
69 | splitPoint, prevSplit, //Point to split point set at if we need more than one curve
70 | //centerVector, toCenterTangent, fromCenterTangent, //Unit tangent vector(s) at splitPoint
71 | beziers, //Array of fitted Bezier curves if we need more than one curve
72 | dist, i;
73 |
74 | //console.log('fitCubic, ', points.length);
75 | const leftTangent = tangents[0];
76 | const rightTangent = maths.mulItems(tangents[points.length - 1], -1);
77 | //Use heuristic if region only has two points in it
78 | if (points.length === 2) {
79 | dist = maths.vectorLen(maths.subtract(points[0], points[1])) / 3.0;
80 | bezCurve = [
81 | points[0],
82 | maths.addArrays(points[0], maths.mulItems(leftTangent, dist)),
83 | maths.addArrays(points[1], maths.mulItems(rightTangent, dist)),
84 | points[1]
85 | ];
86 | return [bezCurve];
87 | }
88 |
89 |
90 | //Parameterize points, and attempt to fit curve
91 | u = chordLengthParameterize(points);
92 | [bezCurve, maxError, splitPoint] = generateAndReport(points, u, u, leftTangent, rightTangent, progressCallback)
93 |
94 | if (maxError < error) {
95 | return [bezCurve];
96 | }
97 | //If error not too large, try some reparameterization and iteration
98 | if (maxError < (error*error)) {
99 |
100 | uPrime = u;
101 | prevErr = maxError;
102 | prevSplit = splitPoint;
103 |
104 | for (i = 0; i < MaxIterations; i++) {
105 |
106 | uPrime = reparameterize(bezCurve, points, uPrime);
107 | [bezCurve, maxError, splitPoint] = generateAndReport(points, u, uPrime, leftTangent, rightTangent, progressCallback);
108 |
109 | if (maxError < error) {
110 | return [bezCurve];
111 | }
112 | //If the development of the fitted curve grinds to a halt,
113 | //we abort this attempt (and try a shorter curve):
114 | else if(splitPoint === prevSplit) {
115 | let errChange = maxError/prevErr;
116 | if((errChange > .9999) && (errChange < 1.0001)) {
117 | break;
118 | }
119 | }
120 |
121 | prevErr = maxError;
122 | prevSplit = splitPoint;
123 | }
124 | }
125 |
126 | //Fitting failed -- split at max error point and fit recursively
127 | beziers = [];
128 |
129 | //To and from need to point in opposite directions:
130 | /*
131 | Note: An alternative to this "divide and conquer" recursion could be to always
132 | let new curve segments start by trying to go all the way to the end,
133 | instead of only to the end of the current subdivided polyline.
134 | That might let many segments fit a few points more, reducing the number of total segments.
135 |
136 | However, a few tests have shown that the segment reduction is insignificant
137 | (240 pts, 100 err: 25 curves vs 27 curves. 140 pts, 100 err: 17 curves on both),
138 | and the results take twice as many steps and milliseconds to finish,
139 | without looking any better than what we already have.
140 | */
141 | beziers = beziers.concat(fitCubic_tang(points.slice(0, splitPoint + 1), tangents.slice(0, splitPoint + 1), error, progressCallback));
142 | beziers = beziers.concat(fitCubic_tang(points.slice(splitPoint), tangents.slice(splitPoint) , error, progressCallback));
143 | return beziers;
144 | };
145 |
146 |
147 |
148 |
149 | function fitCubic(points, leftTangent, rightTangent, error, progressCallback) {
150 | const MaxIterations = 20; //Max times to try iterating (to find an acceptable curve)
151 |
152 | var bezCurve, //Control points of fitted Bezier curve
153 | u, //Parameter values for point
154 | uPrime, //Improved parameter values
155 | maxError, prevErr, //Maximum fitting error
156 | splitPoint, prevSplit, //Point to split point set at if we need more than one curve
157 | centerVector, toCenterTangent, fromCenterTangent, //Unit tangent vector(s) at splitPoint
158 | beziers, //Array of fitted Bezier curves if we need more than one curve
159 | dist, i;
160 |
161 | //console.log('fitCubic, ', points.length);
162 | //console.log(points);
163 | //console.log(tangents);
164 | //console.log(leftTangent,rightTangent);
165 |
166 | //Use heuristic if region only has two points in it
167 | if (points.length === 2) {
168 | dist = maths.vectorLen(maths.subtract(points[0], points[1])) / 3.0;
169 | bezCurve = [
170 | points[0],
171 | maths.addArrays(points[0], maths.mulItems(leftTangent, dist)),
172 | maths.addArrays(points[1], maths.mulItems(rightTangent, dist)),
173 | points[1]
174 | ];
175 | return [bezCurve];
176 | }
177 |
178 |
179 | //Parameterize points, and attempt to fit curve
180 | u = chordLengthParameterize(points);
181 | [bezCurve, maxError, splitPoint] = generateAndReport(points, u, u, leftTangent, rightTangent, progressCallback)
182 |
183 | if (maxError < error) {
184 | return [bezCurve];
185 | }
186 | //If error not too large, try some reparameterization and iteration
187 | if (maxError < (error*error)) {
188 |
189 | uPrime = u;
190 | prevErr = maxError;
191 | prevSplit = splitPoint;
192 |
193 | for (i = 0; i < MaxIterations; i++) {
194 |
195 | uPrime = reparameterize(bezCurve, points, uPrime);
196 | [bezCurve, maxError, splitPoint] = generateAndReport(points, u, uPrime, leftTangent, rightTangent, progressCallback);
197 |
198 | if (maxError < error) {
199 | return [bezCurve];
200 | }
201 | //If the development of the fitted curve grinds to a halt,
202 | //we abort this attempt (and try a shorter curve):
203 | else if(splitPoint === prevSplit) {
204 | let errChange = maxError/prevErr;
205 | if((errChange > .9999) && (errChange < 1.0001)) {
206 | break;
207 | }
208 | }
209 |
210 | prevErr = maxError;
211 | prevSplit = splitPoint;
212 | }
213 | }
214 |
215 | //Fitting failed -- split at max error point and fit recursively
216 | beziers = [];
217 |
218 | //To create a smooth transition from one curve segment to the next, we
219 | //calculate the line between the points directly before and after the
220 | //center, and use that as the tangent both to and from the center point.
221 | centerVector = maths.subtract(points[splitPoint-1], points[splitPoint+1]);
222 | //However, this won't work if they're the same point, because the line we
223 | //want to use as a tangent would be 0. Instead, we calculate the line from
224 | //that "double-point" to the center point, and use its tangent.
225 | if(centerVector.every(val => val === 0)) {
226 | //[x,y] -> [-y,x]: http://stackoverflow.com/a/4780141/1869660
227 | centerVector = maths.subtract(points[splitPoint-1], points[splitPoint]);
228 | [centerVector[0],centerVector[1]] = [-centerVector[1],centerVector[0]];
229 | }
230 | toCenterTangent = maths.normalize(centerVector);
231 | //To and from need to point in opposite directions:
232 | fromCenterTangent = maths.mulItems(toCenterTangent, -1);
233 |
234 | /*
235 | Note: An alternative to this "divide and conquer" recursion could be to always
236 | let new curve segments start by trying to go all the way to the end,
237 | instead of only to the end of the current subdivided polyline.
238 | That might let many segments fit a few points more, reducing the number of total segments.
239 |
240 | However, a few tests have shown that the segment reduction is insignificant
241 | (240 pts, 100 err: 25 curves vs 27 curves. 140 pts, 100 err: 17 curves on both),
242 | and the results take twice as many steps and milliseconds to finish,
243 | without looking any better than what we already have.
244 | */
245 | beziers = beziers.concat(fitCubic(points.slice(0, splitPoint + 1), leftTangent, toCenterTangent, error, progressCallback));
246 | beziers = beziers.concat(fitCubic(points.slice(splitPoint), fromCenterTangent, rightTangent, error, progressCallback));
247 | return beziers;
248 | };
249 |
250 | function generateAndReport(points, paramsOrig, paramsPrime, leftTangent, rightTangent, progressCallback) {
251 | var bezCurve, maxError, splitPoint;
252 |
253 | bezCurve = generateBezier(points, paramsPrime, leftTangent, rightTangent, progressCallback);
254 | //Find max deviation of points to fitted curve.
255 | //Here we always use the original parameters (from chordLengthParameterize()),
256 | //because we need to compare the current curve to the actual source polyline,
257 | //and not the currently iterated parameters which reparameterize() & generateBezier() use,
258 | //as those have probably drifted far away and may no longer be in ascending order.
259 | [maxError, splitPoint] = computeMaxError(points, bezCurve, paramsOrig);
260 |
261 | if(progressCallback) {
262 | progressCallback({
263 | bez: bezCurve,
264 | points: points,
265 | params: paramsOrig,
266 | maxErr: maxError,
267 | maxPoint: splitPoint,
268 | });
269 | }
270 |
271 | return [bezCurve, maxError, splitPoint];
272 | }
273 |
274 | /**
275 | * Use least-squares method to find Bezier control points for region.
276 | *
277 | * @param {Array>} points - Array of digitized points
278 | * @param {Array} parameters - Parameter values for region
279 | * @param {Array} leftTangent - Unit tangent vector at start point
280 | * @param {Array} rightTangent - Unit tangent vector at end point
281 | * @returns {Array>} Approximated Bezier curve: [first-point, control-point-1, control-point-2, second-point] where points are [x, y]
282 | */
283 | function generateBezier(points, parameters, leftTangent, rightTangent) {
284 | var bezCurve, //Bezier curve ctl pts
285 | A, a, //Precomputed rhs for eqn
286 | C, X, //Matrices C & X
287 | det_C0_C1, det_C0_X, det_X_C1, //Determinants of matrices
288 | alpha_l, alpha_r, //Alpha values, left and right
289 |
290 | epsilon, segLength,
291 | i, len, tmp, u, ux,
292 | firstPoint = points[0],
293 | lastPoint = points[points.length-1];
294 |
295 | bezCurve = [firstPoint, null, null, lastPoint];
296 | //console.log('gb', parameters.length);
297 |
298 | //Compute the A's
299 | A = maths.zeros_Xx2x2(parameters.length);
300 | for (i = 0, len = parameters.length; i < len; i++) {
301 | u = parameters[i];
302 | ux = 1 - u;
303 | a = A[i];
304 |
305 | a[0] = maths.mulItems(leftTangent, 3 * u * (ux*ux));
306 | a[1] = maths.mulItems(rightTangent, 3 * ux * (u*u));
307 | }
308 |
309 | //Create the C and X matrices
310 | C = [[0,0], [0,0]];
311 | X = [0,0];
312 | for (i = 0, len = points.length; i < len; i++) {
313 | u = parameters[i];
314 | a = A[i];
315 |
316 | C[0][0] += maths.dot(a[0], a[0]);
317 | C[0][1] += maths.dot(a[0], a[1]);
318 | C[1][0] += maths.dot(a[0], a[1]);
319 | C[1][1] += maths.dot(a[1], a[1]);
320 |
321 | tmp = maths.subtract(points[i], bezier.q([firstPoint, firstPoint, lastPoint, lastPoint], u));
322 |
323 | X[0] += maths.dot(a[0], tmp);
324 | X[1] += maths.dot(a[1], tmp);
325 | }
326 |
327 | //Compute the determinants of C and X
328 | det_C0_C1 = (C[0][0] * C[1][1]) - (C[1][0] * C[0][1]);
329 | det_C0_X = (C[0][0] * X[1] ) - (C[1][0] * X[0] );
330 | det_X_C1 = (X[0] * C[1][1]) - (X[1] * C[0][1]);
331 |
332 | //Finally, derive alpha values
333 | alpha_l = det_C0_C1 === 0 ? 0 : det_X_C1 / det_C0_C1;
334 | alpha_r = det_C0_C1 === 0 ? 0 : det_C0_X / det_C0_C1;
335 |
336 | //If alpha negative, use the Wu/Barsky heuristic (see text).
337 | //If alpha is 0, you get coincident control points that lead to
338 | //divide by zero in any subsequent NewtonRaphsonRootFind() call.
339 | segLength = maths.vectorLen(maths.subtract(firstPoint, lastPoint));
340 | epsilon = 1.0e-6 * segLength;
341 | if (alpha_l < epsilon || alpha_r < epsilon) {
342 | //Fall back on standard (probably inaccurate) formula, and subdivide further if needed.
343 | bezCurve[1] = maths.addArrays(firstPoint, maths.mulItems(leftTangent, segLength / 3.0));
344 | bezCurve[2] = maths.addArrays(lastPoint, maths.mulItems(rightTangent, segLength / 3.0));
345 | } else {
346 | //First and last control points of the Bezier curve are
347 | //positioned exactly at the first and last data points
348 | //Control points 1 and 2 are positioned an alpha distance out
349 | //on the tangent vectors, left and right, respectively
350 | bezCurve[1] = maths.addArrays(firstPoint, maths.mulItems(leftTangent, alpha_l));
351 | bezCurve[2] = maths.addArrays(lastPoint, maths.mulItems(rightTangent, alpha_r));
352 | }
353 |
354 | return bezCurve;
355 | };
356 |
357 | /**
358 | * Given set of points and their parameterization, try to find a better parameterization.
359 | *
360 | * @param {Array>} bezier - Current fitted curve
361 | * @param {Array>} points - Array of digitized points
362 | * @param {Array} parameters - Current parameter values
363 | * @returns {Array} New parameter values
364 | */
365 | function reparameterize(bezier, points, parameters) {
366 | /*
367 | var j, len, point, results, u;
368 | results = [];
369 | for (j = 0, len = points.length; j < len; j++) {
370 | point = points[j], u = parameters[j];
371 |
372 | results.push(newtonRaphsonRootFind(bezier, point, u));
373 | }
374 | return results;
375 | //*/
376 | return parameters.map((p, i) => newtonRaphsonRootFind(bezier, points[i], p));
377 | };
378 |
379 | /**
380 | * Use Newton-Raphson iteration to find better root.
381 | *
382 | * @param {Array>} bez - Current fitted curve
383 | * @param {Array} point - Digitized point
384 | * @param {Number} u - Parameter value for "P"
385 | * @returns {Number} New u
386 | */
387 | function newtonRaphsonRootFind(bez, point, u) {
388 | /*
389 | Newton's root finding algorithm calculates f(x)=0 by reiterating
390 | x_n+1 = x_n - f(x_n)/f'(x_n)
391 | We are trying to find curve parameter u for some point p that minimizes
392 | the distance from that point to the curve. Distance point to curve is d=q(u)-p.
393 | At minimum distance the point is perpendicular to the curve.
394 | We are solving
395 | f = q(u)-p * q'(u) = 0
396 | with
397 | f' = q'(u) * q'(u) + q(u)-p * q''(u)
398 | gives
399 | u_n+1 = u_n - |q(u_n)-p * q'(u_n)| / |q'(u_n)**2 + q(u_n)-p * q''(u_n)|
400 | */
401 |
402 | var d = maths.subtract(bezier.q(bez, u), point),
403 | qprime = bezier.qprime(bez, u),
404 | numerator = maths.mulMatrix(d, qprime),
405 | denominator = maths.sum(maths.squareItems(qprime)) + 2 * maths.mulMatrix(d, bezier.qprimeprime(bez, u));
406 |
407 | if (denominator === 0) {
408 | return u;
409 | } else {
410 | return u - (numerator/denominator);
411 | }
412 | };
413 |
414 | /**
415 | * Assign parameter values to digitized points using relative distances between points.
416 | *
417 | * @param {Array>} points - Array of digitized points
418 | * @returns {Array} Parameter values
419 | */
420 | function chordLengthParameterize(points) {
421 | var u = [], currU, prevU, prevP;
422 |
423 | points.forEach((p, i) => {
424 | currU = i ? prevU + maths.vectorLen(maths.subtract(p, prevP))
425 | : 0;
426 | u.push(currU);
427 |
428 | prevU = currU;
429 | prevP = p;
430 | })
431 | u = u.map(x => x/prevU);
432 |
433 | return u;
434 | };
435 |
436 | /**
437 | * Find the maximum squared distance of digitized points to fitted curve.
438 | *
439 | * @param {Array>} points - Array of digitized points
440 | * @param {Array>} bez - Fitted curve
441 | * @param {Array} parameters - Parameterization of points
442 | * @returns {Array} Maximum error (squared) and point of max error
443 | */
444 | function computeMaxError(points, bez, parameters) {
445 | var dist, //Current error
446 | maxDist, //Maximum error
447 | splitPoint, //Point of maximum error
448 | v, //Vector from point to curve
449 | i, count, point, t;
450 |
451 | maxDist = 0;
452 | splitPoint = points.length / 2;
453 |
454 | const t_distMap = mapTtoRelativeDistances(bez, 10);
455 |
456 | for (i = 0, count = points.length; i < count; i++) {
457 | point = points[i];
458 | //Find 't' for a point on the bez curve that's as close to 'point' as possible:
459 | t = find_t(bez, parameters[i], t_distMap, 10);
460 |
461 | v = maths.subtract(bezier.q(bez, t), point);
462 | dist = v[0]*v[0] + v[1]*v[1];
463 |
464 | if (dist > maxDist) {
465 | maxDist = dist;
466 | splitPoint = i;
467 | }
468 | }
469 |
470 | return [maxDist, splitPoint];
471 | };
472 |
473 | //Sample 't's and map them to relative distances along the curve:
474 | var mapTtoRelativeDistances = function (bez, B_parts) {
475 | var B_t_curr;
476 | var B_t_dist = [0];
477 | var B_t_prev = bez[0];
478 | var sumLen = 0;
479 |
480 | for (var i=1; i<=B_parts; i++) {
481 | B_t_curr = bezier.q(bez, i/B_parts);
482 |
483 | sumLen += maths.vectorLen(maths.subtract(B_t_curr, B_t_prev));
484 |
485 | B_t_dist.push(sumLen);
486 | B_t_prev = B_t_curr;
487 | }
488 |
489 | //Normalize B_length to the same interval as the parameter distances; 0 to 1:
490 | B_t_dist = B_t_dist.map(x => x/sumLen);
491 | return B_t_dist;
492 | };
493 |
494 | function find_t(bez, param, t_distMap, B_parts) {
495 | if(param < 0) { return 0; }
496 | if(param > 1) { return 1; }
497 |
498 | /*
499 | 'param' is a value between 0 and 1 telling us the relative position
500 | of a point on the source polyline (linearly from the start (0) to the end (1)).
501 | To see if a given curve - 'bez' - is a close approximation of the polyline,
502 | we compare such a poly-point to the point on the curve that's the same
503 | relative distance along the curve's length.
504 |
505 | But finding that curve-point takes a little work:
506 | There is a function "B(t)" to find points along a curve from the parametric parameter 't'
507 | (also relative from 0 to 1: http://stackoverflow.com/a/32841764/1869660
508 | http://pomax.github.io/bezierinfo/#explanation),
509 | but 't' isn't linear by length (http://gamedev.stackexchange.com/questions/105230).
510 |
511 | So, we sample some points along the curve using a handful of values for 't'.
512 | Then, we calculate the length between those samples via plain euclidean distance;
513 | B(t) concentrates the points around sharp turns, so this should give us a good-enough outline of the curve.
514 | Thus, for a given relative distance ('param'), we can now find an upper and lower value
515 | for the corresponding 't' by searching through those sampled distances.
516 | Finally, we just use linear interpolation to find a better value for the exact 't'.
517 |
518 | More info:
519 | http://gamedev.stackexchange.com/questions/105230/points-evenly-spaced-along-a-bezier-curve
520 | http://stackoverflow.com/questions/29438398/cheap-way-of-calculating-cubic-bezier-length
521 | http://steve.hollasch.net/cgindex/curves/cbezarclen.html
522 | https://github.com/retuxx/tinyspline
523 | */
524 | var lenMax, lenMin, tMax, tMin, t;
525 |
526 | //Find the two t-s that the current param distance lies between,
527 | //and then interpolate a somewhat accurate value for the exact t:
528 | for(var i = 1; i <= B_parts; i++) {
529 |
530 | if(param <= t_distMap[i]) {
531 | tMin = (i-1) / B_parts;
532 | tMax = i / B_parts;
533 | lenMin = t_distMap[i-1];
534 | lenMax = t_distMap[i];
535 |
536 | t = (param-lenMin)/(lenMax-lenMin) * (tMax-tMin) + tMin;
537 | break;
538 | }
539 | }
540 | return t;
541 | }
542 |
543 | /**
544 | * Creates a vector of length 1 which shows the direction from B to A
545 | */
546 | function createTangent(pointA, pointB) {
547 | return maths.normalize(maths.subtract(pointA, pointB));
548 | }
549 |
550 | /*
551 | Simplified versions of what we need from math.js
552 | Optimized for our input, which is only numbers and 1x2 arrays (i.e. [x, y] coordinates).
553 | */
554 | class maths {
555 | //zeros = logAndRun(math.zeros);
556 | static zeros_Xx2x2(x) {
557 | var zs = [];
558 | while(x--) { zs.push([0,0]); }
559 | return zs
560 | }
561 |
562 | //multiply = logAndRun(math.multiply);
563 | static mulItems(items, multiplier) {
564 | return items.map(x => x*multiplier);
565 | }
566 | static mulMatrix(m1, m2) {
567 | //https://en.wikipedia.org/wiki/Matrix_multiplication#Matrix_product_.28two_matrices.29
568 | //Simplified to only handle 1-dimensional matrices (i.e. arrays) of equal length:
569 | return m1.reduce((sum,x1,i) => sum + (x1*m2[i]), 0);
570 | }
571 |
572 | //Only used to subract to points (or at least arrays):
573 | // subtract = logAndRun(math.subtract);
574 | static subtract(arr1, arr2) {
575 | return arr1.map((x1, i) => x1 - arr2[i]);
576 | }
577 |
578 | //add = logAndRun(math.add);
579 | static addArrays(arr1, arr2) {
580 | return arr1.map((x1, i) => x1 + arr2[i]);
581 | }
582 | static addItems(items, addition) {
583 | return items.map(x => x+addition);
584 | }
585 |
586 | //var sum = logAndRun(math.sum);
587 | static sum(items) {
588 | return items.reduce((sum,x) => sum + x);
589 | }
590 |
591 | //chain = math.chain;
592 |
593 | //Only used on two arrays. The dot product is equal to the matrix product in this case:
594 | // dot = logAndRun(math.dot);
595 | static dot(m1, m2) {
596 | return maths.mulMatrix(m1, m2);
597 | }
598 |
599 | //https://en.wikipedia.org/wiki/Norm_(mathematics)#Euclidean_norm
600 | // var norm = logAndRun(math.norm);
601 | static vectorLen(v) {
602 | return Math.hypot(...v);
603 | }
604 |
605 | //math.divide = logAndRun(math.divide);
606 | static divItems(items, divisor) {
607 | return items.map(x => x/divisor);
608 | }
609 |
610 | //var dotPow = logAndRun(math.dotPow);
611 | static squareItems(items) {
612 | return items.map(x => x*x);
613 | }
614 |
615 | static normalize(v) {
616 | return this.divItems(v, this.vectorLen(v));
617 | }
618 |
619 | //Math.pow = logAndRun(Math.pow);
620 | }
621 |
622 |
623 | class bezier {
624 | //Evaluates cubic bezier at t, return point
625 | static q(ctrlPoly, t) {
626 | var tx = 1.0 - t;
627 | var pA = maths.mulItems( ctrlPoly[0], tx * tx * tx ),
628 | pB = maths.mulItems( ctrlPoly[1], 3 * tx * tx * t ),
629 | pC = maths.mulItems( ctrlPoly[2], 3 * tx * t * t ),
630 | pD = maths.mulItems( ctrlPoly[3], t * t * t );
631 | return maths.addArrays(maths.addArrays(pA, pB), maths.addArrays(pC, pD));
632 | }
633 |
634 | //Evaluates cubic bezier first derivative at t, return point
635 | static qprime(ctrlPoly, t) {
636 | var tx = 1.0 - t;
637 | var pA = maths.mulItems( maths.subtract(ctrlPoly[1], ctrlPoly[0]), 3 * tx * tx ),
638 | pB = maths.mulItems( maths.subtract(ctrlPoly[2], ctrlPoly[1]), 6 * tx * t ),
639 | pC = maths.mulItems( maths.subtract(ctrlPoly[3], ctrlPoly[2]), 3 * t * t );
640 | return maths.addArrays(maths.addArrays(pA, pB), pC);
641 | }
642 |
643 | //Evaluates cubic bezier second derivative at t, return point
644 | static qprimeprime(ctrlPoly, t) {
645 | return maths.addArrays(maths.mulItems( maths.addArrays(maths.subtract(ctrlPoly[2], maths.mulItems(ctrlPoly[1], 2)), ctrlPoly[0]), 6 * (1.0 - t) ),
646 | maths.mulItems( maths.addArrays(maths.subtract(ctrlPoly[3], maths.mulItems(ctrlPoly[2], 2)), ctrlPoly[1]), 6 * t ));
647 | }
648 | }
649 |
650 | //module.exports = fitCurve;
651 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/mincho.js:
--------------------------------------------------------------------------------
1 | import { FontCanvas } from "./fontcanvas.js";
2 | import { norm2, get_dir, get_rad, rad_to_dir, moved_point, get_extended_dest , widfun, widfun_d, widfun_stop, widfun_stop_d, widfun_fat, widfun_fat_d, DIR_POSX, DIR_NEGX, bezier_to_y, bezier_to_line, CURVE_THIN} from "./util.js";
3 | import { STROKETYPE, STARTTYPE, ENDTYPE} from "./stroketype.js";
4 | import { Bezier} from "./bezier.js";
5 | import { Polygon } from "./polygon.js";
6 | import {isCrossBoxWithOthers,isCrossWithOthers} from "./2d.js";
7 | export class Mincho {
8 | constructor(size) {
9 | //if (!size) size=2.3;
10 | this.kRate = 50;
11 | if (size == 1) {
12 | this.kMinWidthY = 1.2;
13 | this.kMinWidthU = 1.2;
14 | this.kMinWidthT = 3.6;
15 | this.kWidth = 3;
16 | this.kKakato = 1.8;
17 | this.kL2RDfatten = 1.1;
18 | this.kMage = 6;
19 |
20 | this.kAdjustKakatoL = ([8, 5, 3, 1]); // for KAKATO adjustment 000,100,200,300,400
21 | this.kAdjustKakatoR = ([4, 3, 2, 1]); // for KAKATO adjustment 000,100,200,300
22 | this.kAdjustKakatoRangeX = 12; // check area width
23 | this.kAdjustKakatoRangeY = ([1, 11, 14, 18]); // 3 steps of checking
24 | this.kAdjustKakatoStep = 3; // number of steps
25 |
26 | this.kAdjustUrokoX = ([14, 12, 9, 7]); // for UROKO adjustment 000,100,200,300
27 | this.kAdjustUrokoY = ([7, 6, 5, 4]); // for UROKO adjustment 000,100,200,300
28 | this.kAdjustUrokoLength = ([13, 21, 30]); // length for checking
29 | this.kAdjustUrokoLengthStep = 3; // number of steps
30 | this.kAdjustUrokoLine = ([13, 15, 18]); // check for crossing. corresponds to length
31 |
32 | this.kAdjustUroko2Step = 3;
33 | this.kAdjustUroko2Length = 25;
34 |
35 | this.kAdjustTateStep = 4;
36 | this.kAdjustMageStep = 5;
37 | } else if (size == 3) {
38 | this.kMinWidthY = 3;
39 | this.kMinWidthU = 3;
40 | this.kMinWidthT = 8;
41 | this.kWidth = 6;
42 | this.kKakato = 4;
43 | this.kL2RDfatten = 1.1;
44 | this.kMage = 14;
45 |
46 | this.kAdjustKakatoL = ([20, 13, 7, 3]); // for KAKATO adjustment 000,100,200,300,400
47 | this.kAdjustKakatoR = ([12, 9, 6, 3]); // for KAKATO adjustment 000,100,200,300
48 | this.kAdjustKakatoRangeX = 26; // check area width
49 | this.kAdjustKakatoRangeY = ([2, 26, 30, 40]); // 3 steps of checking
50 | this.kAdjustKakatoStep = 3; // number of steps
51 |
52 | this.kAdjustUrokoX = ([30, 25, 20, 15]); // for UROKO adjustment 000,100,200,300
53 | this.kAdjustUrokoY = ([15, 14, 13, 12]); // for UROKO adjustment 000,100,200,300
54 | this.kAdjustUrokoLength = ([29, 40, 62]); // length for checking
55 | this.kAdjustUrokoLengthStep = 3; // number of steps
56 | this.kAdjustUrokoLine = ([29, 34, 39]); // check for crossing. corresponds to length
57 |
58 | this.kAdjustUroko2Step = 3;
59 | this.kAdjustUroko2Length = 50;
60 |
61 | this.kAdjustTateStep = 4;
62 | this.kAdjustMageStep = 5;
63 | } else if (size > 1) {
64 | this.kMinWidthY = size;
65 | this.kMinWidthU = size;
66 | this.kMinWidthT = size * 2.6;
67 | this.kWidth = size * 2.2;
68 | this.kKakato = size * 1.2 + 0.6;
69 | this.kL2RDfatten = 1.2;
70 | this.kMage = size * 4 + 2;
71 |
72 | this.kAdjustKakatoL = ([14, 11, 8, 6, 4, 2]); // for KAKATO adjustment 000,100,200,300,400
73 | this.kAdjustKakatoR = ([8, 6.5, 5, 4.5, 3, 2]); // for KAKATO adjustment 000,100,200,300
74 | this.kAdjustKakatoRangeX = 20; // check area width
75 | this.kAdjustKakatoRangeY = ([3, 9, 15, 21, 27, 32]); // 5 steps of checking
76 | this.kAdjustKakatoStep = 5; // number of steps
77 |
78 | this.kAdjustUrokoX = ([size * 9.5 + 4, size * 8 + 3.5, size * 6.5 + 3, size * 5 + 2.5]); // for UROKO adjustment 000,100,200,300
79 | this.kAdjustUrokoY = ([size * 4.6 + 2, size * 4.4 + 1.5, size * 4.2 + 1, size * 4.0 + 0.5]); // for UROKO adjustment 000,100,200,300
80 | this.kAdjustUrokoLength = ([size * 7 + 7, size * 11 + 11, size * 15 + 15]); // length for checking
81 | this.kAdjustUrokoLengthStep = 3; // number of steps
82 | this.kAdjustUrokoLine = ([size * 7 + 7, size * 9 + 8, size * 11 + 9]); // check for crossing. corresponds to length
83 |
84 | this.kAdjustUroko2Step = 3;
85 | this.kAdjustUroko2Length = size * 20;
86 |
87 | this.kAdjustTateStep = 4;
88 | this.kAdjustMageStep = 5;
89 | } else {
90 | this.kMinWidthY = 2;
91 | this.kMinWidthYY = 2;//横線の太さのみを決める。kMinWidthY以上の値が望ましい
92 | this.kMinWidthYY *= 1.2;
93 | this.kMinWidthU = 2;
94 | this.kMinWidthT = 6;
95 | this.kMinWidthT_adjust = 6;//use consistent parameter for adjustment
96 | //this.kMinWidthT = 5.7;
97 | this.kMinWidthC = 1;//開放・左下右下カドの傾きを決める。1なら元と同じ
98 | this.kMinWidthC *= 0.8;
99 | this.kWidth = 5;
100 | this.kKakato = 3;
101 | this.kL2RDfatten = 1.1;
102 | this.kMage = 10;
103 | //size = "HAIR_LINE"
104 | if (size == "HAIR_LINE"){//デバッグ用
105 | this.kMinWidthY *= 0.3;
106 | this.kMinWidthYY *= 0.3;
107 | this.kMinWidthT *= 0.2;
108 | } else if (size == "EXTRA_LIGHT"){
109 | this.kMinWidthU *= 0.71;
110 | this.kMinWidthY *= 0.89;
111 | this.kMinWidthYY *= 0.85;
112 | this.kMinWidthT *= 0.76;
113 | } else if (size == "LIGHT"){
114 | this.kMinWidthU *= 0.85;
115 | this.kMinWidthY *= 0.94;
116 | this.kMinWidthYY *= 0.92;
117 | this.kMinWidthT *= 0.87;
118 | } else if (size == "MEDIUM"){
119 | this.kMinWidthU *= 1.2;
120 | this.kMinWidthYY *= 1.1;
121 | this.kMinWidthT *= 1.15;
122 | } else if (size == "DEMIBOLD"){//曲線に課題あり
123 | this.kMinWidthU *= 1.44;
124 | this.kMinWidthYY *= 1.21;
125 | this.kMinWidthT *= 1.34;
126 | }
127 |
128 | this.kAdjustKakatoL = ([12, 9.6, 7.3, 5, 3, 2]); // for KAKATO adjustment
129 | this.kAdjustKakatoR = ([10, 8, 6, 4, 2, 1]); // for KAKATO adjustment
130 | this.kAdjustKakatoRangeX = 20; // check area width
131 | this.kAdjustKakatoRangeY = ([1, 14, 19, 24, 29, 35]); //[0]は0以上[1]以下であればなんでもよい?
132 | this.kAdjustKakatoStep = 5; // number of steps
133 |
134 | this.kAdjustUrokoX = ([24, 21, 18, 16, 14, 12]); // for UROKO adjustment 000,100,200,300
135 | this.kAdjustUrokoY = ([12, 11, 10, 9, 8.5, 8]); // for UROKO adjustment 000,100,200,300
136 | this.kAdjustUrokoLength = ([16, 23, 30, 38, 46, 55]); // length for checking
137 | this.kAdjustUrokoLengthStep = 5; // number of steps
138 | this.kAdjustUrokoLine = ([18, 20, 23, 26, 30, 35]); // check for crossing. corresponds to length
139 |
140 | this.kAdjustUroko2Step = 5;//max value
141 | this.kAdjustUroko2Length = 40;
142 |
143 | this.kAdjustTateStep = 4;
144 | this.kAdjustMageStep = 5;
145 | }
146 | }
147 |
148 | getPolygons(glyphData) {
149 | var cv = new FontCanvas();
150 | for (var i = 0; i < glyphData.length; i++) {
151 | var tempdata = glyphData.slice();
152 | tempdata.splice(i, 1);
153 | this.drawAdjustedStroke(cv, glyphData[i], tempdata);
154 | }
155 | return cv.getPolygons();
156 | }
157 | getPolygonsSeparated(strokesArrays) {
158 | return strokesArrays.map((glyphData, index) => {
159 | const cp = strokesArrays.slice();
160 | cp.splice(index,1)
161 | const other_groups = cp.flat();
162 |
163 | var cv = new FontCanvas();
164 | for (var i = 0; i < glyphData.length; i++) {
165 | var tempdata = glyphData.slice();
166 | tempdata.splice(i, 1);
167 | this.drawAdjustedStroke(cv, glyphData[i], other_groups.concat(tempdata));
168 | }
169 | return cv.getPolygons();
170 | });
171 | }
172 | drawAdjustedStroke(cv, s, others) {//draw stroke on the canvas
173 |
174 | const a1 = s[0] % 100;
175 | const a2 = s[1] % 100;
176 | const a3 = (s[2] == ENDTYPE.LOWER_LEFT_ZH_OLD || s[2] == ENDTYPE.LOWER_LEFT_ZH_NEW) ? s[2] : s[2] % 100;
177 | const x1 = s[3];
178 | const y1 = s[4];
179 | const x2 = s[5];
180 | const y2 = s[6];
181 | const x3 = s[7];
182 | const y3 = s[8];
183 | const x4 = s[9];
184 | const y4 = s[10];
185 | //if(a2>100){
186 | // console.log("error: start type"+a2)
187 | //}
188 | //if(a3>100){
189 | // console.log("error: end type"+a3)
190 | //}
191 | const dir12 = get_dir(x2-x1, y2-y1);
192 | const dir23 = get_dir(x3-x2, y3-y2);
193 | const dir34 = get_dir(x4-x3, y4-y3);
194 | const rad12 = get_rad(x2-x1, y2-y1);
195 | const rad23 = get_rad(x3-x2, y3-y2);
196 |
197 | switch (a1) {
198 | case 0: {//rotate and flip
199 | if (a2 == 98) {
200 | cv.flip_left_right(x1, y1, x2, y2);
201 | } else if (a2 == 97) {
202 | cv.flip_up_down(x1, y1, x2, y2);
203 | } else if (a2 == 99 && a3 == 1) {
204 | cv.rotate90(x1, y1, x2, y2);
205 | } else if (a2 == 99 && a3 == 2) {
206 | cv.rotate180(x1, y1, x2, y2);
207 | } else if (a2 == 99 && a3 == 3) {
208 | cv.rotate270(x1, y1, x2, y2);
209 | }
210 | break;
211 | }
212 | case STROKETYPE.STRAIGHT: {
213 | const dir = get_dir(x2-x1, y2-y1);
214 | if (a3 == ENDTYPE.CONNECTING_H) {//usually horizontal
215 | cv.drawLine(x1, y1, x2, y2, this.kMinWidthYY);
216 | } else if (a3 == ENDTYPE.OPEN && Math.abs(y2 - y1) < x2 - x1) { //horizontal or gentle slope
217 | const param_uroko = this.adjustUrokoParam(s, others);
218 | const param_uroko2 = this.adjustUroko2Param(s, others);
219 | cv.drawLine(x1, y1, x2, y2, this.kMinWidthYY);
220 | const urokoScale = (this.kMinWidthU / this.kMinWidthY - 1.0) / 4.0 + 1.0;
221 | if (y1 == y2) {//horizontal
222 | const uroko_max = Math.floor(norm2(param_uroko, param_uroko2))
223 | //const uroko_max = param_uroko == 0 ? param_uroko2 : param_uroko
224 | //↑元の実装だとadjustUrokoによる調整がかかったものはadjustUroko2を一切通らないのでそれ以上小さくならない。
225 | //Math.max(param_uroko, param_uroko2) などのほうが合理的
226 | cv.drawUroko_h(x2, y2, this.kMinWidthYY, this.kAdjustUrokoX[uroko_max] * urokoScale, this.kAdjustUrokoY[uroko_max] * urokoScale);
227 | } else {
228 | cv.drawUroko(x2, y2, dir, this.kMinWidthYY, this.kAdjustUrokoX[param_uroko] * urokoScale, this.kAdjustUrokoY[param_uroko] * urokoScale);
229 | }
230 | } else {//vertical or steep slope
231 | let poly_end = new Polygon(2);
232 | const param_tate = this.adjustTateParam(s, others);
233 | const kMinWidthT_m = this.kMinWidthT - param_tate / 2;
234 | //head
235 | let poly_start = this.getStartOfVLine(x1, y1, x2, y2, a2, kMinWidthT_m, cv);
236 | if (a2 == STARTTYPE.CONNECTING_MANUAL){
237 | var r = get_rad(x1-x3, y1-y3) - get_rad(x2-x1, y2-y1) - Math.PI/2;
238 | poly_start = this.getStartOfOffsetLine(x1, y1, dir, kMinWidthT_m, kMinWidthT_m * Math.tan(r), kMinWidthT_m * -Math.tan(r));
239 | }
240 | //tail
241 | switch (a3) {
242 | case ENDTYPE.OPEN: {
243 | const right2 = this.kMinWidthC * kMinWidthT_m / 2;
244 | const left2 = this.kMinWidthC * -kMinWidthT_m / 2;
245 | poly_end = this.getEndOfOffsetLine(x1, y1, x2, y2, kMinWidthT_m, right2, left2);
246 | break;
247 | }
248 | case ENDTYPE.TURN_LEFT: {
249 | let [tx1, ty1] = moved_point(x2, y2, dir12, -this.kMage);
250 | const width_func = (t) => { return kMinWidthT_m; }
251 | const new_x2 = x2 - this.kMage * (((this.kAdjustTateStep + 4) - param_tate) / (this.kAdjustTateStep + 4));
252 | cv.drawQBezier(tx1, ty1, x2, y2,
253 | new_x2, y2, width_func, t => 0, undefined, true, false);
254 | const param_hane = this.adjustHaneParam(s, x2, y2, others);
255 | cv.drawTurnLeft(new_x2, y2, kMinWidthT_m, this.kWidth * 4 * Math.min(1 - param_hane / 10, Math.pow(kMinWidthT_m / this.kMinWidthT, 3)));
256 | poly_end = this.getEndOfLine(x1, y1, tx1, ty1, kMinWidthT_m);
257 | break;
258 | }
259 | case ENDTYPE.LOWER_LEFT_CORNER: {
260 | const param_kakato = this.adjustKakatoParam(s, others);
261 | const right2 = this.kAdjustKakatoL[param_kakato] + this.kMinWidthC * kMinWidthT_m;
262 | const left2 = this.kAdjustKakatoL[param_kakato];
263 | poly_end = this.getEndOfOffsetLine(x1, y1, x2, y2, kMinWidthT_m, right2, left2);
264 | break;
265 | }
266 | case ENDTYPE.LOWER_RIGHT_CORNER: {
267 | const param_kakato = this.adjustKakatoParam(s, others);
268 | const right2 = this.kAdjustKakatoR[param_kakato] + this.kMinWidthC * kMinWidthT_m;
269 | const left2 = this.kAdjustKakatoR[param_kakato];
270 | poly_end = this.getEndOfOffsetLine(x1, y1, x2, y2, kMinWidthT_m, right2, left2);
271 | break;
272 | }
273 | case ENDTYPE.LOWER_LEFT_ZH_NEW: {
274 | if (x1 == x2) {//vertical
275 | cv.drawNewGTHbox_v(x2, y2, kMinWidthT_m, this.kMinWidthY);
276 | } else {
277 | var m = 0;
278 | if (x1 > x2 && y1 != y2) {
279 | m = Math.floor((x1 - x2) / (y2 - y1) * 3);
280 | }
281 | cv.drawNewGTHbox(x2 + m, y2, kMinWidthT_m, this.kMinWidthY);
282 | }
283 | //in the original implementation, opt2 is calculated to 413 % 100 = 4, and kAdjustKakatoL[4] is manually set to 0.
284 | //The appearance is typically remedied by the crossing horizontal line.
285 | const right2 = this.kMinWidthC * kMinWidthT_m;
286 | const left2 = 0;
287 | poly_end = this.getEndOfOffsetLine(x1, y1, x2, y2, kMinWidthT_m, right2, left2);
288 | break;
289 | }
290 | case ENDTYPE.LOWER_LEFT_ZH_OLD: {
291 | //in the original implementation, opt2 is calculated to 313 % 100 = 3, corresponding to (original) kAdjustKakatoStep.
292 | const right2 = this.kAdjustKakatoL[this.kAdjustKakatoStep] + this.kMinWidthC * kMinWidthT_m;
293 | const left2 = this.kAdjustKakatoL[this.kAdjustKakatoStep];
294 | poly_end = this.getEndOfOffsetLine(x1, y1, x2, y2, kMinWidthT_m, right2, left2);
295 | break;
296 | }
297 | case ENDTYPE.CONNECTING_V: {
298 | if (y1 == y2) {//horizontal (error)
299 | console.log("error: connecting_v at the end of the horizontal line")
300 | cv.drawLine(x1, y1, x2, y2, kMinWidthT_m);
301 | } else if (x1 == x2) {//vertical
302 | poly_end.set(0, x2 + kMinWidthT_m, y2 + this.kMinWidthY - 0.001);
303 | poly_end.set(1, x2 - kMinWidthT_m, y2 + this.kMinWidthY - 0.001);
304 | } else {
305 | const rad = Math.atan((y2 - y1) / (x2 - x1));
306 | const v = (x1 > x2) ? -1 : 1;
307 | poly_end.set(0, x2 + (kMinWidthT_m * v) / Math.sin(rad), y2);
308 | poly_end.set(1, x2 - (kMinWidthT_m * v) / Math.sin(rad), y2);
309 | }
310 | break;
311 | }
312 | case ENDTYPE.LOWER_RIGHT_HT: {
313 | if (x1 == x2) {//vertical
314 | cv.drawLowerRightHT_v(x2, y2, kMinWidthT_m, this.kMinWidthY);
315 | } else {
316 | cv.drawLowerRightHT(x2, y2, kMinWidthT_m, this.kMinWidthY);
317 | }
318 | if (y1 == y2) {//horizontal (error)
319 | console.log("error: connecting_v at the end of the horizontal line")
320 | cv.drawLine(x1, y1, x2, y2, kMinWidthT_m);
321 | } else if (x1 == x2) {//vertical
322 | poly_end.set(0, x2 + kMinWidthT_m, y2 + this.kMinWidthY);
323 | poly_end.set(1, x2 - kMinWidthT_m, y2 + this.kMinWidthY);
324 | } else {
325 | const rad = Math.atan((y2 - y1) / (x2 - x1));
326 | const v = (x1 > x2) ? -1 : 1;
327 | poly_end.set(0, x2 + (kMinWidthT_m * v) / Math.sin(rad), y2);
328 | poly_end.set(1, x2 - (kMinWidthT_m * v) / Math.sin(rad), y2);
329 | }
330 | break;
331 | }
332 | default:
333 | throw ("error: unknown end type at the straight line: "+a3);
334 | break;
335 | }
336 | //body
337 | poly_start.concat(poly_end);
338 | cv.addPolygon(poly_start);
339 | }
340 | break;
341 | }
342 |
343 |
344 |
345 | case STROKETYPE.CURVE: {
346 |
347 | //for CONNECTING_MANUAL stroke (very very tricky implementation)
348 | if (a2 == STARTTYPE.CONNECTING_MANUAL){
349 | s[0] = s[0]-1//CURVE -> STRAIGHT
350 | this.drawAdjustedStroke(cv, s, others)//treat as STRAIGHT line data
351 | return
352 | }
353 |
354 | const kMinWidthT_mod = this.kMinWidthT - ~~((s[1] % 10000) / 1000) / 2
355 | const end_width_factor = (~~(s[2] / 1000) / 2) / this.kMinWidthT
356 | //head
357 | if (a2 == STARTTYPE.OPEN) {
358 | let [x1ext, y1ext] = moved_point(x1, y1, dir12, 1 * this.kMinWidthY * 0.5);
359 | if (y1ext <= y3) { //from up to bottom
360 | cv.drawOpenBegin_curve_down(x1ext, y1ext, rad12, kMinWidthT_mod, this.kMinWidthY);
361 | }
362 | else { //from bottom to up
363 | cv.drawOpenBegin_curve_up(x1ext, y1ext, dir12, kMinWidthT_mod, this.kMinWidthY);
364 | }
365 | } else if (a2 == STARTTYPE.UPPER_RIGHT_CORNER || a2 == STARTTYPE.ROOFED_THIN) {
366 | cv.drawUpperRightCorner2(x1, y1, kMinWidthT_mod, this.kMinWidthY, this.kWidth, a2 == STARTTYPE.ROOFED_THIN);
367 | } else if (a2 == STARTTYPE.UPPER_LEFT_CORNER) {
368 | let [x1ext, y1ext] = moved_point(x1, y1, dir12, -this.kMinWidthY);
369 | cv.drawUpperLeftCorner(x1ext, y1ext, dir12, kMinWidthT_mod);//this.kMinWidthC * ?
370 | }
371 | //body
372 | const a2temp = (a2 == STARTTYPE.CONNECTING_V && this.adjustKirikuchiParam(s, others)) ? 100 + a2 : a2;
373 | this.minchoDrawCurve(x1, y1, x2, y2, x3, y3, a2temp, a3, cv, kMinWidthT_mod, end_width_factor);
374 | //tail
375 | switch (a3) {
376 | case ENDTYPE.TURN_LEFT: {
377 | let [tx1, ty1] = moved_point(x3, y3, dir23, -this.kMage*0.439);
378 | const param_hane = this.adjustHaneParam(s, x3, y3, others);
379 | const width_func = (t) => { return kMinWidthT_mod; }
380 | cv.drawQBezier(tx1, ty1, x3, y3, x3 - this.kMage, y3, width_func, t => 0, undefined, true, false);
381 | cv.drawTurnLeft(x3 - this.kMage, y3, kMinWidthT_mod, this.kWidth * 4 * Math.min(1 - param_hane / 10, Math.pow(kMinWidthT_mod / this.kMinWidthT, 3)));
382 | break;
383 | }
384 | case ENDTYPE.TURN_UPWARDS: {
385 | cv.drawTailCircle(x3, y3, dir23, kMinWidthT_mod);
386 | cv.drawTurnUpwards_pos(x3, y3, kMinWidthT_mod, this.kWidth*5, (y1 0, undefined);
436 | poly_start.concat(Bezier.bez_to_poly(bez1));
437 | let edd = this.getEndOfLine(tx2, ty2, x3, y3, kMinWidthT_mage);
438 | poly_start.concat(edd);
439 | poly_start.concat(Bezier.bez_to_poly(bez2));
440 | cv.addPolygon(poly_start);
441 |
442 | if (y2 == y3) {
443 | if (tx2 < x3) {
444 | cv.drawCircle_bend_pos(x3, y3, DIR_POSX, kMinWidthT_mage);
445 | } else {
446 | cv.drawCircle_bend_neg(x3, y3, DIR_NEGX, kMinWidthT_mage);
447 | }
448 | if (a3 == ENDTYPE.TURN_UPWARDS) {
449 | if (tx2 < x3) {
450 | cv.drawTurnUpwards_pos(x3, y3, kMinWidthT_mage, this.kWidth * (4 * (1 - param_mage / this.kAdjustMageStep) + 1), DIR_POSX);
451 | } else {
452 | cv.drawTurnUpwards_neg(x3, y3, kMinWidthT_mage, this.kWidth * (4 * (1 - param_mage / this.kAdjustMageStep) + 1), DIR_NEGX);
453 | }
454 | }
455 | } else {
456 | const dir = get_dir(x3-x2, y3-y2);
457 | if (tx2 < x3) {
458 | cv.drawCircle_bend_pos(x3, y3, dir, kMinWidthT_mage);
459 | } else {
460 | cv.drawCircle_bend_neg(x3, y3, dir, kMinWidthT_mage);
461 | }
462 | if (a3 == ENDTYPE.TURN_UPWARDS) {
463 | if (tx2 < x3) {
464 | cv.drawTurnUpwards_pos(x3, y3, kMinWidthT_mage, this.kWidth*5, dir);
465 | } else {
466 | cv.drawTurnUpwards_neg(x3, y3, kMinWidthT_mage, this.kWidth*5, dir);
467 | }
468 | }
469 | }
470 | break;
471 | }
472 |
473 |
474 | case 12: {
475 | throw "error: unknown stroketype 12";
476 | break;
477 | }
478 |
479 |
480 |
481 |
482 | case STROKETYPE.BEZIER: {
483 | const kMinWidthT_mod = this.kMinWidthT - ~~((s[1] % 10000) / 1000) / 2
484 | //head
485 | if (a2 == STARTTYPE.OPEN) {
486 | let [x1ext, y1ext] = moved_point(x1, y1, dir12, 1 * this.kMinWidthY * 0.5);
487 |
488 | if (y1ext <= y4) { //from up to bottom
489 | cv.drawOpenBegin_curve_down(x1ext, y1ext, rad12, kMinWidthT_mod, this.kMinWidthY);
490 | }
491 | else { //from bottom to up
492 | cv.drawOpenBegin_curve_up(x1ext, y1ext, dir12, kMinWidthT_mod, this.kMinWidthY);
493 | }
494 | } else if (a2 == STARTTYPE.UPPER_RIGHT_CORNER || a2 == STARTTYPE.ROOFED_THIN) {
495 | cv.drawUpperRightCorner2(x1, y1, kMinWidthT_mod, this.kMinWidthY, this.kWidth, a2 == STARTTYPE.ROOFED_THIN);
496 | } else if (a2 == STARTTYPE.UPPER_LEFT_CORNER) {
497 | let [x1ext, y1ext] = moved_point(x1, y1, dir12, -this.kMinWidthY);
498 | cv.drawUpperLeftCorner(x1ext, y1ext, dir12, kMinWidthT_mod);
499 | }
500 | //body
501 | let [tan1, tan2] = this.minchoDrawBezier(x1, y1, x2, y2, x3, y3, x4, y4, a2, a3, cv, kMinWidthT_mod);
502 | //tail
503 | switch (a3) {
504 | case ENDTYPE.TURN_LEFT:
505 | let [tx1, ty1] = moved_point(x4, y4, dir34, -this.kMage*0.439);
506 | const width_func = (t) => { return kMinWidthT_mod; }
507 | cv.drawQBezier(tx1, ty1, x4, y4, x4 - this.kMage, y4, width_func, t => 0, undefined, true, false);
508 | const param_hane = this.adjustHaneParam(s, x4, y4, others);
509 | cv.drawTurnLeft(x4 - this.kMage, y4, kMinWidthT_mod, this.kWidth * 4 * Math.min(1 - param_hane / 10, Math.pow(kMinWidthT_mod / this.kMinWidthT, 3)));
510 | break;
511 | case ENDTYPE.TURN_UPWARDS:
512 | cv.drawTailCircle(x4, y4, dir34, kMinWidthT_mod);
513 | cv.drawTurnUpwards_pos(x4, y4, kMinWidthT_mod, this.kWidth*5, (y1 -1.8 * Math.pow(t, 0.8) * 0.85 * kMinWidthT_m);
551 | break;
552 | }
553 | case 9: // may not be exist ... no need
554 | //kageCanvas[y1][x1] = 0;
555 | //kageCanvas[y2][x2] = 0;
556 | break;
557 | default:
558 | throw "error: unknown stroke: "+s;
559 | break;
560 | }
561 | }
562 |
563 | minchoDrawCurve(x1pre, y1pre, sx, sy, x2pre, y2pre, a1, a2, cv, kMinWidthT_mod, end_width_factor) {
564 | var delta;
565 | switch (a1) {
566 | case STARTTYPE.OPEN:
567 | case STARTTYPE.THIN:
568 | case STARTTYPE.ROOFED_THIN:
569 | delta = -1 * this.kMinWidthY * 0.5;
570 | break;
571 | case STARTTYPE.UPPER_LEFT_CORNER:
572 | //case 32:
573 | delta = this.kMinWidthY;
574 | break;
575 | default:
576 | delta = 0;
577 | break;
578 | }
579 | let [x1, y1] = get_extended_dest(x1pre, y1pre, sx, sy, delta);
580 |
581 | switch (a2) {
582 | case ENDTYPE.STOP: // get shorten for tail's circle
583 | delta = -1 * kMinWidthT_mod * 0.52;
584 | break;
585 | case ENDTYPE.TURN_LEFT:
586 | delta = -this.kMage * 0.439;
587 | break;
588 | default:
589 | delta = 0;
590 | break;
591 | }
592 | let [x2, y2] = get_extended_dest(x2pre, y2pre, sx, sy, delta);
593 |
594 | var cornerOffset = 0;
595 | if((a1 == STARTTYPE.UPPER_RIGHT_CORNER || a1 == STARTTYPE.ROOFED_THIN) && a2 == ENDTYPE.LEFT_SWEEP){
596 | var sx1 = sx; var sx2 = sx; var sy1 = sy; var sy2 = sy;//元の実装と名前を揃える
597 | function hypot() {
598 | return Math.sqrt(arguments[0] * arguments[0] + arguments[1] * arguments[1]);
599 | }
600 | var contourLength = hypot(sx1-x1, sy1-y1) + hypot(sx2-sx1, sy2-sy1) + hypot(x2-sx2, y2-sy2);
601 | if (contourLength < 100){
602 | cornerOffset = (kMinWidthT_mod > 6) ? (kMinWidthT_mod - 6) * ((100 - contourLength) / 100) : 0;
603 | x1 += cornerOffset;
604 | }
605 | }
606 |
607 | var width_func;
608 | var width_func_d;
609 | let bez1, bez2;
610 | let thin_stop_param;
611 | if ((a1 == STARTTYPE.THIN || a1 == STARTTYPE.ROOFED_THIN) && a2 == ENDTYPE.STOP) { //stop
612 | //const slant_cos =
613 | const len=Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
614 | thin_stop_param = (1 + (len-100)*0.0007);
615 |
616 | width_func = t => widfun_stop(t, x1, y1, x2, y2, kMinWidthT_mod)*thin_stop_param;
617 | width_func_d = t => widfun_stop_d(t, x1, y1, x2, y2, kMinWidthT_mod)*thin_stop_param;
618 | [bez1, bez2] = Bezier.qBezier2(x1, y1, sx, sy, x2, y2, width_func, width_func_d);
619 | }
620 | else {
621 | if ((a1 == STARTTYPE.THIN || a1 == STARTTYPE.ROOFED_THIN) && a2 == ENDTYPE.OPEN) { // L2RD: fatten
622 | width_func = t => widfun(t, x1, y1, x2, y2, kMinWidthT_mod) * this.kL2RDfatten;
623 | width_func_d = t => widfun_d(t, x1, y1, x2, y2, kMinWidthT_mod) * this.kL2RDfatten;
624 | }
625 | else if (a1 == STARTTYPE.CONNECTING_V && a2 == ENDTYPE.LOWER_LEFT_CORNER) { //未使用。さんずい用 (実験)
626 | width_func = t => {return ((1-t)*0.628+Math.pow((1-t),30)*0.600+0.222)*kMinWidthT_mod};
627 | //don't feel like 'export'ing CURVE_THIN for this experimental change...
628 | width_func_d = t => {return (-0.628-30*Math.pow((1-t),29)*0.600)*kMinWidthT_mod};
629 | }
630 | else if (a1 == STARTTYPE.THIN || a1 == STARTTYPE.ROOFED_THIN || a1 == STARTTYPE.CONNECT_THIN) {
631 | width_func = t => widfun(t, x1, y1, x2, y2, kMinWidthT_mod);
632 | width_func_d = t => widfun_d(t, x1, y1, x2, y2, kMinWidthT_mod);
633 | }
634 | else if (a2 == ENDTYPE.LEFT_SWEEP) {
635 | width_func = t => widfun(1 - t, x1, y1, x2, y2, kMinWidthT_mod);
636 | width_func_d = t => -widfun_d(1 - t, x1, y1, x2, y2, kMinWidthT_mod);
637 | }
638 | else {
639 | if (a2 == ENDTYPE.TURN_LEFT) end_width_factor = 0;
640 | width_func = t => kMinWidthT_mod * (1 - t*end_width_factor);
641 | width_func_d = t => -kMinWidthT_mod*end_width_factor;
642 | }
643 | [bez1, bez2] = Bezier.qBezier(x1, y1, sx, sy, x2, y2, width_func, width_func_d);
644 | }
645 | if (a1 == 132 && x1 != sx) {
646 | let b1 = bezier_to_y(bez2[bez2.length - 1], y1);
647 | if (b1) { bez2[bez2.length - 1] = b1; }
648 | var temp = bez1[0].concat();//deep copy
649 | let b2 = bezier_to_y(temp.reverse(), y1);
650 | if (b2) { bez1[0] = b2.reverse(); }
651 | } else if (40 <=a1 && a1 <= 80) {
652 | var r = get_rad((x2pre - x1pre) * Math.pow(1.4, (a1%10) - 4.5), y2pre - y1pre)
653 | if(a1 >= 50){
654 | r = -r
655 | }
656 | if(a1 == 60){
657 | r = Math.PI* 0.5 // vertical edge
658 | }
659 | let b1 = bezier_to_line(bez2[bez2.length - 1], x1, y1, r);
660 | if (b1) { bez2[bez2.length - 1] = b1; }
661 | var temp = bez1[0].concat();//deep copy
662 | let b2 = bezier_to_line(temp.reverse(), x1, y1, r);
663 | if (b2) { bez1[0] = b2.reverse(); }
664 | } else if (a1 == 22 && x1 != sx) {
665 | let b1 = bezier_to_y(bez2[bez2.length - 1], y1);
666 | if (b1) { bez2[bez2.length - 1] = b1; }
667 |
668 | /*} else if (a1 == 22 && x1 != sx && y1 > y2) {
669 | let b1 = bezier_to_y(bez2[bez2.length - 1], y1);
670 | if (b1) { bez2[bez2.length - 1] = b1; }
671 | var temp = bez1[0].concat();//deep copy
672 | let b2 = bezier_to_y(temp.reverse(), y1 + 1);//??
673 | if (b2) { bez1[0] = b2.reverse(); }
674 | */
675 | }
676 | var poly = Bezier.bez_to_poly(bez1);
677 | poly.concat(Bezier.bez_to_poly(bez2));
678 | if(a1==22){
679 | poly.push(x1, y1);
680 | }
681 | cv.addPolygon(poly);
682 | if(a2 == ENDTYPE.STOP){
683 | if(a1 == STARTTYPE.THIN || a1 == STARTTYPE.ROOFED_THIN){
684 | const bez1e = bez1[bez1.length - 1][3];
685 | const bez1c2 = bez1[bez1.length - 1][2];
686 | const bez2s = bez2[0][0];
687 | const bez2c1 = bez2[0][1];
688 | const tan1 = [bez1e[0] - bez1c2[0], bez1e[1] - bez1c2[1]];
689 | const tan2 = [bez2s[0] - bez2c1[0], bez2s[1] - bez2c1[1]];
690 | const cent_x = (x1 + 4*sx + x2) / 6;
691 | const cent_y = (y1 + 4*sy + y2) / 6;
692 | var rad_end = get_dir(x2-cent_x, y2-cent_y);
693 | cv.drawTailCircle_tan(x2, y2, rad_end, kMinWidthT_mod*1.1*thin_stop_param, tan1, tan2);
694 | }else{//CONNECT_THIN or others
695 | const enddir = get_dir(x2-sx, y2-sy);
696 | cv.drawTailCircle(x2, y2, enddir, kMinWidthT_mod* (1-end_width_factor));
697 | }
698 | }
699 | if (a1 == STARTTYPE.CONNECT_THIN){
700 | var dir_to_start = get_dir(x1-sx, y1-sy);
701 | cv.drawTailCircle(x1, y1, dir_to_start, kMinWidthT_mod*CURVE_THIN);
702 | }
703 | }
704 |
705 | minchoDrawBezier(x1pre, y1pre, sx1, sy1, sx2, sy2, x2pre, y2pre, a1, a2, cv, kMinWidthT_mod) {
706 | var delta;
707 | switch (a1) {
708 | case STARTTYPE.OPEN:
709 | case STARTTYPE.THIN:
710 | delta = -1 * this.kMinWidthY * 0.5;
711 | break;
712 | case STARTTYPE.UPPER_LEFT_CORNER:
713 | //case 32:
714 | delta = this.kMinWidthY;
715 | break;
716 | default:
717 | delta = 0;
718 | break;
719 | }
720 | let [x1, y1] = get_extended_dest(x1pre, y1pre, sx1, sy1, delta);
721 |
722 | switch (a2) {
723 | case ENDTYPE.STOP: // get shorten for tail's circle
724 | delta = -1 * kMinWidthT_mod * 0.52;
725 | break;
726 | case ENDTYPE.TURN_LEFT:
727 | delta = -this.kMage*0.439;
728 | break;
729 | default:
730 | delta = 0;
731 | break;
732 | }
733 | let [x2, y2] = get_extended_dest(x2pre, y2pre, sx2, sy2, delta);
734 |
735 | var cornerOffset = 0;
736 | if((a1 == STARTTYPE.UPPER_RIGHT_CORNER || a1 == STARTTYPE.ROOFED_THIN) && a2 == ENDTYPE.LEFT_SWEEP){
737 | function hypot() {
738 | return Math.sqrt(arguments[0] * arguments[0] + arguments[1] * arguments[1]);
739 | }
740 | var contourLength = hypot(sx1-x1, sy1-y1) + hypot(sx2-sx1, sy2-sy1) + hypot(x2-sx2, y2-sy2);
741 | if (contourLength < 100){
742 | cornerOffset = (kMinWidthT_mod > 6) ? (kMinWidthT_mod - 6) * ((100 - contourLength) / 100) : 0;
743 | x1 += cornerOffset;
744 | }
745 | }
746 |
747 | var width_func;
748 | var width_func_d;
749 | let bez1, bez2;
750 |
751 | if ((a1 == STARTTYPE.THIN || a1 == STARTTYPE.ROOFED_THIN) && a2 == ENDTYPE.STOP) { //stop
752 | width_func = t => widfun_stop(t, x1, y1, x2, y2, kMinWidthT_mod);
753 | width_func_d = t => widfun_stop_d(t, x1, y1, x2, y2, kMinWidthT_mod);
754 |
755 | [bez1, bez2] = Bezier.cBezier(x1, y1, sx1, sy1, sx2, sy2, x2, y2, width_func, width_func_d);
756 |
757 | //width_func = t => widfun_fat(t, x1, y1, x2, y2, kMinWidthT_mod);
758 | //width_func_d = t => widfun_fat_d(t, x1, y1, x2, y2, kMinWidthT_mod);
759 | //[bez1, bez2] = Bezier.cBezier_slant(x1, y1, sx1, sy1, sx2, sy2, x2, y2, width_func, width_func_d);
760 | }
761 | else {
762 | if ((a1 == STARTTYPE.THIN || a1 == STARTTYPE.ROOFED_THIN) && a2 == ENDTYPE.OPEN) { // L2RD: fatten
763 | width_func = t => widfun(t, x1, y1, x2, y2, kMinWidthT_mod) * this.kL2RDfatten;
764 | width_func_d = t => widfun_d(t, x1, y1, x2, y2, kMinWidthT_mod) * this.kL2RDfatten;
765 | }
766 | else if (a1 == STARTTYPE.THIN || a1 == STARTTYPE.ROOFED_THIN || a1 == STARTTYPE.CONNECT_THIN) {
767 | width_func = t => widfun_fat(t, x1, y1, x2, y2, kMinWidthT_mod);
768 | width_func_d = t => widfun_fat_d(t, x1, y1, x2, y2, kMinWidthT_mod);
769 | }
770 | else if (a2 == ENDTYPE.LEFT_SWEEP) {
771 | width_func = t => widfun(1 - t, x1, y1, x2, y2, kMinWidthT_mod);
772 | width_func_d = t => -widfun_d(1 - t, x1, y1, x2, y2, kMinWidthT_mod);
773 | }
774 | else {
775 | width_func = t => kMinWidthT_mod;
776 | width_func_d = t => 0;
777 | }
778 | [bez1, bez2] = Bezier.cBezier(x1, y1, sx1, sy1, sx2, sy2, x2, y2, width_func, width_func_d);
779 | }
780 | //以下は今は実行されないコードだが実行時には2次ベジエのときと同様にdeep copyが必要か?
781 | if (a1 == 132 && x1 != sx1) {
782 | let b1 = bezier_to_y(bez2[bez2.length - 1], y1);
783 | if (b1) { bez2[bez2.length - 1] = b1; }
784 | var temp = bez1[0];
785 | let b2 = bezier_to_y(temp.reverse(), y1);
786 | if (b2) { bez1[0] = b2.reverse(); }
787 | } else if (a1 == 22 && x1 > sx1) {
788 | let b1 = bezier_to_y(bez2[bez2.length - 1], y1);
789 | if (b1) { bez2[bez2.length - 1] = b1; }
790 | var temp = bez1[0];
791 | let b2 = bezier_to_y(temp.reverse(), y1 + 1);//" + 1" ??
792 | if (b2) { bez1[0] = b2.reverse(); }
793 | }
794 | var poly = Bezier.bez_to_poly(bez1);
795 | poly.concat(Bezier.bez_to_poly(bez2));
796 | cv.addPolygon(poly);
797 |
798 | if (a1 == STARTTYPE.CONNECT_THIN){
799 | var dir_to_start = get_dir(x1-sx1, y1-sy1);
800 | cv.drawTailCircle(x1, y1, dir_to_start, kMinWidthT_mod*CURVE_THIN);
801 | }
802 |
803 | const bez1e = bez1[bez1.length - 1][3];
804 | const bez1c2 = bez1[bez1.length - 1][2];
805 |
806 | const bez2s = bez2[0][0];
807 | const bez2c1 = bez2[0][1];
808 | const tan1 = [bez1e[0] - bez1c2[0], bez1e[1] - bez1c2[1]];
809 | const tan2 = [bez2s[0] - bez2c1[0], bez2s[1] - bez2c1[1]];
810 |
811 | return [tan1, tan2];
812 | }
813 |
814 | getStartOfVLine(x1, y1, x2, y2, a1, kMinWidthT, cv) {
815 | const rad = get_rad(x2 - x1, y2 - y1);
816 | const dir = get_dir(x2 - x1, y2 - y1);
817 | var poly_start = new Polygon(2);
818 | if (dir.cos==0) {//vertical
819 | var left1, right1;
820 | switch (a1) {
821 | case 0:
822 | right1 = -this.kMinWidthT * 0.5;
823 | left1 = -this.kMinWidthT * 0.7;
824 | break;
825 | case 12:
826 | right1 = this.kMinWidthY + kMinWidthT;
827 | left1 = this.kMinWidthY;
828 | break;
829 | case 32:
830 | right1 = this.kMinWidthY - 0.001;
831 | left1 = this.kMinWidthY - 0.001;
832 | break;
833 | case 1:
834 | case 6: //... no need
835 | case 22:
836 | default:
837 | right1 = 0;
838 | left1 = 0;
839 | break;
840 | }
841 | poly_start = this.getStartOfOffsetLine(x1, y1, dir, kMinWidthT, right1, left1);
842 | if (a1 == 22) { //box's right top corner
843 | cv.drawUpperRightCorner_straight_v(x1, y1, kMinWidthT, this.kMinWidthYY, this.kWidth);
844 | }
845 | if (a1 == 0) { //beginning of the stroke
846 | cv.drawOpenBegin_straight(x1, y1, kMinWidthT, this.kMinWidthYY, rad);
847 | }
848 | } else {
849 | const v = 1 //previously (x1 > x2) ? -1 : 1;
850 | if (a1 == 22) {
851 | if (dir.sin==0) {//error
852 | console.log("error: connecting_v at the end of the horizontal line")
853 | poly_start = this.getStartOfLine(x1, y1, dir, kMinWidthT);
854 | } else {
855 | //poly_start.set(1, x1 + (kMinWidthT * v + 1) / Math.sin(rad), y1 + 1);//" + 1" ??
856 | poly_start.set(1, x1 + (kMinWidthT * v) / Math.sin(rad), y1);
857 | poly_start.set(0, x1 - (kMinWidthT * v) / Math.sin(rad), y1);
858 | }
859 | } else if (a1 == 32) {
860 | if (dir.sin==0) {//error
861 | console.log("error: connecting_v at the end of the horizontal line")
862 | poly_start = this.getStartOfLine(x1, y1, dir, kMinWidthT);
863 | } else {
864 | poly_start.set(1, x1 + (kMinWidthT * v) / Math.sin(rad), y1);
865 | poly_start.set(0, x1 - (kMinWidthT * v) / Math.sin(rad), y1);
866 | }
867 | } else {
868 | var left1, right1;
869 | switch (a1) {
870 | case 0:
871 | right1 = -this.kMinWidthT * 0.5;
872 | left1 = -this.kMinWidthT * 0.7;
873 | break;
874 | case 12:
875 | right1 = this.kMinWidthY + kMinWidthT;
876 | left1 = this.kMinWidthY;
877 | break;
878 | case 1:
879 | case 6:
880 | default:
881 | right1 = 0;
882 | left1 = 0;
883 | break;
884 | }
885 | poly_start = this.getStartOfOffsetLine(x1, y1, dir, kMinWidthT, right1, left1);
886 | }
887 | if (a1 == 22) { //SHIKAKU MIGIUE UROKO NANAME DEMO MASSUGU MUKI
888 | cv.drawUpperRightCorner(x1, y1, kMinWidthT, this.kMinWidthYY, this.kWidth);
889 | }
890 | if (a1 == 0) { //beginning of the stroke
891 | cv.drawOpenBegin_straight(x1, y1, kMinWidthT, this.kMinWidthYY, rad);
892 | }
893 | }
894 | return poly_start;
895 | }
896 |
897 | getStartOfLine(x1, y1, dir, halfWidth) {
898 | //get polygon data for the start of line
899 | var poly = new Polygon(2);
900 | poly.set(1, x1 + dir.sin * halfWidth,
901 | y1 - dir.cos * halfWidth);
902 | poly.set(0, x1 - dir.sin * halfWidth,
903 | y1 + dir.cos * halfWidth);
904 | return poly;
905 | }
906 |
907 | getStartOfOffsetLine(x1, y1, dir, halfWidth, off_right1, off_left1) {
908 | //get polygon data for the start of line (with offset)
909 | var poly = new Polygon(2);
910 | poly.set(1, x1 + dir.sin * halfWidth - dir.cos * off_left1,
911 | y1 - dir.cos * halfWidth - dir.sin * off_left1);
912 | poly.set(0, x1 - dir.sin * halfWidth - dir.cos * off_right1,
913 | y1 + dir.cos * halfWidth - dir.sin * off_right1);
914 | return poly;
915 | }
916 |
917 | getEndOfLine(x1, y1, x2, y2, halfWidth) {
918 | //get polygon data for the end of line
919 | const dir = get_dir(x2 - x1, y2 - y1);
920 | var poly = new Polygon(2);
921 | poly.set(0, x2 + dir.sin * halfWidth,
922 | y2 - dir.cos * halfWidth);
923 | poly.set(1, x2 - dir.sin * halfWidth,
924 | y2 + dir.cos * halfWidth);
925 | return poly;
926 | }
927 |
928 | getEndOfOffsetLine(x1, y1, x2, y2, halfWidth, off_right2, off_left2) {
929 | //get polygon data for the end of line (with offset)
930 | const dir = get_dir(x2 - x1, y2 - y1);
931 | var poly = new Polygon(2);
932 | poly.set(0, x2 + dir.sin * halfWidth + off_left2 * dir.cos,
933 | y2 - dir.cos * halfWidth + off_left2 * dir.sin);
934 | poly.set(1, x2 - dir.sin * halfWidth + off_right2 * dir.cos,
935 | y2 + dir.cos * halfWidth + off_right2 * dir.sin);
936 | return poly;
937 | }
938 |
939 | //functions for adjustment
940 |
941 | adjustTateParam(stroke, others) { // strokes
942 | //for illegal strokes
943 | if (stroke[1] >= 1000) return ~~((stroke[1] % 10000) / 1000);
944 | if (stroke[0] >= 100) return 0;
945 |
946 | //(STROKETYPE.STRAIGHT || STROKETYPE.BENDING || STROKETYPE.VCURVE)
947 | if (stroke[3] != stroke[5]) return 0;
948 | var res_arr = [];
949 | for (let other of others) {
950 | if ((other[0] == 1 || other[0] == 3 || other[0] == 7) && other[3] == other[5] &&
951 | !(stroke[4] + 1 > other[6] || stroke[6] - 1 < other[4]) &&
952 | Math.abs(stroke[3] - other[3]) < this.kMinWidthT_adjust * this.kAdjustTateStep) {
953 | res_arr.push(this.kAdjustTateStep - Math.floor(Math.abs(stroke[3] - other[3]) / this.kMinWidthT_adjust));
954 | }
955 | }
956 | const kAdjustTateStep_org = 4;//original implementation
957 | var res = res_arr.reduce((acc, val) => norm2(acc, val), 0)*1.1//1.1を取ってnorm2ではなく+にすると以前と同じ
958 |
959 | res = Math.min(res, kAdjustTateStep_org);
960 | return res;//a2 += res * 1000
961 | }
962 |
963 | adjustUrokoParam(stroke, others) { // strokes
964 | //for illegal strokes
965 | if (stroke[2] >= 100) return ~~((stroke[2] % 1000) / 100);
966 | if (stroke[0] >= 100) return 0;
967 |
968 | //STROKETYPE.STRAIGHT && ENDTYPE.OPEN
969 | for (var k = 0; k < this.kAdjustUrokoLengthStep; k++) {
970 | var tx, ty, tlen;
971 | if (stroke[4] == stroke[6]) { // YOKO
972 | tx = stroke[5] - this.kAdjustUrokoLine[k];
973 | ty = stroke[6] - 0.5;
974 | tlen = stroke[5] - stroke[3];
975 | } else {
976 | var rad = Math.atan((stroke[6] - stroke[4]) / (stroke[5] - stroke[3]));
977 | tx = stroke[5] - this.kAdjustUrokoLine[k] * Math.cos(rad) - 0.5 * Math.sin(rad);
978 | ty = stroke[6] - this.kAdjustUrokoLine[k] * Math.sin(rad) - 0.5 * Math.cos(rad);
979 | tlen = Math.sqrt((stroke[6] - stroke[4]) * (stroke[6] - stroke[4]) +
980 | (stroke[5] - stroke[3]) * (stroke[5] - stroke[3]));
981 | }
982 | if (tlen < this.kAdjustUrokoLength[k] ||
983 | isCrossWithOthers(others, -1, tx, ty, stroke[5], stroke[6])
984 | ) {
985 | return (this.kAdjustUrokoLengthStep - k);
986 | }
987 | }
988 | return 0;//a3 += res * 100;
989 | }
990 |
991 | adjustUroko2Param(stroke, others) { // strokes
992 | //for illegal strokes
993 | if (stroke[2] >= 100) return ~~((stroke[2] % 1000) / 100);
994 | if (stroke[0] >= 100) return 0;
995 |
996 | //STROKETYPE.STRAIGHT && ENDTYPE.OPEN && y1==y2
997 | var pressures = [];
998 | for (let other of others) {
999 | if (
1000 | (other[0] == 1 && other[4] == other[6] &&
1001 | !(stroke[3] + 1 > other[5] || stroke[5] - 1 < other[3]) &&
1002 | Math.abs(stroke[4] - other[4]) < this.kAdjustUroko2Length) ||
1003 | (other[0] == 3 && other[6] == other[8] &&
1004 | !(stroke[3] + 1 > other[7] || stroke[5] - 1 < other[5]) &&
1005 | Math.abs(stroke[4] - other[6]) < this.kAdjustUroko2Length)
1006 | ) {
1007 | pressures.push(Math.pow(this.kAdjustUroko2Length - Math.abs(stroke[4] - other[6]), 1.1));
1008 | }
1009 | }
1010 | var pressure = pressures.reduce((acc, val) => norm2(acc, val), 0)*1.9//1.7を取ってnorm2ではなく+にすると以前と同じ
1011 | var result = Math.min(Math.floor(pressure / this.kAdjustUroko2Length), this.kAdjustUroko2Step);
1012 | return result;//a3 += res * 100;
1013 | }
1014 |
1015 | adjustHaneParam(stroke, epx, epy, others) { // adjust "Hane" (short line turning to the left)
1016 | //for illegal strokes
1017 | if (stroke[2] >= 100) return ~~((stroke[2] % 1000) / 100);
1018 | if (stroke[0] >= 100) return 0;
1019 |
1020 | //endPointX, endPointY
1021 | //(STROKETYPE.STRAIGHT || STROKETYPE.CURVE || STROKETYPE.BEZIER) && ENDTYPE.TURN_LEFT
1022 | var res = 0;
1023 | var nearest = Infinity; // the nearest point to the short line
1024 | if (epx + 18 < 100) {
1025 | nearest = epx + 18;
1026 | }
1027 | for (let other of others) {
1028 | if (other[0] == STROKETYPE.STRAIGHT && other[3] == other[5] && other[3] < epx && other[4] <= epy && other[6] >= epy) {
1029 | if (epx - other[3] < 100) {
1030 | nearest = Math.min(nearest, epx - other[3]);
1031 | }
1032 | }
1033 | }
1034 | if (nearest != Infinity) {
1035 | res = 7 - Math.floor(nearest / 15);
1036 | }
1037 | return res;//a3 += res * 100;
1038 | }
1039 |
1040 | adjustMageParam(stroke, others) {
1041 | //for illegal strokes
1042 | if (stroke[2] >= 1000) return ~~((stroke[2] % 10000) / 1000);
1043 | if (stroke[0] >= 100) return 0;
1044 |
1045 | //STROKETYPE.BENDING
1046 | //applied only if y2=y3
1047 | if (stroke[6] != stroke[8]) return 0;
1048 | var res0_above = [];
1049 | var res0_below = [];
1050 | for (let other of others) {
1051 | if (
1052 | (other[0] == 1 && other[4] == other[6] &&
1053 | !(stroke[5] + 1 > other[5] || stroke[7] - 1 < other[3]) &&
1054 | Math.abs(stroke[6] - other[4]) < this.kMinWidthT_adjust * this.kAdjustMageStep) ||
1055 | (other[0] == 3 && other[6] == other[8] &&
1056 | !(stroke[5] + 1 > other[7] || stroke[7] - 1 < other[5]) &&
1057 | Math.abs(stroke[6] - other[6]) < this.kMinWidthT_adjust * this.kAdjustMageStep)
1058 | ) {
1059 | var p = this.kAdjustMageStep - Math.floor(Math.abs(stroke[6] - other[6]) / this.kMinWidthT_adjust);
1060 | if ((other[0] == 1 && stroke[6] < other[4]) || (other[0] == 3 && stroke[6] < other[6])) //lines "above" the hane
1061 | {
1062 | res0_above.push(p);
1063 | }else{
1064 | res0_below.push(p);
1065 | }
1066 | }
1067 | }
1068 | var res_above = res0_above.reduce((acc, val) => Math.max(acc, val), 0)
1069 | var res_below = res0_below.reduce((acc, val) => Math.max(acc, val), 0)*1.3
1070 | var res = Math.max(res_above, res_below)//1.3とかを外して上2行を含めたmaxとかnorm2を+にすると以前と同じ
1071 |
1072 | const maxlen = (stroke[6] - stroke[4]) * 0.6//y2-y1から算出
1073 | const res2 = maxlen <= 0 ? 0 : (1 - (maxlen/this.kWidth - 1)/4 ) * this.kAdjustMageStep//"this.kWidth * (4 * (1 - param_mage / this.kAdjustMageStep) + 1)" を参考に逆算
1074 | res = Math.max(res,res2)//小数値が返るため、問題が出る可能性もある?今のところ問題なし
1075 | res = Math.min(res, this.kAdjustMageStep);
1076 | return res;//a3 += res * 1000;
1077 | }
1078 |
1079 | adjustKirikuchiParam(stroke, others) { // connecting to other strokes.
1080 | //for illegal strokes
1081 | if (~~((stroke[1]%1000) / 100) == 1) return true;
1082 | if (stroke[0] >= 100) return false;
1083 |
1084 | //STROKETYPE.CURVE, STARTTYPE.CONNECTING_V
1085 | if (stroke[3] > stroke[5] &&
1086 | stroke[4] < stroke[6]) {
1087 | for (let other of others) {
1088 | if (other[0] == 1 &&
1089 | other[3] < stroke[3] && other[5] > stroke[3] &&
1090 | other[4] == stroke[4] && other[4] == other[6]) {
1091 | return true;
1092 | }
1093 | }
1094 | }
1095 | return false;
1096 | //if (res) a2 += 100;
1097 | }
1098 |
1099 | adjustKakatoParam(stroke, others) {
1100 | //for illegal strokes
1101 | if (stroke[2] >= 100) return ~~((stroke[2] % 1000) / 100);
1102 | if (stroke[0] >= 100) return 0;
1103 |
1104 |
1105 | //if (STROKETYPE.STRAIGHT && (LOWER_LEFT_CORNER || LOWER_RIGHT_CORNER))
1106 | for (var k = 0; k < this.kAdjustKakatoStep; k++) {
1107 | if (isCrossBoxWithOthers(others, -1,
1108 | stroke[5] - this.kAdjustKakatoRangeX / 2,
1109 | stroke[6] + this.kAdjustKakatoRangeY[k],
1110 | stroke[5] + this.kAdjustKakatoRangeX / 2,
1111 | stroke[6] + this.kAdjustKakatoRangeY[k + 1])
1112 | | stroke[6] + this.kAdjustKakatoRangeY[k + 1] > 200 // adjust for baseline
1113 | | stroke[6] - stroke[4] < this.kAdjustKakatoRangeY[k + 1] // for thin box
1114 | ) {
1115 | return (this.kAdjustKakatoStep - k);
1116 | }
1117 | }
1118 | return 0;
1119 | //a3 += res * 100;
1120 | }
1121 | }
--------------------------------------------------------------------------------