UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

504 lines (418 loc) • 16.2 kB
import { mat4 } from "gl-matrix"; import { Color } from "../../../src/core/color/Color.js"; import Signal from "../../../src/core/events/signal/Signal.js"; import { compose_matrix4_array } from "../../../src/core/geom/3d/mat4/compose_matrix4_array.js"; import Quaternion from "../../../src/core/geom/Quaternion.js"; import { v2_distance } from "../../../src/core/geom/vec2/v2_distance.js"; import Vector2 from "../../../src/core/geom/Vector2.js"; import Vector3 from "../../../src/core/geom/Vector3.js"; import { clamp01 } from "../../../src/core/math/clamp01.js"; import { inverseLerp } from "../../../src/core/math/inverseLerp.js"; import { lerp } from "../../../src/core/math/lerp.js"; import { quaternion_invert_orientation } from "../../../src/engine/graphics/ecs/camera/quaternion_invert_orientation.js"; import { readPositionFromMouseEvent } from "../../../src/engine/input/devices/PointerDevice.js"; import { CanvasView } from "../../../src/view/elements/CanvasView.js"; const scratch_color = new Color(); const scratch_quat = new Quaternion(); const scratch_m4 = new Float32Array(16); class DirectionStyle { constructor() { this.near = { fill: new Color(1, 0, 0), stroke_width: 0, stroke_color: new Color(0, 0, 0) }; this.far = { fill: new Color(1, 0, 0), stroke_width: 0, stroke_color: new Color(0, 0, 0) }; } fromJSON({ near_fill = '#FF0000', near_stroke_width = 0, near_stroke_color = '#000000', far_fill = '#FF0000', far_stroke_width = 0, far_stroke_color = '#000000' }) { this.near.fill.parse(near_fill); this.near.stroke_width = near_stroke_width; this.near.stroke_color.parse(near_stroke_color); this.far.fill.parse(far_fill); this.far.stroke_width = far_stroke_width; this.far.stroke_color.parse(far_stroke_color); } static fromJSON(j) { const r = new DirectionStyle(); r.fromJSON(j); return r; } } function apply_hierarchical_options_from_json(source, target) { for (const prop_name in source) { if (!target.hasOwnProperty(prop_name)) { console.warn(`Property '${prop_name}' doesn't exist, valid properties are : ${Object.keys(target)}`); continue; } const existing_target_value = target[prop_name]; const source_value = source[prop_name]; if (typeof existing_target_value === "object") { if (typeof existing_target_value.fromJSON === "function") { existing_target_value.fromJSON(source_value); } else { apply_hierarchical_options_from_json(source_value, existing_target_value); } } else { target[prop_name] = source_value; } } } /** * Modelled on https://github.com/jrj2211/three-orientation-gizmo/blob/master/src/OrientationGizmo.js */ export class BlenderCameraOrientationGizmo extends CanvasView { /** * */ constructor(options = {}) { super(); /** * * @type {Quaternion} */ this.orientation = null; this.options = { size: 100, padding: 8, bubbleSizePrimary: 10, bubbleSizeSecondary: 8, showSecondary: true, lineWidth: 2, fontSize: "11px", fontFamily: "arial", fontWeight: "bold", fontColor: "rgba(0,0,0,0.8)", fontYAdjust: 1, selectionFontColor: "#FFFFFF", invertOrientation: true, axisStyles: { px: DirectionStyle.fromJSON({ near_fill: "#FF3653", far_fill: "#9D3B4A" }), py: DirectionStyle.fromJSON({ near_fill: "#8ADB00", far_fill: "#648C23" }), pz: DirectionStyle.fromJSON({ near_fill: "#2C8FFF", far_fill: "#36679D" }), nx: DirectionStyle.fromJSON({ near_stroke_color: "#FF3653", near_stroke_width: 1, near_fill: "#6E3D44", far_stroke_color: "#9D3B4A", far_stroke_width: 1, far_fill: 'rgba(0,0,0,0)' }), ny: DirectionStyle.fromJSON({ near_stroke_color: "#8ADB00", near_stroke_width: 1, near_fill: "#526532", far_stroke_color: "#648C23", far_stroke_width: 1, far_fill: 'rgba(0,0,0,0)' }), nz: DirectionStyle.fromJSON({ near_stroke_color: "#2C8FFF", near_stroke_width: 1, near_fill: "#3B536E", far_stroke_color: "#36679D", far_stroke_width: 1, far_fill: 'rgba(0,0,0,0)' }) } }; // read options apply_hierarchical_options_from_json(options, this.options); // Generate list of axes this.bubbles = [ { axis: "x", direction: new Vector3(1, 0, 0), size: this.options.bubbleSizePrimary, style: this.options.axisStyles.px, line: this.options.lineWidth, label: "X", primary: true }, { axis: "y", direction: new Vector3(0, 1, 0), size: this.options.bubbleSizePrimary, style: this.options.axisStyles.py, line: this.options.lineWidth, label: "Y", primary: true }, { axis: "z", direction: new Vector3(0, 0, 1), size: this.options.bubbleSizePrimary, style: this.options.axisStyles.pz, line: this.options.lineWidth, label: "Z", primary: true }, { axis: "-x", direction: new Vector3(-1, 0, 0), size: this.options.bubbleSizeSecondary, style: this.options.axisStyles.nx, label: "-X", labelHideUnselected: true, primary: false }, { axis: "-y", direction: new Vector3(0, -1, 0), size: this.options.bubbleSizeSecondary, style: this.options.axisStyles.ny, label: "-Y", labelHideUnselected: true, primary: false }, { axis: "-z", direction: new Vector3(0, 0, -1), size: this.options.bubbleSizeSecondary, style: this.options.axisStyles.nz, label: "-Z", labelHideUnselected: true, primary: false }, ]; /** * * @type {Vector3|null} */ this.input_pointer_position = null; this.center = new Vector3(this.options.size / 2, this.options.size / 2, 0); this.selectedAxis = null; this.size.setScalar(this.options.size); this.onMouseMove = this.onMouseMove.bind(this); this.onMouseOut = this.onMouseOut.bind(this); this.onMouseClick = this.onMouseClick.bind(this); /** * * Invoked when axis is clicked * @type {Signal<string,Vector3>} */ this.on.axisSelected = new Signal(); /** * * @type {boolean} * @private */ this.__needs_update = true; } link() { super.link(); const el = this.el; el.addEventListener('mousemove', this.onMouseMove); el.addEventListener('mouseout', this.onMouseOut); el.addEventListener('click', this.onMouseClick); } unlink() { const el = this.el; el.removeEventListener('mousemove', this.onMouseMove); el.removeEventListener('mouseout', this.onMouseOut); el.removeEventListener('click', this.onMouseClick); super.unlink(); } /** * * @param {MouseEvent} evt */ onMouseMove(evt) { this._setPointerPositionFromEvent(evt); this.__try_update(); } /** * * @param {MouseEvent} evt * @private */ _setPointerPositionFromEvent(evt) { const v2 = new Vector2(); readPositionFromMouseEvent(v2, evt); const position_changed = (this.input_pointer_position === null || this.input_pointer_position.x !== v2.x || this.input_pointer_position.y !== v2.y); this.input_pointer_position = new Vector3(v2.x, v2.y, 0); if (position_changed) { this.__needs_update = true; } return position_changed; } /** * * @param {MouseEvent} evt */ onMouseOut(evt) { this.input_pointer_position = null; this.__needs_update = true; this.__try_update(); } /** * * @param {MouseEvent} evt */ onMouseClick(evt) { this._setPointerPositionFromEvent(evt); this.__try_update(); if (this.selectedAxis !== null) { if (this.on.axisSelected.hasHandlers()) { // axis selection will be handled, stop propagation evt.stopPropagation(); } this.on.axisSelected.send2( this.selectedAxis.axis, this.selectedAxis.direction.clone() ); } } drawCircle(p, radius = 10, fill_color = "#FF0000", stroke_width = 0, stroke_color = "#FFFFFF") { const ctx = this.context2d; ctx.beginPath(); ctx.arc(p.x, p.y, radius, 0, 2 * Math.PI, false); ctx.fillStyle = fill_color; ctx.fill(); if (stroke_width > 0) { ctx.lineWidth = stroke_width; ctx.strokeStyle = stroke_color; ctx.stroke(); } ctx.closePath(); } drawLine(p1, p2, width = 1, color = "#FF0000") { const ctx = this.context2d; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.lineWidth = width; ctx.strokeStyle = color; ctx.stroke(); ctx.closePath(); } __try_update() { if (!this.__needs_update) { return; } this.update(); } update() { // reset flag this.__needs_update = false; this.clear(); // Calculate the rotation matrix from the camera const rotation_matrix = scratch_m4; let rotation = this.orientation; if (rotation === null) { // not connected return; } if (this.options.invertOrientation) { const q = scratch_quat; quaternion_invert_orientation(q, rotation); rotation = q; } compose_matrix4_array(rotation_matrix, Vector3.zero, rotation, Vector3.one); mat4.invert(rotation_matrix, rotation_matrix); for (const bubble of this.bubbles) { const direction = bubble.direction.clone(); direction.applyMatrix4(rotation_matrix); bubble.position = this.getBubblePosition(direction); } // Generate a list of layers to draw const layers = []; for (const bubble of this.bubbles) { // Check if the name starts with a negative and dont add it to the layer list if secondary axis is turned off const is_secondary = bubble.axis[0] !== "-"; if (this.options.showSecondary === true || is_secondary) { layers.push(bubble); } } // Sort the layers where the +Z position is last so its drawn on top of anything below it layers.sort((a, b) => (a.position.z > b.position.z) ? 1 : -1); // If the mouse is over the gizmo, find the closest axis and highlight it this.selectedAxis = null; if (this.input_pointer_position) { let closestDist = Infinity; let closestZ = -Infinity; // Loop through each layer for (let bubble of layers) { const distance = v2_distance(this.input_pointer_position.x, this.input_pointer_position.y, bubble.position.x, bubble.position.y); // Only select the axis if its closer to the mouse than the previous or if its within its bubble circle if ( distance < closestDist && closestZ < bubble.position.z && distance <= bubble.size ) { closestDist = distance; closestZ = bubble.position.z; this.selectedAxis = bubble; } } } // Draw the layers this.drawLayers(layers); } /** * * @param {{style:DirectionStyle, position:Vector3, size:number,line:boolean, label?:string,labelHideUnselected?:boolean}[]} layers */ drawLayers(layers) { // For each layer, draw the bubble for (let bubble of layers) { // Find the color const is_selected = this.selectedAxis === bubble; const closeness = clamp01(inverseLerp(-1, 0.8, bubble.position.z)); const style = bubble.style; scratch_color.lerpColors(style.far.fill, style.near.fill, closeness); const fill_color = scratch_color.toCssRGBAString(); scratch_color.lerpColors(style.far.stroke_color, style.near.stroke_color, closeness); const stroke_color = scratch_color.toCssRGBAString(); const stroke_width = lerp(style.far.stroke_width, style.near.stroke_width, closeness); // Draw the circle for the bubbble this.drawCircle(bubble.position, bubble.size, fill_color, stroke_width, stroke_color); // Draw the line that connects it to the center if enabled if (bubble.line) { this.drawLine(this.center, bubble.position, bubble.line, fill_color); } // Write the axis label (X,Y,Z) if provided if (bubble.label && (is_selected || (bubble.labelHideUnselected !== true))) { const ctx = this.context2d; ctx.font = [this.options.fontWeight, this.options.fontSize, this.options.fontFamily].join(" "); ctx.fillStyle = is_selected ? this.options.selectionFontColor : this.options.fontColor; ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; ctx.fillText(bubble.label, bubble.position.x, bubble.position.y + this.options.fontYAdjust); } } } /** * * @param {Vector3} position * @returns {Vector3} */ getBubblePosition(position) { return new Vector3( (position.x * (this.center.x - (this.options.bubbleSizePrimary / 2) - this.options.padding)) + this.center.x, this.center.y - (position.y * (this.center.y - (this.options.bubbleSizePrimary / 2) - this.options.padding)), position.z ); } }