plugins_drawUtilities.js

/**
 * LittleJS Drawing Utilities Plugin
 * - Extra drawing functions for LittleJS
 * - Nine slice and three slice drawing
 * @namespace DrawUtilities
 */

'use strict';

///////////////////////////////////////////////////////////////////////////////

/** Draw a scalable nine-slice UI element to the main canvas in screen space
 *  This function can not apply color because it draws using the 2d context
 *  @param {Vector2} pos - Screen space position
 *  @param {Vector2} size - Screen space size
 *  @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample (see drawNineSlice)
 *  @param {number} [borderSize] - Rendered thickness of the border sections
 *  @param {number} [extraSpace] - Extra spacing adjustment
 *  @param {number} [angle] - Angle to rotate by
 *  @memberof DrawUtilities */
function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
{
    drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true);
}

/** Draw a scalable nine-slice UI element in world space
 *  This function can apply color and additive color if WebGL is enabled
 *  The nine-slice samples a 3x3 block of tiles from the tilesheet, it does not
 *  subdivide a single tile. Pass the top-left tile of that block as startTile;
 *  the other 8 tiles (edges, corners, and center) are taken automatically from
 *  the 3x3 grid of tiles extending right and down from it. borderSize only sets
 *  the rendered thickness of the edges and corners, not how the texture is cut.
 *  @param {Vector2} pos - World space position
 *  @param {Vector2} size - World space size
 *  @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample the nine-slice from
 *  @param {Color} [color] - Color to modulate with
 *  @param {number} [borderSize] - Rendered thickness of the border sections
 *  @param {Color} [additiveColor] - Additive color
 *  @param {number} [extraSpace] - Extra spacing adjustment
 *  @param {number} [angle] - Angle to rotate by
 *  @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
 *  @param {boolean} [screenSpace] - Use screen space coordinates
 *  @param {CanvasRenderingContext2D} [context] - Canvas context to use
 *  @memberof DrawUtilities */
function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
{
    // setup nine slice tiles - startTile is the top-left of a 3x3 tile block,
    // so the center tile is one tile down and right from it
    const centerTile = startTile.offset(startTile.size);
    const centerSize = size.add(vec2(extraSpace-borderSize*2));
    const cornerSize = vec2(borderSize);
    const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5));
    const flip = screenSpace ? -1 : 1;
    const rotateAngle = screenSpace ? -angle : angle;

    // center
    drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
    for (let i=4; i--;)
    {
        // sides
        const horizontal = i%2;
        const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-1:1));
        const sideSize = vec2(horizontal ? borderSize : centerSize.x, horizontal ? centerSize.y : borderSize);
        const sideTile = centerTile.offset(startTile.size.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)))
        drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
    }
    for (let i=4; i--;)
    {
        // corners
        const flipX = i>1;
        const flipY = i && i<3;
        const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-1:1));
        const cornerTile = centerTile.offset(startTile.size.multiply(vec2(flipX?-1:1,flipY?flip:-flip)));
        drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
    }
}

/** Draw a scalable three-slice UI element to the main canvas in screen space
 *  This function can not apply color because it draws using the 2d context
 *  @param {Vector2} pos - Screen space position
 *  @param {Vector2} size - Screen space size
 *  @param {TileInfo} startTile - First of 3 consecutive tiles: corner, side, center (see drawThreeSlice)
 *  @param {number} [borderSize] - Rendered thickness of the border sections
 *  @param {number} [extraSpace] - Extra spacing adjustment
 *  @param {number} [angle] - Angle to rotate by
 *  @memberof DrawUtilities */
function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
{
    drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true);
}

/** Draw a scalable three-slice UI element in world space
 *  This function can apply color and additive color if WebGL is enabled
 *  The three-slice samples 3 consecutive tiles from the tilesheet, it does not
 *  subdivide a single tile. Pass the first tile as startTile; the three tiles
 *  are used in order as corner, side, and center, then rotated and mirrored to
 *  build all four edges and corners. borderSize only sets the rendered thickness.
 *  @param {Vector2} pos - World space position
 *  @param {Vector2} size - World space size
 *  @param {TileInfo} startTile - First of 3 consecutive tiles (corner, side, center) for the three-slice
 *  @param {Color} [color] - Color to modulate with
 *  @param {number} [borderSize] - Rendered thickness of the border sections
 *  @param {Color} [additiveColor] - Additive color
 *  @param {number} [extraSpace] - Extra spacing adjustment
 *  @param {number} [angle] - Angle to rotate by
 *  @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
 *  @param {boolean} [screenSpace] - Use screen space coordinates
 *  @param {CanvasRenderingContext2D} [context] - Canvas context to use
 *  @memberof DrawUtilities */
