UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

337 lines (287 loc) • 13.6 kB
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); } }