HaxePunk/HaxePunk

View on GitHub
haxepunk/utils/DrawContext.hx

Summary

Maintainability
Test Coverage
package haxepunk.utils;

import haxepunk.utils.BlendMode;
import haxepunk.Entity;
import haxepunk.HXP;
import haxepunk.Graphic;
import haxepunk.graphics.hardware.DrawCommand;
import haxepunk.graphics.shader.ColorShader;
import haxepunk.graphics.shader.Shader;
import haxepunk.math.Vector2;
import haxepunk.utils.Color;

class DrawContext
{
    /**
     * The Scene to draw to. If null, will draw to the current active scene.
     */
    public var scene:Scene;

    /**
     * The blending mode used by Draw functions. This will not
     * apply to Draw.line(), but will apply to Draw.linePlus().
     */
    public var blend:BlendMode = BlendMode.Alpha;

    /**
     * The shader used by Draw functions. This will default to
     * a color shader if not set.
     */
    public var shader:Shader;

    /**
     * Whether shapes should be drawn with antialiasing.
     */
    public var smooth:Bool = true;

    /**
     * The red, green, and blue values in a single integer value.
     */
    public var color:Color = 0xFFFFFF;

    /**
     * The alpha value to draw. Ranges between 0-1 where 0 is completely transparent and 1 is opaque.
     */
    public var alpha:Float = 1;

    /**
     * If true, scale coordinates using the current screen scale.
     */
    public var scale:Bool = false;

    /**
     * The line thickness to use when drawing lines. Defaults to a single pixel wide.
     */
    public var lineThickness:Float = 1;

    public function new(lineThickness:Float = 1, color:Color = Color.White, alpha:Float = 1)
    {
        this.lineThickness = lineThickness;
        this.color = color;
        this.alpha = alpha;
    }

    /**
     * Convenience function to set both color and alpha at the same time.
     */
    public inline function setColor(color:Color = 0xFFFFFF, alpha:Float = 1)
    {
        this.color = color;
        this.alpha = alpha;
    }

    /**
     * Draws a straight line.
     * @param    x1            Starting x position.
     * @param    y1            Starting y position.
     * @param    x2            Ending x position.
     * @param    y2            Ending y position.
     */
    public function line(x1:Float, y1:Float, x2:Float, y2:Float)
    {
        // create perpendicular delta vector
        var a = new Vector2(x1, y1);
        var b = new Vector2(x2 - a.x, y2 - a.y);
        b.normalize(lineThickness / 2);
        b.perpendicular();

        begin();
        drawQuad(
            x1 + b.x, y1 + b.y,
            x1 - b.x, y1 - b.y,
            x2 - b.x, y2 - b.y,
            x2 + b.x, y2 + b.y
        );
    }

    /**
     * Draws a triangulated line polyline to the screen. This must be a closed loop of concave lines
     * @param    points        An array of floats containing the points of the polyline. The array is ordered in x, y format and must have an even number of values.
     */
    public function polyline(points:Array<Float>, miterJoint:Bool = false)
    {
        if (points.length < 4 || (points.length % 2) == 1)
        {
            throw "Invalid number of values. Expected an even number greater than 4.";
        }

        var halfThick = lineThickness / 2;
        var last = Std.int(points.length / 2);
        var a        = new Vector2(),
            b        = new Vector2(),
            pos      = new Vector2(points[0], points[1]), // current
            prev     = new Vector2(pos.x - points[2], pos.y - points[3]), // direction
            next     = new Vector2(prev.x, prev.y),
            inner    = new Vector2(),
            outer    = new Vector2(),
            nextPrev = new Vector2();
        begin();

        a.setTo(pos.x, pos.y);
        b.setTo(pos.x, pos.y);

        // calculate first cap
        next.perpendicular();
        next.normalize(halfThick);
        a.add(next);
        b.subtract(next);

        prev.normalize(1); // unit length

        var over180:Bool, angle:Float, index:Int;

        for (i in 1...last-1)
        {
            index = i * 2;

            pos.x = points[index];
            pos.y = points[index+1];

            // vector v (difference between current and next)
            next.x = pos.x - points[index + 2];
            next.y = pos.y - points[index + 3];

            next.normalize(1); // unit length
            nextPrev.copyFrom(next); // we clobber the "next" value so it needs to be saved

            over180 = prev.zcross(next) > 0;
            // calculate half angle from two vectors
            // normally this would require knowing the vector lengths but because
            // they both should be unit vectors we can ignore dividing by length
            angle = Math.acos(prev.dot(next)) / 2;

            inner.copyFrom(prev);
            inner.add(next);
            inner.perpendicular();
            if (over180)
            {
                inner.inverse();
            }
            inner.normalize(halfThick / Math.cos(angle));
            if (miterJoint)
            {
                outer.copyFrom(pos);
                outer.subtract(inner);
            }
            inner.add(pos);

            // calculate joint points
            prev.perpendicular();
            prev.normalize(halfThick);

            next.perpendicular();
            next.normalize(halfThick);

            if (!over180)
            {
                prev.inverse();
                next.inverse();
            }

            prev.add(pos);
            next.add(pos);

            // draw line connection
            if (over180)
            {
                drawTriangle(a, b, prev);
            }
            else
            {
                drawTriangle(a, b, inner);
            }
            drawTriangle(b, prev, inner);
            // draw bevel joint
            drawTriangle(next, prev, inner);
            if (miterJoint)
            {
                drawTriangle(next, prev, outer);
            }

            if (over180)
            {
                a.copyFrom(next);
                b.copyFrom(inner);
            }
            else
            {
                a.copyFrom(inner);
                b.copyFrom(next);
            }

            prev.copyFrom(nextPrev);
        }

        // end cap
        next.x = points[points.length - 2];
        next.y = points[points.length - 1];
        pos.subtract(next);
        pos.perpendicular();
        pos.normalize(halfThick);
        prev.copyFrom(next);
        prev.add(pos);
        next.subtract(pos);

        // draw final line
        drawTriangle(a, b, prev);
        drawTriangle(b, prev, next);
    }

