UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

284 lines (233 loc) • 9.67 kB
import EmptyView from "../../../../view/elements/EmptyView.js"; import {DraggableAspect} from "../../../ui/DraggableAspect.js"; import ObservedValue from "../../../../core/model/ObservedValue.js"; import {Keyframe} from "../Keyframe.js"; import Vector1 from "../../../../core/geom/Vector1.js"; import {position_curve_to_canvas} from "./position_curve_to_canvas.js"; import {normalize_angle_rad} from "../../../../core/geom/normalize_angle_rad.js"; import {TangentChangeAction} from "../actionProcessorOperations/curveActions.js"; import ObservedBoolean from "../../../../core/model/ObservedBoolean.js"; import {clamp} from "../../../../core/math/clamp.js"; import {EPSILON} from "../../../../core/math/EPSILON.js"; import {createKeyframeMarker} from "../editor/createKeyframeMarker.js"; const tangentHandlerDir = { incoming:1, outgoing:2 }; function updateTangentValues(keyframePos, cursorPos, keyframe, isAligned, activeTangent) { function calculateNewAngle(){ const passAngle = normalize_angle_rad(Math.atan2(cursorPos.y - keyframePos.y, cursorPos.x - keyframePos.x)); const maxBound = Math.PI / 2 - EPSILON; const minBound = -Math.PI / 2 + EPSILON; let tempAngle = 0; if (activeTangent === tangentHandlerDir.outgoing) { tempAngle = clamp(passAngle, minBound, maxBound); } else if (activeTangent === tangentHandlerDir.incoming) { const flipAngle = clamp(normalize_angle_rad((passAngle + Math.PI)), minBound, maxBound); tempAngle = normalize_angle_rad((flipAngle-Math.PI)); } return tempAngle; } const newAngle = calculateNewAngle(); const newTangentValue = Math.tan(newAngle); if (activeTangent === tangentHandlerDir.outgoing){ keyframe.outTangent = -newTangentValue; } else if (activeTangent === tangentHandlerDir.incoming){ keyframe.inTangent = -newTangentValue; } else{ throw new Error(`Incorrect activeTangent value ${activeTangent}`); } if (isAligned.getValue()){ if (activeTangent === tangentHandlerDir.outgoing){ const flipAngle = normalize_angle_rad((newAngle - Math.PI)) keyframe.inTangent = -Math.tan(flipAngle); } else if (activeTangent === tangentHandlerDir.incoming){ const flipAngle = normalize_angle_rad((newAngle + Math.PI)) keyframe.outTangent = -Math.tan(flipAngle); } } } /** * * @param {CanvasRenderingContext2D} ctx * @param {Keyframe} keyframe * @param {Vector2} size * @param {AABB2} frame * @param {Vector2} margin * @param {ObservedBoolean} enableTangentAlignment * @param cbFnCurveUpdate * @param {Signal} funcTrigger * @param {ActionProcessor} actionProcessor * @param {keyframesContext} actionProcessorCTX */ export function build_tangent_editor({ ctx, keyframe, size, frame, margin, enableTangentAlignment = new ObservedBoolean(), cbFnCurveUpdate, funcTrigger, actionProcessor = null, actionProcessorCTX = null }) { if (actionProcessor === null){ throw new Error("Empty action processor argument") } if (actionProcessorCTX === null){ throw new Error("Empty action processor ctx argument") } const scale = 36; const noActiveHandler = null; /** * * @type {ObservedValue<Vector1>} */ const activeHandler = new ObservedValue(noActiveHandler); function buildHandle(handlerDirection) { const handleID = new Vector1(); const marker = createKeyframeMarker(); marker.css({ pointerEvents: "auto", borderRadius: "0px" }) const markerElement = marker.el; const keyframeValueStart = new Keyframe(), keyframeValueEnd = new Keyframe(); const rotatable = new DraggableAspect({ el: markerElement, drag(cursorCanvasPosition){ const keyframeCanvasPosition = position_curve_to_canvas(size, frame, margin, keyframe.time, keyframe.value); updateTangentValues(keyframeCanvasPosition, cursorCanvasPosition, keyframe, enableTangentAlignment, activeHandler.get().x); handleTangentUpdate() cbFnCurveUpdate(); }, dragStart(){ handleID.x = handlerDirection.x; activeHandler.set(handleID); keyframeValueStart.copy(keyframe); }, dragEnd() { keyframeValueEnd.copy(keyframe) keyframe.copy(keyframeValueStart); actionProcessor.mark('move tangent'); actionProcessor.do(new TangentChangeAction(keyframe, keyframeValueStart, keyframeValueEnd, handleTangentUpdate, actionProcessorCTX)); } }); rotatable.getPointer().on.tap.add( () => { handleID.x = handlerDirection.x; activeHandler.set(handleID); }); function updateActiveState() { if (activeHandler.get() === handleID) { marker.css({ background: '#FFFFFF' }); } else { marker.css({ background: '#00ff00' }); } } marker.on.linked.add(rotatable.start, rotatable); marker.on.unlinked.add(rotatable.stop, rotatable); marker.on.unlinked.add(() => { activeHandler.set(noActiveHandler) }); marker.bindSignal(activeHandler.onChanged, updateActiveState); marker.bindSignal(funcTrigger, handleTangentUpdate); return marker; } function buildTangent() { const view = new EmptyView({ css: { position: "absolute", left: "0", top: "0", background: 'red', width: `10px`, } }); view.position.set(-0.5, -0.5); view.transformOrigin.set(0, 0.5); function updateAlignmentState(){ if (!enableTangentAlignment.getValue()) { view.css({ background: 'red' }); } else { view.css({ background: 'white' }); } } view.bindSignal(enableTangentAlignment.onChanged, updateAlignmentState); view.on.unlinked.add(() => { //Maybe redundant, but used as a safeguard enableTangentAlignment.setFalse(); }) return view; } const handleIn = buildHandle(new Vector1(tangentHandlerDir.incoming)); const handleOut = buildHandle(new Vector1(tangentHandlerDir.outgoing)); const tangentIn = buildTangent(); const tangentOut = buildTangent(); function handleTangentUpdate() { tangentIn.size.set(scale, 1); const angleIn = Math.PI - Math.atan2(keyframe.inTangent, 1); tangentIn.rotation.set(angleIn); tangentOut.size.set(scale, 1); const angleOut = -Math.atan2(keyframe.outTangent, 1); tangentOut.rotation.set(angleOut); handleIn.position.set( Math.cos(angleIn) * scale, Math.sin(angleIn) * scale ); handleOut.position.set( Math.cos(angleOut) * scale, Math.sin(angleOut) * scale ); } const keyframeValueStart = new Keyframe(), keyframeValueEnd = new Keyframe(); function modifyTangentAlignmentPosition(){ function isNotAlreadyAligned(){ return !(keyframe.inTangent === -keyframe.outTangent) } function validateCalculatedAvgTangent(){ let newTangent = (keyframe.outTangent + keyframe.inTangent) * 0.5; if(isNaN(newTangent) || Math.abs(newTangent) > Infinity){ newTangent = 0; } return newTangent; } if (enableTangentAlignment.getValue() === true && isNotAlreadyAligned){ keyframeValueStart.copy(keyframe); const avgTangent = validateCalculatedAvgTangent(); keyframe.outTangent = avgTangent; keyframe.inTangent = avgTangent; keyframeValueEnd.copy(keyframe); keyframe.copy(keyframeValueStart); actionProcessor.mark('auto move tangents'); actionProcessor.do(new TangentChangeAction(keyframe, keyframeValueStart, keyframeValueEnd, handleTangentUpdate, actionProcessorCTX)); } } const vContainer = new EmptyView({ css: { position: 'absolute', left: 0, top: 0 } }); vContainer.addChild(handleIn); vContainer.addChild(handleOut); vContainer.addChild(tangentIn); vContainer.addChild(tangentOut); vContainer.bindSignal(enableTangentAlignment.onChanged, modifyTangentAlignmentPosition) vContainer.on.linked.add(handleTangentUpdate); return vContainer; }