UNPKG

littlejsengine

Version:

LittleJS - Tiny and Fast HTML5 Game Engine

470 lines (429 loc) 14.8 kB
/** * LittleJS User Interface Plugin * - Nested Menus * - Text * - Buttons * - Checkboxes * - Images * @namespace UISystemPlugin */ 'use strict'; /////////////////////////////////////////////////////////////////////////////// // ui defaults /** Default fill color for UI elements * @type {Color} * @memberof UISystemPlugin */ let uiDefaultColor = WHITE; /** Default outline color for UI elements * @type {Color} * @memberof UISystemPlugin */ let uiDefaultLineColor = BLACK; /** Default text color for UI elements * @type {Color} * @memberof UISystemPlugin */ let uiDefaultTextColor = BLACK; /** Default button color for UI elements * @type {Color} * @memberof UISystemPlugin */ let uiDefaultButtonColor = hsl(0,0,.5); /** Default hover color for UI elements * @type {Color} * @memberof UISystemPlugin */ let uiDefaultHoverColor = hsl(0,0,.7); /** Default line width for UI elements * @type {number} * @memberof UISystemPlugin */ let uiDefaultLineWidth = 4; /** Default font for UI elements * @type {string} * @memberof UISystemPlugin */ let uiDefaultFont = 'arial'; /** List of all UI elements * @type {Array<UIObject>} * @memberof UISystemPlugin */ let uiObjects = []; /** Context to render UI elements to * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} * @memberof UISystemPlugin */ let uiContext; /** Set up the UI system, typically called in gameInit * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] * @memberof UISystemPlugin */ function initUISystem(context=overlayContext) { uiContext = context; engineAddPlugin(uiUpdate, uiRender); // setup recursive update and render function uiUpdate() { function updateObject(o) { if (!o.visible) return; if (o.parent) o.pos = o.localPos.add(o.parent.pos); o.update(); for(const c of o.children) updateObject(c); } uiObjects.forEach(o=> o.parent || updateObject(o)); } function uiRender() { function renderObject(o) { if (!o.visible) return; if (o.parent) o.pos = o.localPos.add(o.parent.pos); o.render(); for(const c of o.children) renderObject(c); } uiObjects.forEach(o=> o.parent || renderObject(o)); } } /** Draw a rectangle to the UI context * @param {Vector2} pos * @param {Vector2} size * @param {Color} [color=uiDefaultColor] * @param {number} [lineWidth=uiDefaultLineWidth] * @param {Color} [lineColor=uiDefaultLineColor] * @memberof UISystemPlugin */ function drawUIRect(pos, size, color=uiDefaultColor, lineWidth=uiDefaultLineWidth, lineColor=uiDefaultLineColor) { uiContext.fillStyle = color.toString(); uiContext.beginPath(); uiContext.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y); uiContext.fill(); if (lineWidth) { uiContext.strokeStyle = lineColor.toString(); uiContext.lineWidth = lineWidth; uiContext.stroke(); } } /** Draw a line to the UI context * @param {Vector2} posA * @param {Vector2} posB * @param {number} [lineWidth=uiDefaultLineWidth] * @param {Color} [lineColor=uiDefaultLineColor] * @memberof UISystemPlugin */ function drawUILine(posA, posB, lineWidth=uiDefaultLineWidth, lineColor=uiDefaultLineColor) { uiContext.strokeStyle = lineColor.toString(); uiContext.lineWidth = lineWidth; uiContext.beginPath(); uiContext.lineTo(posA.x, posA.y); uiContext.lineTo(posB.x, posB.y); uiContext.stroke(); } /** Draw a tile to the UI context * @param {Vector2} pos * @param {Vector2} size * @param {TileInfo} tileInfo * @param {Color} [color=uiDefaultColor] * @param {number} [angle] * @param {boolean} [mirror] * @memberof UISystemPlugin */ function drawUITile(pos, size, tileInfo, color=uiDefaultColor, angle=0, mirror=false) { drawTile(pos, size, tileInfo, color, angle, mirror, BLACK, false, true, uiContext); } /** Draw text to the UI context * @param {string} text * @param {Vector2} pos * @param {Vector2} size * @param {Color} [color=uiDefaultColor] * @param {number} [lineWidth=uiDefaultLineWidth] * @param {Color} [lineColor=uiDefaultLineColor] * @param {string} [align] * @param {string} [font=uiDefaultFont] * @memberof UISystemPlugin */ function drawUIText(text, pos, size, color=uiDefaultColor, lineWidth=uiDefaultLineWidth, lineColor=uiDefaultLineColor, align='center', font=uiDefaultFont) { drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, size.x, uiContext); } /////////////////////////////////////////////////////////////////////////////// /** * UI Object - Base level object for all UI elements */ class UIObject { /** Create a UIObject * @param {Vector2} [pos=(0,0)] * @param {Vector2} [size=(1,1)] */ constructor(pos=vec2(), size=vec2()) { /** @property {Vector2} - Local position of the object */ this.localPos = pos.copy(); /** @property {Vector2} - Screen space position of the object */ this.pos = pos.copy(); /** @property {Vector2} - Screen space size of the object */ this.size = size.copy(); /** @property {Color} */ this.color = uiDefaultColor; /** @property {Color} */ this.lineColor = uiDefaultLineColor; /** @property {Color} */ this.textColor = uiDefaultTextColor; /** @property {Color} */ this.hoverColor = uiDefaultHoverColor; /** @property {number} */ this.lineWidth = uiDefaultLineWidth; /** @property {string} */ this.font = uiDefaultFont; /** @property {boolean} */ this.visible = true; /** @property {Array<UIObject>} */ this.children = []; /** @property {UIObject} */ this.parent = undefined; uiObjects.push(this); } /** Add a child UIObject to this object * @param {UIObject} child */ addChild(child) { ASSERT(!child.parent && !this.children.includes(child)); this.children.push(child); child.parent = this; } /** Remove a child UIObject from this object * @param {UIObject} child */ removeChild(child) { ASSERT(child.parent == this && this.children.includes(child)); this.children.splice(this.children.indexOf(child), 1); child.parent = undefined; } /** Update the object, called automatically by plugin once each frame */ update() { // track mouse input const mouseWasOver = this.mouseIsOver; const mouseDown = mouseIsDown(0); if (!mouseDown || isTouchDevice) { this.mouseIsOver = isOverlapping(this.pos, this.size, mousePosScreen); if (!mouseDown && isTouchDevice) this.mouseIsOver = false; if (this.mouseIsOver && !mouseWasOver) this.onEnter(); if (!this.mouseIsOver && mouseWasOver) this.onLeave(); } if (mouseWasPressed(0) && this.mouseIsOver) { this.mouseIsHeld = true; this.onPress(); if (isTouchDevice) this.mouseIsOver = false; } else if (this.mouseIsHeld && !mouseDown) { this.mouseIsHeld = false; this.onRelease(); } } /** Render the object, called automatically by plugin once each frame */ render() { if (this.size.x && this.size.y) drawUIRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor); } /** Called when the mouse enters the object */ onEnter() {} /** Called when the mouse leaves the object */ onLeave() {} /** Called when the mouse is pressed while over the object */ onPress() {} /** Called when the mouse is released while over the object */ onRelease() {} /** Called when the state of this object changes */ onChange() {} } /////////////////////////////////////////////////////////////////////////////// /** * UIText - A UI object that displays text * @extends UIObject */ class UIText extends UIObject { /** Create a UIText object * @param {Vector2} [pos] * @param {Vector2} [size] * @param {string} [text] * @param {string} [align] * @param {string} [font=uiDefaultFont] */ constructor(pos, size, text='', align='center', font=uiDefaultFont) { super(pos, size); /** @property {string} */ this.text = text; /** @property {string} */ this.align = align; this.font = font; // set font this.lineWidth = 0; // set text to not be outlined by default } render() { drawUIText(this.text, this.pos, this.size, this.textColor, this.lineWidth, this.lineColor, this.align, this.font); } } /////////////////////////////////////////////////////////////////////////////// /** * UITile - A UI object that displays a tile image * @extends UIObject */ class UITile extends UIObject { /** Create a UITile object * @param {Vector2} [pos] * @param {Vector2} [size] * @param {TileInfo} [tileInfo] * @param {Color} [color=WHITE] * @param {number} [angle] * @param {boolean} [mirror] */ constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false) { super(pos, size); /** @property {TileInfo} - Tile image to use */ this.tileInfo = tileInfo; /** @property {number} - Angle to rotate in radians */ this.angle = angle; /** @property {boolean} - Should it be mirrored? */ this.mirror = mirror; this.color = color; } render() { drawUITile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror); } } /////////////////////////////////////////////////////////////////////////////// /** * UIButton - A UI object that acts as a button * @extends UIObject */ class UIButton extends UIObject { /** Create a UIButton object * @param {Vector2} [pos] * @param {Vector2} [size] * @param {string} [text] * @param {Color} [color=uiDefaultButtonColor] */ constructor(pos, size, text, color=uiDefaultButtonColor) { super(pos, size); /** @property {string} */ this.text = text; this.color = color; } render() { const lineColor = this.mouseIsHeld ? this.color : this.lineColor; const color = this.mouseIsOver? this.hoverColor : this.color; drawUIRect(this.pos, this.size, color, this.lineWidth, lineColor); const textSize = vec2(this.size.x, this.size.y*.8); drawUIText(this.text, this.pos, textSize, this.textColor, 0, undefined, this.align, this.font); } } /////////////////////////////////////////////////////////////////////////////// /** * UICheckbox - A UI object that acts as a checkbox * @extends UIObject */ class UICheckbox extends UIObject { /** Create a UICheckbox object * @param {Vector2} [pos] * @param {Vector2} [size] * @param {boolean} [checked] */ constructor(pos, size, checked=false) { super(pos, size); /** @property {boolean} */ this.checked = checked; } onPress() { this.checked = !this.checked; this.onChange(); } render() { const color = this.mouseIsOver? this.hoverColor : this.color; drawUIRect(this.pos, this.size, color, this.lineWidth, this.lineColor); if (this.checked) { // draw an X if checked drawUILine(this.pos.add(this.size.multiply(vec2(-.5,-.5))), this.pos.add(this.size.multiply(vec2(.5,.5))), this.lineWidth, this.lineColor); drawUILine(this.pos.add(this.size.multiply(vec2(-.5,.5))), this.pos.add(this.size.multiply(vec2(.5,-.5))), this.lineWidth, this.lineColor); } } } /////////////////////////////////////////////////////////////////////////////// /** * UIScrollbar - A UI object that acts as a scrollbar * @extends UIObject */ class UIScrollbar extends UIObject { /** Create a UIScrollbar object * @param {Vector2} [pos] * @param {Vector2} [size] * @param {number} [value] * @param {string} [text] * @param {Color} [color=uiDefaultButtonColor] * @param {Color} [handleColor=WHITE] */ constructor(pos, size, value=.5, text='', color=uiDefaultButtonColor, handleColor=WHITE) { super(pos, size); /** @property {number} */ this.value = value; /** @property {string} */ this.text = text; this.color = color; this.handleColor = handleColor; } update() { super.update(); if (this.mouseIsHeld) { const handleSize = vec2(this.size.y); const handleWidth = this.size.x - handleSize.x; const p1 = this.pos.x - handleWidth/2; const p2 = this.pos.x + handleWidth/2; const oldValue = this.value; this.value = percent(mousePosScreen.x, p1, p2); this.value == oldValue || this.onChange(); } } render() { const lineColor = this.mouseIsHeld ? this.color : this.lineColor; const color = this.mouseIsOver? this.hoverColor : this.color; drawUIRect(this.pos, this.size, color, this.lineWidth, lineColor); const handleSize = vec2(this.size.y); const handleWidth = this.size.x - handleSize.x; const p1 = this.pos.x - handleWidth/2; const p2 = this.pos.x + handleWidth/2; const handlePos = vec2(lerp(this.value, p1, p2), this.pos.y); const barColor = this.mouseIsHeld ? this.color : this.handleColor; drawUIRect(handlePos, handleSize, barColor, this.lineWidth, this.lineColor); const textSize = vec2(this.size.x, this.size.y*.8); drawUIText(this.text, this.pos, textSize, this.textColor, 0, undefined, this.align, this.font); } }