@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
337 lines (287 loc) • 13.6 kB
JavaScript
import Vector2 from "../../../../core/geom/Vector2.js";
import AABB2 from "../../../../core/geom/2d/aabb/AABB2.js";
import {ActionProcessor} from "../../../../core/process/undo/ActionProcessor.js";
import EmptyView from "../../../../view/elements/EmptyView.js";
import {CanvasView} from "../../../../view/elements/CanvasView.js";
import {createKeyCoordinateLabelView} from "./createKeyCoordinateLabelView.js";
import Signal from "../../../../core/events/signal/Signal.js";
import KeyboardDevice from "../../../input/devices/KeyboardDevice.js";
import {KeyframeStateManager} from "./KeyframeStateManager.js";
import {keyframesContext} from "../actionProcessorOperations/curveActions.js";
import {createKeyframeMarker} from "./createKeyframeMarker.js";
import {updateMarkerPosition} from "./updateMarkerPosition.js";
import {updateMarkerVisual} from "./updateMarkerVisual.js";
import {createKeyframeDraggableAspect} from "./createKeyframeDraggableAspect.js";
import {
handleKeyDownEvents,
handleKeyframeSelectedByDevice,
handleMouseDoubleClickEvents,
handleMouseDownEvents
} from "./inputEventHandlers.js";
import {build_tangent_editor} from "../draw/build_tangent_editor.js";
import {buildReadOnlyDisplay} from "../draw/buildReadOnlyDisplay.js";
import {KeyboardEvents} from "../../../input/devices/events/KeyboardEvents.js";
import {MouseEvents} from "../../../input/devices/events/MouseEvents.js";
import {canvas2dPlotCurveLine} from "./canvas2dPlotCurveLine.js";
import {updateAxisRange} from "./updateAxisRange.js";
import {determineUnitSpacing} from "./determineUnitSpacing.js";
import {animation_curve_compute_aabb} from "../animation_curve_compute_aabb.js";
import {displayMousePos} from "../draw/displayMousePos.js";
import {PointerDevice} from "../../../input/devices/PointerDevice.js";
import {createSelectionBoxTool} from "./createSelectionBoxTool.js";
import {createPanTool} from "./createPanTool.js";
import {OperationRouter} from "./OperationRouter.js";
import {decodeMouseEventButtons} from "../../../input/devices/mouse/decodeMouseEventButtons.js";
import {createCurveUploader} from "./createCurveUploader.js";
const graphToolType = ({
SELECT: "select",
PAN: "pan",
});
export class CurveEditorView extends EmptyView{
/**
*
* @param {AnimationCurve} curve
* @param {Vector2} [size]
* @param {Vector2} [margin] How much space to leave empty around the plotted bounds
* @param {boolean} [enableEdit]
* @param {AABB2} [validEditableBounds]
* @param {ActionProcessor} [actionProcessor]
*/
constructor({
curve,
size = new Vector2(400, 400),
margin = new Vector2(46, 46),
enableEdit= true,
validEditableBounds = new AABB2(-Infinity,-Infinity,Infinity,Infinity),
actionProcessor = new ActionProcessor({})
}) {
super({
css: {
pointerEvents: "auto",
outline: 'none',
overflow: 'hidden',
position: 'relative'
}
});
this.curve = curve;
this.size.copy(size);
this.margin = margin;
this.enableEdit = enableEdit;
this.validEditableBounds = validEditableBounds;
this.actionProcessor = actionProcessor;
this.gridSpacing = 32;
this.zoomLevel = 1;
this.build();
}
build() {
this.el.setAttribute('tabindex', '-1'); //allow programmatic focus only
// ---- Interface Setup
const graph = new CanvasView();
graph.size.copy(this.size);
const frameRegion = new AABB2();
const offset = 0.5;
//Initialize the frameRegion size
if (this.curve.length > 1) {
animation_curve_compute_aabb(frameRegion, this.curve);
if (frameRegion.y0 === frameRegion.y1) {
frameRegion.y0 -= offset
frameRegion.y1 += offset
}
}
else {
const key = this.curve.keys?.[0]
frameRegion.set(
key ? key.time - offset : 0,
key ? key.value - offset : 0,
key ? key.time + offset : 1,
key ? key.value + offset : 1
);
}
const keyCoordinateDisplay = createKeyCoordinateLabelView();
// ---- Triggers
const frameUpdated = new Signal();
const keyframeChange = new Signal();
const curveUploadChange = createCurveUploader(this.el);
// ---- Keyboard Startup
const keyboardElement = new KeyboardDevice(this.el);
keyboardElement.start();
// Todo maybe have const uiStateManager = new UIContext(curve, size, margin, frameRegion, validEditableBounds)
const keyframeStateManager = new KeyframeStateManager();
// ----
const actionProcessorCTX = new keyframesContext({
curve: this.curve,
removeKeyframe: removeKeyframe,
addKeyframe: addKeyframe,
keyframeViews: keyframeStateManager.keyframeViews,
activeKeyframe: keyframeStateManager.observedActiveKeyframe,
selectedKeyframes: keyframeStateManager.selectedKeyframes,
handleCurveUpdate: handleCurveUpdate,
updateGraph: updateGraph
});
const self = this;
function reinitCurve(newCurve) {
self.curve.keys.forEach(removeKeyframe);
self.curve.copy(newCurve)
self.curve.keys.forEach(addKeyframe);
handleCurveUpdate();
}
function handleCurveUpdate() {
frameUpdated.send0();
updateGraph();
}
function updateGraph() {
canvas2dPlotCurveLine({
curve: self.curve,
ctx: graph.context2d,
width: graph.size.x,
height: graph.size.y,
margin: self.margin,
range_y: [frameRegion.y0, frameRegion.y1],
range_x: [frameRegion.x0, frameRegion.x1],
spacing: self.gridSpacing
});
}
handleCurveUpdate()
/**
*
* @param {Keyframe} keyframe
*/
function addKeyframe(keyframe) {
const vContainerMarker = new EmptyView({
css: {
position: "absolute",
top: 0,
left: 0,
pointerEvents: "none"
}
});
const marker = createKeyframeMarker();
const fnUpdateMarkerPosition = () => updateMarkerPosition(graph, frameRegion, self.margin, keyframe, vContainerMarker);
const fnUpdateMarkerVisual = () => updateMarkerVisual(keyframeStateManager, keyframe, vTangentEditor, marker);
if (self.enableEdit) {
const markerElement = marker.el;
const draggable = createKeyframeDraggableAspect({
markerElement: markerElement,
graph: graph,
frame: frameRegion,
margin: self.margin,
validEditableBounds: self.validEditableBounds,
curve: self.curve,
fnUpdateGraph: updateGraph,
keyframe: keyframe,
selectedKeyframes: keyframeStateManager.selectedKeyframes,
activeKeyframe: keyframeStateManager.observedActiveKeyframe,
keyCoordinateDisplay: keyCoordinateDisplay,
fnUpdateMarkerPosition: fnUpdateMarkerPosition,
frameUpdated: frameUpdated,
keyframeChange: keyframeChange,
vContainer: self,
actionProcessor: self.actionProcessor,
actionProcessorCTX: actionProcessorCTX
});
draggable.getPointer().on.down.add(() => {
handleKeyframeSelectedByDevice(keyboardElement, keyframe, self.actionProcessor, actionProcessorCTX, keyframeStateManager);
});
marker.on.linked.add(draggable.start, draggable);
marker.on.unlinked.add(draggable.stop, draggable);
}
marker.bindSignal(frameUpdated, fnUpdateMarkerPosition);
marker.bindSignal(keyframeStateManager.observedActiveKeyframe.onChanged, fnUpdateMarkerVisual);
marker.bindSignal(keyframeStateManager.selectedKeyframes.on.added, fnUpdateMarkerVisual);
marker.bindSignal(keyframeStateManager.selectedKeyframes.on.removed, fnUpdateMarkerVisual);
const vTangentEditor = build_tangent_editor({
keyframe: keyframe,
size: graph.size,
ctx: graph.context2d,
frame: frameRegion,
margin: self.margin,
enableTangentAlignment: keyframeStateManager.tangentAlignmentEnabled,
cbFnCurveUpdate: updateGraph,
funcTrigger: keyframeChange,
actionProcessor: self.actionProcessor,
actionProcessorCTX: actionProcessorCTX
});
keyframeStateManager.keyframeViews.set(keyframe, vContainerMarker);
vContainerMarker.addChild(marker);
self.addChild(vContainerMarker);
}
function removeKeyframe(keyframe) {
const markerView = keyframeStateManager.keyframeViews.get(keyframe);
self.removeChild(markerView);
keyframeStateManager.keyframeViews.delete(keyframe)
}
graph.on.linked.add(() => {
this.curve.keys.forEach(addKeyframe);
});
graph.on.unlinked.add(() => {
this.curve.keys.forEach(removeKeyframe);
});
if (!this.enableEdit){
const labelReadOnly = buildReadOnlyDisplay();
this.addChild(labelReadOnly);
}
this.el.addEventListener(KeyboardEvents.KeyDown, (event) => {
handleKeyDownEvents(
event,
keyboardElement,
this.curve,
keyframeStateManager,
this.actionProcessor,
actionProcessorCTX
);
});
const graphPointer = new PointerDevice(this.el);
this.on.linked.add(graphPointer.start, graphPointer);
this.on.unlinked.add(graphPointer.stop, graphPointer);
const selectionBoxTool = new createSelectionBoxTool({
keyframeStateManager: keyframeStateManager,
graph: graph,
frame: frameRegion,
margin: this.margin,
fnUpdate: updateGraph,
actionProcessor: this.actionProcessor,
actionCTX: actionProcessorCTX
})
const panTool = new createPanTool({
graph: graph,
frame: frameRegion,
margin: this.margin,
fnUpdate: handleCurveUpdate,
})
const graphToolList = {
[graphToolType.SELECT]: selectionBoxTool,
[graphToolType.PAN]: panTool,
}
const operationRouter = new OperationRouter(graphPointer, (event) => {
if (decodeMouseEventButtons(event.buttons)[0] && this.enableEdit) return graphToolList[graphToolType.SELECT];
if (decodeMouseEventButtons(event.buttons)[1]) return graphToolList[graphToolType.PAN];
return null;
});
this.el.addEventListener(MouseEvents.Down, (e) => {
handleMouseDownEvents(e, graph, keyboardElement, this.actionProcessor, actionProcessorCTX, keyframeStateManager);
});
if (this.enableEdit) {
//create new keyframe at location
this.el.addEventListener(MouseEvents.DoubleClick, (e) => {
handleMouseDoubleClickEvents(e, this, graph, frameRegion, this.margin, this.curve, this.validEditableBounds, this.actionProcessor, actionProcessorCTX);
});
}
this.el.addEventListener(MouseEvents.Wheel, (e) => {
const zoomFactor = e.deltaY < 0 ? 1.2 : 1/1.2;
this.zoomLevel *= zoomFactor;
//Reminder: apply vertical flip to Y axis for graph coordinates
const cursorToGraphPos = new Vector2(e.x - this.margin.x, (graph.size.y - this.margin.y) - e.y);
const newX = updateAxisRange(graph.size.x - this.margin.x * 2, cursorToGraphPos.x, frameRegion.x0, frameRegion.x1, zoomFactor);
const newY = updateAxisRange(graph.size.y - this.margin.y * 2, cursorToGraphPos.y, frameRegion.y0, frameRegion.y1, zoomFactor);
frameRegion.set(newX[0], newY[0], newX[1], newY[1]);
this.gridSpacing = determineUnitSpacing(graph.size.x - this.margin.x * 2, this.zoomLevel);
handleCurveUpdate();
},
{passive: true});
graph.on.linked.add(handleCurveUpdate);
// graph.bindSignal(curveUploadChange, handleCurveUpdate);
graph.bindSignal(curveUploadChange, (data) => reinitCurve(data));
displayMousePos.call(this, graph, frameRegion);
this.addChild(graph);
}
}