diff --git a/examples/bezier/.gitignore b/examples/bezier/.gitignore new file mode 100644 index 00000000..7c617ab4 --- /dev/null +++ b/examples/bezier/.gitignore @@ -0,0 +1,14 @@ +.dub +docs.json +__dummy.html +docs/ +bezier.so +bezier.dylib +bezier.dll +bezier.a +bezier.lib +bezier-test-* +*.exe +*.o +*.obj +*.lst diff --git a/examples/bezier/bezier_sample.png b/examples/bezier/bezier_sample.png new file mode 100644 index 00000000..2e4a6c0b Binary files /dev/null and b/examples/bezier/bezier_sample.png differ diff --git a/examples/bezier/dub.json b/examples/bezier/dub.json new file mode 100644 index 00000000..2488598f --- /dev/null +++ b/examples/bezier/dub.json @@ -0,0 +1,32 @@ +{ + "name": "bezier", + "description": "dlangui bezier curves samples", + "license": "Boost", + + "targetPath": "bin", + "targetType": "executable", + "targetName": "bezier", + + "sourceFiles-windows-x86-dmd": ["$PACKAGE_DIR/../../src/win_app.def"], + + "dependencies": { + "dlangui": {"path": "../../"} + }, + "configurations" : [ + { + "name" : "default" + }, + { + "name" : "sdl", + "subConfigurations" : { + "dlangui" : "sdl" + } + }, + { + "name" : "x11", + "subConfigurations" : { + "dlangui" : "x11" + } + } + ] +} diff --git a/examples/bezier/source/app.d b/examples/bezier/source/app.d new file mode 100644 index 00000000..760716cc --- /dev/null +++ b/examples/bezier/source/app.d @@ -0,0 +1,230 @@ +module app; + +static assert(ENABLE_OPENGL, "All bezier samples in this module using + floating point drawing functions which is not supported in minimal config"); + +import dlangui; + +import std.algorithm.comparison; + +mixin APP_ENTRY_POINT; + +// helper for scaling relative to average 96dpi FullHD, IDK but maybe a bad idea after all +T scaledByDPI(T)(T val) { + return val *= (SCREEN_DPI()/cast(T)96); +} + +/// Entry point for dlangui based application +extern (C) int UIAppMain(string[] args) { + // portrait "mode" window + Window window = Platform.instance.createWindow("Bezier curves", null, WindowFlag.Resizable, 480.scaledByDPI, 600.scaledByDPI); + window.mainWidget = new BezierSamples(); + window.show(); + return Platform.instance.enterMessageLoop(); +} + +class BezierSamples : VerticalLayout { + + this() { + this(null); + } + this(string id) { + super(id); + addChild(new CubicTraceSample); + addChild(new FlattenCubicSample); + addChild(new ColoredCubicTraceSample); + addChild(new FlattenCubicGuidesSample); + addChild(new FlattenQuadraticSample); + } + + override bool animating() { return true; } +} + + +abstract class SampleCanvas : CanvasWidget { + + dstring _sampleName = "Bezier sample"; + static immutable vec2[] _controlPointsDefaultRatios = [vec2(0,0.2), vec2(0.2,0.2), vec2(0.8,0.8), vec2(1,0.8)]; + static immutable vec2[] _controlPointsQuadratic = [vec2(0.2,0.8), vec2(0.7,0.4), vec2(0.3,0.2)]; + + this() { + fillHorizontal(); + auto p = 5.scaledByDPI; + margins(Rect(p, p, p, p)); + p = 15.scaledByDPI; + padding(Rect(p, p, p, p)); + minHeight = 250.scaledByDPI; + } + + dstring sampleName() { return _sampleName; } + + override protected void measuredContent(int parentWidth, int parentHeight, int contentWidth, int contentHeight) { + _measuredWidth = max(minHeight, contentWidth); + _measuredHeight = minHeight; + } + + protected void drawText(DrawBuf buf, Rect rc, dstring text) { + FontRef font = font(); + Point sz = font.textSize(text); + applyAlign(rc, sz, Align.HCenter, Align.Bottom ); + font.drawText(buf, rc.left, rc.top, text, textColor, 4, 0, textFlags); + } + + protected void calcRectSize(const vec2[] controlPoints, vec2[] result) { + assert(result.length >= controlPoints.length); + auto r = _pos; + applyMargins(r); + applyPadding(r); + vec2 pos = vec2(r.left, r.top); + vec2 size = vec2(r.width, r.height); + result[] = controlPoints[]; // copy points + result[0] = result[0].mul(size) + pos; + result[1] = result[1].mul(size) + pos; + result[2] = result[2].mul(size) + pos; + if(controlPoints.length > 3) + result[3] = result[3].mul(size) + pos; + } + + // override to draw + override void doDraw(DrawBuf buf, Rect rc) { + } +} + +class CubicTraceSample : SampleCanvas { + this() { + _sampleName = "Cubic bezier curve drawn with ellipses (slow, high overdraw)"; + } + override void doDraw(DrawBuf buf, Rect rc) { + vec2[4] points; + calcRectSize(_controlPointsDefaultRatios, points); + auto len = (points[0]-points[3]).magnitude; + auto interval = 1f/len; + auto step = interval; + // evaluate normal bezier curve and trace with circles + foreach ( i ; 0..len ) { + auto b = bezierCubic(points, interval); + interval+=step; + buf.drawEllipseF(b.x, b.y, 3, 3, 0, Color.black, Color.black); + } + drawText(buf,rc, sampleName()); + } +} + +class FlattenCubicSample : SampleCanvas { + this() { + _sampleName = "Flattened cubic bezier curve drawn with lines (fast)"; + } + override void doDraw(DrawBuf buf, Rect rc) { + vec2[4] points; + calcRectSize(_controlPointsDefaultRatios, points); + enum segments = 10; + auto lines = flattenBezierCubic(points, segments); + buf.polyLineF(lines, 3f.scaledByDPI, Color.black); + drawText(buf,rc, sampleName()); + } +} + +class ColoredCubicTraceSample : SampleCanvas { + this() { + _sampleName = "Simple colored cubic bezier curve drawn with ellipsises"; + } + override void doDraw(DrawBuf buf, Rect rc) { + vec2[4] points; + calcRectSize(_controlPointsDefaultRatios, points); + auto len = (points[0]-points[3]).magnitude; + auto interval = 1/len; + auto step = interval; + // evaluate normal bezier curve and trace with circles + foreach ( i ; 0..len ) { + auto b = bezierCubic(points, interval); + interval+=step; + buf.drawEllipseF(b.x, b.y, 3, 3, 0, COLOR_TRANSPARENT, lerpColor01(Color.blue, Color.red, interval)); + } + drawText(buf,rc, sampleName()); + } + + // clamp to [0,1] and lerp color + static uint lerpColor01(uint a, uint b, float ratio) { + ratio = clamp(ratio, 0, 1); + return blendARGB(b, a, cast(uint)(ratio * 255)); + } +} + +class FlattenCubicGuidesSample : SampleCanvas { + this() { + _sampleName = "Flattened cubic bezier curve with direction and normal vectors"; + } + override void doDraw(DrawBuf buf, Rect rc) { + vec2[4] points; + calcRectSize(_controlPointsDefaultRatios, points); + enum segmentsCount = 10; + auto lines = flattenBezierCubic(points, segmentsCount); + drawCubicControlsGuides(buf, points); + buf.polyLineF(lines, 3f.scaledByDPI, Color.black); + drawCubicSegmentsNormDir(buf, points, segmentsCount, lines, true, true); + drawText(buf,rc, sampleName()); + } +} + +class FlattenQuadraticSample : SampleCanvas { + this() { + _sampleName = "Flattened quadratic bezier curve"; + } + override void doDraw(DrawBuf buf, Rect rc) { + vec2[3] points; + calcRectSize(_controlPointsQuadratic, points); + + // guide lines + buf.drawLineF(points[0], points[1], 1, Color.dark_gray); + buf.drawLineF(points[1], points[2], 1, Color.dark_gray); + + + enum segmentCount = 10; + auto lines = flattenBezierQuadratic(points, segmentCount); + buf.polyLineF(lines, 3f.scaledByDPI, Color.black); + + // end points + buf.drawEllipseF(points[0].x, points[0].y, 5,5, 1, Color.antique_white, Color.cyan); + buf.drawEllipseF(points[2].x, points[2].y, 5,5, 1, Color.antique_white, Color.cyan); + buf.drawEllipseF(points[1].x, points[1].y, 3,3, 0, Color.antique_white, Color.cyan); + + // draw the direction & normal vectors + auto segStep = 1f/segmentCount; + foreach(i; 0..segmentCount) { + auto dir = bezierQuadraticDirection(points, i*segStep); + auto norm = dir.rotated90ccw; + auto point = lines[i]; + buf.drawLineF(point, point + (dir * 15f), 3.scaledByDPI, Color.yellow ); + buf.drawLineF(point, point + (norm * 15f), 3.scaledByDPI, Color.red ); + } + + drawText(buf,rc, sampleName()); + } +} + + + +void drawCubicControlsGuides(DrawBuf buf, vec2[] controls) { + buf.drawLineF(controls[0], controls[1], 1, Color.black); + buf.drawLineF(controls[2], controls[3], 1, Color.black); + buf.drawEllipseF(controls[0].x, controls[0].y, 5,5, 1, Color.antique_white, Color.cyan); + buf.drawEllipseF(controls[3].x, controls[3].y, 5,5, 1, Color.antique_white, Color.cyan); + buf.drawEllipseF(controls[1].x, controls[1].y, 3,3, 0, Color.antique_white, Color.cyan); + buf.drawEllipseF(controls[2].x, controls[2].y, 3,3, 0, Color.antique_white, Color.cyan); +} + +void drawCubicSegmentsNormDir(DrawBuf buf, vec2[] controls, int segmentCount, vec2[] segments, bool direction, bool normals) { + if ( !direction && !normals && segments.length < 2) + return; + // draw the direction & normal vectors + auto segStep = 1f/segmentCount; + foreach(i; 0..segmentCount) { + auto dir = bezierCubicDirection(controls, i*segStep); + auto norm = dir.rotated90ccw; + auto point = segments[i]; + if ( direction ) + buf.drawLineF(point, point + (dir * 15f), 3.scaledByDPI, Color.yellow ); + if ( normals ) + buf.drawLineF(point, point + (norm * 15f), 3.scaledByDPI, Color.red ); + } +} \ No newline at end of file diff --git a/src/dlangui/core/math3d.d b/src/dlangui/core/math3d.d index 424212f7..19f627af 100644 --- a/src/dlangui/core/math3d.d +++ b/src/dlangui/core/math3d.d @@ -1740,3 +1740,143 @@ vec3 triangleNormal(float[3] p1, float[3] p2, float[3] p3) { /// Alias for 2d float point alias PointF = vec2; + + + + +// this form can be used within shaders +/// cubic bezier curve +PointF bezierCubic(const PointF[] cp, float t) pure @nogc @safe + in { assert(cp.length > 3); } do +{ + // control points + auto p0 = cp[0]; + auto p1 = cp[1]; + auto p2 = cp[2]; + auto p3 = cp[3]; + + float u1 = (1.0 - t); + float u2 = t * t; + // the polynomials + float b3 = u2 * t; + float b2 = 3.0 * u2 * u1; + float b1 = 3.0 * t * u1 * u1; + float b0 = u1 * u1 * u1; + // cubic bezier interpolation + PointF p = p0 * b0 + p1 * b1 + p2 * b2 + p3 * b3; + return p; +} + +/// quadratic bezier curve (not tested) +PointF bezierQuadratic(const PointF[] cp, float t) pure @nogc @safe + in { assert(cp.length > 2); } do +{ + auto p0 = cp[0]; + auto p1 = cp[1]; + auto p2 = cp[2]; + + float u1 = (1.0 - t); + float u2 = u1 * u1; + + float b2 = t * t; + float b1 = 2.0 * u1 * t; + float b0 = u2; + + PointF p = p0 * b0 + p1 * b1 + p2 * b2; + return p; +} + +/// cubic bezier (first) derivative +PointF bezierCubicDerivative(const PointF[] cp, float t) pure @nogc @safe + in { assert(cp.length > 3); } do +{ + auto p0 = cp[0]; + auto p1 = cp[1]; + auto p2 = cp[2]; + auto p3 = cp[3]; + + float u1 = (1.0 - t); + float u2 = t * t; + float u3 = 6*(u1)*t; + float d0 = 3 * u1 * u1; + // -3*P0*(1-t)^2 + P1*(3*(1-t)^2 - 6*(1-t)*t) + P2*(6*(1-t)*t - 3*t^2) + 3*P3*t^2 + PointF d = p0*(-d0) + p1*(d0 - u3) + p2*(u3 - 3*u2) + (p3*3)*u2; + return d; +} + +/// quadratic bezier (first) derivative +PointF bezierQuadraticDerivative(const PointF[] cp, float t) pure @nogc @safe + in { assert(cp.length > 2); } do +{ + auto p0 = cp[0]; + auto p1 = cp[1]; + auto p2 = cp[2]; + + float u1 = (1.0 - t); + // -2*(1-t)*(p1-p0) + 2*t*(p2-p1); + PointF d = (p0-p1)*-2*u1 + (p2-p1)*2*t; + return d; +} + +// can't be pure due to normalize & vec2 ctor +/// evaluates cubic bezier direction(tangent) at point t +PointF bezierCubicDirection(const PointF[] cp, float t) { + auto d = bezierCubicDerivative(cp,t); + d.normalize(); + return PointF(tan(d.x), tan(d.y)); +} + +/// evaluates quadratic bezier direction(tangent) at point t +PointF bezierQuadraticDirection(const PointF[] cp, float t) { + auto d = bezierQuadraticDerivative(cp,t); + d.normalize(); + return PointF(tan(d.x), tan(d.y)); +} + +/// templated version of bezier flatten curve function, allocates temporary buffer +PointF[] flattenBezier(alias BezierFunc)(const PointF[] cp, int segmentCountInclusive) + if (is(typeof(BezierFunc)==function)) +{ + if (segmentCountInclusive < 2) + return PointF[].init; + PointF[] coords = new PointF[segmentCountInclusive+1]; + flattenBezier!BezierFunc(cp, segmentCountInclusive, coords); + return coords; +} + +/// flatten bezier curve function, writes to provided buffer instead of allocation +void flattenBezier(alias BezierFunc)(const PointF[] cp, int segmentCountInclusive, PointF[] outSegments) + if (is(typeof(BezierFunc)==function)) +{ + if (segmentCountInclusive < 2) + return; + float step = 1f/segmentCountInclusive; + outSegments[0] = BezierFunc(cp, 0); + foreach(i; 1..segmentCountInclusive) { + outSegments[i] = BezierFunc(cp, i*step); + } + outSegments[segmentCountInclusive] = BezierFunc(cp, 1f); +} + + +/// flattens cubic bezier curve, returns PointF[segmentCount+1] array or empty array if <1 segments +PointF[] flattenBezierCubic(const PointF[] cp, int segmentCount) { + return flattenBezier!bezierCubic(cp,segmentCount); +} + +/// flattens quadratic bezier curve, returns PointF[segmentCount+1] array or empty array if <1 segments +PointF[] flattenBezierQuadratic(const PointF[] cp, int segmentCount) { + return flattenBezier!bezierQuadratic(cp, segmentCount); +} + +/// calculates normal vector at point t using direction +PointF bezierCubicNormal(const PointF[] cp, float t) { + auto d = bezierCubicDirection(cp, t); + return d.rotated90ccw; +} + +/// calculates normal vector at point t using direction +PointF bezierQuadraticNormal(const PointF[] cp, float t) { + auto d = bezierQuadraticDerivative(cp, t); + return d.rotated90ccw; +}