function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
{
    // setup three slice tiles - 3 tiles in a row starting at startTile
    const cornerTile = startTile.frame(0);
    const sideTile   = startTile.frame(1);
    const centerTile = startTile.frame(2);
    const centerSize = size.add(vec2(extraSpace-borderSize*2));
    const cornerSize = vec2(borderSize);
    const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5));
    const flip = screenSpace ? -1 : 1;
    const rotateAngle = screenSpace ? -angle : angle;

    // center
    drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
    for (let i=4; i--;)
    {
        // sides
        const a = angle + i*PI/2;
        const horizontal = i%2;
        const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-flip:flip));
        const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize);
        drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
    }
    for (let i=4; i--;)
    {
        // corners
        const a = angle + i*PI/2;
        const flipX = !i || i>2;
        const flipY = i>1;
        const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-flip:flip));
        drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
    }
}

/** Draw a crescent / moon-phase shape built from a polygon
 *  Routes through drawPoly, so it supports WebGL, screen space, color, and outlines
 *  @param {Vector2} pos - Center position
 *  @param {number}  [size] - Diameter
 *  @param {number}  [percent] - Moon phase over a full cycle (0=new, .25=first quarter, .5=full, .75=last quarter), wraps
 *  @param {Color}   [color] - Fill color
 *  @param {number}  [angle] - Angle to rotate by
 *  @param {boolean} [invert] - Flip which side is illuminated
 *  @param {number}  [lineWidth] - Outline width, 0 for no outline
 *  @param {Color}   [lineColor] - Outline color
 *  @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
 *  @param {boolean} [screenSpace] - Use screen space coordinates
 *  @param {CanvasRenderingContext2D} [context] - Canvas context to use
 *  @memberof DrawUtilities */
function drawCrescent(pos, size=1, percent=0, color=WHITE, angle=0, invert=false, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
{
    // build local-space points and let drawPoly apply pos/angle so screen space works
    const points = getCrescentPoints(vec2(), size, percent, 0, invert);
    drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
}

/** Get the list of points that make up a crescent / moon-phase shape
 *  Returns world-space points with pos and angle baked in, ready for drawPoly or other use
 *  @param {Vector2} pos - Center position
 *  @param {number}  [size] - Diameter
 *  @param {number}  [percent] - Moon phase over a full cycle (0=new, .25=first quarter, .5=full, .75=last quarter), wraps
 *  @param {number}  [angle] - Angle to rotate by
 *  @param {boolean} [invert] - Flip which side is illuminated
 *  @param {number}  [sides=glCircleSides] - Number of sides for a full circle (halved per arc)
 *  @return {Array<Vector2>} - List of points making up the crescent
 *  @memberof DrawUtilities */
function getCrescentPoints(pos, size=1, percent=0, angle=0, invert=false, sides=glCircleSides)
{
    ASSERT(isVector2(pos), 'pos must be a vec2');
    ASSERT(isNumber(size) && isNumber(percent), 'size and percent must be numbers');

    // map phase to a signed terminator curve: -1 new, 0 half, 1 full
    let p = mod(percent*4, 4); // quarter phase 0..4
    if (p >= 2)                // second half of cycle flips orientation
        angle += PI;
    p = p <= 2 ? p-1 : 3-p;
    if (invert)                // flip the illuminated side
    {
        p = -p;
        angle += PI;
    }

    // build the crescent: outer semicircle, then inner half-ellipse traced back
    const points = [];
    const segs = max(3, sides>>1);
    const radius = size/2;
    for (let i=0; i<=segs; i++)
    {
        const t = i/segs*PI;
        points.push(vec2(radius*cos(t), radius*sin(t)).rotate(angle).add(pos));
    }
    for (let i=segs; i>=0; i--)
    {
        const t = i/segs*PI;
        points.push(vec2(radius*cos(t), -radius*p*sin(t)).rotate(angle).add(pos));
    }
    return points;
}