UNPKG

littlejsengine

Version:

LittleJS - Tiny and Fast HTML5 Game Engine

206 lines (194 loc) 10.8 kB
/** * 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; }