UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

264 lines (191 loc) • 7.62 kB
import { assert } from "../../../../core/assert.js"; import { SignalBinding } from "../../../../core/events/signal/SignalBinding.js"; import AABB2 from "../../../../core/geom/2d/aabb/AABB2.js"; import { EPSILON } from "../../../../core/math/EPSILON.js"; import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js"; import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js"; import { System } from '../../System.js'; import GUIElement from "../GUIElement.js"; import { GUIElementEvent } from "../GUIElementEvent.js"; import ViewportPosition from './ViewportPosition.js'; const CSS_CLASS = 'ecs-viewport-position-component'; /** * @this {{system:ViewportPositionSystem, vp: ViewportPosition, el: GUIElement}} */ function updatePosition() { this.system.positionComponent(this.el, this.vp); } const aabb2 = new AABB2(); class ViewportPositionSystem extends System { constructor(viewportSize) { super(); this.dependencies = [ViewportPosition, GUIElement]; this.components_used = [ ResourceAccessSpecification.from(GUIElement, ResourceAccessKind.Read | ResourceAccessKind.Write), ]; this.viewportSize = viewportSize; this.viewportSizeChangeReactor = new SignalBinding(viewportSize.onChanged, () => { const ecd = this.entityManager.dataset; if (ecd !== null) { ecd.traverseEntities([GUIElement, ViewportPosition], this.positionComponent, this); } }); this.data = {}; } async startup(entityManager) { this.viewportSizeChangeReactor.link(); } async shutdown(entityManager) { this.viewportSizeChangeReactor.unlink(); } /** * @param {GUIElement} el * @param {ViewportPosition} vp */ positionComponent(el, vp) { assert.notEqual(this, undefined, 'this is undefined'); assert.notEqual(this, null, 'this is null'); assert.ok(this.isViewportPositionSystem, 'this is not ViewportPositionSystem'); if (!vp.enabled.getValue()) { // positioning is disabled, bail return; } const viewportSize = this.viewportSize; const view = el.view; const viewport_width = viewportSize.x; const viewport_height = viewportSize.y; if (viewport_width === 0 || viewport_height === 0) { // viewport size is 0 return; } // convert size of the hud view into normalized (0-1) form let extentX = view.size.x / viewport_width; let extentY = view.size.y / viewport_height; // convert screen-space offset from pixels to normalized (0-1) form const nOffsetX = vp.offset.x / viewport_width; const nOffsetY = vp.offset.y / viewport_height; // determine visibility in clip space const nPositionX = vp.position.x + nOffsetX; const nPositionY = vp.position.y + nOffsetY; const anchorX = vp.anchor.x; const anchorY = vp.anchor.y; const x0 = nPositionX - extentX * anchorX; const y0 = nPositionY - extentY * anchorY; const x1 = nPositionX + extentX * (1 - anchorX); const y1 = nPositionY + extentY * (1 - anchorY); aabb2.set( x0, y0, x1, y1 ); const trackedPositionOutOfBounds = x0 > 1 || x1 < 0 || y0 > 1 || y1 < 0; const visible = (vp.stickToScreenEdge || !trackedPositionOutOfBounds); el.visible.set(visible); if (visible) { //we can now use this vector setElementPosition(view, aabb2, viewportSize, vp); } } /** * * @param {ViewportPosition} vp * @param {GUIElement} el * @param entity */ link(vp, el, entity) { try { this.positionComponent(el, vp, entity); } catch (e) { console.error(`Failed to position view (entity=${entity}):`, e); } const eventContext = { vp, el, system: this }; this.data[entity] = eventContext; vp.position.onChanged.add(updatePosition, eventContext); vp.offset.onChanged.add(updatePosition, eventContext); vp.anchor.onChanged.add(updatePosition, eventContext); vp.enabled.onChanged.add(updatePosition, eventContext); // track size of the element as that has an impact as well el.view.size.onChanged.add(updatePosition, eventContext); this.entityManager.dataset.addEntityEventListener(entity, GUIElementEvent.Initialized, updatePosition, eventContext); el.view.addClass(CSS_CLASS); } /** * * @param {ViewportPosition} vp * @param {GUIElement} el * @param entity */ unlink(vp, el, entity) { const eventContext = this.data[entity]; if (eventContext !== undefined) { delete this.data[entity]; vp.position.onChanged.remove(updatePosition, eventContext); vp.offset.onChanged.remove(updatePosition, eventContext); vp.anchor.onChanged.remove(updatePosition, eventContext); vp.enabled.onChanged.remove(updatePosition, eventContext); } this.entityManager.dataset.removeEntityEventListener(entity, GUIElementEvent.Initialized, updatePosition, eventContext); el.view.removeClass(CSS_CLASS); } } const STICKY_FLAG_CLASS_NAME = 'hud-system-sticky-flag'; const stickyBounds = new AABB2(); /** * * @param {View} view * @param {AABB2} bounds * @param {Vector2} viewportSize * @param {ViewportPosition} vp */ function setElementPosition(view, bounds, viewportSize, vp) { //deal with stick-to-edge behaviour let x, y; if (vp.stickToScreenEdge) { const screenEdgeWidth = vp.screenEdgeWidth; const clipEdgeX = screenEdgeWidth / viewportSize.x; const clipEdgeY = screenEdgeWidth / viewportSize.y; stickyBounds.set(clipEdgeX, clipEdgeY, 1 - clipEdgeX, 1 - clipEdgeY); let stickFlag = false; if (bounds.x0 < stickyBounds.x0) { x = stickyBounds.x0; stickFlag = true; } else if (bounds.x1 > stickyBounds.x1) { x = stickyBounds.x1 - bounds.getWidth(); stickFlag = true; } else { x = bounds.x0; } if (bounds.y0 < stickyBounds.y0) { y = stickyBounds.y0; stickFlag = true; } else if (bounds.y1 > stickyBounds.y1) { y = stickyBounds.y1 - bounds.getHeight(); stickFlag = true; } else { y = bounds.y0; } view.setClass(STICKY_FLAG_CLASS_NAME, stickFlag); } else { x = bounds.x0; y = bounds.y0; } const targetX = x * (viewportSize.x); const targetY = y * (viewportSize.y); const viewPosition = view.position; if (Math.abs(targetX - viewPosition.x) > EPSILON || Math.abs(targetY - viewPosition.y) > EPSILON) { viewPosition.set(targetX, targetY); } view.transformOrigin.copy(vp.anchor); } /** * @readonly * @type {boolean} */ ViewportPositionSystem.prototype.isViewportPositionSystem = true; export default ViewportPositionSystem;