    /**
     * Draws a rectangle outline. Lines are drawn inside the width and height.
     * @param    x            X position of the rectangle.
     * @param    y            Y position of the rectangle.
     * @param    width        Width of the rectangle.
     * @param    height        Height of the rectangle.
     * @since    2.5.2
     */
    public function rect(x:Float, y:Float, width:Float, height:Float)
    {
        var ht = lineThickness / 2;
        var x2 = x + width,
            y2 = y + height;
        line(x - ht, y , x2 + ht, y ); // top
        line(x - ht, y2, x2 + ht, y2); // bottom
        line(x , y + ht, x , y2 - ht); // left
        line(x2, y + ht, x2, y2 - ht); // right
    }

    /**
     * Draws a filled rectangle.
     * @param    x            X position of the rectangle.
     * @param    y            Y position of the rectangle.
     * @param    width        Width of the rectangle.
     * @param    height        Height of the rectangle.
     * @since    4.0.0
     */
    public function rectFilled(x:Float, y:Float, width:Float, height:Float)
    {
        begin();
        drawQuad(
            x, y,
            x + width, y,
            x + width, y + height,
            x, y + height
        );
    }

    /**
     * Draws a circle to the screen.
     * @param    x            X position of the circle's center.
     * @param    y            Y position of the circle's center.
     * @param    radius        Radius of the circle.
     * @param    segments    Increasing will smooth the circle but takes longer to render. Must be a value greater than zero.
     * @param    scaleX        Scales the circle horizontally.
     * @param    scaleY        Scales the circle vertically.
     */
    public inline function circle(x:Float, y:Float, radius:Float, segments:Int = 25, scaleX:Float = 1, scaleY:Float = 1)
    {
        var radians = 2 * Math.PI / segments;
        var halfThick = lineThickness / 2;
        var innerRadius = radius - halfThick;
        var outerRadius = radius + halfThick;
        var inner = new Vector2(),
            outer = new Vector2(),
            lastOuter = new Vector2(),
            lastInner = new Vector2();

        begin();

        for (segment in 0...segments+1)
        {
            var theta = segment * radians;
            var sin = Math.sin(theta);
            var cos = Math.cos(theta);
            inner.setTo(x + sin * innerRadius * scaleX, y + cos * innerRadius * scaleY);
            outer.setTo(x + sin * outerRadius * scaleX, y + cos * outerRadius * scaleY);

            if (segment != 0)
            {
                drawTriangle(lastInner, lastOuter, outer);
                drawTriangle(lastInner, outer, inner);
            }

            lastOuter.copyFrom(outer);
            lastInner.copyFrom(inner);
        }
    }

