@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
402 lines (275 loc) • 10.3 kB
JavaScript
import GuiControl from "../../../../../src/view/controller/controls/GuiControl.js";
import View from "../../../../../src/view/View.js";
import { PointerDevice } from "../../../../../src/engine/input/devices/PointerDevice.js";
import Vector2 from "../../../../../src/core/geom/Vector2.js";
import {
ParameterLookupTableFlags
} from "../../../../../src/engine/graphics/particles/particular/engine/parameter/ParameterLookupTableFlags.js";
import LabelView from "../../../../../src/view/common/LabelView.js";
import Vector1 from "../../../../../src/core/geom/Vector1.js";
import { clamp01 } from "../../../../../src/core/math/clamp01.js";
function computeVerticalValuePosition(v, min, max, height, lineWidth) {
const margin = height * 0.15;
const valueRange = max - min;
const drawRangeY = height - lineWidth - margin * 2;
const drawOffsetY = lineWidth / 2 + margin;
let normalizedValue;
if (valueRange === 0) {
normalizedValue = 0.5;
} else {
normalizedValue = (v - min) / (valueRange);
}
const scaledValue = (1 - normalizedValue) * drawRangeY + drawOffsetY;
return scaledValue;
}
/**
*
* @param level
* @param {CanvasRenderingContext2D} context2d
* @param width
* @param height
* @param {ParameterLookupTable} lut
* @param lineWidth
*/
function drawVerticalLine(level, context2d, width, height, lut, lineWidth) {
context2d.fillStyle = 'none';
context2d.strokeStyle = 'rgba(255,0,0,1)';
context2d.lineWidth = lineWidth;
lut.computeStatistics();
//figure out Y coordinate
const y = computeVerticalValuePosition(level, lut.valueMin, lut.valueMax, height, lineWidth);
context2d.beginPath();
context2d.moveTo(0, y);
context2d.lineTo(width, y);
context2d.stroke();
}
/**
*
* @param {CanvasRenderingContext2D} context2d
* @param {number} width
* @param {number} height
* @param {ParameterLookupTable} lut
* @param {number} lineWidth
*/
function drawPlot(context2d, width, height, lut, lineWidth) {
context2d.fillStyle = 'none';
context2d.strokeStyle = 'rgba(255,255,255,1)';
context2d.lineWidth = lineWidth;
context2d.beginPath();
lut.computeStatistics();
let i;
const numValues = lut.positions.length;
for (i = 0; i < numValues; i++) {
const j = i * lut.itemSize;
const datum = lut.data[j];
const position = lut.positions[i];
const x = position * width;
const scaledValue = computeVerticalValuePosition(datum, lut.valueMin, lut.valueMax, height, lineWidth);
if (i === 0) {
if (position !== 0) {
context2d.moveTo(0, scaledValue);
context2d.lineTo(x, scaledValue);
} else {
context2d.moveTo(x, scaledValue);
}
} else {
context2d.lineTo(x, scaledValue);
}
if (i === numValues - 1 && position !== 1) {
context2d.lineTo(width, scaledValue);
}
}
context2d.stroke();
}
function buildMarker(markers, model, update, canvas) {
const marker = new MarkerView();
const pMarker = new PointerDevice(marker.el);
const pGlobal = new PointerDevice(window);
const dragAnchor = new Vector2();
let dragScale;
let oldValue, oldPosition;
let dragging = false;
function handleMove(position, event) {
event.preventDefault();
event.stopPropagation();
const delta = position.clone().sub(dragAnchor);
const markerIndex = markers.indexOf(marker);
if (markerIndex === -1) {
//dead marker
pMarker.stop();
pGlobal.stop();
return;
}
const lut = model();
const positionDelta = delta.x / canvas.width;
const valueDeltaNormalized = (-delta.y / canvas.height);
const valueDelta = valueDeltaNormalized * dragScale;
const newPosition = clamp01(oldPosition + positionDelta);
lut.positions[markerIndex] = newPosition;
const newValue = oldValue + valueDelta;
lut.data[markerIndex * lut.itemSize] = newValue;
update();
}
function handleDragEnd() {
pGlobal.on.move.remove(handleMove);
pGlobal.on.up.remove(handleDragEnd);
}
pGlobal.on.down.add(function (position) {
dragAnchor.copy(position);
});
pMarker.on.down.add(function (position, event) {
if (event.which === 2) {
//middle mouse button
event.preventDefault();
event.stopPropagation();
const markerIndex = markers.indexOf(marker);
if (markerIndex === -1) {
//dead marker
pMarker.stop();
pGlobal.stop();
return;
}
const lut = model();
lut.positions.splice(markerIndex, 1);
lut.data.splice(markerIndex * lut.itemSize, lut.itemSize);
update();
}
});
pMarker.on.dragStart.add(function () {
dragging = true;
const lut = model();
dragScale = lut.valueMax - lut.valueMin;
if (dragScale === 0) {
dragScale = 0.1;
}
const markerIndex = markers.indexOf(marker);
oldValue = lut.data[markerIndex * lut.itemSize];
oldPosition = lut.positions[markerIndex];
pGlobal.on.move.add(handleMove);
pGlobal.on.up.add(handleDragEnd);
});
pMarker.start();
pGlobal.start();
return marker;
}
class MarkerView extends View {
constructor() {
super();
this.el = document.createElement('div');
this.el.classList.add('marker-view');
//add value label
this.value = new Vector2(0, 0);
const yValue = new Vector1(0);
const lY = new LabelView(yValue, { classList: ['value'] });
this.addChild(lY);
this.value.onChanged.add((x, y) => yValue.set(y));
}
}
export class ScalarParameterLUTController extends GuiControl {
constructor() {
super();
this.el.classList.add('scalar-parameter-lut-controller');
const lineWidth = 2;
const self = this;
const canvas = document.createElement('canvas');
canvas.width = 170;
const context2d = canvas.getContext('2d');
const vCanvas = new View();
vCanvas.el = canvas;
const markers = [];
/**
*
* @returns {ParameterLookupTable}
*/
function model() {
return self.model.getValue();
}
function updatePlot() {
const h = canvas.height;
const w = canvas.width;
context2d.clearRect(0, 0, w, h);
context2d.fillStyle = 'black';
context2d.fillRect(0, 0, w, h);
/**
*
* @type {ParameterLookupTable}
*/
const lut = self.model.getValue();
if (lut !== null) {
//draw a line through zero on Y axis
drawVerticalLine(0, context2d, w, h, lut, 1);
drawPlot(context2d, w, h, lut, lineWidth);
}
}
function createMarker() {
const marker = buildMarker(markers, model, update, canvas);
markers.push(marker);
self.addChild(marker);
}
function resizeMarkerPool(size) {
while (size < markers.length) {
const marker = markers.pop();
self.removeChild(marker);
}
while (size > markers.length) {
createMarker();
}
}
function updateMarkers() {
/**
*
* @type {ParameterLookupTable}
*/
const lut = model();
if (lut !== null) {
const positions = lut.positions;
const numValues = positions.length;
//resize marker pool
resizeMarkerPool(numValues);
//move markers
for (let i = 0; i < numValues; i++) {
const marker = markers[i];
const position = positions[i];
const datum = lut.data[i * lut.itemSize];
const y = computeVerticalValuePosition(datum, lut.valueMin, lut.valueMax, canvas.height, lineWidth);
const x = position * canvas.width;
marker.value.set(position, datum);
marker.position.set(x, y);
}
} else {
//clear all markers
resizeMarkerPool(0);
}
}
function update() {
updatePlot();
updateMarkers();
}
const pCanvas = new PointerDevice(canvas);
pCanvas.on.down.add(function (position, event) {
const p = event.offsetX / canvas.width;
const valueNormalized = event.offsetY / canvas.height;
const lut = model();
lut.setFlag(ParameterLookupTableFlags.WriteMode);
if (lut.positions.length > 0) {
//ensure that statistics are present
lut.computeStatistics();
const valueMin = lut.valueMin;
const valueMax = lut.valueMax;
const valueRange = valueMax - valueMin;
const value = (1 - valueNormalized) * valueRange + valueMin;
lut.addValue(p, [value]);
} else {
lut.addValue(p, [0]);
lut.computeStatistics();
}
lut.clearFlag(ParameterLookupTableFlags.WriteMode);
update();
});
pCanvas.start();
this.addChild(vCanvas);
this.model.onChanged.add(function () {
update();
});
}
}