How to generate a "thick" bezier curve? - geometry
I'm looking for a way to generate a polygon programatically by "thickening" a Bezier curve. Something like this:
My initial idea was to find the normals in the line, and generate the polygon from them:
But the problem is that the normals can cross each other in steep curves, like this:
Are there any formulas or algorithms that generate a polygon from a bezier curve? I couldn't find any information on the internet, but perhaps I'm searching using the wrong words...
If you want a constant thickness, this is called an offset curve and your idea of using normals is correct.
This indeed raises two difficulties:
The offset curve is not exactly representable as a Bezier curve; you can use a polyline instead, or retrofit Beziers to the polyline;
There are indeed cusps appearing when the radius of curvature becomes smaller than the offset width. You will have to detect the self-intersections of the polyline.
As far as I know, there is no easy solution.
For a little more info, check 38. Curve offsetting.
Step-by-step process detailed here: How to Draw an Offset Curve
Solution based on the paper ‘Quadratic bezier offsetting with selective subdivision‘ by Gabriel Suchowolski. More from the author: MATH+CODE
Interactive example: CodePen
var canvas, ctx;
var drags;
var thickness = 30;
var drawControlPoints = true;
var useSplitCurve = true;
function init() {
canvas = document.createElement('canvas');
ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
drags = [];
window.addEventListener('resize', resize);
window.addEventListener('mousedown', mousedown);
window.addEventListener('mouseup', mouseup);
window.addEventListener('mousemove', mousemove);
document.getElementById('btnControl').addEventListener('click', function(e) {
drawControlPoints = !drawControlPoints
});
document.getElementById('btnSplit').addEventListener('click', function(e) {
useSplitCurve = !useSplitCurve
});
resize();
draw();
var positions = [{
x: canvas.width * 0.3,
y: canvas.height * 0.4
}, {
x: canvas.width * 0.35,
y: canvas.height * 0.85
}, {
x: canvas.width * 0.7,
y: canvas.height * 0.25
}];
for (var i = 0; i < positions.length; i++) {
drags.push(new Drag(ctx, new Vec2D(positions[i].x, positions[i].y)));
}
}
function draw() {
requestAnimationFrame(draw);
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.lineWidth = 1;
for (var i = 0; i < drags.length; i++) {
d = drags[i];
d.draw();
}
for (var i = 1; i < drags.length - 1; i++) {
/*
var d1 = (i == 0) ? drags[i].pos : drags[i - 1].pos;
var d2 = drags[i].pos;
var d3 = (i == drags.length - 1) ? drags[drags.length - 1].pos : drags[i + 1].pos;
var v1 = d2.sub(d1);
var v2 = d3.sub(d2);
var p1 = d2.sub(v1.scale(0.5));
var p2 = d3.sub(v2.scale(0.5));
var c = d2;
*/
var p1 = drags[i - 1].pos;
var p2 = drags[i + 1].pos;
var c = drags[i].pos;
var v1 = c.sub(p1);
var v2 = p2.sub(c);
var n1 = v1.normalizeTo(thickness).getPerpendicular();
var n2 = v2.normalizeTo(thickness).getPerpendicular();
var p1a = p1.add(n1);
var p1b = p1.sub(n1);
var p2a = p2.add(n2);
var p2b = p2.sub(n2);
var c1a = c.add(n1);
var c1b = c.sub(n1);
var c2a = c.add(n2);
var c2b = c.sub(n2);
var line1a = new Line2D(p1a, c1a);
var line1b = new Line2D(p1b, c1b);
var line2a = new Line2D(p2a, c2a);
var line2b = new Line2D(p2b, c2b);
var split = (useSplitCurve && v1.angleBetween(v2, true) > Math.PI / 2);
if (!split) {
var ca = line1a.intersectLine(line2a).pos;
var cb = line1b.intersectLine(line2b).pos;
} else {
var t = MathUtils.getNearestPoint(p1, c, p2);
var pt = MathUtils.getPointInQuadraticCurve(t, p1, c, p2);
var t1 = p1.scale(1 - t).add(c.scale(t));
var t2 = c.scale(1 - t).add(p2.scale(t));
var vt = t2.sub(t1).normalizeTo(thickness).getPerpendicular();
var qa = pt.add(vt);
var qb = pt.sub(vt);
var lineqa = new Line2D(qa, qa.add(vt.getPerpendicular()));
var lineqb = new Line2D(qb, qb.add(vt.getPerpendicular()));
var q1a = line1a.intersectLine(lineqa).pos;
var q2a = line2a.intersectLine(lineqa).pos;
var q1b = line1b.intersectLine(lineqb).pos;
var q2b = line2b.intersectLine(lineqb).pos;
}
if (drawControlPoints) {
// draw control points
var r = 2;
ctx.beginPath();
if (!split) {
ctx.rect(ca.x - r, ca.y - r, r * 2, r * 2);
ctx.rect(cb.x - r, cb.y - r, r * 2, r * 2);
} else {
// ctx.rect(pt.x - r, pt.y - r, r * 2, r * 2);
ctx.rect(p1a.x - r, p1a.y - r, r * 2, r * 2);
ctx.rect(q1a.x - r, q1a.y - r, r * 2, r * 2);
ctx.rect(p2a.x - r, p2a.y - r, r * 2, r * 2);
ctx.rect(q2a.x - r, q2a.y - r, r * 2, r * 2);
ctx.rect(qa.x - r, qa.y - r, r * 2, r * 2);
ctx.rect(p1b.x - r, p1b.y - r, r * 2, r * 2);
ctx.rect(q1b.x - r, q1b.y - r, r * 2, r * 2);
ctx.rect(p2b.x - r, p2b.y - r, r * 2, r * 2);
ctx.rect(q2b.x - r, q2b.y - r, r * 2, r * 2);
ctx.rect(qb.x - r, qb.y - r, r * 2, r * 2);
ctx.moveTo(qa.x, qa.y);
ctx.lineTo(qb.x, qb.y);
}
ctx.closePath();
ctx.strokeStyle = '#0072bc';
ctx.stroke();
ctx.fillStyle = '#0072bc';
ctx.fill();
// draw dashed lines
ctx.beginPath();
if (!split) {
ctx.moveTo(p1a.x, p1a.y);
ctx.lineTo(ca.x, ca.y);
ctx.lineTo(p2a.x, p2a.y);
ctx.moveTo(p1b.x, p1b.y);
ctx.lineTo(cb.x, cb.y);
ctx.lineTo(p2b.x, p2b.y);
} else {
ctx.moveTo(p1a.x, p1a.y);
ctx.lineTo(q1a.x, q1a.y);
ctx.lineTo(qa.x, qa.y);
ctx.lineTo(q2a.x, q2a.y);
ctx.lineTo(p2a.x, p2a.y);
ctx.moveTo(p1b.x, p1b.y);
ctx.lineTo(q1b.x, q1b.y);
ctx.lineTo(qb.x, qb.y);
ctx.lineTo(q2b.x, q2b.y);
ctx.lineTo(p2b.x, p2b.y);
}
ctx.setLineDash([2, 4]);
ctx.stroke();
ctx.closePath();
ctx.setLineDash([]);
}
// central line
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.quadraticCurveTo(c.x, c.y, p2.x, p2.y);
ctx.strokeStyle = '#959595';
ctx.stroke();
// offset curve a
ctx.beginPath();
ctx.moveTo(p1a.x, p1a.y);
if (!split) {
ctx.quadraticCurveTo(ca.x, ca.y, p2a.x, p2a.y);
} else {
ctx.quadraticCurveTo(q1a.x, q1a.y, qa.x, qa.y);
ctx.quadraticCurveTo(q2a.x, q2a.y, p2a.x, p2a.y);
}
ctx.strokeStyle = '#0072bc';
ctx.lineWidth = 2;
ctx.stroke();
// offset curve b
ctx.beginPath();
ctx.moveTo(p1b.x, p1b.y);
if (!split) {
ctx.quadraticCurveTo(cb.x, cb.y, p2b.x, p2b.y);
} else {
ctx.quadraticCurveTo(q1b.x, q1b.y, qb.x, qb.y);
ctx.quadraticCurveTo(q2b.x, q2b.y, p2b.x, p2b.y);
}
ctx.strokeStyle = '#0072bc';
ctx.stroke();
}
}
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
function mousedown(e) {
e.preventDefault();
var m = new Vec2D(e.clientX, e.clientY);
for (var i = 0; i < drags.length; i++) {
var d = drags[i];
var dist = d.pos.distanceToSquared(m);
if (dist < d.hitRadiusSq) {
d.down = true;
break;
}
}
}
function mouseup() {
for (var i = 0; i < drags.length; i++) {
var d = drags[i];
d.down = false;
}
}
function mousemove(e) {
var m = new Vec2D(e.clientX, e.clientY);
for (var i = 0; i < drags.length; i++) {
var d = drags[i];
if (d.down) {
d.pos.x = m.x;
d.pos.y = m.y;
break;
}
}
}
function Drag(ctx, pos) {
this.ctx = ctx;
this.pos = pos;
this.radius = 6;
this.hitRadiusSq = 900;
this.down = false;
}
Drag.prototype = {
draw: function() {
this.ctx.beginPath();
this.ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI * 2);
this.ctx.closePath();
this.ctx.strokeStyle = '#959595'
this.ctx.stroke();
}
}
// http://toxiclibs.org/docs/core/toxi/geom/Vec2D.html
function Vec2D(a, b) {
this.x = a;
this.y = b;
}
Vec2D.prototype = {
add: function(a) {
return new Vec2D(this.x + a.x, this.y + a.y);
},
angleBetween: function(v, faceNormalize) {
if (faceNormalize === undefined) {
var dot = this.dot(v);
return Math.acos(this.dot(v));
}
var theta = (faceNormalize) ? this.getNormalized().dot(v.getNormalized()) : this.dot(v);
return Math.acos(theta);
},
distanceToSquared: function(v) {
if (v !== undefined) {
var dx = this.x - v.x;
var dy = this.y - v.y;
return dx * dx + dy * dy;
} else {
return NaN;
}
},
dot: function(v) {
return this.x * v.x + this.y * v.y;
},
getNormalized: function() {
return new Vec2D(this.x, this.y).normalize();
},
getPerpendicular: function() {
return new Vec2D(this.x, this.y).perpendicular();
},
interpolateTo: function(v, f) {
return new Vec2D(this.x + (v.x - this.x) * f, this.y + (v.y - this.y) * f);
},
normalize: function() {
var mag = this.x * this.x + this.y * this.y;
if (mag > 0) {
mag = 1.0 / Math.sqrt(mag);
this.x *= mag;
this.y *= mag;
}
return this;
},
normalizeTo: function(len) {
var mag = Math.sqrt(this.x * this.x + this.y * this.y);
if (mag > 0) {
mag = len / mag;
this.x *= mag;
this.y *= mag;
}
return this;
},
perpendicular: function() {
var t = this.x;
this.x = -this.y;
this.y = t;
return this;
},
scale: function(a) {
return new Vec2D(this.x * a, this.y * a);
},
sub: function(a, b) {
return new Vec2D(this.x - a.x, this.y - a.y);
},
}
// http://toxiclibs.org/docs/core/toxi/geom/Line2D.html
function Line2D(a, b) {
this.a = a;
this.b = b;
}
Line2D.prototype = {
intersectLine: function(l) {
var isec,
denom = (l.b.y - l.a.y) * (this.b.x - this.a.x) - (l.b.x - l.a.x) * (this.b.y - this.a.y),
na = (l.b.x - l.a.x) * (this.a.y - l.a.y) - (l.b.y - l.a.y) * (this.a.x - l.a.x),
nb = (this.b.x - this.a.x) * (this.a.y - l.a.y) - (this.b.y - this.a.y) * (this.a.x - l.a.x);
if (denom !== 0) {
var ua = na / denom,
ub = nb / denom;
if (ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0) {
isec = new Line2D.LineIntersection(Line2D.LineIntersection.Type.INTERSECTING, this.a.interpolateTo(this.b, ua));
} else {
isec = new Line2D.LineIntersection(Line2D.LineIntersection.Type.NON_INTERSECTING, this.a.interpolateTo(this.b, ua));
}
} else {
if (na === 0 && nb === 0) {
isec = new Line2D.LineIntersection(Line2D.LineIntersection.Type.COINCIDENT, undefined);
} else {
isec = new Line2D.LineIntersection(Line2D.LineIntersection.Type.COINCIDENT, undefined);
}
}
return isec;
}
}
Line2D.LineIntersection = function(type, pos) {
this.type = type;
this.pos = pos;
}
Line2D.LineIntersection.Type = {
COINCIDENT: 0,
PARALLEL: 1,
NON_INTERSECTING: 2,
INTERSECTING: 3
};
window.MathUtils = {
getPointInQuadraticCurve: function(t, p1, pc, p2) {
var x = (1 - t) * (1 - t) * p1.x + 2 * (1 - t) * t * pc.x + t * t * p2.x;
var y = (1 - t) * (1 - t) * p1.y + 2 * (1 - t) * t * pc.y + t * t * p2.y;
return new Vec2D(x, y);
},
// http://microbians.com/math/Gabriel_Suchowolski_Quadratic_bezier_offsetting_with_selective_subdivision.pdf
// http://www.math.vanderbilt.edu/~schectex/courses/cubic/
getNearestPoint: function(p1, pc, p2) {
var v0 = pc.sub(p1);
var v1 = p2.sub(pc);
var a = v1.sub(v0).dot(v1.sub(v0));
var b = 3 * (v1.dot(v0) - v0.dot(v0));
var c = 3 * v0.dot(v0) - v1.dot(v0);
var d = -1 * v0.dot(v0);
var p = -b / (3 * a);
var q = p * p * p + (b * c - 3 * a * d) / (6 * a * a);
var r = c / (3 * a);
var s = Math.sqrt(q * q + Math.pow(r - p * p, 3));
var t = MathUtils.cbrt(q + s) + MathUtils.cbrt(q - s) + p;
return t;
},
// http://stackoverflow.com/questions/12810765/calculating-cubic-root-for-negative-number
cbrt: function(x) {
var sign = x === 0 ? 0 : x > 0 ? 1 : -1;
return sign * Math.pow(Math.abs(x), 1 / 3);
}
}
init();
html,
body {
height: 100%;
margin: 0
}
canvas {
display: block
}
#btnControl {
position: absolute;
top: 10px;
left: 10px;
}
#btnSplit {
position: absolute;
top: 35px;
left: 10px;
}
<button type="button" id="btnControl">control points on/off</button>
<button type="button" id="btnSplit">split curve on/off</button>
This is a hard problem. There are reasonable approximations like Tiller-Hanson (see my answer to this question: How to get the outline of a stroke?) but the questioner specifically raises the difficulty that 'the normals can cross each other in steep curves'; another way of looking at it is that an envelope created using normals can produce an indefinitely large number of loops, depending on how closely spaced the normals are.
A perfect solution, without self-intersections, is the envelope of the Minkowski sum of a circle and the line. I think it's impractical to get such an envelope, though: you may have to accept the intersections.
Another interesting but daunting fact is that, as Richard Kinch notes in MetaFog: Converting METAFONT Shapes to Contours, "Algebra tells us
that stroking a 3rd degree polynomial curve (the ellipse
approximated by Bézier curves) along a 3rd
degree polynomial curve (the Bézier curve of the
stroked path) results in a 6th degree envelope curve.
We will have to approximate these 6th degree exact
envelope curves with 3rd degree (Bezier) curves".
Here I had the math papers about that theme.
The “Quadratic bezier offsetting with selective subdivision" covers a method to offset quadratic beziers using a criterion that set the parametric value on which the quadratic bezier is subdivided at the start in order to generate an offset approximation with other quadratic beziers segments. This method, obviously, may not be the most perfect approximation of a hypothetical “real” offset, but a fast algorithm for drawing strokes that can be performed on different quality levels by using a non recursive algorithm.
Here all the papers and examples https://microbians.com/mathcode
that imbrizi use for the codepen he put in the answers.
https://codepen.io/microbians/pen/OJPmBZg
code in the link
Related
handling a view frustum when not facing the object
i have made a view frustum from scratch in javascript. i have been having trouble with objects that the camera is place within and also facing away from. an example of the problem is below ive been stuck on this for months with little help, chatgpt recommended that i switch to a right handed corodinate system, so i did, but it didnt seem to fix the problem. my proccess for putting pixels on the screen is described below step 1 = cross product with camera matrix step 2 = cross product with projection matrix step 3 = divide co-ordinates by their own w co-ord (normalize w to 1) step 3.5 = im currently skipping culling. culling would go here, but since my object is made of multiple vertices i cant cull a vertex just because its offscreen as it forms part of a whole object, and that would deform the rest of the object step 4 = cross product with projection to screen matrix below shows how the co-ordinates of 1 vertex changes with each step co-ordinates of vertex before projection [5.00,9.00,10.00,1.00] good projection (tile is good) step 1 = [5, 9, -4.799999999999979, 10] step 2 = [-8.660254038143698, 15.588457268658656, 15.353535353535376, 4.799999999999979] step 3 = [-1.8042195912799448, 3.2475952643039006, 3.198653198653217, 1] step 4 = [-120.63293869199174, 637.1392896455851, 3.198653198653217, 1] pixels = -120.63293869199174,637.1392896455851 ############################# bad projection from within the object (tile is deformed) step 1 = [5, 9, 0.6000000000000014, 10] step 2 = [-8.660254038143698, 15.588457268658656, 20.808080808080813, -0.6000000000000014] step 3 = [14.433756730239462, -25.980762114431034, -34.68013468013461, 1] step 4 = [2315.0635095359194, -3747.114317164655, -34.68013468013461, 1] pixels = 2315.0635095359194,-3747.114317164655 ############################### bad projection from behind (tile appears on the ceiling, when A it should be on the floor and B it shouldn't be visible) step 1 = [5, 9, 16.800000000000004, 10] step 2 = [-8.660254038143698, 15.588457268658656, 37.17171717171718, -16.800000000000004] step 3 = [0.5154913117942677, -0.9278843612296817, -2.2126022126022122, 1] step 4 = [227.32369676914016, 10.817345815547753, -2.2126022126022122, 1] pixels = 227.32369676914016, 10.817345815547753 does anyone know which step could be wrong or need changing in situation 2 and 3? and why? below is a minimal (i know its 500 lines... but its about as minimal as i can get it) just open it in a browser and use wasd to control it. <head> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/11.3.3/math.js"></script> <script async src="https://unpkg.com/es-module-shims#1.3.6/dist/es-module-shims.js"></script> </head> <body> <div id="canvas div" style = "position: relative; left: 0px; float:left; top: 0px;" > <h1> first person below </h1> <canvas id="mi_canvas" width="300" height="300" style="border-style: solid;"></canvas> <br> <h1> radar below </h1> <canvas id="radar_canvas" width="300" height="300" style="border-style: solid;"></canvas> </div> <div id="big info div" style = "position: relative; left: 310px; float:left; float:top; top: 0px; width:400px;" > <div id = "info_1111"> </div><br> </div> <script> var floor_y_pos = 9 canvas = document.getElementById("mi_canvas"); ctx = canvas.getContext("2d"); radar = document.getElementById("radar_canvas"); radar_ctx = radar.getContext("2d"); render_distance = 1000; fov = math.pi / 2 class Projection{ constructor(){ var NEAR = player.near_plane var FAR = player.far_plane var RIGHT = Math.tan(player.h_fov/2) var LEFT = - RIGHT var TOP = Math.tan(player.v_fov /2) var BOTTOM = -TOP var m00 = 2*NEAR / (RIGHT - LEFT) var m02 = (RIGHT + LEFT)/(RIGHT - LEFT) var m11 = 2*NEAR / (TOP - BOTTOM) var m12 = (TOP + BOTTOM) /(TOP - BOTTOM) var m22 = (FAR * NEAR) / (FAR - NEAR) var m23 = -2 * NEAR * FAR / (FAR-NEAR) this.projection_matrix = [ [-m00,0,m02,0], [0,m11,0,0], [m02,m12,-m22,-1], [0,0,m23,0] ] var HW=player.H_WIDTH var HH = player.H_HEIGHT this.to_screen_matrix = [ [HW,0,0,0], [0,HH,0,0], [0,0,1,0], [HW,HH,0,1] ] } } function multiply(a, b) { var aNumRows = a.length, aNumCols = a[0].length, bNumRows = b.length, bNumCols = b[0].length, m = new Array(aNumRows); // initialize array of rows for (var r = 0; r < aNumRows; ++r) { m[r] = new Array(bNumCols); // initialize the current row for (var c = 0; c < bNumCols; ++c) { m[r][c] = 0; // initialize the current cell for (var i = 0; i < aNumCols; ++i) { m[r][c] += a[r][i] * b[i][c]; } } } return m; } function mi_position_matrix_multiplier(A, B) { var new_matrix = [] for (var new_num_ind = 0; new_num_ind < A.length; ++new_num_ind) { this_num = 0; for (var a_ind = 0; a_ind < A.length; ++a_ind) { this_num += (A[a_ind] * B[a_ind][new_num_ind]) } // console.log("just added this num to my new matrix = "+this_num.toString()) new_matrix.push(this_num) } return new_matrix; } function pythagoras(thing1, thing2) { dist = (((thing1[0]-thing2[0])**2)+((thing1[1]-thing2[1])**2))**0.5 return dist } class vertex{ constructor(x, y,z , id){ this.id = id this.position = [x,y,z,1] this.min_dist = 1.5 // minimum possible distance between player and object } is_this_object_behind_player(){ var arrow_length = 0.0001; var pointing_position = [player.position[0]+(player.forward[0]*arrow_length) , player.position[2]-(player.forward[2]*arrow_length)] var dist1 = pythagoras([this.position[0],this.position[2]], pointing_position) var dist2 = pythagoras([this.position[0],this.position[2]], [player.position[0],player.position[2]]) if (dist1 < dist2){ return true;} else if (dist1 > dist2){ return false;} else{console.log(" else ");} } screen_projection(){ var position = mi_position_matrix_multiplier(this.position , player.camera_matrix()) console.log(position+" = position , which is a cross product of this.position"+this.position+" & "+ player.camera_matrix()+ " = player.camera_matrix()") update_matrix_info_debug("camera_matrix",player.camera_matrix()) update_matrix_info_debug("position", position) position = mi_position_matrix_multiplier(position , projection.projection_matrix) // does this just convert the position to cameras reference frame. console.log(position+" = position , which is a cross product of position"+position+" & "+ projection.projection_matrix+ " = projection.projection_matrix") update_matrix_info_debug("projection_matrix",projection.projection_matrix) update_matrix_info_debug("position after being multiplied by proj matrix", position) // if so then i image to screen matrix is insufficient for (let i = 0; i < position.length; i++) { position[i] = position[i]/position[3] } console.log(position+" = position after being normaslized") for (let i = 0; i < position.length; i++) { if (i != 9787781){ console.log(i+" =-= "+position[i]) if (this.is_this_object_behind_player()){for (let ii = 0; ii < position.length; ii++) {position[ii] = -999999999;} console.log("culling1")} if (position[i] > 2){for (let ii = 0; ii < position.length; ii++) {position[ii] = -9999;} console.log("culling2")} if (position[i] < -2){for (let ii = 0; ii < position.length; ii++) {position[ii] = -9999;} console.log("culling3")} } } // also all examples say set position = 0 if culling console.log(position+" = position after being culled") position = mi_position_matrix_multiplier(position , projection.to_screen_matrix) console.log(position+" = position after being multiplied by "+projection.to_screen_matrix+ " = projection.to_screen_matrix") update_matrix_info_debug("projection.to_screen_matrix",projection.to_screen_matrix) update_matrix_info_debug("position after being multiplied by projection.to_screen_matrix", position) ctx.beginPath(); var scale_multiplier = (render_distance / pythagoras([this.position[0],this.position[2]] , [player.position[0],player.position[2]]))*1.5 var arrow_size = 0.02 * scale_multiplier; ctx.moveTo(position[0]-arrow_size ,position[1]+arrow_size); ctx.lineTo(position[0]+arrow_size ,position[1]-arrow_size); ctx.moveTo(position[0]+arrow_size ,position[1]+arrow_size); ctx.lineTo(position[0]-arrow_size, position[1]-arrow_size); ctx.stroke(); } return_screen_projection(dont_cull = false){ var position = mi_position_matrix_multiplier(this.position , player.camera_matrix()) position = mi_position_matrix_multiplier(position , projection.projection_matrix) // does this just convert the position to cameras reference frame. for (let i = 0; i < position.length; i++) { position[i] = position[i]/position[3] } position = mi_position_matrix_multiplier(position , projection.to_screen_matrix) return [position[0],position[1]] } } class player{ constructor(){ this.position =[0,0,0,1.0] this.forward = [0,0,1,1] this.up = [0,1,0,1] this.right =[1,0,0,1] this.h_fov = 3.1415926535/3 this.v_fov = this.h_fov * (canvas.height / canvas.width) this.near_plane = 1 this.far_plane = 100 this.moving_speed = 0.2 this.rotation_speed = 0.1 this.H_WIDTH = canvas.width/2 this.H_HEIGHT = canvas.height/2 this.anglePitch = 0 this.angleYaw = 0 } set_camera_angle(){ var rotate = multiply(rotate_x(this.anglePitch) , rotate_y(this.angleYaw)) this.forward = [0, 0, 1, 1] this.up = [0, 1, 0, 1] this.right = [1, 0, 0, 1] this.forward = mi_position_matrix_multiplier(this.forward , rotate) this.right = mi_position_matrix_multiplier(this.right , rotate) this.up = mi_position_matrix_multiplier(this.up , rotate) } camera_yaw(angle){ this.angleYaw += angle} translate_matrix(self){ var x = this.position[0]; var y = this.position[1]; var z = this.position[2]; var w = this.position[3]; return [ [1,0,0,0], [0,1,0,1], [0,0,1,0], [-x,-y,z, 1] ]} rotate_matrix(){ var rx = this.right[0] var ry = this.right[1] var rz = this.right[2] var w = this.right[3] var fx = this.forward[0] var fy = this.forward[1] var fz = this.forward[2] var w = this.forward[3] var ux = this.up[0] var uy = this.up[1] var uz = this.up[2] var w = this.up[3] return [ [rx,ux,fx,0], [ry,uy,fy,0], [rz,uz,fz,0], [0,0,0,1] ] } camera_matrix(){ return multiply(this.translate_matrix(), this.rotate_matrix()); } check_min_distance_isnt_overcome_by_this_move(dx, dy){ var can_move = true; console.log(" zzzzzzzzzzzzz ") for (let i = 0; i < objects.length; i++) { var dist=Math.abs(pythagoras([objects[i].position[0], objects[i].position[2]] , [this.position[0], this.position[2]])) var dist2=Math.abs(pythagoras([objects[i].position[0], objects[i].position[2]] , [this.position[0]+dx, this.position[2]+dy])) console.log(dist +" ########################### " +dist2) if ((dist2 < objects[i].min_dist)&(dist > dist2)) {can_move = false; console.log(objects[i].min_dist +" yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy")} else{console.log(objects[i].min_dist+" can move this is bloody min dist xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx "+dist2);} } return can_move; } move(event) { var key_code = parseInt(event.keyCode) if (key_code == 37 || key_code == 39 || key_code == 83 || key_code == 87 || key_code == 119|| key_code == 115) { var dx = Math.cos(this.angleYaw)*this.moving_speed var dy = Math.sin(this.angleYaw)*this.moving_speed console.log("that were moving = dx , dy = "+dx.toString()+" , "+dy.toString()) if ( key_code == 37 || key_code == 87 || key_code == 119) { if (this.check_min_distance_isnt_overcome_by_this_move(dx, -dy)){ this.position[0] += -dy this.position[2] += dx } } if (key_code == 39 || key_code == 83 || key_code == 115) { for (let i = 0; i < this.position.length; i++) { if (this.check_min_distance_isnt_overcome_by_this_move(dx, dy)){ this.position[0] += dy this.position[2] += -dx } } } } else { if ( key_code == 38 || key_code == 65 || key_code == 97) { this.camera_yaw(-this.rotation_speed) } if (key_code == 40 || key_code == 68 || key_code == 100) { this.camera_yaw(this.rotation_speed) } this.set_camera_angle() } } } function translate(pos){ tx,ty,tz=pos return np.array([ [1,0,0,0], [0,1,0,0], [0,0,1,0], [tx,ty,tz,1] ])} function rotate_x(angle){ return [ [1,0,0,0], [0,Math.cos(angle),Math.sin(angle),0], [0,-Math.sin(angle),Math.cos(angle),0], [0,0,0,1] ] } function rotate_y(a){ return [ [math.cos(a),0, -math.sin(a),0], [0,1,0,0], [math.sin(a), 0 , math.cos(a),0], [0,0,0,1] ] } function update_radar(){ var arrow_length = 4; var object_size = 6.5; radar_ctx.beginPath(); var mid_screen = [radar.width/2,radar.height/2]; pointing_position = [mid_screen[0]+(player.forward[0]*arrow_length) , mid_screen[1]-(player.forward[2]*arrow_length)] radar_ctx.moveTo(mid_screen[0], mid_screen[1]); // start of player pos on radar radar_ctx.lineTo(pointing_position[0], pointing_position[1]); radar_ctx.lineTo(pointing_position[0]-2, pointing_position[1]-2); radar_ctx.lineTo(pointing_position[0]+2, pointing_position[1]+2); radar_ctx.lineTo(pointing_position[0]-2, pointing_position[1]+2); radar_ctx.moveTo(mid_screen[0], mid_screen[1]); for (let i = 0; i < objects.length; i++) { var dx = (player.position[0]-objects[i].position[0]) var dz = player.position[2]+objects[i].position[2] var x = (dx*2) + mid_screen[0] var z = (dz*2) + mid_screen[1] x = x +(object_size/2) z = z +(object_size/2) radar_ctx.moveTo(x-object_size,z-object_size); radar_ctx.lineTo(x+object_size,z+object_size); radar_ctx.moveTo(x+object_size,z-object_size); radar_ctx.lineTo(x-object_size,z+object_size); } radar_ctx.stroke(); } function update_matrix_info_debug(matrix_name, matrix){ if (matrix[0].length > 1) { for (let x = 1; x < matrix.length+1; x++) { for (let y = 1; y < matrix.length+1; y++) { // console.log(matrix_name.toString()+"_"+x.toString()+y.toString()); document.getElementById(matrix_name.toString()+"_"+x.toString()+y.toString()).innerHTML = matrix[x-1][y-1] } } } else { for (let x = 1; x < matrix.length+1; x++) {document.getElementById(matrix_name.toString()+"_"+"1"+x.toString()).innerHTML = matrix[x-1]} } } class box{ constructor(x,z,size){ var low_y = 0.5 var high_y = low_y - size this.position = [x+(size/2),0,z+(size/2)] this.vertices = [new vertex(x,low_y,z,0),new vertex(x+size,low_y,z,1),new vertex(x,low_y,z+size,2),new vertex(x+size,low_y,z+size,3), new vertex(x,high_y,z,4),new vertex(x+size,high_y,z,5),new vertex(x,high_y,z+size,6),new vertex(x+size,high_y,z+size,7) ] this.faces=[ [0,1,3,2,0], [0,1,5,4,0] , [1,3,7,5,1] , [4,5,7,6,4] , [2,6,7,3,2] , [0,4,6,2,0]] // this.faces=[ [0,4,6,2,0]] } draw_all_vertices(){ for (let i = 0; i < this.vertices.length; i++) { this.vertices[i].screen_projection() } } draw_all_faces(){ var each_point = [] for (let i = 0; i < this.vertices.length; i++) { each_point.push(this.vertices[i].return_screen_projection()) } var skip_drawing = if_most_of_these_numbers_are_off_screen(each_point) if (skip_drawing){console.log(" skipp drawing any faces init ");return;} ctx.fillStyle = '#f00'; var moved_to_first_yet = false for (let face = 0; face < this.faces.length; face++) { ctx.beginPath(); console.log("%%%%%%%%%%%%%%%%%%%%%%%%%"); console.log(this.faces); console.log(this.faces[face]); for (let vertex = 0; vertex < this.faces[face].length; vertex++) { console.log(vertex+" vertex bef dddddddddddddddddddddddddddddd") var vertex2 = this.faces[face][vertex] console.log(vertex2+" vertex aft ddddddddd ") if (moved_to_first_yet == false) { moved_to_first_yet = true ctx.moveTo( each_point[this.vertices[vertex2].id][0],each_point[this.vertices[vertex2].id][1]); } else{ctx.lineTo( each_point[this.vertices[vertex2].id][0],each_point[this.vertices[vertex2].id][1]);} } ctx.closePath(); ctx.fill(); } } } class two_d_surdace { constructor(verex1,verex2,verex3,verex4 , colour){ this.vertices = [verex1,verex2,verex3,verex4] this.colour = colour } draw_all_faces(){ var each_point = [] for (let i = 0; i < this.vertices.length; i++) { each_point.push(this.vertices[i].return_screen_projection(true)) } ctx.fillStyle = this.colour; var moved_to_first_yet = false for (let vertex = 0; vertex < this.vertices.length; vertex++) { console.log(each_point[vertex][0]+" , "+each_point[vertex][1]+ " actual x y points on screen for this vertex of corner of floor ") if (moved_to_first_yet == false) { moved_to_first_yet = true ctx.moveTo( each_point[vertex][0],each_point[vertex][1]); } else{ctx.lineTo( each_point[vertex][0],each_point[vertex][1]);} } ctx.closePath(); ctx.fill(); } } function if_off_screen(x, y) { if (x> canvas.width || x < 0){console.log(x +" x = off screen "); return true;} if (y > canvas.height || y < 0){console.log(y +" y = off screen "); return true;} console.log(x +" , "+y + " =x,y = not off screen "); return false; } function if_most_of_these_numbers_are_off_screen(numbers){ var threshold = 1; //Math.floor(numbers.length*0.49) var counter = 0 console.log(numbers + " xxxx numbers as they come in ") for (let i = 0; i < numbers.length; i++) { if (if_off_screen(numbers[i][0], numbers[i][1])){console.log(numbers[i]+" , "+numbers[i+1]+ " = numbers[i] are off screen"); counter +=1} else{console.log(numbers[i]+" , "+numbers[i+1]+ " = numbers[i] not off screen")} } console.log("quuin quoirs of raptor"); if (counter >= threshold){console.log(threshold+" < " + counter);return true} console.log(threshold+" > " + counter); return false; } player = new player(); projection = new Projection() objects = [] // floor = new two_d_surdace(new vertex(50,floor_y_pos,50) , new vertex(-50,floor_y_pos,50) , new vertex(-50,floor_y_pos,-50) , new vertex(50,floor_y_pos,-50) , '#F90' ) update_radar() $(document).on("keypress", function (event) { player.move(event) ctx.beginPath(); radar_ctx.beginPath(); radar_ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i < objects.length; i++) { objects[i].draw_all_faces() objects[i].draw_all_vertices() } floor.draw_all_faces() update_radar() }); </script> </body>
Filter Fabricjs to remove the red-eye effect or change part of the image object
I'm doing a red-eye filter for fabricjs: (function(global) { 'use strict'; var filters = fabric.Image.filters, createClass = fabric.util.createClass; filters.NoRedEyes = createClass(filters.BaseFilter, { type: 'NoRedEyes', fragmentSource: 'precision highp float;\n' + 'uniform sampler2D uTexture;\n' + 'uniform float uMyParameter;\n' + 'varying vec2 vTexCoord;\n' + 'void main() {\n' + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + // add your gl code here 'gl_FragColor = color;\n' + '}', myParameter: 0, mainParameter: 'myParameter', applyTo: function (options) { console.log(options); var canvasEl = options.targetCanvas; var context = canvasEl.getContext('2d'), imageData = context.getImageData(this.left, this.top, this.width, this.height), data = imageData.data; var p = imageData.width * imageData.height, pix = p * 4, r, g, b; while (p--) { pix -= 4; r = data[pix]; b = data[pix + 1]; g = data[pix + 2]; if (parseFloat(r / (g + b) / 2) > 0.5) // лучший результат - 0.4 / 1.5 because it gives the best results { imageData.data[pix] = Math.round((g + b) / 2); } } context.putImageData(imageData, this.left, this.top); }, }); fabric.Image.filters.NoRedEyes.fromObject = fabric.Image.filters.BaseFilter.fromObject; })(typeof exports !== 'undefined' ? exports : this); I have errors: Error: WebGL warning: readPixels: Framebuffer not complete. (status: 0x8cd7) fabric.js:19844:3 Error: WebGL warning: readPixels: Framebuffer must be complete.
Fabric custom object
fabric.ThreePointArc = fabric.util.createClass(fabric.Circle, { type: 'threePointArc', points: [], //array of startPoint, intermediatePoint, endPoint arcCenter: new fabric.Point(null, null), arcBounds: null, radius: null, initialize: function (points, options) { if (!points || points.length === 0) { return; } this.points = points; this.callSuper('initialize', options); // supports only originX and originY as center this.originX = this.originY = 'center'; }, _set: function(key, value) { this.callSuper('_set', key, value); if (key === 'points') { this._calcArcCenter(); this._calcDimensions(); this.setCoords(); } return this; }, setRadius: function(value) { this.radius = value; return this; }, _calcDimensions: function() { this._calcArcAngles(); this.setRadius(cMath.getLength(this.arcCenter, this.points[0])); this._calcArcBounds(); this.width = this.arcBounds.width; this.height = this.arcBounds.height; this.top = this.arcBounds.y + this.arcBounds.height / 2; this.left = this.arcBounds.x + this.arcBounds.width / 2; }, _calcArcCenter: function() { var c1Mp = cMath.getMidPoint(this.points[0], this.points[1]), // chord 1 midpoint c2Mp = cMath.getMidPoint(this.points[1], this.points[2]), // chord 2 midpoint c1pm = -(1 / cMath.getSlope(this.points[0], this.points[1])), // chord 1 perpendicular bisector slope c2pm = -(1 / cMath.getSlope(this.points[1], this.points[2])); // chord 2 perpendicular bisector slope // chord perpendicular bisectors meet at the center this.arcCenter.x = (c2Mp.y - (c2pm * c2Mp.x) + (c1pm * c1Mp.x) - c1Mp.y) / (c1pm - c2pm); this.arcCenter.y = (c2pm * (this.arcCenter.x - c2Mp.x)) + c2Mp.y; }, _calcArcBounds: function() { var validPoints = this.buildValidPointsForArc(), minX = fabric.util.array.min(validPoints, 'x'), minY = fabric.util.array.min(validPoints, 'y'), maxX = fabric.util.array.max(validPoints, 'x'), maxY = fabric.util.array.max(validPoints, 'y'), width = (maxX - minX) || 1, height = (maxY - minY) || 1; this.arcBounds = { x: minX, y: minY, width: width, height: height } }, buildValidPointsForArc: function() { var direction = this.getRenderingDirection(), possibleBoundingPoints = this.points.concat(); !this.arcAngles && this._calcArcAngles(); if (direction) { for (var i = 1; i <= 4; i++) { var randomAngle = i * (PI / 2); if (this.arcAngles.startAngle < this.arcAngles.endAngle) { !(this.arcAngles.startAngle <= randomAngle && randomAngle <= this.arcAngles.endAngle) && possibleBoundingPoints.push(this.generateArcPoint(randomAngle)); } else { (this.arcAngles.endAngle <= randomAngle && randomAngle <= this.arcAngles.startAngle) && possibleBoundingPoints.push(this.generateArcPoint(randomAngle)); } } } else { for (var i = 4; i >= 1; i--) { var randomAngle = i * (PI / 2); if (this.arcAngles.startAngle < this.arcAngles.endAngle) { (this.arcAngles.startAngle <= randomAngle && randomAngle <= this.arcAngles.endAngle) && possibleBoundingPoints.push(this.generateArcPoint(randomAngle)); } else { !(this.arcAngles.endAngle <= randomAngle && randomAngle <= this.arcAngles.startAngle) && possibleBoundingPoints.push(this.generateArcPoint(randomAngle)); } } } return possibleBoundingPoints; }, generateArcPoint: function(angle) { return new fabric.Point(this.arcCenter.x + this.radius * Math.cos(angle), this.arcCenter.y + this.radius * Math.sin(angle)); }, _calcArcAngles: function() { var angleKeyRepo = ["startAngle", "intermediateAngle", "endAngle"]; this.arcAngles = this.arcAngles || {}; this.points.forEach(function(point, index) { var a = cMath.getAngle(this.arcCenter, point); this.arcAngles[angleKeyRepo[index]] = a < 0 ? ((PI * 2) + a) : a > 2 * PI ? ((PI * 2) - a) : a; }, this); }, getRenderingDirection: function() { return (((this.points[1].x - this.points[0].x) * (this.points[2].y - this.points[0].y)) - ((this.points[1].y - this.points[0].y) * (this.points[2].x - this.points[0].x))) < 0; }, _render: function(ctx, noTransform) { if (!this.visible) { return; } ctx.beginPath(); ctx.arc( noTransform ? this.left : 0, noTransform ? this.top : 0, this.radius, this.arcAngles.startAngle, this.arcAngles.endAngle, this.getRenderingDirection() ); this._renderFill(ctx); this._renderStroke(ctx); }, toObject: function (propertiesToInclude) { return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), { points: this.points }); } }); fabric.ThreePointArc.fromObject = function(object) { return new fabric.ThreePointArc(object.points, object); }; fabric.ThreePointArc.async = false; fabric.util.cMath = { getSlope : function (startPoint, endPoint) { if (!startPoint || !endPoint) { console.error('startPoint and endPoint are required to evaluate slope'); return; } // hack to get around the indefinte slope problem if (endPoint.x == startPoint.x) startPoint.x = startPoint.x + 0.01; if (endPoint.y == startPoint.y) endPoint.y = endPoint.y + 0.01; return (endPoint.y - startPoint.y) / (endPoint.x - startPoint.x); }, getMidPoint: function (startPoint, endPoint) { if (!startPoint || !endPoint) { console.error('startPoint and endPoint are required to evaluate slope'); return; } return { x: (startPoint.x + endPoint.x) / 2, y: (startPoint.y + endPoint.y) / 2 }; }, getAngle: function (startPoint, endPoint, isDegree) { if (!startPoint || !endPoint) { console.error('startPoint and endPoint are required to evaluate slope'); return; } var radians = Math.atan2((endPoint.y - startPoint.y), (endPoint.x - startPoint.x)), degrees = fabric.util.radiansToDegrees(radians); return isDegree ? degrees < 0 ? 360 + degrees : degrees : radians; }, getLength: function (startPoint, endPoint) { if (!startPoint || !endPoint) { console.error('startPoint and endPoint are required to evaluate slope'); return; } return Math.sqrt(Math.pow(endPoint.y - startPoint.y, 2) + Math.pow(endPoint.x - startPoint.x, 2)); } } var canvas = new fabric.Canvas('c'); var startPoint = new fabric.Point(47.25423728813553, 56.91525423728814), intermediatePoint = new fabric.Point( 76.33898305084739,19.8983050847458 ), endPoint = new fabric.Point( 105.42372881355931,86 ); var arc = new fabric.ThreePointArc([startPoint, intermediatePoint, endPoint] , { fill: "#FF0000", stroke: "#000", strokeWidth: 10 }); canvas.add(arc); <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.4.10/fabric.js"></script> <canvas id="c" width="800" height="800"></canvas> I'm trying to create a three point arc on fabric js (ver: 1.4.10). What I want is exactly like this:- https://www.screencast.com/t/dMLvcOduEF I have a points array from which I'm calcualting the startAngle, endAngle, arcCenter, radius, bounding boxc height and widht, top and left. When I draw it, the actual object is not drawing where my mouse points. It adds up the left and top value with the arcCenter.x and arcCenter.y respectively and draws there. Is there any workaround so that my arc can be drawn where my pointer is ?. So that I don't need to write any extra lines of code for moving, rotating and scaling. What I'm getting is like this:- https://www.screencast.com/t/V9MUgB3pB added the fiddle!! Is there any workaround for this ??
nodejs split image into chunks
Is there a nodeJS-way to split an image into single chunks? Probably with different dimensions? Like the photos on image below
I've done something like this using jimp, however it can probably be much more concise... var Jimp = require("jimp"); /** * #param filename - input file * #param numSlices - how many slices * #param attenuation - how quickly slices get smaller */ function sliceImage(filename, numSlices, attenuation) { Jimp.read(filename).then(image => { let w = image.bitmap.width; let h = image.bitmap.height; let sliceWidth = w / numSlices; let midlane = w / 2; let slack = 0.001; let slices = []; function slicePair(left, right) { if (left < (0 - slack) || (right - sliceWidth) > (w + slack)) { return; } let leftSlice = image.clone(); let rightSlice = image.clone(); leftSlice.crop(left, 0, sliceWidth, h); rightSlice.crop(right - sliceWidth, 0, sliceWidth, h); slices.push(leftSlice); slices.push(rightSlice); slicePair(left - sliceWidth, right + sliceWidth); } function doSlice() { if (numSlices % 2 == 0) { slicePair(midlane - sliceWidth, midlane + sliceWidth); } else { let middle = image.clone(); middle.crop(midlane - (sliceWidth / 2), 0, sliceWidth, h); slices.push(middle); slicePair(midlane - (1.5 * sliceWidth), midlane + (1.5 * sliceWidth)); } } function overlay() { let canvas = image.clone().brightness(1); let odd = !(slices.length % 2 === 0); let adjust = odd ? (sliceWidth / 2) : 0; slices.reverse(); if (odd) { let middle = slices.pop(); canvas.composite(middle, (midlane - (sliceWidth / 2)), 0); } for (let i = 0; i < (slices.length + 2); i++) { let k = odd ? (i + 1) : i; let left = slices.pop().crop(0, attenuation * k, sliceWidth, (h - (attenuation * k) * 2)); let right = slices.pop().crop(0, attenuation * k, sliceWidth, (h - (attenuation * k) * 2)); canvas.composite(left, (midlane - ((i + 1) * sliceWidth)) - adjust, (attenuation * k)); canvas.composite(right, (midlane + (i * sliceWidth)) + adjust, (attenuation * k)); } canvas.write("result.jpg"); return canvas; } doSlice(); return overlay(); }); }
const Jimp = require('jimp') ; const forloop=require('async-for-loop'); const width=1000, height=716,h=7,v=7; async function crop(source,filename,offSetX, offSetY, width,height,cb) { // Reading Image const image = await Jimp.read (source); image.crop(offSetX, offSetY, width,height) .write(filename); } // forloop(h,(hi,nextH)=>{ forloop(v,async (vi,nextV)=>{ await crop('/Users/you/Projects/sliceit/imgs/image.jpg',`/Users/you/Projects/sliceit/out/slice_${hi}_${vi}.jpg`, hi*width,vi*height,width,height,nextV ); nextV(); },()=>{ nextH(); }) },()=>{ console.log("Image is processed successfully"); })
Why are my curved edges not updating correctly?
I'm trying to customize a layout of JGraph. I want to create curved edges, but I have a problem. Every time I create a group of vertex on JGraph, and I am firing an event to update this graph, the edge is missing points compared to the previous state. Here is a example: Can someone help me? Here is my code: class CurveGraphView extends mxGraphView { public CurveGraphView(mxGraph graph) { super(graph); } /* Only override this if you want the label to automatically position itself on the control point */ #Override public mxPoint getPoint(mxCellState state, mxGeometry geometry) { double x = state.getCenterX(); double y = state.getCenterY(); if (state.getAbsolutePointCount() == 3) { mxPoint mid = state.getAbsolutePoint(1); x = mid.getX(); y = mid.getY(); } return new mxPoint(x, y); } // /* Makes sure that the full path of the curve is included in the bounding box */ #Override public mxRectangle updateBoundingBox(mxCellState state) { List<mxPoint> points = state.getAbsolutePoints(); mxRectangle bounds = super.updateBoundingBox(state); Object style = state.getStyle().get("edgeStyle"); if (CurvedEdgeStyle.KEY.equals(style) && points != null && points.size() == 3) { Rectangle pathBounds = CurvedShape.createPath(state.getAbsolutePoints()).getBounds(); Rectangle union = bounds.getRectangle().union(pathBounds); bounds = new mxRectangle(union); state.setBoundingBox(bounds); } return bounds; } } class CurvedEdgeStyle implements mxEdgeStyle.mxEdgeStyleFunction { public static final String KEY = "curvedEdgeStyle"; #Override public void apply(mxCellState state, mxCellState source, mxCellState target, List<mxPoint> points, List<mxPoint> result) { mxPoint pt = (points != null && points.size() > 0) ? points.get(0) : null; if (source != null && target != null) { double x = 0; double y = 0; if (pt != null) { result.add(pt); } else { x = (target.getCenterX() + source.getCenterX()) / 2; y = (target.getCenterY() + source.getCenterY()) / 2; mxPoint point = new mxPoint(x, y); result.add(point); } } } } class CurvedShape extends mxConnectorShape { public static final String KEY = "curvedEdge"; private GeneralPath path; #Override public void paintShape(mxGraphics2DCanvas canvas, mxCellState state) { List<mxPoint> abs = state.getAbsolutePoints(); int n = state.getAbsolutePointCount(); mxCell aux = (mxCell)state.getCell(); if (n < 3) { super.paintShape(canvas, state); } else if (configureGraphics(canvas, state, false)) { Graphics2D g = canvas.getGraphics(); path = createPath(abs); g.draw(path); paintMarker(canvas, state, false); paintMarker(canvas, state, true); } } /* Code borrowed from here: http://www.codeproject.com/Articles/31859/Draw-a-Smooth-Curve-through-a-Set-of-2D-Points-wit */ public static GeneralPath createPath(List<mxPoint> abs) { mxPoint[] knots = abs.toArray(new mxPoint[abs.size()]); int n = knots.length - 1; mxPoint[] firstControlPoints = new mxPoint[n]; mxPoint[] secondControlPoints = new mxPoint[n]; // Calculate first Bezier control points // Right hand side vector double[] rhs = new double[n]; // Set right hand side X values for (int i = 1; i < n - 1; ++i) { rhs[i] = 4 * knots[i].getX() + 2 * knots[i + 1].getX(); } rhs[0] = knots[0].getX() + 2 * knots[1].getX(); rhs[n - 1] = (8 * knots[n - 1].getX() + knots[n].getX()) / 2.0; // Get first control points X-values double[] x = getFirstControlPoints(rhs); // Set right hand side Y values for (int i = 1; i < n - 1; ++i) { rhs[i] = 4 * knots[i].getY() + 2 * knots[i + 1].getY(); } rhs[0] = knots[0].getY() + 2 * knots[1].getY(); rhs[n - 1] = (8 * knots[n - 1].getY() + knots[n].getY()) / 2.0; // Get first control points Y-values double[] y = getFirstControlPoints(rhs); // Fill output arrays. for (int i = 0; i < n; ++i) { // First control point firstControlPoints[i] = new mxPoint(x[i], y[i]); // Second control point if (i < n - 1) { secondControlPoints[i] = new mxPoint(2 * knots[i + 1].getX() - x[i + 1], 2 * knots[i + 1].getY() - y[i + 1]); } else { secondControlPoints[i] = new mxPoint((knots[n].getX() + x[n - 1]) / 2, (knots[n].getY() + y[n - 1]) / 2); } } GeneralPath path = new GeneralPath(); path.moveTo(knots[0].getX(), knots[0].getY()); for (int i = 1; i < n + 1; i++) { path.curveTo(firstControlPoints[i - 1].getX(), firstControlPoints[i - 1].getY(), secondControlPoints[i - 1].getX(), secondControlPoints[i - 1].getY(), knots[i].getX(), knots[i].getY()); } return path; }/// <summary>/// Solves a tridiagonal system for one of coordinates (x or y)/// of first Bezier control points./// </summary>/// <param name="rhs">Right hand side vector.</param>/// <returns>Solution vector.</returns> private static double[] getFirstControlPoints(double[] rhs) { int n = rhs.length; double[] x = new double[n]; // Solution vector. double[] tmp = new double[n]; // Temp workspace. double b = 2.0; x[0] = rhs[0] / b; for (int i = 1; i < n; i++) // Decomposition and forward substitution. { tmp[i] = 1 / b; b = (i < n - 1 ? 4.0 : 3.5) - tmp[i]; x[i] = (rhs[i] - x[i - 1]) / b; } for (int i = 1; i < n; i++) { x[n - i - 1] -= tmp[n - i] * x[n - i]; // Backsubstitution. } return x; } #Override protected mxLine getMarkerVector(List<mxPoint> points, boolean source, double markerSize) { if (path == null || points.size() < 3) { return super.getMarkerVector(points, source, markerSize); } double coords[] = new double[6]; double x0 = 0; double y0 = 0; double x1 = 0; double y1 = 0; PathIterator p = path.getPathIterator(null, 2.0); if (source) { p.currentSegment(coords); x1 = coords[0]; y1 = coords[1]; p.next(); p.currentSegment(coords); x0 = coords[0]; y0 = coords[1]; } else { while (!p.isDone()) { p.currentSegment(coords); x0 = x1; y0 = y1; x1 = coords[0]; y1 = coords[1]; p.next(); } } return new mxLine(x0, y0, new mxPoint(x1, y1)); } } extends mxGraph and Override the constructor adding: mxGraphics2DCanvas.putShape(CurvedShape.KEY, new CurvedShape()); mxStyleRegistry.putValue(CurvedEdgeStyle.KEY, new CurvedEdgeStyle()); getStylesheet().getDefaultEdgeStyle().put(mxConstants.STYLE_SHAPE, CurvedShape.KEY); getStylesheet().getDefaultEdgeStyle().put(mxConstants.STYLE_EDGE, CurvedEdgeStyle.KEY); and Override createGraphView() returning CurveGraphView. Have told me it has to do something with absolute points.