@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
504 lines (418 loc) • 16.2 kB
JavaScript
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
);
}
}