├── 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"; 57 | for(var i = 0; i < this.array.length; i++){ 58 | buffer += "\n"; 61 | } 62 | buffer += "\n"; 63 | return buffer; 64 | } 65 | generateSVG2(){ // string 66 | var buffer = ""; 67 | buffer += "\n"; 68 | buffer += "\n"; 71 | 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 | } --------------------------------------------------------------------------------