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