repond-movers
Version:
movers for repond
301 lines (254 loc) • 10 kB
text/typescript
import { addToLimitedArray } from "chootils/dist/arrays";
import {
Point3D,
addPoints as addPointsImmutable,
copyPoint,
defaultPosition,
interpolatePoints,
pointBasicallyZero,
pointIsZero,
subtractPointsSafer,
updatePoint,
} from "chootils/dist/points3d";
import { addPoints, dividePoint, multiplyPoint, subtractPoints } from "chootils/dist/points3dInPlace";
import { getAverageSpeed } from "chootils/dist/speedAngleDistance";
import {
getSpeedAndAngleFromVector,
getVectorFromSpeedAndAngle,
getVectorSpeed,
} from "chootils/dist/speedAngleDistance3d";
import { AllRefs, AllState, getRefs, getState, onNextTick, setState, whenSettingStates } from "repond";
import {
defaultOptions,
defaultPhysics,
physicsTimestep,
physicsTimestepInSeconds,
recentSpeedsAmount,
} from "./consts";
import { AnyMoverStateNames, MoveMode, PhysicsConfig, PhysicsOptions } from "./types";
import { makeMoverStateMaker, makeStateNames, normalizeDefinedPhysicsConfig } from "./utils";
export type PositionAndVelocity = {
position: Point3D;
velocity: Point3D;
};
const DEFAULT_SPRING_STOP_SPEED = defaultPhysics("3d").stopSpeed;
type MainValueType = ReturnType<typeof defaultPosition>;
export const mover3dState = makeMoverStateMaker(defaultPosition) as <
T_Name extends string,
T_PhysicsNames extends string,
T_InitialState extends {
value?: MainValueType; // T_ValueType
valueGoal?: MainValueType; // T_ValueType
isMoving?: boolean;
moveConfigName?: T_PhysicsNames;
moveMode?: MoveMode;
moveConfigs?: Record<T_PhysicsNames, PhysicsOptions>;
}
>(
newName: T_Name,
initialState?: T_InitialState
) => Record<T_Name, MainValueType> &
Record<`${T_Name}Goal`, MainValueType> &
Record<`${T_Name}IsMoving`, boolean> &
Record<`${T_Name}MoveMode`, MoveMode> &
(T_InitialState["moveConfigName"] extends undefined ? {} : Record<`${T_Name}MoveConfigName`, T_PhysicsNames>) &
(T_InitialState["moveConfigs"] extends undefined
? {}
: Record<`${T_Name}MoveConfigs`, Record<T_PhysicsNames, PhysicsOptions>>);
export function mover3dRefs<T_Name extends string>(newName: T_Name, config?: PhysicsConfig) {
const newRefs = {
velocity: defaultPosition(),
recentSpeeds: [] as number[],
averageSpeed: 0,
canRunOnSlow: true,
stateNames: makeStateNames(newName),
physicsConfigs: normalizeDefinedPhysicsConfig(config, "3d"),
};
return {
[`${newName}MoverRefs`]: newRefs,
} as Record<`${T_Name}MoverRefs`, typeof newRefs>;
}
// export function makeMover3dUtils() {
// ---------------------------
// types
type ItemType = keyof AllState & keyof AllRefs;
type ItemState<T_ItemType extends ItemType> = AllState[T_ItemType][keyof AllState[T_ItemType]];
type StateNameProperty<T_ItemType extends ItemType> = keyof ItemState<T_ItemType>;
type RunMoverOptions<T_ItemType extends ItemType> = {
onSlow?: () => any;
id: string;
type: T_ItemType;
frameDuration?: number;
mover: StateNameProperty<T_ItemType> & string;
autoRerun?: boolean;
};
// ---------------------------
const rerunOptions: RunMoverOptions<any> = {
frameDuration: 16.6667,
id: "",
type: "",
onSlow: undefined,
mover: "",
autoRerun: true,
};
export function runMover3d<T_ItemType extends ItemType>({
frameDuration = 16.6667,
id: itemId,
type: itemType,
onSlow,
mover: moverName,
autoRerun,
}: RunMoverOptions<T_ItemType>) {
// repeated for all movers Start
const itemRefs = getRefs(itemType, itemId) as any;
const itemState = getState(itemType, itemId);
const moverRefs = itemRefs[`${moverName}MoverRefs`];
const keys: AnyMoverStateNames = moverRefs.stateNames;
const nowStepState = {
position: copyPoint(itemState[keys.value]),
velocity: copyPoint(moverRefs.velocity),
};
const prevStepState = {
position: copyPoint(nowStepState.position),
velocity: copyPoint(nowStepState.velocity),
};
const moveMode = itemState[keys.moveMode] ?? defaultOptions.moveMode;
const physicsConfigs = itemState[keys.physicsConfigs] ?? moverRefs.physicsConfigs;
const physicsOptions =
physicsConfigs[itemState?.[keys?.physicsConfigName]] ?? physicsConfigs[defaultOptions.physicsConfigName];
const targetPosition = itemState[keys.valueGoal];
let timeRemainingForPhysics = frameDuration;
// repeated for all movers End
const originalPositon = copyPoint(itemState[keys.value]);
const springStopSpeed = physicsOptions.stopSpeed ?? DEFAULT_SPRING_STOP_SPEED;
while (timeRemainingForPhysics >= physicsTimestep) {
// prevStepState = currentStepState;
updatePoint(prevStepState.position, nowStepState.position);
updatePoint(prevStepState.velocity, nowStepState.velocity);
// currentStepState = runPhysicsStep({
run3dPhysicsStep(nowStepState, moveMode, physicsOptions, targetPosition);
timeRemainingForPhysics -= physicsTimestep;
}
// can just use the current position if interpolating isn't needed
const newPosition = interpolatePoints(
nowStepState.position,
prevStepState.position,
timeRemainingForPhysics / physicsTimestep // remainingTimestepPercent
);
moverRefs.velocity = nowStepState.velocity;
// Check shouldKeepMoving
// note could move this to inside the setState to get latest state and use actualNewPosition
const currentSpeed = getVectorSpeed(moverRefs.velocity);
addToLimitedArray(moverRefs.recentSpeeds, currentSpeed, recentSpeedsAmount);
const hasEnoughSpeeds = moverRefs.recentSpeeds.length >= recentSpeedsAmount - 1;
const averageSpeed = hasEnoughSpeeds ? getAverageSpeed(moverRefs.recentSpeeds) : Infinity;
// FIXME a problem happens when moving from "push" to "spring",
// there are already recentSpeeds that might have a lot of 0's
// so when the spring starts the average speed is very low, so it stops springing
moverRefs.averageSpeed = averageSpeed;
const isAutoMovementType = itemState[keys.moveMode] === "spring" || itemState[keys.moveMode] === "slide";
// NOTE was it needed to check the latest state here?
// const isAutoMovementType = moveMode === "spring" || moveMode === "slide";
let shouldKeepMoving = true;
if (isAutoMovementType) {
const targetPointDifference = subtractPointsSafer(newPosition, targetPosition);
const isGoingFasterThanStopSpeed = averageSpeed > springStopSpeed;
const quickDistance =
Math.abs(targetPointDifference.x) + Math.abs(targetPointDifference.y) + Math.abs(targetPointDifference.z);
const isQuiteClose = quickDistance < 0.15;
if (isGoingFasterThanStopSpeed) {
shouldKeepMoving = itemState[keys.isMoving];
} else {
shouldKeepMoving = itemState[keys.isMoving] && !isQuiteClose;
}
}
if (!shouldKeepMoving) setState(`${itemType}.${keys.isMoving}`, false, itemId);
whenSettingStates(() => {
const currentPosition = getState(itemType, itemId)[keys.value];
const positionDifference = subtractPointsSafer(newPosition, originalPositon);
const actualNewPosition = addPointsImmutable(currentPosition, positionDifference);
if (pointIsZero(positionDifference) || pointBasicallyZero(positionDifference)) {
setState(`${itemType}.${keys.isMoving}`, false, itemId);
}
setState(`${itemType}.${keys.value}`, actualNewPosition, itemId);
});
onNextTick((nextFrameDuration) => {
if (isAutoMovementType) {
if (moverRefs.canRunOnSlow && averageSpeed < 150) {
moverRefs.canRunOnSlow = false;
onSlow?.();
}
}
if (!autoRerun) return;
if (itemState[keys.isMoving]) {
rerunOptions.frameDuration = nextFrameDuration;
rerunOptions.id = itemId;
rerunOptions.type = itemType;
rerunOptions.onSlow = onSlow;
rerunOptions.mover = moverName;
rerunOptions.autoRerun = autoRerun;
runMover3d(rerunOptions);
}
});
}
// runPhysicsStepObjectPool
const pool = {
// new both start out as the original value , and get updated in place
newVelocity: defaultPosition(),
newPosition: defaultPosition(),
//
positionCopy: defaultPosition(),
velocityCopy: defaultPosition(),
velocityCopyB: defaultPosition(),
amountMoved: defaultPosition(),
};
function run3dPhysicsStep(
stepState: PositionAndVelocity,
moveMode: MoveMode,
physicsOptions: PhysicsOptions,
targetPosition: Point3D
) {
const { mass, stiffness, damping, friction } = physicsOptions;
updatePoint(pool.newVelocity, stepState.velocity);
updatePoint(pool.newPosition, stepState.position);
switch (moveMode) {
case "spring":
{
// so we dont edit the original object directly
updatePoint(pool.positionCopy, stepState.position);
updatePoint(pool.velocityCopy, stepState.velocity);
updatePoint(pool.velocityCopyB, stepState.velocity);
const positionDifference = subtractPoints(pool.positionCopy, targetPosition);
const springForce = multiplyPoint(positionDifference, -stiffness);
const dampingForce = multiplyPoint(pool.velocityCopy, damping);
const force = subtractPoints(springForce, dampingForce);
const acceleration = dividePoint(force, mass);
const accelerationWithTime = multiplyPoint(acceleration, physicsTimestep);
addPoints(pool.newVelocity, accelerationWithTime);
}
break;
case "slide":
{
const { speed, angle } = getSpeedAndAngleFromVector(stepState.velocity);
const newSpeed = Math.max(speed * Math.pow(1 - friction, physicsTimestepInSeconds * 10), 0);
pool.newVelocity = getVectorFromSpeedAndAngle(newSpeed, angle);
}
break;
case "drag":
break;
case "push":
break;
default:
}
updatePoint(pool.amountMoved, pool.newVelocity);
multiplyPoint(pool.amountMoved, physicsTimestepInSeconds);
addPoints(pool.newPosition, pool.amountMoved);
updatePoint(stepState.position, pool.newPosition);
updatePoint(stepState.velocity, pool.newVelocity);
}
// return {
// mover3dState,
// mover3dRefs,
// runMover3d,
// };
// }