var Transform = require('./transform.js'); var ANGLES = 12; var CROWD = 4; var LOOSE = 6; var SMALL = 1e-4 function cross(z1, z2, z3){ return (z2.x - z1.x) * (z3.y - z1.y) - (z3.x - z1.x) * (z2.y - z1.y) }; function dot(z1, z2, z3){ return (z2.x - z1.x) * (z3.x - z1.x) + (z3.y - z1.y) * (z2.y - z1.y) }; function solveTS(a, b, c, out, flag){ var delta = b * b - 4 * a * c; if(delta > 0){ var t1 = (Math.sqrt(delta) - b) / (2 * a) var t2 = (-Math.sqrt(delta) - b) / (2 * a) if(flag){ if(t1 >= 0 && t1 <= 1) out.push(t1) if(t2 >= 0 && t2 <= 1) out.push(t2) } else { if(t1 > 0 && t1 < 1) out.push(t1) if(t2 > 0 && t2 < 1) out.push(t2) } } else if (delta === 0){ var t = -b / (2 * a) if(flag){ if(t >= 0 && t <= 1) out.push(t) } else { if(t > 0 && t < 1) out.push(t) } }; } function findExtrema(z1, z2, z3, z4, out){ var a = 3 * (-z1.y + 3 * z2.y - 3 * z3.y + z4.y); var b = 6 * (z1.y - 2 * z2.y + z3.y); var c = 3 * (z2.y - z1.y); solveTS(a, b, c, out); }; function findInflections(z1, z2, z3, z4, out){ var ax = z2.x - z1.x; var ay = z2.y - z1.y; var bx = z3.x - z2.x - ax; var by = z3.y - z2.y - ay; var cx = z4.x - z3.x - ax - 2 * bx; var cy = z4.y - z3.y - ay - 2 * by; solveTS(bx * cy - by * cx, ax * cy - ay * cx, ax * by - ay * bx, out, true) } function rotate(z, angle){ var c = Math.cos(angle), s = Math.sin(angle); return { x: c * z.x + s * z.y, y : -s * z.x + c * z.y } }; function ASCEND(a, b){ return a - b } function fineAllExtremae(z1, z2, z3, z4, angles){ var exs = []; findInflections(z1, z2, z3, z4, exs); for(var a = 0; a < angles; a += 1){ findExtrema(z1, z2, z3, z4, exs); z1 = rotate(z1, Math.PI / angles); z2 = rotate(z2, Math.PI / angles); z3 = rotate(z3, Math.PI / angles); z4 = rotate(z4, Math.PI / angles); } return exs.sort(ASCEND) }; function mix(z1, z2, t){ return {x: (1 - t) * z1.x + t * z2.x, y: (1 - t) * z1.y + t * z2.y} }; function bez2(z1, z2, z3, t){ return { x: (1 - t) * (1 - t) * z1.x + 2 * (1 - t) * t * z2.x + t * t * z3.x, y: (1 - t) * (1 - t) * z1.y + 2 * (1 - t) * t * z2.y + t * t * z3.y } }; function bez3(z1, z2, z3, z4, t){ return { x: (1 - t) * (1 - t) * (1 - t) * z1.x + 3 * t * (1 - t) * (1 - t) * z2.x + 3 * t * t * (1 - t) * z3.x + t * t * t *z4.x, y: (1 - t) * (1 - t) * (1 - t) * z1.y + 3 * t * (1 - t) * (1 - t) * z2.y + 3 * t * t * (1 - t) * z3.y + t * t * t *z4.y } }; function splitBefore(z1, z2, z3, z4, t){ return [z1, mix(z1, z2, t), bez2(z1, z2, z3, t), bez3(z1, z2, z3, z4, t)] }; function splitAfter(z1, z2, z3, z4, t){ return [bez3(z1, z2, z3, z4, t), bez2(z2, z3, z4, t), mix(z3, z4, t), z4] }; function splitAtExtremae(z1, z2, z3, z4, angles, splitpoints){ var ts = fineAllExtremae(z1, z2, z3, z4, angles); if(ts[0] < SMALL) { ts[0] = 0; } else { ts.unshift(0); } if(ts[ts.length - 1] > 1 - SMALL) { ts[ts.length - 1] = 1; } else { ts.push(1); }; for(var k = 0; k < ts.length; k++){ if(k > 0){ var t1 = ts[k - 1]; var t2 = ts[k]; var bef = splitBefore(z1, z2, z3, z4, t2); var seg = splitAfter(bef[0], bef[1], bef[2], bef[3], t1 / t2); seg[1].onCurve = seg[2].onCurve = false; seg[1].cubic = seg[2].cubic = true; seg[3].onCurve = true; splitpoints.push(seg[1],seg[2],seg[3]); } }; }; function splitSegment(z1, z2, z3, z4, angles, splitpoints){ var ts = []; var inflectAtEnd = false; findInflections(z1, z2, z3, z4, ts); ts = ts.sort(ASCEND); if(ts[0] < SMALL) { ts[0] = 0; splitpoints[splitpoints.length - 1].inflect = true; } else { ts.unshift(0); } if(ts[ts.length - 1] > 1 - SMALL) { inflectAtEnd = true; ts[ts.length - 1] = 1; } else { ts.push(1); }; for(var k = 0; k < ts.length; k++){ if(k > 0) { var t1 = ts[k - 1]; var t2 = ts[k]; var bef = splitBefore(z1, z2, z3, z4, t2); var seg = splitAfter(bef[0], bef[1], bef[2], bef[3], t1 / t2); splitAtExtremae(seg[0], seg[1], seg[2], seg[3], angles, splitpoints); if(t2 < 1 || inflectAtEnd) splitpoints[splitpoints.length - 1].inflect = true; } }; }; function fitpts(p1, c1, c2, p2){ var d1 = {x : c1.x - p1.x, y: c1.y - p1.y} var d2 = {x : c2.x - p2.x, y: c2.y - p2.y} var det = d2.x * d1.y - d2.y * d1.x; if(Math.abs(det) < 1e-6) return null; var u = ((p2.y - p1.y) * d2.x - (p2.x - p1.x) * d2.y) / det var v = ((p2.y - p1.y) * d1.x - (p2.x - p1.x) * d1.y) / det if(u <= 0 || v <= 0) return null; var mid = { x: p1.x + d1.x * u, y: p1.y + d1.y * u } return [mix(mid, p1, 1/3), mix(mid, p2, 1/3)] }; function close(z1, z2){ return (z1.x - z2.x) * (z1.x - z2.x) + (z1.y - z2.y) * (z1.y - z2.y) <= SMALL } function fairify(scurve, gizmo, angles){ angles = angles || ANGLES for(var j = 0; j < scurve.length; j++){ scurve[j] = Transform.untransform(gizmo, scurve[j]) } var splitpoints = [scurve[0]]; var last = scurve[0]; for(var j = 1; j < scurve.length; j++){ if(scurve[j].onCurve){ splitpoints.push(last = scurve[j]) } else if(scurve[j].cubic) { var z1 = last, z2 = scurve[j], z3 = scurve[j + 1], z4 = scurve[j + 2]; if(!(close(z1, z2) && close(z2, z3) && close(z3, z4))) { splitSegment(z1, z2, z3, z4, ANGLES, splitpoints); last = z4; } j += 2; } else { var z1 = last, zm = scurve[j], z4 = scurve[j + 1] if(!(close(z1, zm) && close(zm, z4))) { var z2 = mix(zm, z1, 1/3); var z3 = mix(zm, z4, 1/3); splitSegment(z1, z2, z3, z4, ANGLES, splitpoints); last = z4; } j += 1; } }; // Mark infections and extremae for(var j = 1; j < splitpoints.length - 1; j++) { if(splitpoints[j].onCurve && !splitpoints[j - 1].onCurve && !splitpoints[j + 1].onCurve){ var z1 = splitpoints[j], z0 = splitpoints[j - 1], z2 = splitpoints[j + 1] if(cross(z1, z0, z2) < 1e-6 && dot(z1, z0, z2) < 0){ var isInflection = false; if(j > 2 && j < splitpoints.length - 2){ var za = bez3(z1, z0, splitpoints[j - 2], splitpoints[j - 3], SMALL); var zb = bez3(z1, z2, splitpoints[j + 2], splitpoints[j + 3], SMALL); var inflect = ((z0.y-z2.y)*(za.x-z0.x) + (z2.x-z0.x)*(za.y-z0.y)) * ((z0.y-z2.y)*(zb.x-z0.x) + (z2.x-z0.x)*(zb.y-z0.y)); if(inflect < 0) isInflection = true; }; if(!(z1.inflect || isInflection)) { var angle0 = Math.atan2(z2.y - z0.y, z2.x - z0.x); var angle = Math.abs(angle0 / Math.PI * 2 % 1); if(Math.abs(Math.abs(angle0) - Math.PI / 2) <= SMALL || angle <= SMALL || angle >= 1 - SMALL){ z1.mark = true; } } else { z1.mark = true; } } else { z1.mark = true; } } else if(splitpoints[j].onCurve) { splitpoints[j].mark = true // corner } }; splitpoints[0].mark = splitpoints[splitpoints.length - 1].mark = true; // Mark diagonals var lastmark = splitpoints[0]; for(var j = 1; j < splitpoints.length - 1; j++) { if(splitpoints[j].mark){ lastmark = splitpoints[j]; for(var k = j + 1; k < splitpoints.length && !splitpoints[k].mark; k++); nextmark = splitpoints[k]; var hspan = Math.abs(lastmark.x - nextmark.x); var vspan = Math.abs(lastmark.y - nextmark.y); } if(splitpoints[j].onCurve && !splitpoints[j].mark){ var z1 = splitpoints[j], z0 = splitpoints[j - 1], z2 = splitpoints[j + 1]; var angle0 = Math.atan2(z2.y - z0.y, z2.x - z0.x); var angle = Math.abs(angle0 / Math.PI * (hspan <= 10 * LOOSE || vspan <= 10 * LOOSE ? CROWD : LOOSE) % 1); if(!(Math.abs(Math.abs(angle0) - Math.PI / 2) <= SMALL || angle <= SMALL || angle >= 1 - SMALL)){ z1.remove = z0.remove = z2.remove = true; } } }; for(var j = 0; j < splitpoints.length; j++) if(splitpoints[j].onCurve && !splitpoints[j].remove && splitpoints[j + 1] && !splitpoints[j + 1].onCurve){ for(var k = j + 2; k < splitpoints.length && splitpoints[k].remove; k++); if(k - j > 2){ var zs = fitpts(splitpoints[j], splitpoints[j + 1], splitpoints[k], splitpoints[k + 1]); if(zs){ zs[0].onCurve = zs[1].onCurve = false; zs[0].cubic = zs[1].cubic = true; splitpoints[j + 1] = zs[0]; splitpoints[k] = zs[1]; } } j = k; }; var ans = [] for(var j = 0; j < splitpoints.length; j++) if(splitpoints[j] && !splitpoints[j].remove) { ans.push(Transform.transformPoint(gizmo, splitpoints[j])) } return ans } module.exports = fairify;