Here's my GetSmoothPath() routine. It requires no specific graphics library to use, just a few extra functions (also included below).
This function generates an array of control points that's very easily converted into a flattened cubic bezier path using just about any 2D graphics library.
(nb: The code below has been written with simplicity as the focus rather than performance.)
uses
SysUtils, Math;
type
TPointD = record
X, Y: double;
end;
TPathD = array of TPointD;
TArrayOfDouble = array of double;
function DistanceSqrd(const pt1, pt2: TPointD): double;
begin
result := Sqr(pt1.X - pt2.X) + Sqr(pt1.Y - pt2.Y);
end;
function Distance(const pt1, pt2: TPointD): double;
begin
Result := Sqrt(DistanceSqrd(pt1, pt2));
end;
function OffsetPoint(const pt: TPointD; dx, dy: double): TPointD;
begin
result.x := pt.x + dx;
result.y := pt.y + dy;
end;
function GetAvgUnitVector(const vec1, vec2: TPointD): TPointD;
var
inverseHypot: Double;
begin
Result.X := (vec1.X + vec2.X) * 0.5;
Result.y := (vec1.Y + vec2.Y) * 0.5;
inverseHypot := 1 / Hypot(Result.X, Result.Y);
Result.X := Result.X * inverseHypot;
Result.Y := Result.Y * inverseHypot;
end;
procedure MakeSymmetric(var val1, val2: double);
begin
val1 := (val1 + val2) * 0.5;
val2 := val1;
end;
function GetUnitVector(const pt1, pt2: TPointD): TPointD;
var
dx, dy, inverseHypot: Double;
begin
if (pt1.x = pt2.x) and (pt1.y = pt2.y) then
begin
Result.X := 0;
Result.Y := 0;
Exit;
end;
dx := (pt2.X - pt1.X);
dy := (pt2.Y - pt1.Y);
inverseHypot := 1 / Hypot(dx, dy);
dx := dx * inverseHypot;
dy := dy * inverseHypot;
Result.X := dx;
Result.Y := dy;
end;
// GetSmoothPath - returns cubic bezier control points
// parameters: 1. path for smoothing
// 2. whether or not the smoothed path will closed
// 3. percent smoothness (0..100)
// 4. maximum dist control pts from path pts (0 = no limit)
// 5. symmetric vs asymmmetric control pts
function GetSmoothPath(const path: TPathD; pathIsClosed: Boolean;
percentOffset, maxCtrlOffset: double;
symmetric: Boolean): TPathD;
var
i, len, prev: integer;
vec: TPointD;
pl: TArrayOfDouble;
unitVecs: TPathD;
d, d1,d2: double;
begin
Result := nil;
len := Length(path);
if len < 3 then Exit;
d := Max(0, Min(100, percentOffset))/200;
if maxCtrlOffset <= 0 then maxCtrlOffset := MaxDouble;
SetLength(Result, len *3 +1);
prev := len-1;
SetLength(pl, len);
SetLength(unitVecs, len);
for i := 0 to len -1 do
begin
pl[i] := Distance(path[prev], path[i]);
unitVecs[i] := GetUnitVector(path[prev], path[i]);
prev := i;
end;
Result[len*3] := path[0];
for i := 0 to len -1 do
begin
if i = len -1 then
begin
vec := GetAvgUnitVector(unitVecs[i], unitVecs[0]);
d2 := pl[0]*d;
end else
begin
vec := GetAvgUnitVector(unitVecs[i], unitVecs[i+1]);
d2 := pl[i+1]*d;
end;
d1 := pl[i]*d;
if symmetric then MakeSymmetric(d1, d2);
if i = 0 then
Result[len*3-1] := OffsetPoint(path[i],
-vec.X * Min(maxCtrlOffset, d1), -vec.Y * Min(maxCtrlOffset, d1))
else
Result[i*3-1] := OffsetPoint(path[i],
-vec.X * Min(maxCtrlOffset, d1), -vec.Y * Min(maxCtrlOffset, d1));
Result[i*3] := path[i];
Result[i*3+1] := OffsetPoint(path[i],
vec.X * Min(maxCtrlOffset, d2), vec.Y * Min(maxCtrlOffset, d2));
end;
if not pathIsClosed then
begin
Result[1] := Result[0];
dec(len);
Result[len*3-1] := Result[len*3];
SetLength(Result, Len*3 +1);
end;
end;
And here's what it produces ...
the path to smooth (black),
the cubic bezier control path produced by GetSmoothPath() (blue)
and the flattened cubic bezier path (2D graphics library of you choice required) (red).
var
TPathD path;
begin
path := MakePath([190,120, 260,270, 560,120, 190,490]);
path := GetSmoothPath(path, true, 20, 0, false);
path := ThirdParty2DGraphicsLibrary.FlattenCBezier(path);
end;
var
TPathD path;
begin
path := MakePath([190,120, 260,270, 560,120, 190,490]);
path := GetSmoothPath(path, true, 80, 0, false);
path := ThirdParty2DGraphicsLibrary.FlattenCBezier(path);
end;
Edit: The best way to avoid intersections is to make sure you have enough data points before generating your curves.