    /**
     * Draws a circle to the screen.
     * @param    x            X position of the circle's center.
     * @param    y            Y position of the circle's center.
     * @param    radius        Radius of the circle.
     * @param    segments    Increasing will smooth the circle but takes longer to render. Must be a value greater than zero.
     * @param    scaleX        Scales the circle horizontally.
     * @param    scaleY        Scales the circle vertically.
     */
    public function circleFilled(x:Float, y:Float, radius:Float, segments:Int = 25, scaleX:Float = 1, scaleY:Float = 1)
    {
        var radians = (2 * Math.PI) / segments;
        var x1 = x,
            y1 = y + radius;
        begin();
        for (segment in 1...segments+1)
        {
            var theta = segment * radians;
            var x2 = x + (Math.sin(theta) * radius) * scaleX;
            var y2 = y + (Math.cos(theta) * radius) * scaleY;
            addTriangle(x, y, 0, 0, x1, y1, 0, 0, x2, y2, 0, 0, color, alpha);
            x1 = x2; y1 = y2;
        }
    }

    /**
     * Draws a circle to the screen.
     * @param    x            X position of the circle's center.
     * @param    y            Y position of the circle's center.
     * @param    radius        Radius of the circle.
     * @param    start        The starting angle in radians.
     * @param    angle        The arc size in radians.
     * @param    segments    Increasing will smooth the circle but takes longer to render. Must be a value greater than zero.
     */
    public function arc(x:Float, y:Float, radius:Float, start:Float, angle:Float, segments:Int = 25)
    {
        var radians = angle / segments;
        var points = [];
        for (segment in 0...segments+1)
        {
            var theta = segment * radians + start;
            points.push(x + (Math.sin(theta) * radius));
            points.push(y + (Math.cos(theta) * radius));
        }
        polyline(points, true);
    }

    /**
     * Draws a quadratic curve.
     * @param    x1            X start.
     * @param    y1            Y start.
     * @param    x2            X control point, used to determine the curve.
     * @param    y2            Y control point, used to determine the curve.
     * @param    x3            X finish.
     * @param    y3            Y finish.
     * @param    segments    Increasing will smooth the curve but takes longer to render. Must be a value greater than zero.
     */
    public function curve(x1:Int, y1:Int, x2:Int, y2:Int, x3:Int, y3:Int, segments:Int = 25)
    {
        var points:Array<Float> = [];
        points.push(x1);
        points.push(y1);

        var deltaT:Float = 1 / segments;

        for (segment in 1...segments)
        {
            var t:Float = segment * deltaT;
            var x:Float = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * x2 + t * t * x3;
            var y:Float = (1 - t) * (1 - t) * y1 + 2 * t * (1 - t) * y2 + t * t * y3;
            points.push(x);
            points.push(y);
        }

        points.push(x3);
        points.push(y3);

        polyline(points);
    }

    /** @private Helper function to grab a DrawCommand object from the current scene */
    @:access(haxepunk.graphics.hardware.SceneRenderer)
    inline function begin()
    {
        if (shader == null) shader = new ColorShader();
        var scene = (this.scene == null) ? (HXP.renderingScene == null ? HXP.scene : HXP.renderingScene) : this.scene;
        command = scene.batch.getDrawCommand(null, shader, smooth, blend, null);
    }

    inline function drawTriangle(v1:Vector2, v2:Vector2, v3:Vector2):Void
    {
        addTriangle(v1.x, v1.y, 0, 0, v2.x, v2.y, 0, 0, v3.x, v3.y, 0, 0, color, alpha);
    }

    /** @private Helper function to add a quad to the buffer */
    inline function drawQuad(x1, y1, x2, y2, x3, y3, x4, y4)
    {
        addTriangle(x1, y1, 0, 0, x2, y2, 0, 0, x3, y3, 0, 0, color, alpha);
        addTriangle(x1, y1, 0, 0, x3, y3, 0, 0, x4, y4, 0, 0, color, alpha);
    }

    inline function addTriangle(tx1:Float, ty1:Float, uvx1:Float, uvy1:Float, tx2:Float, ty2:Float, uvx2:Float, uvy2:Float, tx3:Float, ty3:Float, uvx3:Float, uvy3:Float, color:Color, alpha:Float)
    {
        if (scale)
        {
            tx1 *= HXP.screen.scaleX;
            tx2 *= HXP.screen.scaleX;
            tx3 *= HXP.screen.scaleX;
            ty1 *= HXP.screen.scaleY;
            ty2 *= HXP.screen.scaleY;
            ty3 *= HXP.screen.scaleY;
        }
        command.addTriangle(tx1, ty1, uvx1, uvy1, tx2, ty2, uvx2, uvy2, tx3, ty3, uvx3, uvy3, color, alpha);
    }

    // Drawing information.
    var command:DrawCommand;
}